fix(archreview): security config guards (SEC-01/04/06)
- SEC-01: derive AuthenticationOptions.SqlitePath / TlsOptions.SelfSignedCertPath defaults from SpecialFolder.CommonApplicationData (was Windows-literal strings that became CWD-relative files off-Windows); validator rejects non-rooted overrides; delete the stray C:\ProgramData\...gateway-auth.db files that materialized under src/; add a tree-hygiene test failing on any *.db under src/. - SEC-04: GatewayOptionsValidator fails startup when IsProduction() && DisableLogin. - SEC-06: validator rejects LDAP Transport=None in Production; document the MxGateway__Ldap__ServiceAccountPassword env override + dev-credential rotation. Galaxy SnapshotCachePath rooting could not be validator-enforced (bound by the external GalaxyRepository package, not GatewayOptions) — documented instead. archreview: SEC-01/04/06 (P1). Verified on the Auth-0.1.4 baseline: Server build clean, GatewayOptionsValidator + GatewayTreeHygiene tests 53/53.
This commit is contained in:
@@ -1,5 +1,6 @@
|
||||
using Microsoft.Extensions.Options;
|
||||
using ZB.MOM.WW.MxGateway.Server.Configuration;
|
||||
using LdapTransport = ZB.MOM.WW.Auth.Abstractions.Ldap.LdapTransport;
|
||||
|
||||
namespace ZB.MOM.WW.MxGateway.Tests.Configuration;
|
||||
|
||||
@@ -548,4 +549,138 @@ public sealed class GatewayOptionsValidatorTests
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SEC-01: security-sensitive paths must be rooted (absolute)
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static GatewayOptions CloneWithAuthentication(GatewayOptions source, AuthenticationOptions authentication)
|
||||
=> new()
|
||||
{
|
||||
Authentication = authentication,
|
||||
Ldap = source.Ldap,
|
||||
Worker = source.Worker,
|
||||
Sessions = source.Sessions,
|
||||
Events = source.Events,
|
||||
Dashboard = source.Dashboard,
|
||||
Protocol = source.Protocol,
|
||||
Alarms = source.Alarms,
|
||||
Tls = source.Tls,
|
||||
};
|
||||
|
||||
/// <summary>Verifies the default (CommonApplicationData-derived) auth DB path is rooted and passes validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_Succeeds_WithDefaultRootedSqlitePath()
|
||||
{
|
||||
// The default AuthenticationOptions.SqlitePath is derived from CommonApplicationData,
|
||||
// which is rooted on every host OS.
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, ValidOptions());
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
|
||||
/// <summary>Verifies a non-rooted <see cref="AuthenticationOptions.SqlitePath"/> fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_Fails_WhenSqlitePathNotRooted()
|
||||
{
|
||||
GatewayOptions options = CloneWithAuthentication(
|
||||
ValidOptions(),
|
||||
new AuthenticationOptions { SqlitePath = "gateway-auth.db" });
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
||||
Assert.True(result.Failed);
|
||||
Assert.Contains(
|
||||
result.Failures!,
|
||||
f => f.Contains("MxGateway:Authentication:SqlitePath") && f.Contains("rooted"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies a non-rooted <see cref="TlsOptions.SelfSignedCertPath"/> fails validation.</summary>
|
||||
[Fact]
|
||||
public void Validate_Fails_WhenSelfSignedCertPathNotRooted()
|
||||
{
|
||||
GatewayOptions options = CloneWithTls(
|
||||
ValidOptions(),
|
||||
new TlsOptions { SelfSignedCertPath = "gateway-selfsigned.pfx" });
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator().Validate(null, options);
|
||||
Assert.True(result.Failed);
|
||||
Assert.Contains(
|
||||
result.Failures!,
|
||||
f => f.Contains("MxGateway:Tls:SelfSignedCertPath") && f.Contains("rooted"));
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SEC-04: DisableLogin production guard
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
private static GatewayOptions CloneWithDashboard(GatewayOptions source, DashboardOptions dashboard)
|
||||
=> new()
|
||||
{
|
||||
Authentication = source.Authentication,
|
||||
Ldap = source.Ldap,
|
||||
Worker = source.Worker,
|
||||
Sessions = source.Sessions,
|
||||
Events = source.Events,
|
||||
Dashboard = dashboard,
|
||||
Protocol = source.Protocol,
|
||||
Alarms = source.Alarms,
|
||||
Tls = source.Tls,
|
||||
};
|
||||
|
||||
/// <summary>Verifies <see cref="DashboardOptions.DisableLogin"/> set to <see langword="true"/> aborts startup in Production.</summary>
|
||||
[Fact]
|
||||
public void Validate_Fails_WhenDisableLoginTrueInProduction()
|
||||
{
|
||||
GatewayOptions options = CloneWithDashboard(
|
||||
ValidOptions(),
|
||||
new DashboardOptions { DisableLogin = true });
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator(isProduction: true).Validate(null, options);
|
||||
Assert.True(result.Failed);
|
||||
Assert.Contains(
|
||||
result.Failures!,
|
||||
f => f.Contains("MxGateway:Dashboard:DisableLogin") && f.Contains("Production"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies <see cref="DashboardOptions.DisableLogin"/> set to <see langword="true"/> is accepted outside Production.</summary>
|
||||
[Fact]
|
||||
public void Validate_Succeeds_WhenDisableLoginTrueInDevelopment()
|
||||
{
|
||||
GatewayOptions options = CloneWithDashboard(
|
||||
ValidOptions(),
|
||||
new DashboardOptions { DisableLogin = true });
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator(isProduction: false).Validate(null, options);
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
|
||||
// -------------------------------------------------------------------------
|
||||
// SEC-06: LDAP plaintext transport production guard
|
||||
// -------------------------------------------------------------------------
|
||||
|
||||
/// <summary>Verifies plaintext LDAP transport (None) aborts startup in Production.</summary>
|
||||
[Fact]
|
||||
public void Validate_Fails_WhenLdapTransportNoneInProduction()
|
||||
{
|
||||
// The class default LDAP options ship Transport=None + AllowInsecure=true (dev posture).
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator(isProduction: true).Validate(null, ValidOptions());
|
||||
Assert.True(result.Failed);
|
||||
Assert.Contains(
|
||||
result.Failures!,
|
||||
f => f.Contains("MxGateway:Ldap:Transport") && f.Contains("Production"));
|
||||
}
|
||||
|
||||
/// <summary>Verifies plaintext LDAP transport (None) is accepted outside Production.</summary>
|
||||
[Fact]
|
||||
public void Validate_Succeeds_WhenLdapTransportNoneInDevelopment()
|
||||
{
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator(isProduction: false).Validate(null, ValidOptions());
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
|
||||
/// <summary>Verifies secure LDAP transport (Ldaps) passes validation in Production.</summary>
|
||||
[Fact]
|
||||
public void Validate_Succeeds_WhenLdapTransportLdapsInProduction()
|
||||
{
|
||||
GatewayOptions options = CloneWithLdap(
|
||||
ValidOptions(),
|
||||
new LdapOptions { Enabled = true, Transport = LdapTransport.Ldaps, AllowInsecure = false });
|
||||
ValidateOptionsResult result = new GatewayOptionsValidator(isProduction: true).Validate(null, options);
|
||||
Assert.True(result.Succeeded);
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user