Skip to content

fix: scope suspension events via impersonated parent context (#748) - #749

Open
yahyafakhroji wants to merge 1 commit into
mainfrom
fix/suspension-event-scope
Open

fix: scope suspension events via impersonated parent context (#748)#749
yahyafakhroji wants to merge 1 commit into
mainfrom
fix/suspension-event-scope

Conversation

@yahyafakhroji

@yahyafakhroji yahyafakhroji commented Aug 5, 2026

Copy link
Copy Markdown
Contributor

Fixes #748.

What

1. Suspension events now carry tenant scope, set via impersonation.

Scope annotations (platform.miloapis.com/scope.*) are derived from the requesting user's iam.miloapis.com/parent-type / parent-name extras. The propagator emitted through the manager's shared EventRecorder, which authenticates as the controller and carries no parent context — so its events were stored untagged and invisible to project-scoped feed queries.

ProjectEventEmitter emits through a client that impersonates the controller's service account plus parent-type: Project / parent-name: <project>, so injectScopeAnnotations takes its existing full-parent-context branch unchanged. Per @scotwells' review, no second scope-derivation path.

Verified at the code level: WithImpersonation is enabled in Milo's handler chain (cmd/milo/apiserver/config.go:525) and runs before the Project/Org context decorators, which pass through untouched when the request isn't project-scoped (pkg/server/filters/projects.go:86-91).

Events are created as events.k8s.io/v1 so they genuinely carry the Regarding/Note fields the ActivityPolicy reads, with no conversion in between.

2. Internal admin detail redacted. The Suspended event message and Project.status.conditions["Suspended"].message both embedded ProjectSuspension resource names. Both now carry only the reason category (e.g. Abuse). Project status is tenant-readable, so it needed the same treatment as the event.

3. ActivityPolicy CEL fields corrected. eventRules read event.involvedObject.name / event.message; the events.k8s.io/v1 contract exposes event.regarding.name / event.note. The summary would have silently rendered empty.

4. status.suspensions[] filtered to active phase, with the active-only contract documented on the API types. No-op today since lift is implemented as delete.

5. RBAC regenerated from new kubebuilder markers: impersonate on serviceaccounts and on the two userextras parent keys, plus create on events.k8s.io/events — the generated ClusterRole had no events-create rule in any API group.

Notes for reviewers

The involved-object scope fallback is still in the diff. I kept it rather than removing it here: impersonation is verified at the code level but has not yet run at runtime (see below). The two coexist safely — impersonation supplies full parent context, so the fallback branch is never reached for these events. Happy to remove it if you'd rather the PR show only the impersonation path.

The impersonation RBAC is not exercised by CI. The controller-manager authenticates as admin / system:masters in test-infra, so a green run there will not prove the new rules are correct. Moving it onto its service account identity changes authentication for every controller in that binary at once and would surface any other gaps in the generated ClusterRole, so it belongs in its own change.

Test plan

  • go build ./..., gofmt -l . — clean
  • go vet ./... — one pre-existing, unrelated finding (organization_types.go:127), confirmed present on main
  • go test -timeout 5m ./... — 772 passed, 72 packages

@yahyafakhroji yahyafakhroji self-assigned this Aug 5, 2026

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@scotwells could you review this file? All other files, except this and internal/apiserver/events/eventsv1_rest.go looks correct.

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

@scotwells could you take a look at this file? everything else, expect this file and internal/apiserver/events/scope.go looks correct

@yahyafakhroji yahyafakhroji changed the title fix: scope-tag suspension events and redact internal detail (#748) fix: scope suspension events via impersonated parent context (#748) Aug 5, 2026
@yahyafakhroji
yahyafakhroji force-pushed the fix/suspension-event-scope branch 2 times, most recently from 084bdf2 to d463fcd Compare August 5, 2026 23:31
@scotwells

Copy link
Copy Markdown
Contributor

@yahyafakhroji we should not fallback to annotations. They're untrusted. Upstream controllers must always use impersonation to authenticate to a project.

@yahyafakhroji
yahyafakhroji force-pushed the fix/suspension-event-scope branch from d463fcd to 43b9f91 Compare August 6, 2026 00:49
@yahyafakhroji

Copy link
Copy Markdown
Contributor Author

@scotwells Agreed — fallback removed entirely. Scope now comes only from the request's parent context; nothing on the event body can influence it, and the old fallback tests are inverted to assert exactly that. The suspension controller impersonates parent-type: Project / parent-name: <project> to emit. Net −189 lines.

Separately: the quota controllers emit via the shared EventRecorder with no parent context, so they're unscoped too — happy to open a follow-up.

… context

Controller-emitted Suspended/Reinstated events carried no tenant scope, so
they never reached consumer activity feeds. Emit them through a client that
impersonates the controller's service account plus the project's parent
context, so the events proxy scope-tags them through its existing path
rather than a second scope-derivation mechanism.

Also in this change:

- Redact ProjectSuspension resource names from the event message and from
  the Suspended condition; both now carry only the reason category.
- Fix the ActivityPolicy eventRules to read event.regarding.name and
  event.note, matching the events.k8s.io/v1 CEL contract.
- Filter status.suspensions[] to active phase and document the contract.
- Add impersonate and events-create RBAC via kubebuilder markers.

The involved-object scope fallback is retained for now; impersonation
supplies full parent context, so it is never reached for these events.

Fixes #748
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.

Suspension events are invisible to consumer activity feeds (blocks cloud-portal#1356 AC #2)

3 participants