From e89604298dafbff844cb0b4ef994b9e23c72ad0e Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Tue, 16 Jun 2026 08:47:19 -0400 Subject: [PATCH] =?UTF-8?q?feat(security):=20wire=20DisableLogin=20flag=20?= =?UTF-8?q?=E2=80=94=20auto-login=20scheme=20+=20startup=20warning?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../2026-06-16-disable-login.md.tasks.json | 4 +-- src/ZB.MOM.WW.ScadaBridge.Host/Program.cs | 10 +++++- .../ServiceCollectionExtensions.cs | 35 +++++++++++++++++-- .../DisableLoginRegistrationTests.cs | 35 +++++++++++++++++++ 4 files changed, 78 insertions(+), 6 deletions(-) create mode 100644 tests/ZB.MOM.WW.ScadaBridge.Security.Tests/DisableLoginRegistrationTests.cs diff --git a/docs/plans/2026-06-16-disable-login.md.tasks.json b/docs/plans/2026-06-16-disable-login.md.tasks.json index 087f4a86..03f87887 100644 --- a/docs/plans/2026-06-16-disable-login.md.tasks.json +++ b/docs/plans/2026-06-16-disable-login.md.tasks.json @@ -4,8 +4,8 @@ "branch": "feature/disable-login", "tasks": [ {"id": 62, "ref": "DL-1", "subject": "AuthDisableLoginOptions + Roles.All", "class": "small", "status": "completed", "commits": ["72691e5"]}, - {"id": 63, "ref": "DL-2", "subject": "AutoLoginAuthenticationHandler + tests", "class": "high-risk", "status": "completed", "blockedBy": [62]}, - {"id": 64, "ref": "DL-3", "subject": "Wire flag into AddSecurity + Host + startup warning", "class": "standard", "status": "pending", "blockedBy": [62, 63]}, + {"id": 63, "ref": "DL-2", "subject": "AutoLoginAuthenticationHandler + tests", "class": "high-risk", "status": "completed", "blockedBy": [62], "commits": ["dcd445a", "0926ce4"]}, + {"id": 64, "ref": "DL-3", "subject": "Wire flag into AddSecurity + Host + startup warning", "class": "standard", "status": "completed", "blockedBy": [62, 63]}, {"id": 65, "ref": "DL-4", "subject": "Docs + dev config note", "class": "trivial", "status": "pending", "blockedBy": [64]} ], "lastUpdated": "2026-06-16" diff --git a/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs b/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs index 022e296a..3f52a85e 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Host/Program.cs @@ -121,7 +121,15 @@ try builder.Services.AddZbLdapAuth( builder.Configuration, ZB.MOM.WW.ScadaBridge.Security.ServiceCollectionExtensions.LdapSectionPath); - builder.Services.AddSecurity(); + // Dev disable-login flag (config-coupled, so read + bound here at the composition root, + // mirroring AddZbLdapAuth). Default false. See AuthDisableLoginOptions / disable-login design doc. + builder.Services.AddOptions() + .Bind(builder.Configuration.GetSection( + ZB.MOM.WW.ScadaBridge.Security.Auth.AuthDisableLoginOptions.SectionName)); + var disableLogin = builder.Configuration + .GetSection(ZB.MOM.WW.ScadaBridge.Security.Auth.AuthDisableLoginOptions.SectionName) + .GetValue(nameof(ZB.MOM.WW.ScadaBridge.Security.Auth.AuthDisableLoginOptions.DisableLogin)); + builder.Services.AddSecurity(disableLogin); builder.Services.AddCentralUI(); builder.Services.AddInboundAPI(); diff --git a/src/ZB.MOM.WW.ScadaBridge.Security/ServiceCollectionExtensions.cs b/src/ZB.MOM.WW.ScadaBridge.Security/ServiceCollectionExtensions.cs index 9230efe9..f9c3022b 100644 --- a/src/ZB.MOM.WW.ScadaBridge.Security/ServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.ScadaBridge.Security/ServiceCollectionExtensions.cs @@ -36,8 +36,15 @@ public static class ServiceCollectionExtensions /// IConfiguration) and component libraries must not accept IConfiguration. /// /// The service collection to register into. + /// + /// Dev/test flag (bound from by the Host + /// composition root). When true the cookie handler is replaced — under the same cookie + /// scheme name — by an always-succeeding + /// that authenticates every request as the configured dev user with ALL roles, and a loud + /// startup warning is emitted. Default false. Never enable in production. + /// /// The same instance for chaining. - public static IServiceCollection AddSecurity(this IServiceCollection services) + public static IServiceCollection AddSecurity(this IServiceCollection services, bool disableLogin = false) { // Task 1.2 cutover: ScadaBridge's bespoke LdapAuthService was replaced by the // shared ZB.MOM.WW.Auth.Ldap implementation (ScadaBridge was the donor for its @@ -104,8 +111,27 @@ public static class ServiceCollectionExtensions // timeout, HTTPS policy) are applied via the SecurityOptions-bound PostConfigure // below (cookie name through SecurityOptions.CookieName, the rest through // ZbCookieDefaults.Apply). - services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) - .AddCookie(options => + var authBuilder = services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme); + if (disableLogin) + { + // DEV/TEST ONLY: replace the cookie handler with an always-succeeding handler registered + // UNDER the cookie scheme name, so every authorization policy (which names this scheme) + // authenticates through it with all roles — zero policy changes. No cookie is written; + // OnValidatePrincipal (idle/refresh) does not apply in this mode. See AuthDisableLoginOptions. + authBuilder.AddScheme( + CookieAuthenticationDefaults.AuthenticationScheme, _ => { }); + + services.AddOptions() + .PostConfigure((opts, lf) => + lf.CreateLogger("ZB.MOM.WW.ScadaBridge.Security").LogWarning( + "AUTH DISABLED (ScadaBridge:Security:Auth:DisableLogin=true) — every request is " + + "authenticated as '{User}' with FULL permissions ({Roles}) across ALL sites. This " + + "is a SCADA control surface; dev/test ONLY — never enable in production.", + opts.User, string.Join(",", Roles.All))); + } + else + { + authBuilder.AddCookie(options => { options.LoginPath = "/login"; options.LogoutPath = "/auth/logout"; @@ -126,6 +152,7 @@ public static class ServiceCollectionExtensions // keeps the session (mirrors "LDAP failure: active sessions continue"). options.Events.OnValidatePrincipal = OnValidatePrincipalAsync; }); + } // CentralUI-005: configure the cookie session as a sliding window so the // code matches the documented policy ("sliding refresh, 30-minute idle @@ -147,6 +174,8 @@ public static class ServiceCollectionExtensions // the shared ZbCookieDefaults.Apply, with requireHttps + idleTimeout driven // by SecurityOptions so behaviour (30-min sliding idle window, HTTPS-only // unless explicitly opted out) is preserved. + // Harmless/unused when disableLogin is true: the cookie handler is not registered, + // so this CookieAuthenticationOptions PostConfigure has no scheme to configure. services.AddOptions(CookieAuthenticationDefaults.AuthenticationScheme) .Configure, ILoggerFactory>((cookieOptions, securityOptions, loggerFactory) => { diff --git a/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/DisableLoginRegistrationTests.cs b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/DisableLoginRegistrationTests.cs new file mode 100644 index 00000000..c84fb094 --- /dev/null +++ b/tests/ZB.MOM.WW.ScadaBridge.Security.Tests/DisableLoginRegistrationTests.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.Extensions.DependencyInjection; +using ZB.MOM.WW.ScadaBridge.Security; +using ZB.MOM.WW.ScadaBridge.Security.Auth; +using Xunit; + +namespace ZB.MOM.WW.ScadaBridge.Security.Tests; + +public class DisableLoginRegistrationTests +{ + private static async Task ResolveCookieSchemeAsync(bool disableLogin) + { + var services = new ServiceCollection(); + services.AddLogging(); + services.AddSecurity(disableLogin); + await using var sp = services.BuildServiceProvider(); + var provider = sp.GetRequiredService(); + return await provider.GetSchemeAsync(CookieAuthenticationDefaults.AuthenticationScheme); + } + + [Fact] + public async Task FlagTrue_RegistersAutoLoginHandlerUnderCookieScheme() + { + var scheme = await ResolveCookieSchemeAsync(disableLogin: true); + Assert.Equal(typeof(AutoLoginAuthenticationHandler), scheme!.HandlerType); + } + + [Fact] + public async Task FlagFalse_RegistersCookieHandler() + { + var scheme = await ResolveCookieSchemeAsync(disableLogin: false); + Assert.Equal(typeof(CookieAuthenticationHandler), scheme!.HandlerType); + } +}