diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs index b87708b..720f0d8 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardServiceCollectionExtensions.cs @@ -2,6 +2,7 @@ using Microsoft.AspNetCore.Authentication; using Microsoft.AspNetCore.Authentication.Cookies; using Microsoft.AspNetCore.Authorization; using Microsoft.Extensions.Configuration; +using Microsoft.Extensions.Logging; using Microsoft.Extensions.Options; using ZB.MOM.WW.Auth.Abstractions.Roles; using ZB.MOM.WW.Auth.AspNetCore; @@ -55,9 +56,36 @@ public static class DashboardServiceCollectionExtensions .AddInteractiveServerComponents(); services.AddSignalR(); - services - .AddAuthentication(DashboardAuthenticationDefaults.AuthenticationScheme) - .AddCookie(DashboardAuthenticationDefaults.AuthenticationScheme, cookieOptions => + // DEV/TEST ONLY. Read directly from configuration here because authentication scheme + // registration runs before options binding. Key mirrors DashboardOptions.DisableLogin. + bool disableLogin = configuration.GetValue("MxGateway:Dashboard:DisableLogin"); + + AuthenticationBuilder authentication = + services.AddAuthentication(DashboardAuthenticationDefaults.AuthenticationScheme); + + if (disableLogin) + { + // Register an always-authenticating handler UNDER the cookie scheme name, so the + // Viewer/Admin/HubClients policies (which all resolve this scheme) authenticate + // through it as the multi-role dev user — zero policy or page changes. + authentication.AddScheme( + DashboardAuthenticationDefaults.AuthenticationScheme, + _ => { }); + + // Loud, once-at-startup warning (emitted when GatewayOptions is first resolved). + services.AddOptions().PostConfigure((gatewayOptions, loggerFactory) => + loggerFactory + .CreateLogger("ZB.MOM.WW.MxGateway.Server.Dashboard.DisableLogin") + .LogWarning( + "DASHBOARD LOGIN DISABLED (MxGateway:Dashboard:DisableLogin=true) — every request is " + + "authenticated as '{User}' with full permissions ({Roles}). Dev/test only; never " + + "enable in production.", + gatewayOptions.Dashboard.AutoLoginUser ?? DashboardAutoLoginAuthenticationHandler.DefaultUser, + $"{DashboardRoles.Admin}, {DashboardRoles.Viewer}")); + } + else + { + authentication.AddCookie(DashboardAuthenticationDefaults.AuthenticationScheme, cookieOptions => { // Hardened defaults (HttpOnly, SameSite=Strict, SecurePolicy, SlidingExpiration, // ExpireTimeSpan) via the shared ZbCookieDefaults.Apply. requireHttps is set to @@ -73,10 +101,12 @@ public static class DashboardServiceCollectionExtensions cookieOptions.LoginPath = "/login"; cookieOptions.LogoutPath = "/logout"; cookieOptions.AccessDeniedPath = "/denied"; - }) - .AddScheme( - DashboardAuthenticationDefaults.HubAuthenticationScheme, - _ => { }); + }); + } + + authentication.AddScheme( + DashboardAuthenticationDefaults.HubAuthenticationScheme, + _ => { }); // Honour DashboardOptions.RequireHttpsCookie (default true / Always; set false for dev // HTTP deployments → SameAsRequest) and the optional per-environment cookie-name diff --git a/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardDisableLoginTests.cs b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardDisableLoginTests.cs new file mode 100644 index 0000000..2bb525a --- /dev/null +++ b/src/ZB.MOM.WW.MxGateway.Tests/Gateway/Dashboard/DashboardDisableLoginTests.cs @@ -0,0 +1,57 @@ +using System.Security.Claims; +using Microsoft.AspNetCore.Authentication; +using Microsoft.AspNetCore.Authentication.Cookies; +using Microsoft.AspNetCore.Authorization; +using Microsoft.AspNetCore.Builder; +using Microsoft.Extensions.DependencyInjection; +using ZB.MOM.WW.MxGateway.Server; +using ZB.MOM.WW.MxGateway.Server.Dashboard; + +namespace ZB.MOM.WW.MxGateway.Tests.Gateway.Dashboard; + +public sealed class DashboardDisableLoginTests +{ + [Fact] + public async Task DisableLoginOff_CookieSchemeUsesCookieHandler() + { + await using WebApplication app = GatewayApplication.Build([]); + IAuthenticationSchemeProvider provider = + app.Services.GetRequiredService(); + + AuthenticationScheme? scheme = await provider.GetSchemeAsync( + DashboardAuthenticationDefaults.AuthenticationScheme); + + Assert.NotNull(scheme); + Assert.Equal(typeof(CookieAuthenticationHandler), scheme!.HandlerType); + } + + [Fact] + public async Task DisableLoginOn_CookieSchemeUsesAutoLoginHandler() + { + await using WebApplication app = GatewayApplication.Build( + ["--MxGateway:Dashboard:DisableLogin=true"]); + IAuthenticationSchemeProvider provider = + app.Services.GetRequiredService(); + + AuthenticationScheme? scheme = await provider.GetSchemeAsync( + DashboardAuthenticationDefaults.AuthenticationScheme); + + Assert.NotNull(scheme); + Assert.Equal(typeof(DashboardAutoLoginAuthenticationHandler), scheme!.HandlerType); + } + + [Fact] + public async Task DisableLoginOn_AutoLoginPrincipalSatisfiesAdminAndViewerPolicies() + { + await using WebApplication app = GatewayApplication.Build( + ["--MxGateway:Dashboard:DisableLogin=true"]); + IAuthorizationService authorization = + app.Services.GetRequiredService(); + ClaimsPrincipal user = DashboardAutoLoginAuthenticationHandler.CreatePrincipal("multi-role"); + + Assert.True((await authorization.AuthorizeAsync( + user, resource: null, DashboardAuthenticationDefaults.AdminPolicy)).Succeeded); + Assert.True((await authorization.AuthorizeAsync( + user, resource: null, DashboardAuthenticationDefaults.ViewerPolicy)).Succeeded); + } +}