feat(security): wire DisableLogin flag — auto-login scheme + startup warning

This commit is contained in:
Joseph Doherty
2026-06-16 08:47:19 -04:00
parent 0926ce4dda
commit e89604298d
4 changed files with 78 additions and 6 deletions
@@ -4,8 +4,8 @@
"branch": "feature/disable-login", "branch": "feature/disable-login",
"tasks": [ "tasks": [
{"id": 62, "ref": "DL-1", "subject": "AuthDisableLoginOptions + Roles.All", "class": "small", "status": "completed", "commits": ["72691e5"]}, {"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": 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": "pending", "blockedBy": [62, 63]}, {"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]} {"id": 65, "ref": "DL-4", "subject": "Docs + dev config note", "class": "trivial", "status": "pending", "blockedBy": [64]}
], ],
"lastUpdated": "2026-06-16" "lastUpdated": "2026-06-16"
+9 -1
View File
@@ -121,7 +121,15 @@ try
builder.Services.AddZbLdapAuth( builder.Services.AddZbLdapAuth(
builder.Configuration, builder.Configuration,
ZB.MOM.WW.ScadaBridge.Security.ServiceCollectionExtensions.LdapSectionPath); 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<ZB.MOM.WW.ScadaBridge.Security.Auth.AuthDisableLoginOptions>()
.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<bool>(nameof(ZB.MOM.WW.ScadaBridge.Security.Auth.AuthDisableLoginOptions.DisableLogin));
builder.Services.AddSecurity(disableLogin);
builder.Services.AddCentralUI(); builder.Services.AddCentralUI();
builder.Services.AddInboundAPI(); builder.Services.AddInboundAPI();
@@ -36,8 +36,15 @@ public static class ServiceCollectionExtensions
/// <c>IConfiguration</c>) and component libraries must not accept <c>IConfiguration</c>. /// <c>IConfiguration</c>) and component libraries must not accept <c>IConfiguration</c>.
/// </remarks> /// </remarks>
/// <param name="services">The service collection to register into.</param> /// <param name="services">The service collection to register into.</param>
/// <param name="disableLogin">
/// Dev/test flag (bound from <see cref="Auth.AuthDisableLoginOptions"/> by the Host
/// composition root). When true the cookie handler is replaced — under the same cookie
/// scheme name — by an always-succeeding <see cref="Auth.AutoLoginAuthenticationHandler"/>
/// 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.
/// </param>
/// <returns>The same <paramref name="services"/> instance for chaining.</returns> /// <returns>The same <paramref name="services"/> instance for chaining.</returns>
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 // 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 // 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 // timeout, HTTPS policy) are applied via the SecurityOptions-bound PostConfigure
// below (cookie name through SecurityOptions.CookieName, the rest through // below (cookie name through SecurityOptions.CookieName, the rest through
// ZbCookieDefaults.Apply). // ZbCookieDefaults.Apply).
services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme) var authBuilder = services.AddAuthentication(CookieAuthenticationDefaults.AuthenticationScheme);
.AddCookie(options => 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<AuthenticationSchemeOptions, Auth.AutoLoginAuthenticationHandler>(
CookieAuthenticationDefaults.AuthenticationScheme, _ => { });
services.AddOptions<Auth.AuthDisableLoginOptions>()
.PostConfigure<ILoggerFactory>((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.LoginPath = "/login";
options.LogoutPath = "/auth/logout"; options.LogoutPath = "/auth/logout";
@@ -126,6 +152,7 @@ public static class ServiceCollectionExtensions
// keeps the session (mirrors "LDAP failure: active sessions continue"). // keeps the session (mirrors "LDAP failure: active sessions continue").
options.Events.OnValidatePrincipal = OnValidatePrincipalAsync; options.Events.OnValidatePrincipal = OnValidatePrincipalAsync;
}); });
}
// CentralUI-005: configure the cookie session as a sliding window so the // CentralUI-005: configure the cookie session as a sliding window so the
// code matches the documented policy ("sliding refresh, 30-minute idle // 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 // the shared ZbCookieDefaults.Apply, with requireHttps + idleTimeout driven
// by SecurityOptions so behaviour (30-min sliding idle window, HTTPS-only // by SecurityOptions so behaviour (30-min sliding idle window, HTTPS-only
// unless explicitly opted out) is preserved. // 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<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme) services.AddOptions<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
.Configure<IOptions<SecurityOptions>, ILoggerFactory>((cookieOptions, securityOptions, loggerFactory) => .Configure<IOptions<SecurityOptions>, ILoggerFactory>((cookieOptions, securityOptions, loggerFactory) =>
{ {
@@ -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<AuthenticationScheme?> ResolveCookieSchemeAsync(bool disableLogin)
{
var services = new ServiceCollection();
services.AddLogging();
services.AddSecurity(disableLogin);
await using var sp = services.BuildServiceProvider();
var provider = sp.GetRequiredService<IAuthenticationSchemeProvider>();
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);
}
}