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.
This commit is contained in:
@@ -350,3 +350,40 @@ margins). Bumped for family-pin alignment.
|
||||
**Verification.** Build 0 warnings / 0 errors; suite **879/879**; `staticwebassets.build.json`
|
||||
resolves `zb.mom.ww.theme/0.4.1`. No stale-HTTP-cache clear was needed — restore picked 0.4.1
|
||||
directly.
|
||||
|
||||
## 8. Follow-up: role-gate the side rail's Secrets link (family-wide nav task)
|
||||
|
||||
Requested as a family-wide sweep: every app's UI should link to the Secrets management page, visible
|
||||
to Administrator-role users only.
|
||||
|
||||
**Found state.** The link already existed — `MainLayout.razor`, Admin section, `/admin/secrets`. What
|
||||
did not exist was any gate: the rail rendered every item for every visitor, including a Viewer and
|
||||
the anonymous-localhost read-only identity. The premise that there was an "existing role-gated nav
|
||||
pattern" to follow was false; the rail's only `AuthorizeView` was the footer's signed-in/signed-out
|
||||
split, so this introduces the pattern rather than extending it.
|
||||
|
||||
Not an access hole — the mounted page carries `[Authorize(Policy = "secrets:manage")]`, so a Viewer
|
||||
clicking through was denied. It was a dead link presented as a live one.
|
||||
|
||||
**Gate chosen: the policy, not the role.** `<AuthorizeView Policy="@SecretsAuthorization.ManagePolicy">`,
|
||||
i.e. the same policy the page itself enforces, so nav visibility cannot drift from page access. The
|
||||
sweep asked for a role literal (`DashboardRoles.Admin` = `"Administrator"`), and 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. The policy form was preferred because it stays correct if
|
||||
that constraint ever relaxes — a role literal would then hide the link from users who can use the
|
||||
page.
|
||||
|
||||
**Deliberate asymmetry — API Keys stays ungated.** Its sibling item looks like the same case and is
|
||||
not. `ApiKeysPage` renders for a Viewer with write affordances hidden (`@if (CanManageApiKeys)`), so
|
||||
hiding its nav item 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) in `SecretsNavGateTests`, and `/admin/secrets` joins the canonical
|
||||
route list in `GatewayApplicationTests` — 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.
|
||||
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.
|
||||
|
||||
**Verification.** Build 0 warnings / 0 errors; suite **895/895** (892 + 3).
|
||||
|
||||
Reference in New Issue
Block a user