feat(secrets): mount /admin/secrets UI + secrets authorization (Task 5, G-6)

Mount the shared ZB.MOM.WW.Secrets.Ui RCL page at /admin/secrets on admin nodes:
- Register secrets:manage/secrets:reveal policies additively via
  Configure<AuthorizationOptions>(o => o.AddSecretsAuthorization()) in AddAdminUI
  (the admin-only composition layer that already references the RCL — avoids forcing
  an RCL dependency into the core Security lib; mirrors HistorianGateway)
- Register the RCL routable assembly in BOTH the SSR endpoint (AddAdditionalAssemblies)
  and the interactive Router (App.razor AdditionalAssemblies) or the route 404s
- Add a Secrets nav item; the page's own [Authorize(Policy=...)] gates access

Claim-type MATCH: AdminRole=Administrator reads the same ClaimTypes.Role as FleetAdmin,
so existing Administrators are authorized with no new role mapping. Full clean boot
verified; interactive reveal deferred to the live gate (shared UI already proven).
This commit is contained in:
Joseph Doherty
2026-07-16 17:26:54 -04:00
parent 73d8439412
commit a0be76b5f0
3 changed files with 19 additions and 2 deletions
@@ -19,7 +19,7 @@
<HeadOutlet/>
</head>
<body>
<Routes/>
<Routes AdditionalAssemblies="@(new[] { typeof(ZB.MOM.WW.Secrets.Ui.SecretsPage).Assembly })" />
<script src="_content/ZB.MOM.WW.OtOpcUa.AdminUI/lib/bootstrap/js/bootstrap.bundle.min.js"></script>
<ThemeScripts />
<script src="_content/ZB.MOM.WW.OtOpcUa.AdminUI/js/monaco-init.js"></script>
@@ -20,6 +20,7 @@
<NavRailItem Href="/reservations" Text="Reservations" />
<NavRailItem Href="/certificates" Text="Certificates" />
<NavRailItem Href="/role-grants" Text="Role grants" />
<NavRailItem Href="/admin/secrets" Text="Secrets" />
</NavRailSection>
<NavRailSection Title="Scripting" Key="scripting">
<NavRailItem Href="/scripts" Text="Scripts" />
@@ -1,3 +1,4 @@
using Microsoft.AspNetCore.Authorization;
using Microsoft.AspNetCore.Builder;
using Microsoft.AspNetCore.Components;
using Microsoft.AspNetCore.Components.Web;
@@ -10,6 +11,7 @@ using ZB.MOM.WW.OtOpcUa.Commons.Browsing;
using ZB.MOM.WW.OtOpcUa.Core.Abstractions;
using ZB.MOM.WW.OtOpcUa.Driver.Galaxy.Browser;
using ZB.MOM.WW.OtOpcUa.Driver.OpcUaClient.Browser;
using ZB.MOM.WW.Secrets.Ui;
namespace ZB.MOM.WW.OtOpcUa.AdminUI;
@@ -28,8 +30,13 @@ public static class EndpointRouteBuilderExtensions
// Razor class library static assets (_content/ZB.MOM.WW.OtOpcUa.AdminUI/**) are
// served via the Host's app.UseStaticFiles() middleware which must run BEFORE
// UseAuthentication() — see Program.cs.
// The /admin/secrets management page lives in the external ZB.MOM.WW.Secrets.Ui RCL, so its
// routable component must be discovered at the endpoint layer (AddAdditionalAssemblies) —
// the interactive Router's AdditionalAssemblies (App.razor) alone is not enough for SSR
// endpoint discovery. Both registrations are required or /admin/secrets 404s.
app.MapRazorComponents<TApp>()
.AddInteractiveServerRenderMode();
.AddInteractiveServerRenderMode()
.AddAdditionalAssemblies(typeof(ZB.MOM.WW.Secrets.Ui.SecretsPage).Assembly);
return app;
}
@@ -43,6 +50,15 @@ public static class EndpointRouteBuilderExtensions
services.AddRazorComponents().AddInteractiveServerComponents();
services.AddOtOpcUaDriverStatusServices();
// Secrets-management UI (ZB.MOM.WW.Secrets.Ui RCL, mounted at /admin/secrets). Register its
// "secrets:manage"/"secrets:reveal" authorization policies additively onto the AuthorizationOptions
// that AddOtOpcUaAuth (called just before AddAdminUI on admin nodes) sets up — Configure<T> stacks,
// so this ADDS the two policies without disturbing FleetAdmin/DriverOperator/ConfigEditor. The
// policies' AdminRole = "Administrator" reads the same ClaimTypes.Role claim as FleetAdmin, so an
// existing Administrator satisfies them with no new group→role mapping. Registered here (the AdminUI
// composition layer that already references the RCL) rather than in the core Security lib.
services.Configure<AuthorizationOptions>(o => o.AddSecretsAuthorization());
// Browse pipeline — see docs/plans/2026-05-28-driver-browsers-design.md
services.AddSingleton<Browsing.BrowseSessionRegistry>();
services.AddHostedService<Browsing.BrowseSessionReaper>();