Skip to content

feat: cluster-scoped RBAC visibility (denied-state UX)#988

Merged
nadaverell merged 8 commits into
mainfrom
feat/cluster-scoped-rbac-visibility
Jun 24, 2026
Merged

feat: cluster-scoped RBAC visibility (denied-state UX)#988
nadaverell merged 8 commits into
mainfrom
feat/cluster-scoped-rbac-visibility

Conversation

@nadaverell

@nadaverell nadaverell commented Jun 23, 2026

Copy link
Copy Markdown
Contributor

What

RBAC-restricted users (notably Cloud viewer/member/owner tiers) get an honest, actionable view of cluster-scoped resources they cannot list — instead of a kind silently rendering "0 / No X found", which looks like an empty cluster.

Changes

  • Backend (resource_counts.go): when RBAC denies a core cluster-scoped kind (Node, PV, StorageClass, …), report it in forbidden with a reasons map distinguishing rbac_denied (your identity can't list it — a grant helps) from unavailable (Radar's own ServiceAccount can't read it — a user grant won't help).
  • RestrictedState (@skyhook-io/k8s-ui): a reusable "you don't have access to X" state that branches on the reason — rbac_denied shows a copyable ClusterRole/ClusterRoleBinding request to hand to a cluster admin; unavailable explains the platform itself lacks the read and omits the snippet. Replaces the bare "Access Restricted" line in the resource list. Fires on the counts forbidden set too, since cluster-scoped denials return 200 + [] rather than 403.
  • "Your access on this cluster" (My Permissions dialog): a "Restricted or unavailable" section listing the kinds being hidden, alongside the existing SelfSubjectRulesReview rules, with a docs link to the unblock RBAC.

Tests

Unit: TestResourceCountsSurfacesDeniedCoreClusterScopedKindAsForbidden; make tsc + go test ./internal/server green.

Visual — local kind cluster, two Radar instances staging the reason codes against a real apiserver (proxy-mode admin SA + a restricted impersonated user → rbac_denied; no-auth as a restricted ServiceAccount → unavailable):

State Verified on screen
rbac_denied list (/resources/nodes, node-denied user) "You don't have access to Node — Radar is hiding what your identity can't read" + expandable, copyable ClusterRole/ClusterRoleBinding request
"Your access on this cluster" dialog SelfSubjectRulesReview grants + "Restricted or unavailable (6)" section (Node, PV, IngressClass, StorageClass, ClusterRole, ClusterRoleBinding) + docs link
unavailable list (Radar SA can't read the kind) "Node isn't available here … granting your own identity access won't surface it" — no snippet (visibly distinct from rbac_denied)
Dark mode Theme tokens hold; snippet block readable, nothing washed out
02-your-access-dialog v2-01-rbac-denied v2-03-unavailable

Related


Note

Medium Risk
Changes RBAC-aware resource-counts and list empty-state behavior for cluster-scoped kinds; mistakes could mislabel access or leak counts, but scope is bounded and covered by new server tests.

Overview
RBAC-restricted users no longer see "0 / No X found" for cluster-scoped kinds they cannot list (e.g. Nodes). /api/resource-counts now adds a reasons map (rbac_denied vs unavailable) and puts denied core cluster-scoped kinds in forbidden when the user fails SAR but Radar’s SA can still read them.

The resource list uses a shared RestrictedState: rbac_denied shows copyable ClusterRole/ClusterRoleBinding YAML; unavailable explains Radar cannot read the kind and omits the snippet. Detection also uses counts forbidden, not only list 403, since cluster-scoped denials can return 200 + []. Actual rows override a stale forbidden entry.

Your access on this cluster (My Permissions) lists hidden kinds from forbidden with docs link. TestResourceCountsSurfacesDeniedCoreClusterScopedKindAsForbidden covers the Node case.

Reviewed by Cursor Bugbot for commit 98f090a. Bugbot is set up for automated code reviews on this repo. Configure here.

Comment thread packages/k8s-ui/src/components/resources/ResourcesView.tsx
@nadaverell nadaverell force-pushed the feat/cluster-scoped-rbac-visibility branch from 1e0e55f to c220992 Compare June 23, 2026 23:18
When a user's RBAC denies listing a core cluster-scoped kind (Node, PV,
StorageClass, Namespace, ...), resource-counts silently omitted it, so the UI
rendered '0 / No Node found' — indistinguishable from an empty cluster. Surface
these in the existing forbidden set so the frontend's existing 'Access Restricted'
state renders instead. CRDs stay omitted (could be not-installed vs denied).
The forbidden empty state was a bare 'Access Restricted / Insufficient permissions'.
Replace with a reusable RestrictedState component: plain-language framing for a
non-k8s operator (states it's RBAC, not an empty cluster), and a 'How to get access'
disclosure with a generic ClusterRole/ClusterRoleBinding snippet + 'Copy request'
button to forward to whoever administers the cluster. Honest about cause (never
claims tier vs custom-role) and chart-version-agnostic (generic YAML).
Bugbot triage on the forbidden-state component:
- Expanded RBAC snippet could clip on short panes (absolute inset-0 centered,
  no scroll). Wrap in overflow-y-auto + min-h-full so it centers when collapsed
  and scrolls (no top clip) when expanded; cap the snippet at max-h-72.
- selectedKind.name is the API plural in normal use but a CRD deep-link can
  transiently carry the Kind (HTTPRoute) before discovery resolves it, yielding
  a snippet that wouldn't grant access. Only inline the resource when it looks
  like a valid lowercase resource name; otherwise use a <resource> placeholder
  and a generic role name.
Reframe the My Permissions dialog as 'Your access on this cluster' and add a
'Restricted resources' section: the kinds your RBAC can't list (from the
resource-counts forbidden set) shown as chips, with honest framing (an access
boundary, not an empty cluster) and a 'How to get access' link to the RBAC docs.
This is the comprehension anchor for the denied-vs-empty work — what you CAN do
(SelfSubjectRulesReview rules) alongside what's hidden and how to unblock.
…list 403

The main-panel restricted state keyed only off the list query returning 403, but
handleListResources returns 200 with [] for denied cluster-scoped kinds — so a
denied Node/PV fell through to 'No <kind> found' instead of 'Access Restricted',
the exact symptom this change set out to fix. Also trigger isSelectedForbidden
when the selected kind is in the counts forbidden set (which the backend now
populates for denied cluster-scoped kinds). Bugbot caught this.
resource-counts now tags each forbidden kind with a reason: 'rbac_denied' (Radar's
ServiceAccount can read the kind but the user's own RBAC can't — a user grant
surfaces it) vs 'unavailable' (no informer at all: not installed, SA lacks RBAC,
or feature off like rbac.viewRBAC — a user grant won't help).

RestrictedState branches on it: rbac_denied keeps the 'grant your identity' request
+ ClusterRole snippet; unavailable explains that Radar's ServiceAccount can't read
the kind and omits the (useless) user-grant snippet. Fixes the overclaim where
feature-disabled kinds (e.g. viewRBAC-off ClusterRoles) were told 'your RBAC
doesn't allow' with a snippet that wouldn't surface them.
@nadaverell nadaverell force-pushed the feat/cluster-scoped-rbac-visibility branch from 0d220c9 to fc7185c Compare June 24, 2026 10:20

@cursor cursor Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Reviewed by Cursor Bugbot for commit fc7185c. Configure here.

Comment thread packages/k8s-ui/src/components/resources/ResourcesView.tsx Outdated
… pluralized kind

The grant snippet dropped the natural-language preamble (just the
ClusterRole/ClusterRoleBinding YAML now), the Copy button moved into a
header bar so it no longer overlaps the first code line, and the denied
copy reads "<Kind> resources" to avoid awkward singular phrasing.
Guards the counts-driven forbidden gate against a startup timing skew
where a kind is marked forbidden/unavailable in resource-counts before
its informer syncs, while the list query has since returned data. Actual
rows now win; a list 403 still forces the restricted state.
@nadaverell nadaverell merged commit 066c215 into main Jun 24, 2026
9 checks passed
@nadaverell nadaverell deleted the feat/cluster-scoped-rbac-visibility branch June 24, 2026 12:43
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant