Skip to content

🔒 fix: route RBAC introspection through kc-agent (#7993 Phase 6)#8185

Merged
clubanderson merged 1 commit intomainfrom
fix/phase6-7993-tail
Apr 15, 2026
Merged

🔒 fix: route RBAC introspection through kc-agent (#7993 Phase 6)#8185
clubanderson merged 1 commit intomainfrom
fix/phase6-7993-tail

Conversation

@clubanderson
Copy link
Copy Markdown
Collaborator

Summary

Final phase of #7993. Move the last three user-facing RBAC introspection paths off the backend pod ServiceAccount and onto kc-agent so SelfSubjectAccessReviews answer can the user do X? rather than can the pod SA do X? when console is deployed in-cluster.

After this PR, no pkg/api/handlers/ code reaches the shared pkg/k8s.MultiClusterClient on behalf of a user — every such call goes through kc-agent under the user's kubeconfig, which is the load-bearing rule from #7993.

Why this only mattered in-cluster

  • Localhost: backend's k8sClient is loaded from ~/.kube/config, so SSAR already runs as the user. The handlers were correct.
  • In-cluster: backend's k8sClient falls through to rest.InClusterConfig(), so SSAR runs as the pod SA. The answer reflects the pod SA's permissions, not the user's. Wrong answer.

The wrongness was cosmetic, not a privilege escalation — actual destructive actions already route through kc-agent after Phases 1–4, so the apiserver enforces against the user's real identity. But a permission-gated button saying "allowed" when the user can't actually do the action is a confusing UX, and it was the last vestige of pod-SA k8s access in user-facing code.

Changes

Backend (deletes)

  • pkg/api/handlers/rbac.go: delete CheckCanI, GetClusterPermissions, GetPermissionsSummary handlers
  • pkg/api/server.go: drop the three route registrations (/rbac/can-i, /rbac/permissions, /permissions/summary)

kc-agent (new)

  • pkg/agent/server_rbac.go: new handleCanIHTTP, handleClusterPermissionsHTTP, handlePermissionsSummaryHTTP handlers that delegate to the existing MultiClusterClient methods. kc-agent's s.k8sClient is loaded from the user's kubeconfig at startup, so calling these methods is already user-scoped — no logic duplication.
  • pkg/agent/server.go: register the three new routes.

Frontend

  • web/src/hooks/usePermissions.ts: useCanI and usePermissions now call ${LOCAL_AGENT_HTTP_URL}/rbac/can-i and ${LOCAL_AGENT_HTTP_URL}/permissions/summary. Per-request timeout extracted to a named constant.
  • web/src/hooks/useUsers.ts: useClusterPermissions rewritten to fetch ${LOCAL_AGENT_HTTP_URL}/rbac/permissions directly (instead of via api.get) and bail in demo mode via isBackendUnavailable().
  • Tests updated to spy on globalThis.fetch and assert the new URLs.
  • web/src/mocks/handlers.ts: legacy /api/permissions/summary MSW stub kept as a no-op (frontend hooks short-circuit in demo mode anyway).

Architectural notes

The shared pkg/k8s.MultiClusterClient methods (CheckCanI, CheckClusterAdminAccess, GetClusterPermissions, GetAllClusterPermissions, GetPermissionsSummary, GetAllPermissionsSummaries) stay — kc-agent uses them. The privileged-client lint guard checks for mutation patterns (Create|Update|Delete|Patch) in pkg/api/handlers/, so calling these read-ish methods from kc-agent doesn't trip it.

Test plan

  • go build ./... — clean
  • go test ./pkg/agent/... ./pkg/k8s/... — pass
  • go test ./pkg/api/handlers/ -run "TestRBAC|TestCheckCanI|TestGetPermissions|TestGetClusterPermissions" — pass (the broader handlers test suite has the pre-existing TestRecordFocus_BadBody_Returns400 mock failure unrelated to this PR)
  • Privileged Client Lint local check — only allowlist hits
  • npm run build — pass with all post-build safety checks green
  • Build (linux/amd64), Build (linux/arm64), CodeQL Go, CodeQL JS/TS, pr-check, ts-null-safety, fullstack-smoke, dco, Privileged Client Lint
  • Verify in-cluster SSAR answers reflect the user (manual, post-merge)

Closes

Fixes #7993

Move CheckCanI / GetClusterPermissions / GetPermissionsSummary from the
backend pod ServiceAccount to kc-agent so SelfSubjectAccessReviews answer
"can the *user* do X?" instead of "can the *pod SA* do X?" when console
is deployed in-cluster.

This is the last remaining reach into the pod SA from a user-facing
read path. After this, no pkg/api/handlers/ code calls the shared
pkg/k8s.MultiClusterClient on behalf of a user — every such call goes
through kc-agent under the user's kubeconfig.

- pkg/agent/server_rbac.go: new /rbac/can-i, /rbac/permissions,
  /permissions/summary handlers that delegate to the existing
  MultiClusterClient methods (already user-scoped on kc-agent).
- pkg/api/handlers/rbac.go: delete CheckCanI / GetClusterPermissions /
  GetPermissionsSummary backend handlers.
- pkg/api/server.go: drop the three route registrations.
- web/src/hooks/usePermissions.ts: useCanI + usePermissions now POST/GET
  ${LOCAL_AGENT_HTTP_URL}/rbac/can-i and /permissions/summary.
- web/src/hooks/useUsers.ts: useClusterPermissions now GETs
  ${LOCAL_AGENT_HTTP_URL}/rbac/permissions and bails in demo mode via
  isBackendUnavailable().
- Tests updated to match the new fetch URLs.

Fixes #7993

Signed-off-by: Andrew Anderson <[email protected]>
Copilot AI review requested due to automatic review settings April 15, 2026 17:46
@kubestellar-prow kubestellar-prow Bot added the dco-signoff: yes Indicates the PR's author has signed the DCO. label Apr 15, 2026
@kubestellar-prow
Copy link
Copy Markdown
Contributor

[APPROVALNOTIFIER] This PR is NOT APPROVED

This pull-request has been approved by:
Once this PR has been reviewed and has the lgtm label, please assign clubanderson for approval. For more information see the Code Review Process.

The full list of commands accepted by this bot can be found here.

Details Needs approval from an approver in each of these files:

Approvers can indicate their approval by writing /approve in a comment
Approvers can cancel approval by writing /approve cancel in a comment

@netlify
Copy link
Copy Markdown

netlify Bot commented Apr 15, 2026

Deploy Preview for kubestellarconsole ready!

Name Link
🔨 Latest commit 6dce8a5
🔍 Latest deploy log https://app.netlify.com/projects/kubestellarconsole/deploys/69dfcede31d13a0008cff92b
😎 Deploy Preview https://deploy-preview-8185.console-deploy-preview.kubestellar.io
📱 Preview on mobile
Toggle QR Code...

QR Code

Use your smartphone camera to open QR code link.

To edit notification comments on pull requests, go to your Netlify project configuration.

@github-actions
Copy link
Copy Markdown
Contributor

👋 Hey @clubanderson — thanks for opening this PR!

🤖 This project is developed exclusively using AI coding assistants.

Please do not attempt to code anything for this project manually.
All contributions should be authored using an AI coding tool such as:

This ensures consistency in code style, architecture patterns, test coverage,
and commit quality across the entire codebase.


This is an automated message.

@kubestellar-prow kubestellar-prow Bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Apr 15, 2026
@clubanderson clubanderson merged commit 3a9458a into main Apr 15, 2026
34 of 35 checks passed
@kubestellar-prow kubestellar-prow Bot deleted the fix/phase6-7993-tail branch April 15, 2026 17:51
@github-actions
Copy link
Copy Markdown
Contributor

Thank you for your contribution! Your PR has been merged.

Check out what's new:

Stay connected: Slack #kubestellar-dev | Multi-Cluster Survey

Copy link
Copy Markdown
Contributor

Copilot AI left a comment

Choose a reason for hiding this comment

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

Pull request overview

Moves the remaining RBAC “introspection” endpoints (SSAR + permissions summaries) off the backend (which can run as the in-cluster pod ServiceAccount) and onto kc-agent (which runs under the user’s kubeconfig), and updates frontend hooks/tests to call the new local-agent routes.

Changes:

  • Removed backend /api/rbac/permissions, /api/permissions/summary, and /api/rbac/can-i routes/handlers.
  • Added kc-agent HTTP handlers and route registrations for /rbac/permissions, /permissions/summary, and /rbac/can-i.
  • Updated frontend hooks and tests to fetch these RBAC endpoints from LOCAL_AGENT_HTTP_URL.

Reviewed changes

Copilot reviewed 9 out of 9 changed files in this pull request and generated 3 comments.

Show a summary per file
File Description
web/src/mocks/handlers.ts Keeps a legacy MSW stub and documents Phase 6 routing change.
web/src/hooks/useUsers.ts Switches useClusterPermissions to fetch from kc-agent.
web/src/hooks/usePermissions.ts Switches permissions summary + SSAR (useCanI) to kc-agent; introduces a named timeout constant.
web/src/hooks/tests/useUsers.test.ts Updates tests to spy on fetch for the new kc-agent URL.
web/src/hooks/tests/usePermissions.test.ts Updates URL assertions for permissions summary + can-i to the new kc-agent paths.
pkg/api/server.go Removes backend route registrations for the migrated RBAC endpoints.
pkg/api/handlers/rbac.go Deletes the three backend RBAC introspection handlers and leaves notes pointing to kc-agent.
pkg/agent/server_rbac.go Adds new kc-agent RBAC/permissions handlers delegating to MultiClusterClient.
pkg/agent/server.go Registers the new kc-agent RBAC/permissions routes.

Comment thread pkg/agent/server_rbac.go
Comment on lines +38 to +55
func (s *Server) handleCanIHTTP(w http.ResponseWriter, r *http.Request) {
s.setCORSHeaders(w, r)
w.Header().Set("Content-Type", "application/json")
if r.Method == http.MethodOptions {
w.WriteHeader(http.StatusOK)
return
}
if !s.validateToken(r) {
http.Error(w, "Unauthorized", http.StatusUnauthorized)
return
}
if r.Method != http.MethodPost {
writeJSONError(w, http.StatusMethodNotAllowed, "method not allowed")
return
}
if s.k8sClient == nil {
writeJSONError(w, http.StatusServiceUnavailable, "k8s client not initialized")
return
Comment thread web/src/hooks/useUsers.ts
Comment on lines +702 to +706
const token = localStorage.getItem(STORAGE_KEY_TOKEN)
const response = await fetch(`${LOCAL_AGENT_HTTP_URL}/rbac/permissions${params}`, {
headers: {
'Authorization': token ? `Bearer ${token}` : '',
'Content-Type': 'application/json' },
Comment thread pkg/agent/server_rbac.go
// permission checks (SelfSubjectAccessReview + permission summaries) must
// run under the user's kubeconfig, not the backend pod ServiceAccount.
// Otherwise the pod SA's permissions answer the question, not the caller's,
// which is both wrong and a privilege-escalation vector.
@github-actions
Copy link
Copy Markdown
Contributor

Post-merge build verification passed

Both Go and frontend builds compiled successfully against merge commit 3a9458a1d0dcd8e3b8fd4fcf180d2c55beca1d1d.

@github-actions
Copy link
Copy Markdown
Contributor

✅ Post-Merge Verification: passed

Commit: 3a9458a1d0dcd8e3b8fd4fcf180d2c55beca1d1d
Specs run: smoke.spec.ts
Report: https://github.com/kubestellar/console/actions/runs/24469625624

clubanderson added a commit that referenced this pull request Apr 15, 2026
…header comment (#8188)

Addresses three Copilot review comments on merged #8185:

1. handleCanIHTTP is a POST endpoint but setCORSHeaders was defaulting
   Access-Control-Allow-Methods to "GET, OPTIONS", so browser preflight
   requests for POST would fail. Generalized setCORSHeaders with a
   variadic methods parameter — back-compat preserved because callers
   that pass nothing still get defaultCORSAllowedMethods ("GET, OPTIONS").
   handleCanIHTTP now passes http.MethodPost + http.MethodOptions.

2. useClusterPermissions re-implemented the agent auth-header logic and
   sent "Authorization: ''" when no token was stored, which the agent
   rejects. Refactored to reuse the existing agentAuthHeaders() helper
   (it correctly omits the header when no token is configured).

3. server_rbac.go file header comment called the pre-#8185 behavior a
   "privilege-escalation vector". Rewrote it to describe the actual
   impact: misleading RBAC display — CheckCanI ran under the pod SA, so
   the UI showed system permissions instead of the caller's. No real
   privilege escalation occurred (mutations already flowed through
   kc-agent under the user's identity).

Added server_rbac_test.go covering the POST CORS preflight for
handleCanIHTTP plus the setCORSHeaders back-compat default and explicit
methods paths.

Fixes #8188

Signed-off-by: Andy Anderson <[email protected]>
clubanderson added a commit that referenced this pull request Apr 15, 2026
…header comment (#8188) (#8198)

Addresses three Copilot review comments on merged #8185:

1. handleCanIHTTP is a POST endpoint but setCORSHeaders was defaulting
   Access-Control-Allow-Methods to "GET, OPTIONS", so browser preflight
   requests for POST would fail. Generalized setCORSHeaders with a
   variadic methods parameter — back-compat preserved because callers
   that pass nothing still get defaultCORSAllowedMethods ("GET, OPTIONS").
   handleCanIHTTP now passes http.MethodPost + http.MethodOptions.

2. useClusterPermissions re-implemented the agent auth-header logic and
   sent "Authorization: ''" when no token was stored, which the agent
   rejects. Refactored to reuse the existing agentAuthHeaders() helper
   (it correctly omits the header when no token is configured).

3. server_rbac.go file header comment called the pre-#8185 behavior a
   "privilege-escalation vector". Rewrote it to describe the actual
   impact: misleading RBAC display — CheckCanI ran under the pod SA, so
   the UI showed system permissions instead of the caller's. No real
   privilege escalation occurred (mutations already flowed through
   kc-agent under the user's identity).

Added server_rbac_test.go covering the POST CORS preflight for
handleCanIHTTP plus the setCORSHeaders back-compat default and explicit
methods paths.

Fixes #8188

Signed-off-by: Andy Anderson <[email protected]>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

dco-signoff: yes Indicates the PR's author has signed the DCO. size/L Denotes a PR that changes 100-499 lines, ignoring generated files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

refactor: backend pod SA must not be used for user-initiated k8s actions (umbrella — was CheckCanI)

2 participants