From 22a878c36e82bbd1c8a97569b19a6c069b69dde6 Mon Sep 17 00:00:00 2001 From: Joseph Doherty Date: Thu, 16 Jul 2026 11:02:11 -0400 Subject: [PATCH] feat(secrets): mount /admin/secrets + AddSecretsAuthorization in the dashboard (G-6) --- .../Components/Layout/MainLayout.razor | 1 + .../Dashboard/Components/Routes.razor | 3 +- ...DashboardEndpointRouteBuilderExtensions.cs | 1 + .../GatewayApplication.cs | 6 ++ .../Gateway/DashboardSecretsMountTests.cs | 87 +++++++++++++++++++ 5 files changed, 97 insertions(+), 1 deletion(-) create mode 100644 src/ZB.MOM.WW.MxGateway.Tests/Gateway/DashboardSecretsMountTests.cs diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Layout/MainLayout.razor b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Layout/MainLayout.razor index 3d365f8..a6693d7 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Layout/MainLayout.razor +++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Layout/MainLayout.razor @@ -19,6 +19,7 @@ + diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Routes.razor b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Routes.razor index d68962b..c529653 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Routes.razor +++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Routes.razor @@ -1,4 +1,5 @@ - + diff --git a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardEndpointRouteBuilderExtensions.cs b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardEndpointRouteBuilderExtensions.cs index ee9f91c..e273726 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardEndpointRouteBuilderExtensions.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/Dashboard/DashboardEndpointRouteBuilderExtensions.cs @@ -132,6 +132,7 @@ public static class DashboardEndpointRouteBuilderExtensions // cookie scheme and redirected to /login. endpoints.MapRazorComponents() .AddInteractiveServerRenderMode() + .AddAdditionalAssemblies(typeof(ZB.MOM.WW.Secrets.Ui.SecretsPage).Assembly) .RequireAuthorization(DashboardAuthenticationDefaults.ViewerPolicy); return endpoints; diff --git a/src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs b/src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs index 33d6666..93021d9 100644 --- a/src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs +++ b/src/ZB.MOM.WW.MxGateway.Server/GatewayApplication.cs @@ -1,4 +1,5 @@ using System.Security.Cryptography.X509Certificates; +using Microsoft.AspNetCore.Authorization; using Microsoft.AspNetCore.Hosting.StaticWebAssets; using Microsoft.Extensions.Logging; using Microsoft.Extensions.Logging.Configuration; @@ -19,6 +20,7 @@ using ZB.MOM.WW.Secrets.Abstractions; using ZB.MOM.WW.Secrets.Configuration; using ZB.MOM.WW.Secrets.DependencyInjection; using ZB.MOM.WW.Secrets.Sqlite; +using ZB.MOM.WW.Secrets.Ui; using ZB.MOM.WW.Telemetry; using ZB.MOM.WW.Telemetry.Serilog; @@ -121,6 +123,10 @@ public static class GatewayApplication builder.Services.AddGatewaySessions(); builder.Services.AddGatewayAlarms(); builder.Services.AddGatewayDashboard(builder.Configuration); + // Register the shared Secrets UI authorization policies (secrets:manage + secrets:reveal) + // additively so they compose with the dashboard's existing AddAuthorization block. The + // mounted /admin/secrets Blazor page carries [Authorize(Policy = "secrets:manage")]. + builder.Services.Configure(o => o.AddSecretsAuthorization()); // Register the gateway's browse-scope provider before AddZbGalaxyRepository so the // library's TryAddSingleton default (NullGalaxyBrowseScopeProvider) does not win. builder.Services.AddSingleton +/// Verifies the shared Secrets UI is wired into the dashboard: the +/// AddSecretsAuthorization policies are registered so the mounted +/// /admin/secrets Blazor page ([Authorize(Policy = "secrets:manage")]) and its +/// reveal action resolve, AND that the dashboard's real Administrator principal shape actually +/// satisfies them (the claim-type-compatibility risk class that bit HistorianGateway). The +/// browser reveal/redirect behaviour is verified separately on the box. +/// +public sealed class DashboardSecretsMountTests +{ + private const string ManagePolicy = "secrets:manage"; + private const string RevealPolicy = "secrets:reveal"; + + /// + /// Verifies that Build registers the shared Secrets authorization policies + /// (secrets:manage + secrets:reveal) added additively via + /// Configure<AuthorizationOptions>(o => o.AddSecretsAuthorization()). + /// + /// A task that represents the asynchronous operation. + [Fact] + public async Task Build_RegistersSecretsAuthorizationPolicies() + { + await using WebApplication app = GatewayApplication.Build([]); + + IAuthorizationPolicyProvider policyProvider = + app.Services.GetRequiredService(); + + AuthorizationPolicy? managePolicy = await policyProvider.GetPolicyAsync(ManagePolicy); + AuthorizationPolicy? revealPolicy = await policyProvider.GetPolicyAsync(RevealPolicy); + + Assert.NotNull(managePolicy); + Assert.NotNull(revealPolicy); + } + + /// + /// Verifies that the dashboard's Administrator principal — constructed exactly the way + /// mints it + /// (role claim on ZbClaimTypes.Role, carrying Administrator, authenticated) — + /// satisfies BOTH secrets policies. This locks claim-type compatibility: if roles were ever + /// issued on a divergent claim type, RequireRole("Administrator") would no longer match + /// and this test would fail. + /// + /// A task that represents the asynchronous operation. + [Fact] + public async Task AdministratorPrincipal_SatisfiesSecretsPolicies() + { + await using WebApplication app = GatewayApplication.Build([]); + IAuthorizationService authorization = + app.Services.GetRequiredService(); + ClaimsPrincipal administrator = DashboardAutoLoginAuthenticationHandler.CreatePrincipal("multi-role"); + + AuthorizationResult manage = await authorization.AuthorizeAsync(administrator, resource: null, ManagePolicy); + AuthorizationResult reveal = await authorization.AuthorizeAsync(administrator, resource: null, RevealPolicy); + + Assert.True(manage.Succeeded); + Assert.True(reveal.Succeeded); + } + + /// + /// Verifies that an anonymous, role-less principal is DENIED both secrets policies, proving the + /// policies actually gate on the Administrator role rather than admitting everyone. + /// + /// A task that represents the asynchronous operation. + [Fact] + public async Task AnonymousPrincipal_IsDeniedSecretsPolicies() + { + await using WebApplication app = GatewayApplication.Build([]); + IAuthorizationService authorization = + app.Services.GetRequiredService(); + ClaimsPrincipal anonymous = new(new ClaimsIdentity()); + + AuthorizationResult manage = await authorization.AuthorizeAsync(anonymous, resource: null, ManagePolicy); + AuthorizationResult reveal = await authorization.AuthorizeAsync(anonymous, resource: null, RevealPolicy); + + Assert.False(manage.Succeeded); + Assert.False(reveal.Succeeded); + } +}