feat(security): wire DisableLogin flag — auto-login scheme + startup warning
This commit is contained in:
@@ -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"
|
||||
|
||||
@@ -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<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.AddInboundAPI();
|
||||
|
||||
|
||||
@@ -36,8 +36,15 @@ public static class ServiceCollectionExtensions
|
||||
/// <c>IConfiguration</c>) and component libraries must not accept <c>IConfiguration</c>.
|
||||
/// </remarks>
|
||||
/// <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>
|
||||
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<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.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<CookieAuthenticationOptions>(CookieAuthenticationDefaults.AuthenticationScheme)
|
||||
.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);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user