feat(security): wire DisableLogin flag — auto-login scheme + startup warning
This commit is contained in:
@@ -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"
|
||||||
|
|||||||
@@ -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);
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user