Skip to content

Fix: Grant execution SA base read access via shared ClusterRoleBinding#282

Open
blublinsky wants to merge 2 commits into
openshift:mainfrom
blublinsky:execution-sa-fix
Open

Fix: Grant execution SA base read access via shared ClusterRoleBinding#282
blublinsky wants to merge 2 commits into
openshift:mainfrom
blublinsky:execution-sa-fix

Conversation

@blublinsky

@blublinsky blublinsky commented Jul 6, 2026

Copy link
Copy Markdown
Contributor

Summary

OLS-3248 introduced per-proposal ServiceAccounts for execution RBAC isolation, but inadvertently dropped base read access. The previous implementation used lightspeed-agent SA (which already had cluster-reader bound) for both analysis and execution. After OLS-3248, execution uses a fresh SA with only write permissions from the analysis RBAC output — breaking any execution script that reads resources (e.g., kubectl get, kubectl rollout status).

This fix adds the per-proposal execution SA as a subject to the existing lightspeed-agent-cluster-reader ClusterRoleBinding, giving it the same read access as the analysis SA. Subjects are added on execution start and removed on cleanup.

Changes

File What
controller/proposal/rbac.go Added addReaderSubject / removeReaderSubject helpers that manage subjects on the shared lightspeed-agent-cluster-reader ClusterRoleBinding. Both retry on conflict (optimistic concurrency). Called from ensureExecutionSA and cleanupExecutionRBAC.
controller/proposal/reconciler.go Added update verb to the ClusterRoleBinding RBAC marker.

Execution SA permissions (before → after)

Before (broken) After (fixed)
Read (get/list/watch) None cluster-reader via shared binding
Write (from RBACResult) Per-proposal Role in target namespaces Unchanged

Test plan

  • go build ./... — compiles
  • make manifestsrole.yaml includes update on clusterrolebindings
  • make test — unit tests pass
  • E2E: execution step can read resources in target namespace (no 403 on get/list)
  • E2E: after cleanup, SA subject is removed from the shared binding
  • Concurrent proposals don't produce conflict failures (retry handles it)

@openshift-ci openshift-ci Bot requested review from joshuawilson and raptorsun July 6, 2026 17:47
@coderabbitai

coderabbitai Bot commented Jul 6, 2026

Copy link
Copy Markdown

Warning

Review limit reached

@blublinsky, you've reached your PR review limit, so we couldn't start this review.

Next review available in: 46 minutes

Enable usage-based reviews in Billing to review now. Otherwise, wait until the next included review is available.

How can I continue?

After more reviews become available, a review can be triggered using the @coderabbitai review command as a PR comment. Alternatively, push new commits to this PR.

To avoid repeated limits, reduce automatic review volume by pausing incremental auto-reviews earlier, using label-based review opt-in, excluding WIP or generated PR titles, or requesting reviews manually when the PR is ready. If your team needs uninterrupted high-volume reviews, an organization admin can enable usage-based reviews.

How do review limits work?

CodeRabbit enforces per-developer PR review limits for each organization. Most developers receive the normal plan review availability.

For paid Pro and Pro+ PR reviews, CodeRabbit uses adaptive limits for sustained high-volume activity. When a developer's recent PR review activity reaches the 95th percentile or higher among CodeRabbit users, additional reviews become available more gradually as earlier reviews age out of the rolling window.

Please refer docs for additional details.

Review details
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 681aa99d-59a6-4408-9585-a697ed7b3d02

📥 Commits

Reviewing files that changed from the base of the PR and between 01b8282 and 5071a3d.

⛔ Files ignored due to path filters (1)
  • config/rbac/role.yaml is excluded by !config/rbac/role.yaml
📒 Files selected for processing (19)
  • .ai/spec/README.md
  • .ai/spec/health-report.md
  • .ai/spec/how/cli-distribution.md
  • .ai/spec/how/cli.md
  • .ai/spec/how/project-structure.md
  • .ai/spec/how/reconciler.md
  • .ai/spec/what/approval.md
  • .ai/spec/what/audit-logging.md
  • .ai/spec/what/crd-api.md
  • .ai/spec/what/run-lifecycle.md
  • .ai/spec/what/sandbox-execution.md
  • .ai/spec/what/system-config.md
  • .ai/spec/what/system-overview.md
  • .ai/spec/what/templog.md
  • CLAUDE.md
  • controller/proposal/rbac.go
  • controller/proposal/rbac_test.go
  • controller/proposal/reconciler.go
  • controller/proposal/reconciler_test.go
📝 Walkthrough

Walkthrough

This PR adds reader ClusterRoleBinding subject sync for execution ServiceAccounts, updates the controller RBAC to allow binding updates, and adjusts tests/fixtures to include the shared reader binding.

Changes

Shared reader ClusterRoleBinding subject sync

Layer / File(s) Summary
Add reader subject during setup
controller/proposal/rbac.go
ensureExecutionSA now adds the execution ServiceAccount as a subject on the shared reader ClusterRoleBinding, with fallback binding discovery and retry-on-conflict updates.
Remove reader subject during cleanup
controller/proposal/rbac.go
cleanupExecutionRBAC now removes the execution ServiceAccount from the shared reader ClusterRoleBinding before deleting the ServiceAccount.
RBAC permissions and fixtures
controller/proposal/reconciler.go, controller/proposal/reconciler_test.go, controller/proposal/rbac_test.go
The controller RBAC annotation now allows listing and updating clusterroles and clusterrolebindings, and the RBAC tests/fixtures include the pre-existing reader ClusterRoleBinding.

Estimated code review effort: 3 (Moderate) | ~20 minutes

Sequence Diagram(s)

sequenceDiagram
  participant Reconciler
  participant ensureExecutionSA
  participant addReaderSubject
  participant cleanupExecutionRBAC
  participant removeReaderSubject
  participant ClusterRoleBinding

  Reconciler->>ensureExecutionSA: create execution ServiceAccount
  ensureExecutionSA->>addReaderSubject: add ServiceAccount subject
  addReaderSubject->>ClusterRoleBinding: fetch and update subjects

  Reconciler->>cleanupExecutionRBAC: delete proposal RBAC
  cleanupExecutionRBAC->>removeReaderSubject: remove ServiceAccount subject
  removeReaderSubject->>ClusterRoleBinding: fetch and update subjects
Loading

Related issues: None provided.

Related PRs: None provided.

Suggested labels: rbac, controller

Suggested reviewers: None provided.

Poem:
Subject added, subject removed,
Shared binding state kept in move.
Retries on conflict, then done.

🚥 Pre-merge checks | ✅ 5
✅ Passed checks (5 passed)
Check name Status Explanation
Title check ✅ Passed The title matches the main change: restoring execution SA read access through the shared ClusterRoleBinding.
Description check ✅ Passed The description accurately explains the RBAC fix, the shared binding subject updates, and the reconciler RBAC change.
Docstring Coverage ✅ Passed No functions found in the changed files to evaluate docstring coverage. Skipping docstring coverage check.
Linked Issues check ✅ Passed Check skipped because no linked issues were found for this pull request.
Out of Scope Changes check ✅ Passed Check skipped because no linked issues were found for this pull request.

Thanks for using CodeRabbit! It's free for OSS, and your support helps us grow. If you like it, consider giving us a shout-out.

❤️ Share

Comment @coderabbitai help to get the list of available commands.

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/proposal/rbac.go`:
- Around line 250-253: The `removeReaderSubject` callsite in `proposal/rbac.go`
is redundantly wrapping an error that already includes `ErrRemoveReaderSubject`,
which can duplicate the prefix in the final message. Update the
`executionSAName`/`removeReaderSubject` block to return the error directly,
matching the `addReaderSubject` callsite style for consistency, and leave the
existing wrapping inside `removeReaderSubject` as the single source of context.

In `@controller/proposal/reconciler.go`:
- Line 48: The manager RBAC on the proposal reconciler is missing permissions
needed by addReaderSubject/removeReaderSubject to modify the shared reader
binding. Update the kubebuilder RBAC annotation in the reconciler that defines
the manager ClusterRole so it includes update on clusterrolebindings, and add
bind on the shared cluster-reader ClusterRole if the operator service account
may need to attach subjects to it.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Repository: openshift/coderabbit/.coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: 7dfb8b7f-7f68-4b93-acaf-b5f7baefc56f

📥 Commits

Reviewing files that changed from the base of the PR and between 0ebffc5 and 13095d2.

📒 Files selected for processing (2)
  • controller/proposal/rbac.go
  • controller/proposal/reconciler.go

Comment thread controller/proposal/rbac.go
Comment thread controller/proposal/reconciler.go Outdated
@blublinsky blublinsky force-pushed the execution-sa-fix branch 2 times, most recently from 5fea87a to 01b8282 Compare July 6, 2026 20:19
Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>

@coderabbitai coderabbitai 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.

Actionable comments posted: 2

🤖 Prompt for all review comments with AI agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

Inline comments:
In `@controller/proposal/rbac.go`:
- Around line 148-156: `removeReaderSubject` only checks `readerRoleBinding` and
returns early on NotFound, so it can miss a renamed binding and leave the
subject behind. Update `removeReaderSubject` to mirror `addReaderSubject` by
using `discoverReaderBinding` as a fallback when the default binding name is not
found, then retry the removal against the discovered ClusterRoleBinding before
returning. Ensure the lookup and cleanup logic in `removeReaderSubject` stays
consistent with `addReaderSubject` and the shared `readerRoleBinding` handling.
- Line 38: The shared readerRoleBinding state is unsafe because
discoverReaderBinding writes it while addReaderSubject and removeReaderSubject
read it during concurrent reconciles, causing a data race and stale process-wide
binding names. Fix this by removing the package-level mutable variable in
rbac.go and making the binding name local to each reconcile flow, or by guarding
all access with synchronization such as a mutex or atomic. Update the call paths
in discoverReaderBinding, addReaderSubject, and removeReaderSubject to pass the
binding name explicitly rather than relying on shared global state.
🪄 Autofix (Beta)

Fix all unresolved CodeRabbit comments on this PR:

  • Push a commit to this branch (recommended)
  • Create a new PR with the fixes

ℹ️ Review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Enterprise

Run ID: f0fd90d8-ba5e-41dd-aa43-4ac8e046199b

📥 Commits

Reviewing files that changed from the base of the PR and between 13095d2 and 01b8282.

⛔ Files ignored due to path filters (1)
  • config/rbac/role.yaml is excluded by !config/rbac/role.yaml
📒 Files selected for processing (4)
  • controller/proposal/rbac.go
  • controller/proposal/rbac_test.go
  • controller/proposal/reconciler.go
  • controller/proposal/reconciler_test.go
🔗 Linked repositories identified

CodeRabbit considers these linked repositories for cross-repo context during reviews:

  • openshift/lightspeed-agentic-sandbox (manual)
✅ Files skipped from review due to trivial changes (1)
  • controller/proposal/reconciler.go

)

// +kubebuilder:rbac:groups="",resources=serviceaccounts,verbs=create;delete;get
var readerRoleBinding = defaultReaderClusterRoleBinding

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🩺 Stability & Availability | 🔴 Critical | ⚡ Quick win

🧩 Analysis chain

🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- controller/proposal/rbac.go ---'
cat -n controller/proposal/rbac.go | sed -n '1,240p'

echo
echo '--- search MaxConcurrentReconciles ---'
rg -n "MaxConcurrentReconciles|controller\.Options|NewControllerManagedBy|Owns\(" controller config .ai -g '!**/vendor/**'

echo
echo '--- search readerRoleBinding usages ---'
rg -n "readerRoleBinding|discoverReaderBinding|addReaderSubject|removeReaderSubject" controller/proposal -g '!**/vendor/**'

Repository: openshift/lightspeed-agentic-operator

Length of output: 13841


🏁 Script executed:

#!/bin/bash
set -euo pipefail

echo '--- controller/proposal/reconciler.go (around controller setup) ---'
cat -n controller/proposal/reconciler.go | sed -n '230,280p'

echo
echo '--- search maxConcurrent source ---'
rg -n "maxConcurrent" controller main cmd . -g '!**/vendor/**'

Repository: openshift/lightspeed-agentic-operator

Length of output: 2795


Guard readerRoleBinding before concurrent reconciles race here. readerRoleBinding is written in discoverReaderBinding and read in addReaderSubject / removeReaderSubject with no synchronization. controller/proposal/reconciler.go sets MaxConcurrentReconciles from ApprovalPolicy, so this is an active data race, and the discovered binding name can also stick as stale process-wide state for later proposals. Make the binding name per-reconcile or protect it with a mutex/atomic.

🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/proposal/rbac.go` at line 38, The shared readerRoleBinding state
is unsafe because discoverReaderBinding writes it while addReaderSubject and
removeReaderSubject read it during concurrent reconciles, causing a data race
and stale process-wide binding names. Fix this by removing the package-level
mutable variable in rbac.go and making the binding name local to each reconcile
flow, or by guarding all access with synchronization such as a mutex or atomic.
Update the call paths in discoverReaderBinding, addReaderSubject, and
removeReaderSubject to pass the binding name explicitly rather than relying on
shared global state.

Comment on lines +148 to +156
func removeReaderSubject(ctx context.Context, c client.Client, saName, namespace string) error {
for attempts := 0; attempts < 3; attempts++ {
crb := &rbacv1.ClusterRoleBinding{}
if err := c.Get(ctx, client.ObjectKey{Name: readerRoleBinding}, crb); err != nil {
if apierrors.IsNotFound(err) {
return nil
}
return fmt.Errorf("%s: %w", ErrRemoveReaderSubject, err)
}

Copy link
Copy Markdown

Choose a reason for hiding this comment

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

🗄️ Data Integrity & Integration | 🟠 Major | ⚡ Quick win

removeReaderSubject has no discovery fallback, unlike addReaderSubject — orphaned subjects possible.

addReaderSubject calls discoverReaderBinding when the default binding name isn't found (Lines 119-124), but removeReaderSubject simply returns nil on NotFound (Lines 152-154). If the reader binding was ever renamed (triggering discovery during setup) and the process later restarts — resetting readerRoleBinding back to defaultReaderClusterRoleBinding — cleanup will silently no-op against a binding name that doesn't exist, leaving the execution SA subject dangling on the real (renamed) binding indefinitely.

♻️ Proposed fix to mirror `addReaderSubject`'s fallback
 		if err := c.Get(ctx, client.ObjectKey{Name: readerRoleBinding}, crb); err != nil {
 			if apierrors.IsNotFound(err) {
-				return nil
+				if discoverErr := discoverReaderBinding(ctx, c, namespace); discoverErr != nil {
+					// Binding genuinely doesn't exist anywhere — nothing to remove.
+					return nil
+				}
+				continue
 			}
 			return fmt.Errorf("%s: %w", ErrRemoveReaderSubject, err)
 		}
📝 Committable suggestion

‼️ IMPORTANT
Carefully review the code before committing. Ensure that it accurately replaces the highlighted code, contains no missing lines, and has no issues with indentation. Thoroughly test & benchmark the code to ensure it meets the requirements.

Suggested change
func removeReaderSubject(ctx context.Context, c client.Client, saName, namespace string) error {
for attempts := 0; attempts < 3; attempts++ {
crb := &rbacv1.ClusterRoleBinding{}
if err := c.Get(ctx, client.ObjectKey{Name: readerRoleBinding}, crb); err != nil {
if apierrors.IsNotFound(err) {
return nil
}
return fmt.Errorf("%s: %w", ErrRemoveReaderSubject, err)
}
func removeReaderSubject(ctx context.Context, c client.Client, saName, namespace string) error {
for attempts := 0; attempts < 3; attempts++ {
crb := &rbacv1.ClusterRoleBinding{}
if err := c.Get(ctx, client.ObjectKey{Name: readerRoleBinding}, crb); err != nil {
if apierrors.IsNotFound(err) {
if discoverErr := discoverReaderBinding(ctx, c, namespace); discoverErr != nil {
// Binding genuinely doesn't exist anywhere — nothing to remove.
return nil
}
continue
}
return fmt.Errorf("%s: %w", ErrRemoveReaderSubject, err)
}
🤖 Prompt for AI Agents
Verify each finding against current code. Fix only still-valid issues, skip the
rest with a brief reason, keep changes minimal, and validate.

In `@controller/proposal/rbac.go` around lines 148 - 156, `removeReaderSubject`
only checks `readerRoleBinding` and returns early on NotFound, so it can miss a
renamed binding and leave the subject behind. Update `removeReaderSubject` to
mirror `addReaderSubject` by using `discoverReaderBinding` as a fallback when
the default binding name is not found, then retry the removal against the
discovered ClusterRoleBinding before returning. Ensure the lookup and cleanup
logic in `removeReaderSubject` stays consistent with `addReaderSubject` and the
shared `readerRoleBinding` handling.

@openshift-ci

openshift-ci Bot commented Jul 6, 2026

Copy link
Copy Markdown

@blublinsky: all tests passed!

Full PR test history. Your PR dashboard.

Details

Instructions for interacting with me using PR comments are available here. If you have questions or suggestions related to my behavior, please file an issue against the kubernetes-sigs/prow repository. I understand the commands that are listed here.

@blublinsky

Copy link
Copy Markdown
Contributor Author

/approve

@openshift-ci openshift-ci Bot added the approved Indicates a PR has been approved by an approver from all required OWNERS files. label Jul 6, 2026
@blublinsky

Copy link
Copy Markdown
Contributor Author

/approve

@openshift-ci

openshift-ci Bot commented Jul 6, 2026

Copy link
Copy Markdown

[APPROVALNOTIFIER] This PR is APPROVED

This pull-request has been approved by: blublinsky

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

The pull request process is described 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

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

Labels

approved Indicates a PR has been approved by an approver from all required OWNERS files.

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants