Files
mxaccessgw/src/ZB.MOM.WW.MxGateway.Server/Dashboard/Components/Layout/MainLayout.razor
T
Joseph Doherty 1c30611b1e feat(dashboard): gate the side rail's Secrets link on secrets:manage
Family-wide nav sweep: the Secrets management page should be linked from
each app's UI, visible to Administrator-role users only.

The link already existed in MainLayout's Admin section. The gate did not:
the rail rendered every item for every visitor, including a Viewer and the
anonymous-localhost read-only identity. Not an access hole — the mounted
page carries [Authorize(Policy = "secrets:manage")], so a Viewer clicking
through was denied — but a dead link presented as a live one. There was
also no existing role-gated nav pattern to follow; the rail's only
AuthorizeView was the footer's signed-in/signed-out split.

Gated on the POLICY rather than a role literal, so nav visibility cannot
drift from what the page enforces. In this host the two are equivalent:
GatewayOptionsValidator constrains Dashboard:GroupToRole values to
Administrator or Viewer, so the shared library's other manage-granting
roles (secrets-manager, secrets-reveal) are unreachable. The policy form
stays correct if that ever relaxes, where a role literal would then hide
the link from users who can use the page.

API Keys is deliberately left ungated. It looks like the same case and is
not: ApiKeysPage renders for a Viewer with write affordances hidden, so
hiding its link would remove legitimate read access. The secrets page has
no read-only mode. The rule is "gate the link when the page denies the
role outright", not "gate everything under Admin".

Coverage: three tests pin the policy's verdict per principal
(Administrator admitted, Viewer refused, unauthenticated refused), and
/admin/secrets joins the canonical route list — it is the one nav
destination mounted from an RCL rather than declared here, so a routing
regression could remove it without touching this repo's pages. The
principal helper sets an authentication type deliberately: without one
the role assertions would pass vacuously for the wrong reason.

Not a rendering test — the suite has no component-testing harness, and
adding one to assert a single AuthorizeView would be a large dependency
for a small claim.

Build 0 warnings / 0 errors; suite 895/895.
2026-08-13 09:33:04 -04:00

56 lines
2.8 KiB
Plaintext

@inherits LayoutComponentBase
@using ZB.MOM.WW.Secrets.Ui
@* Thin layout: delegates the side-rail chassis (hamburger, brand, responsive
collapse) to the shared ZB.MOM.WW.Theme <ThemeShell>. The nav is reproduced
with the kit's NavRailSection / NavRailItem; section expand-state persistence
is owned by the kit's <details> + ThemeScripts (no JS interop here). *@
<ThemeShell Product="MXAccess Gateway" Accent="#2f5fd0">
<Nav>
<NavRailItem Href="/" Text="Dashboard" Match="NavLinkMatch.All" />
<NavRailSection Title="Runtime" Key="runtime">
<NavRailItem Href="/sessions" Text="Sessions" />
<NavRailItem Href="/workers" Text="Workers" />
<NavRailItem Href="/events" Text="Events" />
<NavRailItem Href="/alarms" Text="Alarms" />
</NavRailSection>
<NavRailSection Title="Galaxy" Key="galaxy">
<NavRailItem Href="/galaxy" Text="Repository" />
<NavRailItem Href="/browse" Text="Browse" />
</NavRailSection>
<NavRailSection Title="Admin" Key="admin">
<NavRailItem Href="/apikeys" Text="API Keys" />
@* Gated on the SAME policy the mounted /admin/secrets page enforces, not on a role
literal, so nav visibility cannot drift from page access. In this host the two are
equivalent — GatewayOptionsValidator constrains Dashboard:GroupToRole values to
Administrator or Viewer, so the shared library's other manage-granting roles
(secrets-manager, secrets-reveal) are unreachable here — but the policy form stays
correct if that ever relaxes. Deliberately NOT applied to the API Keys item above:
that page renders read-only for Viewers, so hiding its link would remove legitimate
read access, whereas the secrets page denies a Viewer outright and its link would be
a dead end. *@
<AuthorizeView Policy="@SecretsAuthorization.ManagePolicy">
<Authorized>
<NavRailItem Href="/admin/secrets" Text="Secrets" />
</Authorized>
</AuthorizeView>
<NavRailItem Href="/settings" Text="Settings" />
</NavRailSection>
</Nav>
<RailFooter>
<AuthorizeView>
<Authorized Context="authState">
<span class="rail-user">@authState.User.Identity?.Name</span>
<form method="post" action="/logout" data-enhance="false">
<AntiforgeryToken />
<button class="rail-btn" type="submit">Sign Out</button>
</form>
</Authorized>
<NotAuthorized>
<a class="rail-btn" href="/login">Sign In</a>
</NotAuthorized>
</AuthorizeView>
</RailFooter>
<ChildContent>@Body</ChildContent>
</ThemeShell>