Skip to content

🐛 fix: exec authz SAR error wrap + structured deny reason (#8140)#8141

Merged
clubanderson merged 1 commit intomainfrom
fix/exec-authz-copilot-8140
Apr 15, 2026
Merged

🐛 fix: exec authz SAR error wrap + structured deny reason (#8140)#8141
clubanderson merged 1 commit intomainfrom
fix/exec-authz-copilot-8140

Conversation

@clubanderson
Copy link
Copy Markdown
Collaborator

Summary

Follow-up to merged PR #8139 addressing three Copilot review comments.

  • pkg/api/handlers/exec.go authorizePodExec SAR-error branch: switched fmt.Errorf("%w: %v", ...) to double-%w (Go 1.20+) so callers can recover BOTH errExecSARFailed AND the underlying SAR error via errors.Is. The pre-fix %v form silently broke the recoverable-underlying-error contract the surrounding tests documented.
  • pkg/api/handlers/exec_test.go TestAuthorizePodExec_SARError: the test comment claimed the underlying SAR error was recoverable, but only the sentinel was asserted. Added require.ErrorIs(t, err, sentinel) so the double-%w contract is locked in by a test assertion rather than a comment.
  • pkg/api/handlers/exec.go HandleExec deny branch: pre-🐛 fix: exec authz typed-nil guard + test coverage (#8137) #8139 logged the SAR deny reason under a structured "reason" slog key; 🐛 fix: exec authz typed-nil guard + test coverage (#8137) #8139 accidentally moved it into the error string only, breaking operator log queries that filter on the reason attribute. To restore the structured field cleanly without parsing error strings, authorizePodExec now returns (execAuthzResult, error) where execAuthzResult.reason is populated ONLY on errExecRBACDenied (empty on every other branch). HandleExec passes authzResult.reason to slog.String("reason", ...) on deny, alongside the wrapped error.

Tests

  • TestAuthorizePodExec_SARError now asserts BOTH errors.Is(err, errExecSARFailed) AND errors.Is(err, sentinel) to lock in the double-wrap.
  • TestAuthorizePodExec_Deny now asserts res.reason == denyReason (non-empty, distinct from the error) — the structured-log contract.
  • New TestAuthorizePodExec_DenyNoReason covers the reason == "" subcase (apiserver denied but gave no reason string).
  • All non-deny branches (allow, nil authorizer, missing subject, SAR failure, empty login) assert res.reason stays empty so callers never accidentally log a synthesized reason.

Security

No change to the fail-closed decision. Every branch from #8134/#8137/#8139 still denies the exec before the executor is built. The h.authorizer == nil typed-nil guard from #8139 is untouched.

Test plan

  • go build ./... passes
  • go vet ./... passes
  • go test ./pkg/api/handlers/... -run Exec -count=1 — 13/13 pass
  • CI build/lint/preview green

Fixes #8140

Addresses three Copilot review comments on merged PR #8139:

1. exec.go authorizePodExec: the SAR-call-failed branch formatted the
   underlying error with `%v`, so `errors.Is(err, underlyingSARError)`
   silently returned false despite surrounding comments and the test
   name implying the underlying error was recoverable. Switched to
   double-%w (Go 1.20+): `fmt.Errorf("%w: %w", errExecSARFailed, err)`
   so callers can recover BOTH the sentinel AND the underlying SAR
   error via errors.Is.

2. exec_test.go TestAuthorizePodExec_SARError: the comment claimed the
   underlying error was recoverable, but the test only asserted the
   sentinel. Added `require.ErrorIs(t, err, sentinel)` so the
   double-%w contract is locked in by a test, not just a comment.

3. exec.go HandleExec deny branch: pre-#8139 logged the SAR deny
   reason under a structured `"reason"` slog key; #8139 accidentally
   embedded it only in the error string, breaking operator log
   queries that filter by the `reason` attribute. To restore the
   structured field without re-parsing error strings, authorizePodExec
   now returns `(execAuthzResult, error)` where execAuthzResult.reason
   is populated ONLY on errExecRBACDenied (empty on every other
   branch — allow, nil authorizer, missing subject, SAR failure).
   HandleExec passes authzResult.reason to slog.String("reason", ...)
   on the deny branch, alongside the wrapped error. Added
   TestAuthorizePodExec_Deny assertion that res.reason is the exact
   apiserver reason string (non-empty, distinct from the error), and
   a new TestAuthorizePodExec_DenyNoReason covering the empty-reason
   subcase. Non-deny branches assert res.reason stays empty so
   callers never accidentally log a synthesized reason.

No behavior change to the security decision: every fail-closed branch
from #8134/#8137/#8139 still denies the exec before the executor is
built. The `h.authorizer == nil` typed-nil guard from #8139 is
untouched.

build/vet/go test ./pkg/api/handlers/... -run Exec: all 13 tests pass.

Fixes #8140

Signed-off-by: Andy Anderson <[email protected]>
Copilot AI review requested due to automatic review settings April 15, 2026 09:41
@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 eeshaansa 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 canceled.

Name Link
🔨 Latest commit f877e2e
🔍 Latest deploy log https://app.netlify.com/projects/kubestellarconsole/deploys/69df5d30828cfa00089d54b3

@clubanderson clubanderson added the ai-generated Pull request generated by AI label Apr 15, 2026
@kubestellar-prow kubestellar-prow Bot added the size/L Denotes a PR that changes 100-499 lines, ignoring generated files. label Apr 15, 2026
@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.

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

This PR is a follow-up fix to the pods/exec RBAC enforcement work, ensuring SubjectAccessReview (SAR) errors are properly double-wrapped for errors.Is recovery and restoring the structured "reason" log attribute on RBAC deny events for operator log querying.

Changes:

  • Update authorizePodExec to return (execAuthzResult, error) so deny reasons can be logged as a structured "reason" attribute.
  • Fix SAR error wrapping to use double %w so both errExecSARFailed and the underlying SAR error are recoverable via errors.Is.
  • Expand unit test coverage to assert the double-wrap contract and the structured deny-reason behavior (including the empty-reason deny case).

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated no comments.

File Description
pkg/api/handlers/exec.go Adds execAuthzResult, double-wraps SAR errors, and restores structured "reason" logging for RBAC deny.
pkg/api/handlers/exec_test.go Updates/extends tests to assert errors.Is behavior for SAR errors and validate deny reason propagation via result struct.

@clubanderson clubanderson merged commit d624042 into main Apr 15, 2026
31 of 32 checks passed
@clubanderson clubanderson deleted the fix/exec-authz-copilot-8140 branch April 15, 2026 09:46
@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

@github-actions
Copy link
Copy Markdown
Contributor

Post-merge build verification passed

Both Go and frontend builds compiled successfully against merge commit d62404214e43d6ee2081b158f4ed4bacee5e1f99.

@github-actions
Copy link
Copy Markdown
Contributor

✅ Post-Merge Verification: passed

Commit: d62404214e43d6ee2081b158f4ed4bacee5e1f99
Specs run: smoke.spec.ts
Report: https://github.com/kubestellar/console/actions/runs/24447754661

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ai-generated Pull request generated by AI 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.

[Copilot Review] 3 comment(s) on merged PR #8139

2 participants