diff --git a/src/ZB.MOM.WW.ScadaBridge.Security/Auth/AuthDisableLoginOptions.cs b/src/ZB.MOM.WW.ScadaBridge.Security/Auth/AuthDisableLoginOptions.cs index ba7d9fa8..dd5a2f63 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Security/Auth/AuthDisableLoginOptions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Security/Auth/AuthDisableLoginOptions.cs @@ -24,4 +24,7 @@ public sealed class AuthDisableLoginOptions /// The username the auto-login principal is minted with. Default "multi-role". public string User { get; set; } = "multi-role"; + + /// Explicit second acknowledgement key: permits DisableLogin outside Development. Default false. + public bool AllowOutsideDevelopment { get; set; } } diff --git a/src/ZB.MOM.WW.ScadaBridge.Security/Auth/DisableLoginGuard.cs b/src/ZB.MOM.WW.ScadaBridge.Security/Auth/DisableLoginGuard.cs new file mode 100644 index 00000000..0c5efc85 --- /dev/null +++ b/src/ZB.MOM.WW.ScadaBridge.Security/Auth/DisableLoginGuard.cs @@ -0,0 +1,25 @@ +namespace ZB.MOM.WW.ScadaBridge.Security.Auth; + +/// +/// Startup guard for the dev/test flag. +/// DisableLogin authenticates EVERY request with ALL roles (including Operator+Verifier, +/// nullifying the two-person secured-write control) on a SCADA control surface, so it is +/// refused outside the Development environment unless the operator sets the explicit +/// second acknowledgement key . +/// Called by the Host composition root before AddSecurity(); fail-fast at boot. +/// +public static class DisableLoginGuard +{ + public static void Validate(bool disableLogin, string environmentName, bool allowOutsideDevelopment) + { + if (!disableLogin) return; + if (string.Equals(environmentName, "Development", StringComparison.OrdinalIgnoreCase)) return; + if (allowOutsideDevelopment) return; + throw new InvalidOperationException( + "ScadaBridge:Security:Auth:DisableLogin=true disables ALL authentication on a SCADA control " + + $"surface and is refused outside the Development environment (current: '{environmentName}'). " + + "Remove the flag, run with ASPNETCORE_ENVIRONMENT=Development, or — only if you fully accept an " + + "unauthenticated deployment — additionally set " + + "ScadaBridge:Security:Auth:AllowOutsideDevelopment=true."); + } +} diff --git a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/DisableLoginGuardTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/DisableLoginGuardTests.cs new file mode 100644 index 00000000..5a8601f0 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/DisableLoginGuardTests.cs @@ -0,0 +1,30 @@ +using ZB.MOM.WW.ScadaBridge.Security.Auth; +using Xunit; + +namespace ZB.MOM.WW.ScadaBridge.Security.Tests; + +public class DisableLoginGuardTests +{ + [Theory] + [InlineData("Production")] + [InlineData("Staging")] + [InlineData("")] + public void Validate_DisableLoginOutsideDevelopment_WithoutAck_Throws(string env) + { + var ex = Assert.Throws( + () => DisableLoginGuard.Validate(disableLogin: true, environmentName: env, allowOutsideDevelopment: false)); + Assert.Contains("DisableLogin", ex.Message); + Assert.Contains("Development", ex.Message); + } + + [Theory] + [InlineData("Development", false)] // dev env: allowed without ack + [InlineData("development", false)] // case-insensitive env name + [InlineData("Production", true)] // explicit second ack key + public void Validate_Allowed_DoesNotThrow(string env, bool ack) + => DisableLoginGuard.Validate(true, env, ack); + + [Fact] + public void Validate_FlagOff_NeverThrows() + => DisableLoginGuard.Validate(false, "Production", false); +}