Skip to content

fix(registry): run validating admission on TenantSecret writes - #4226

Merged
Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/registry-tenantsecret-admission
Sep 14, 2026
Merged

Aleksei Sviridkin (lexfrei) merged 1 commit into
mainfrom
fix/registry-tenantsecret-admission

Conversation

@IvanHunters

@IvanHunters IvanHunters commented Sep 12, 2026 •

Copy link
Copy Markdown
Collaborator

What this PR does

The apiserver hands validating admission to a storage as the ValidateObjectFunc it passes into Create, Update and Delete. Mutating admission runs in the handler, but validating webhooks and ValidatingAdmissionPolicy execute only when the storage calls that callback, as rest.AdmissionToValidateObjectFunc in k8s.io/apiserver builds it for exactly that purpose. TenantSecret bound all three callbacks to _, so every write answered as if no policy existed.

The practical effect is that the cluster-wide cozystack-no-delete-guardrail policy, and any webhook an operator writes for tenantsecrets, are inert on this resource. application and securitygroup call all three callbacks; TenantSecret was the only write-capable storage in the tree that did not.

The callbacks are now called on all four write paths, including the create-on-update branch, which is a create and so carries createValidation rather than updateValidation. What goes to admission is the aggregated TenantSecret the caller submitted, or the stored one it replaces, not the backing Secret, so a policy sees the resource it was written against.

This is a behaviour change for anyone who has a policy matching tenantsecrets today: writes that silently passed will start being evaluated, and may now be denied. That is the point of the fix, but it is worth saying out loud before it ships.

Tests

Each write path is covered by a denying admission callback, asserting both that the callback runs and that the backing Secret is left alone when it denies. A nil callback keeps every path working, which is what the apiserver passes when no plugin handles the operation. Removing any one of the four calls, one at a time, turns the suite red.

Screenshots

Not a UI change.

Downstream repositories

Walked the trigger map in docs/agents/contributing.md against the diff. It touches one aggregated storage's write paths: no package added, renamed or removed, no values.schema.json, no version enum, no changed default, no kind or plural, no release.prefix, no renamed output Secret or Service, no hand-typed CRD, and nothing under hack/.

Release note

fix(registry): run validating admission on TenantSecret writes, so ValidatingAdmissionPolicies and webhooks matching tenantsecrets are no longer bypassed

Summary by CodeRabbit

  • Bug Fixes
    • TenantSecret create, update, force-create, and delete operations now apply validation checks before completing changes.
    • Denied create or update requests are stopped before data is saved.
    • Delete validation runs after the current secret is retrieved and before removal.
    • TenantSecret write operations remain available when no validation callback is provided.

The apiserver hands validating admission to a storage as the ValidateObjectFunc
it passes into Create, Update and Delete: mutating admission runs in the
handler, but validating webhooks and ValidatingAdmissionPolicies only execute
when the storage calls that callback. TenantSecret bound all three to _, so
every write answered as if no policy existed. The cluster-wide no-delete
guardrail and any webhook written for tenantsecrets were inert on this
resource, while application and securitygroup honour them.

Call the callbacks on all four write paths, including the create-on-update
branch, which is a create and so carries createValidation rather than
updateValidation. The object handed to admission is the aggregated TenantSecret
the caller submitted or the stored one it replaces, not the backing Secret, so
a policy sees the resource it was written against.

Assisted-by: LLM
Signed-off-by: IvanHunters <[email protected]>
@coderabbitai

coderabbitai Bot commented Sep 12, 2026 •

Copy link
Copy Markdown
Contributor

Review Change StackReview Change Stack

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration

Configuration used: Path: .coderabbit.yaml

Review profile: CHILL

Plan: Advanced

Run ID: 983558c0-b2fc-40e5-ab7c-18367926e5bb

📥 Commits

Reviewing files that changed from the base of the PR and between 37e22dc and 64eff1f.

📒 Files selected for processing (2)
  • pkg/registry/core/tenantsecret/rest.go
  • pkg/registry/core/tenantsecret/rest_admission_test.go

Included review availability: Your plan provides up to 8 included reviews per hour; 6 remain after this review.


📝 Walkthrough

Walkthrough

TenantSecret REST create, update, force-create, and delete operations now invoke validation callbacks. Denied operations return the callback error before changing the backing Secret. Tests cover denied and nil callback paths.

Changes

TenantSecret admission validation

Layer / File(s) Summary
REST validation wiring
pkg/registry/core/tenantsecret/rest.go
Create and force-create invoke create validation. Existing-object updates invoke update validation. Deletes validate the current object before removing the backing Secret.
Admission and persistence tests
pkg/registry/core/tenantsecret/rest_admission_test.go
Tests verify callback execution, denial errors, prevented persistence, and successful writes with nil callbacks.

Priority: ➖ Normal

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

Change: Bug fix

Sequence Diagram(s)

sequenceDiagram
  participant REST as tenantsecret.REST
  participant Validation as validation callback
  participant SecretStorage as backing Secret storage
  REST->>Validation: validate TenantSecret operation
  Validation-->>REST: return validation result
  REST->>SecretStorage: persist or delete Secret when validation succeeds
Loading

Merge Risk: ⚪ Minimal · up to 64eff

TenantSecret admission validation is applied before writes and deletions, and denied operations preserve the backing Secret.

🚥 Pre-merge checks | ✅ 4 | ❌ 1

❌ Failed checks (1 warning)

Check name Status Explanation Resolution
Docstring Coverage ⚠️ Warning Docstring coverage is 0.00% which is insufficient. The required threshold is 80.00%. Docstring coverage is scoped to functions touched by this diff. Analyzed 9 functions across 2 files. Write docstrings for the functions missing them to satisfy the coverage threshold.
✅ Passed checks (4 passed)
Check name Status Explanation
Description Check ✅ Passed Check skipped - CodeRabbit’s high-level summary is enabled.
Title check ✅ Passed The title clearly and concisely describes the main change: enabling validating admission for TenantSecret write operations. It covers the create, update, create-on-update, and delete paths without unn…
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.
  • Fix all pre-merge checks with AI
✨ Finishing Touches 💡 1
📝 Generate docstrings 💡
  • Create stacked PR
  • Commit on current branch
🧪 Generate unit tests (beta)
  • Create PR with unit tests
  • Commit unit tests in branch fix/registry-tenantsecret-admission

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.

@github-actions github-actions Bot added size/L This PR changes 100-499 lines, ignoring generated files area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug labels Sep 12, 2026
@IvanHunters
IvanHunters marked this pull request as ready for review September 12, 2026 18:38

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.

LGTM. All four write paths call the callback, every one fails closed, and each is pinned by a test that goes red when I delete it.

Business context: validating webhooks and ValidatingAdmissionPolicies matching tenantsecrets never ran, because this storage dropped the ValidateObjectFunc the apiserver hands it. The cluster-wide deletion guardrail was inert on the resource.

What I checked

I took the write paths from the method set, not from the description. Create, Update, Delete, and no DeleteCollection, so a collection delete is not served and needs no callback. Update splits in two, so four paths, and the diff covers all four:

  • rest.go:261, Create calls createValidation(ctx, obj)
  • rest.go:410, the create-on-update branch calls createValidation(ctx, newObj). That branch is a create, so this is right; reaching for updateValidation there is the usual mistake.
  • rest.go:419, Update calls updateValidation(ctx, newObj, secretToTenant(cur))
  • rest.go:447, Delete calls deleteValidation(ctx, secretToTenant(current))

PATCH needs no separate entry. rest.Patcher is Getter plus Updater, so it lands on Update.

All four branches are if err := ...; err != nil { return ..., err }. An unreachable webhook or a cancelled context denies the write instead of waving it through, which is the case the control exists for.

This callback is also the only enforcement for anything scoped to tenantsecrets. The backing write goes out through the controller-runtime client to the main apiserver, which runs its own chain against the resulting Secret: different resource, different rules, and the aggregated server's identity instead of the caller's. The two chains do not overlap, so this is load-bearing.

Mutation ladder, restoring between each. Deleting rest.go:261 reds only TestCreateRunsValidatingAdmission, :410 only TestUpdateForceCreateRunsValidatingAdmission, :419 only TestUpdateRunsValidatingAdmission, :447 only TestDeleteRunsValidatingAdmission. The "did not write" assertions are not vacuous either: keeping the call but swallowing its error fails at the denial assertion instead of the "never invoked" one, and moving the backing create ahead of the callback fails at rest_admission_test.go:71, the assertion that the Secret stayed absent. So the fixture does persist, and that assertion tells the two cases apart.

I walked the other storages too. application and securitygroup call all three. tap is Creater plus GracefulDeleter only, and calls both callbacks its verb set has. option, tenantnamespace and tenantmodule are read-only. The description is right that this was the last gap.

Non-blocking

  1. The rationale for the nil guard is wrong, in the test comment and in the PR description: "the apiserver passes a nil callback when no admission plugin handles the operation". It does not. AdmissionToValidateObjectFunc and AdmissionToValidateObjectUpdateFunc return a closure that returns nil when admit is not a ValidationInterface, AdmissionToValidateObjectDeleteFunc returns one that short-circuits on !mutating && !validating, and withAuthorization wraps instead of replacing. No handler path hands the storage a nil func. Keep the guard, it matches the sibling storages and covers direct in-process callers like the tests, but the comment should say that instead of describing a case that never happens.

  2. Pre-existing, and next door to this diff. The backing writes pass options as &client.CreateOptions{Raw: opts} and the Update and Delete equivalents, and controller-runtime's AsCreateOptions does o.Raw.DryRun = o.DryRun, overwriting Raw.DryRun with the empty structured field. A server-side dry-run write loses its marker and persists. Worth its own fix. It matters a bit more after this change, since admission will now see IsDryRun() on a request the storage commits anyway.

CI

The one red check is a single chainsaw suite failing on status.readyReplicas: Required value: field not found in the input object against opensearch-test-nodes. I don't think it is attributable: this diff is Go-only inside one aggregated storage and cannot reach an OpenSearch StatefulSet, and the same single-suite failure shows up on unrelated PRs that only touch OpenSearch. Not gating on it.

@lexfrei
Aleksei Sviridkin (lexfrei) merged commit 5b91c6b into main Sep 14, 2026
44 of 46 checks passed
@lexfrei
Aleksei Sviridkin (lexfrei) deleted the fix/registry-tenantsecret-admission branch September 14, 2026 12:45
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area/uncategorized PR auto-labeler could not map title scope to a known area/*; please review kind/bug Categorizes issue or PR as related to a bug size/L This PR changes 100-499 lines, ignoring generated files

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants