feat(adminui): gate the Secrets rail link on the page's own policy
v2-ci / build (push) Successful in 4m44s
v2-ci / unit-tests (push) Failing after 17m18s

The /admin/secrets link was already in the side rail, but ungated — every
authenticated user saw it, and a Designer/Operator clicking it was bounced by
the page's own [Authorize(Policy = SecretsAuthorization.ManagePolicy)]. Hide
the advertised dead end.

Gated on that same policy constant rather than Roles="Administrator". The two
are equivalent today (the RCL's AdminRole is "Administrator", read off the same
ClaimTypes.Role claim FleetAdmin uses), so this is not about reach — it is that
the rail cannot drift if the RCL ever changes its policy.

Live-gated on docker-dev, both directions, because the positive half alone
proves nothing here: the link rendered for an admin BEFORE this change too, so
"admin still sees it" is satisfied equally by a working gate and by an inert
one. Negative control — gate swapped to Roles="NoSuchRoleFalsifyProbe", rebuilt,
item went to 0 while /role-grants and /apikeys stayed at 1 (the item is hidden,
not the rail broken). Restored build hashed to the same image ID as the
pre-falsification one, proving the restore was byte-exact. This also rules out
AuthorizeView being inert inside ThemeShell's <Nav> slot under this host's
static router, which would have silently hidden the link from everyone.

SecretsNavLinkTests covers it in the AdminUI's actual idiom — source scan +
metadata cross-check, NOT bUnit (the bunit PackageVersion pin has zero
PackageReferences repo-wide). The load-bearing assertion is index ordering
(AuthorizeView open < NavRailItem < close): a scan that merely finds both tags
in the file passes when they are not nested, which is the ungated state.

Scope: Secrets only. /role-grants and /apikeys are FleetAdmin (Administrator-
only) too and stay visible to everyone, so the rail is now inconsistent —
deliberate, as hiding those is a UX call for the whole rail rather than
something to smuggle into this change. Noted in the markup.

Family-wide task from the scadaproj index session (ScadaBridge ee193cd2,
ignitionoee 3627bb8, HistorianGateway 58b6418, mxgw 87d575d).
This commit is contained in:
Joseph Doherty
2026-08-13 09:59:31 -04:00
parent 08e33fc7ec
commit 259d9621ba
2 changed files with 120 additions and 1 deletions
@@ -25,7 +25,19 @@
<NavRailItem Href="/certificates" Text="Certificates" />
<NavRailItem Href="/role-grants" Text="Role grants" />
<NavRailItem Href="/apikeys" Text="API keys" />
<NavRailItem Href="/admin/secrets" Text="Secrets" />
@* Gated on the page's OWN policy constant, not Roles="Administrator", so the rail
can never drift from what the page actually admits (pinned by SecretsNavLinkTests).
ManagePolicy resolves to AdminRole "Administrator" over the same ClaimTypes.Role
claim FleetAdmin reads.
NOTE this is currently the ONLY gated item, and the rail is therefore inconsistent:
/role-grants and /apikeys are FleetAdmin (Administrator-only) too and stay visible
to everyone, i.e. still advertised dead ends for a Designer/Operator. That is a
deliberate scope boundary, not an oversight — hiding them is a UX call for the
whole rail, filed as a follow-up rather than smuggled in here. *@
<AuthorizeView Policy="@ZB.MOM.WW.Secrets.Ui.SecretsAuthorization.ManagePolicy">
<NavRailItem Href="/admin/secrets" Text="Secrets" />
</AuthorizeView>
</NavRailSection>
<NavRailSection Title="Scripting" Key="scripting">
<NavRailItem Href="/scripts" Text="Scripts" />