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);
+ }
+}