diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Central.json b/src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Central.json index 4da01e02..001e51b4 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Central.json +++ b/src/ZB.MOM.WW.ScadaBridge.Host/appsettings.Central.json @@ -18,9 +18,9 @@ "FailureDetectionThreshold": "00:00:10", "MinNrOfMembers": 1 }, - "_secrets": "Host-003: Secrets are NOT committed in this file. Supply them via environment variables, which the Host's configuration builder (AddEnvironmentVariables) overlays over this file. Required: ScadaBridge__Database__ConfigurationDb, ScadaBridge__Security__Ldap__ServiceAccountPassword, ScadaBridge__Security__JwtSigningKey. The ${...} placeholders below are intentionally non-functional and must be overridden per environment. NOTE (Task 1.4): the LDAP settings moved into the nested Security:Ldap sub-section (bound to the shared ZB.MOM.WW.Auth LdapOptions) — the service-account-password env var is now ScadaBridge__Security__Ldap__ServiceAccountPassword (was ScadaBridge__Security__LdapServiceAccountPassword).", + "_secrets": "Host-003: Secrets are NOT committed in this file. The ${secret:...} references below are resolved at startup by the pre-host secrets expander (before StartupValidator runs) from the encrypted secrets store (SQLite, seeded via the ZB.MOM.WW.Secrets store/CLI/UI). The KEK is supplied out-of-band via the ZB_SECRETS_MASTER_KEY environment variable and never committed; an unseeded reference fails closed (SecretNotFoundException) before any SQL/LDAP/cluster wiring. ROLLBACK: the whole-key environment override still wins — set ScadaBridge__Database__ConfigurationDb / ScadaBridge__Security__Ldap__ServiceAccountPassword / ScadaBridge__Security__JwtSigningKey and .AddEnvironmentVariables() overlays the concrete value before expansion runs, so the ${secret:...} token is never evaluated. NOTE (Task 1.4): the LDAP settings moved into the nested Security:Ldap sub-section (bound to the shared ZB.MOM.WW.Auth LdapOptions) — the service-account-password env var is ScadaBridge__Security__Ldap__ServiceAccountPassword (was ScadaBridge__Security__LdapServiceAccountPassword).", "Database": { - "ConfigurationDb": "${SCADABRIDGE_CONFIGURATIONDB_CONNECTION_STRING}" + "ConfigurationDb": "${secret:sql/scadabridge/configdb-connection}" }, "Security": { "Ldap": { @@ -30,9 +30,9 @@ "AllowInsecure": true, "SearchBase": "dc=zb,dc=local", "ServiceAccountDn": "cn=admin,dc=zb,dc=local", - "ServiceAccountPassword": "${SCADABRIDGE_LDAP_SERVICE_ACCOUNT_PASSWORD}" + "ServiceAccountPassword": "${secret:ldap/scadabridge/service-account-password}" }, - "JwtSigningKey": "${SCADABRIDGE_JWT_SIGNING_KEY}", + "JwtSigningKey": "${secret:security/scadabridge/jwt-signing-key}", "JwtExpiryMinutes": 15, "IdleTimeoutMinutes": 30 }, diff --git a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/ActorPathTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/ActorPathTests.cs index 3cd1a9de..f96a8f54 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/ActorPathTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/ActorPathTests.cs @@ -23,23 +23,19 @@ public class CentralActorPathTests : IAsyncLifetime private WebApplicationFactory? _factory; private ActorSystem? _actorSystem; private string? _previousEnv; + private CentralDbTestEnvironment? _dbEnv; public Task InitializeAsync() { _previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central"); - // Supply the pepper so the Central-role StartupValidator preflight (1fcc4f5) - // passes before WebApplicationFactory gets a chance to overlay DI config. - // The pre-host config builder includes AddEnvironmentVariables(), so this - // env var is visible to StartupValidator.Validate() at Program.cs line 42. - Environment.SetEnvironmentVariable("ScadaBridge__InboundApi__ApiKeyPepper", - CentralDbTestEnvironment.TestPepper); - // Supply MachineDataDb so the reverted Host-008 Require (REQ-HOST-3/4, M2.9 #17) - // passes for Central-role StartupValidator. A non-empty placeholder satisfies - // the preflight; the DI override below replaces the real DbContext anyway. - Environment.SetEnvironmentVariable("ScadaBridge__Database__MachineDataDb", - "Server=localhost;Database=MachineData;"); + // Supply the pepper, MachineDataDb and the G-4 secret-backed keys + // (ConfigurationDb / LDAP service-account password / JWT signing key) as + // whole-key env overrides so the pre-host config builder — which calls + // AddEnvironmentVariables() before the ${secret:...} expander runs — sees + // concrete values and the Central-role StartupValidator preflight passes. + _dbEnv = new CentralDbTestEnvironment(); _factory = new WebApplicationFactory() .WithWebHostBuilder(builder => @@ -99,8 +95,7 @@ public class CentralActorPathTests : IAsyncLifetime { _factory?.Dispose(); Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", _previousEnv); - Environment.SetEnvironmentVariable("ScadaBridge__InboundApi__ApiKeyPepper", null); - Environment.SetEnvironmentVariable("ScadaBridge__Database__MachineDataDb", null); + _dbEnv?.Dispose(); await Task.CompletedTask; } diff --git a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/AkkaHostedServiceAuditWiringTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/AkkaHostedServiceAuditWiringTests.cs index 5fad810a..15afc439 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/AkkaHostedServiceAuditWiringTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/AkkaHostedServiceAuditWiringTests.cs @@ -90,22 +90,19 @@ public class CentralAuditWiringTests : IDisposable { private readonly WebApplicationFactory _factory; private readonly string? _previousEnv; + private readonly CentralDbTestEnvironment _dbEnv; public CentralAuditWiringTests() { _previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central"); - // Supply the pepper so the Central-role StartupValidator preflight (1fcc4f5) - // passes. The pre-host config builder uses AddEnvironmentVariables(), which - // runs before WithWebHostBuilder.ConfigureAppConfiguration applies DI config. - Environment.SetEnvironmentVariable("ScadaBridge__InboundApi__ApiKeyPepper", - CentralDbTestEnvironment.TestPepper); - // Supply MachineDataDb so the reverted Host-008 Require (REQ-HOST-3/4, M2.9 #17) - // passes for Central-role StartupValidator. A non-empty placeholder satisfies - // the preflight; the DI override below replaces the real DbContext anyway. - Environment.SetEnvironmentVariable("ScadaBridge__Database__MachineDataDb", - "Server=localhost;Database=MachineData;"); + // Supply the pepper, MachineDataDb and the G-4 secret-backed keys + // (ConfigurationDb / LDAP service-account password / JWT signing key) as + // whole-key env overrides so the pre-host config builder — which calls + // AddEnvironmentVariables() before the ${secret:...} expander runs — sees + // concrete values and the Central-role StartupValidator preflight passes. + _dbEnv = new CentralDbTestEnvironment(); _factory = new WebApplicationFactory() .WithWebHostBuilder(builder => @@ -161,8 +158,7 @@ public class CentralAuditWiringTests : IDisposable { _factory.Dispose(); Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", _previousEnv); - Environment.SetEnvironmentVariable("ScadaBridge__InboundApi__ApiKeyPepper", null); - Environment.SetEnvironmentVariable("ScadaBridge__Database__MachineDataDb", null); + _dbEnv.Dispose(); } [Fact] diff --git a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CentralDbTestEnvironment.cs b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CentralDbTestEnvironment.cs index 7f3b0171..60940a69 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CentralDbTestEnvironment.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CentralDbTestEnvironment.cs @@ -15,7 +15,17 @@ namespace ZB.MOM.WW.ScadaBridge.Host.Tests; /// Also supplies ScadaBridge__Database__MachineDataDb so the Central-role /// StartupValidator preflight (reverts Host-008, REQ-HOST-3/4, M2.9 #17) does not /// fail for tests that set DOTNET_ENVIRONMENT=Central without an explicit -/// MachineDataDb env var. All vars are restored on Dispose so tests stay isolated. +/// MachineDataDb env var. +/// +/// G-4 (secrets adoption): appsettings.Central.json now carries +/// ${secret:...} references for the ConfigurationDb connection string, the +/// LDAP service-account password and the JWT signing key. The pre-host secrets +/// expander (Program.cs) resolves those before StartupValidator runs and fails +/// closed on an unseeded reference. Supplying the concrete values as whole-key +/// environment overrides (the sanctioned rollback path) makes the expander skip +/// the tokens entirely — so tests that boot the real Program pipeline do +/// not need a seeded secrets store. All vars are restored on Dispose so tests +/// stay isolated. /// internal sealed class CentralDbTestEnvironment : IDisposable { @@ -37,9 +47,22 @@ internal sealed class CentralDbTestEnvironment : IDisposable internal const string TestPepper = "test-pepper-01234567890"; private const string PepperKey = "ScadaBridge__InboundApi__ApiKeyPepper"; + // G-4: whole-key overrides for the two remaining ${secret:...} config references + // in appsettings.Central.json (the LDAP service-account password and JWT signing + // key). Test-only placeholder values — non-empty, and the JWT key clears the + // ≥32-char StartupValidator length rule. Applied via env so the pre-host expander + // sees the concrete value and never evaluates the token. + private const string LdapServiceAccountPassword = "test-ldap-service-account-password"; + private const string LdapPasswordKey = "ScadaBridge__Security__Ldap__ServiceAccountPassword"; + + private const string JwtSigningKey = "test-signing-key-must-be-at-least-32-chars-long!"; + private const string JwtSigningKeyKey = "ScadaBridge__Security__JwtSigningKey"; + private readonly string? _previousConfig; private readonly string? _previousMachineData; private readonly string? _previousPepper; + private readonly string? _previousLdapPassword; + private readonly string? _previousJwtSigningKey; public CentralDbTestEnvironment() { @@ -51,6 +74,12 @@ internal sealed class CentralDbTestEnvironment : IDisposable _previousPepper = Environment.GetEnvironmentVariable(PepperKey); Environment.SetEnvironmentVariable(PepperKey, TestPepper); + + _previousLdapPassword = Environment.GetEnvironmentVariable(LdapPasswordKey); + Environment.SetEnvironmentVariable(LdapPasswordKey, LdapServiceAccountPassword); + + _previousJwtSigningKey = Environment.GetEnvironmentVariable(JwtSigningKeyKey); + Environment.SetEnvironmentVariable(JwtSigningKeyKey, JwtSigningKey); } public void Dispose() @@ -58,5 +87,7 @@ internal sealed class CentralDbTestEnvironment : IDisposable Environment.SetEnvironmentVariable(ConfigKey, _previousConfig); Environment.SetEnvironmentVariable(MachineDataKey, _previousMachineData); Environment.SetEnvironmentVariable(PepperKey, _previousPepper); + Environment.SetEnvironmentVariable(LdapPasswordKey, _previousLdapPassword); + Environment.SetEnvironmentVariable(JwtSigningKeyKey, _previousJwtSigningKey); } } diff --git a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs index 0e871b99..ba54db24 100644 --- a/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs +++ b/tests/ZB.MOM.WW.ScadaBridge.Host.Tests/CompositionRootTests.cs @@ -84,22 +84,19 @@ public class CentralCompositionRootTests : IDisposable { private readonly WebApplicationFactory _factory; private readonly string? _previousEnv; + private readonly CentralDbTestEnvironment _dbEnv; public CentralCompositionRootTests() { _previousEnv = Environment.GetEnvironmentVariable("DOTNET_ENVIRONMENT"); Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", "Central"); - // Supply the pepper so the Central-role StartupValidator preflight (1fcc4f5) - // passes. The pre-host config builder uses AddEnvironmentVariables(), which - // runs before WithWebHostBuilder.ConfigureAppConfiguration applies DI config. - Environment.SetEnvironmentVariable("ScadaBridge__InboundApi__ApiKeyPepper", - CentralDbTestEnvironment.TestPepper); - // Supply MachineDataDb so the reverted Host-008 Require (REQ-HOST-3/4, M2.9 #17) - // passes for Central-role StartupValidator. A non-empty placeholder satisfies - // the preflight; the DI override below replaces the real DbContext anyway. - Environment.SetEnvironmentVariable("ScadaBridge__Database__MachineDataDb", - "Server=localhost;Database=MachineData;"); + // Supply the pepper, MachineDataDb and the G-4 secret-backed keys + // (ConfigurationDb / LDAP service-account password / JWT signing key) as + // whole-key env overrides so the pre-host config builder — which calls + // AddEnvironmentVariables() before the ${secret:...} expander runs — sees + // concrete values and the Central-role StartupValidator preflight passes. + _dbEnv = new CentralDbTestEnvironment(); _factory = new WebApplicationFactory() .WithWebHostBuilder(builder => @@ -164,8 +161,7 @@ public class CentralCompositionRootTests : IDisposable { _factory.Dispose(); Environment.SetEnvironmentVariable("DOTNET_ENVIRONMENT", _previousEnv); - Environment.SetEnvironmentVariable("ScadaBridge__InboundApi__ApiKeyPepper", null); - Environment.SetEnvironmentVariable("ScadaBridge__Database__MachineDataDb", null); + _dbEnv.Dispose(); } // --- Singletons ---