Skip to content

feat(deleter): org delete refuses while resources block it - #2017

Merged
AmanGIT07 merged 2 commits into
mainfrom
feat/org-delete-resource-blockers
Oct 23, 2026
Merged

AmanGIT07 merged 2 commits into
mainfrom
feat/org-delete-resource-blockers

Conversation

@AmanGIT07

@AmanGIT07 AmanGIT07 commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Summary

DeleteOrganization and CheckOrganizationDelete now also report the resources that the deletion config says block an organization delete, as RESOURCES_EXIST blockers in the same list as the billing blockers. The org's own resources and the resources of every project in it, disabled projects included, are judged by the organization rules. The delete runs this check before anything is canceled or deleted. No delete is refused for resources while app.deletion is disabled. Stacked on #2011.

Changes

  • core/deleter/service.go: collectBlockers takes the org id and adds OrganizationResourceBlockers after the billing blockers. ensureDeletable calls it in its first pass, before a free-plan subscription is canceled; CheckOrganizationDelete calls it too.
  • test/e2e/regression/api_test.go: the API suite's deletion config gains a rule for compute/disk (project: block, organization: block_unless_deletable).

Technical Details

  • The org id comes from the caller, not from the billing accounts: an org can have no billing account, and a retried delete finds its accounts already removed.
  • Billing blockers come first, then the resource blockers, sorted by namespace.
  • A refusal with only resource blockers reads organization cannot be deleted yet; resource blockers have no message.
  • On the Connect protocol, failed_precondition is HTTP 400.

Caveats

  • A blocker inside a disabled project can only be cleared after the project is enabled again: the resource RPCs on a disabled project answer internal at the permission step. This is pre-existing. The org delete now shows it, because a disabled project's resources count.
  • The number of blocking resources is only in the CheckOrganizationDelete response. A refused delete lists each blocker's type and namespace only, because google.rpc.PreconditionFailure has no field for a count. This is pre-existing and the same for the project delete.
  • can_delete is left out of the JSON response when it is false (proto3 default), so a client must read a missing value as false. This is pre-existing and the same for CheckProjectDelete.

Test Plan

  • core/deleter/service_test.go TestDeleteOrganization: resources that block an org delete stop it before a free-plan subscription is canceled, and nothing is removed; a failed resource check stops the delete before anything changes; an org whose project holds a resource that only blocks a project delete is deleted.
  • TestCheckOrganizationDelete: the resource blockers come after the billing blocker, with their counts.
  • internal/api/v1beta1connect/deleter_test.go: TestHandler_CheckOrganizationDelete copies count for a resource blocker; TestHandler_DeleteOrganization answers a mixed refusal with one violation per blocker, the resource violation without a description, and the billing messages without an empty piece.
  • e2e TestOrganizationDeleteRefusedWhileResourcesBlock: the project holds one compute/disk resource without the flag and one with deletable: true, and the org holds one more compute/disk resource of its own, made with CreateOrganizationResource. The check reports one blocker with count 2, the delete is refused with that violation, and the org, the project and both blocking resources stay. Once the project's blocking resource is deleted, the org's own resource still blocks with count 1. Once that one is deleted too, the project delete is still refused by the project rule, the check reports can_delete, and the org delete succeeds.
  • Without the resource check, with its error ignored, or with it run after the cancel, the unit cases fail. Without it, with the org's own resources left out, or with the project rules used for the org, the e2e test fails.
  • go test ./core/... ./internal/api/... ./pkg/server/... passes.
  • go test ./test/e2e/... passes.
  • golangci-lint run ./core/deleter/... ./internal/api/v1beta1connect/... ./test/e2e/regression/... reports no issues.

Sandbox results

A binary built from this branch, run against Postgres and SpiceDB, with this deletion config:

deletion:
  enabled: true
  resources:
    resource/aoi:       { project: block, organization: allow }
    resource/workflow:  { project: block, organization: allow }
    resource/orderitem: { project: block, organization: block_unless_deletable }
    resource/transfer:  { organization: block_unless_deletable }
    resource/insight:   { project: allow, organization: allow }
    # resource/report has no rule

Each scenario uses its own new org, and the org owner makes the calls. 49 of 49 checks passed.

# The org holds CheckOrganizationDelete DeleteOrganization Also checked
S1 nothing can_delete true ok the org row is kept with deleted_at; a second delete or check answers permission_denied (pre-existing)
S2 an AOI and a workflow in a project can_delete true ok the project delete alone is refused by the project rule (#2008)
S3 an order item without the flag resource/orderitem (1) refused, HTTP 400 the org, project and order item stay live; the owner keeps both org policies and still has access; an org viewer gets permission_denied on both RPCs (pre-existing)
S4 an order item with deletable: true can_delete true ok the project delete alone is still refused: the project rule ignores the flag (#2008)
S5 an order item with deletable: false resource/orderitem (1) refused
S6 an order item with deletable: "true" (a string) resource/orderitem (1) refused only a JSON true counts
S7 a resource/report, which has no rule resource/report (1) refused a missing rule blocks
S8 an order item in a disabled project resource/orderitem (1) refused deleting the order item while the project is disabled answers internal (pre-existing); after enabling the project and deleting it, the check reports can_delete and the delete succeeds
S9 a transfer on the org itself resource/transfer (1) refused after UpdateOrganizationResource sets deletable: true, the check reports can_delete and the delete succeeds
S10 project A: 2 order items without the flag, 1 with it, 1 AOI; disabled project B: 1 report; the org: 1 transfer, 1 insight resource/orderitem (2), resource/report (1), resource/transfer (1) refused with 3 violations all 7 resources stay live
S11 a billing account owing 50 tokens, and an order item NEGATIVE_TOKEN_BALANCE, then resource/orderitem (1) refused with 2 violations the message is organization cannot be deleted yet: billing account[<id>] owes 50 tokens: contact support to settle the balance, then retry the delete
S12 (an unknown or deleted org id) permission_denied, for a platform superuser too the permission check runs before the handler (pre-existing)
OFF an order item, a report and a transfer, with enabled: false can_delete true ok

The sandbox has no billing provider. The billing account in S11 is an offline one, its debt is one debit row written with SQL, and its delete was not run past the refusal. ACTIVE_SUBSCRIPTION and UNPAID_INVOICE need a billing provider and are covered by the unit tests.

Refused delete in S10:

{
  "code": "failed_precondition",
  "message": "organization cannot be deleted yet",
  "details": [{ "type": "google.rpc.PreconditionFailure", "debug": { "violations": [
    { "type": "RESOURCES_EXIST", "subject": "resource/orderitem" },
    { "type": "RESOURCES_EXIST", "subject": "resource/report" },
    { "type": "RESOURCES_EXIST", "subject": "resource/transfer" }
  ] } }]
}
All 49 checks
ID Check Observed
S1.1 check can_delete=true; no blockers
S1.2 delete ok
S1.3 org row kept and marked deleted 1
S1.4 check on the deleted org permission_denied (pre-existing)
S1.5 second delete permission_denied (pre-existing)
S2.1 project delete (project rule: block) failed_precondition; RESOURCES_EXIST resource/aoi, RESOURCES_EXIST resource/workflow (#2008)
S2.2 org check (org rule: allow) can_delete=true; no blockers
S2.3 org delete ok
S3.1 check can_delete=false; RESOURCES_EXIST resource/orderitem (1)
S3.2 delete failed_precondition (HTTP 400); RESOURCES_EXIST resource/orderitem
S3.3 error message organization cannot be deleted yet
S3.4 org row, project row, resource still live 1, 1, 1
S3.5 live org policies before → after 2 → 2
S3.6 owner can get the org, before → after true → true
S3.7 check by an org viewer permission_denied (pre-existing)
S3.8 delete by an org viewer permission_denied (pre-existing)
S4.1 project delete (project rule: block, flag ignored) failed_precondition; RESOURCES_EXIST resource/orderitem (#2008)
S4.2 org check (org rule: block_unless_deletable) can_delete=true; no blockers
S4.3 org delete ok
S5.1 check can_delete=false; RESOURCES_EXIST resource/orderitem (1)
S5.2 delete failed_precondition; RESOURCES_EXIST resource/orderitem
S6.1 check can_delete=false; RESOURCES_EXIST resource/orderitem (1)
S6.2 delete failed_precondition; RESOURCES_EXIST resource/orderitem
S7.1 check can_delete=false; RESOURCES_EXIST resource/report (1)
S7.2 delete failed_precondition; RESOURCES_EXIST resource/report
S8.1 check can_delete=false; RESOURCES_EXIST resource/orderitem (1)
S8.2 delete failed_precondition; RESOURCES_EXIST resource/orderitem
S8.3 delete the order item while the project is disabled internal (pre-existing)
S8.4 enable the project, then delete the order item ok
S8.5 check after that can_delete=true; no blockers
S8.6 org delete ok
S9.1 check can_delete=false; RESOURCES_EXIST resource/transfer (1)
S9.2 delete failed_precondition; RESOURCES_EXIST resource/transfer
S9.3 mark the transfer deletable with UpdateOrganizationResource ok
S9.4 check after that can_delete=true; no blockers
S9.5 org delete ok
S10.1 check can_delete=false; RESOURCES_EXIST resource/orderitem (2), resource/report (1), resource/transfer (1)
S10.2 delete failed_precondition; RESOURCES_EXIST resource/orderitem, resource/report, resource/transfer
S10.3 org row, projects, resources still live 1, 2, 7
S11.1 create an offline billing account ok
S11.2 check can_delete=false; NEGATIVE_TOKEN_BALANCE, RESOURCES_EXIST resource/orderitem (1)
S11.3 delete failed_precondition; NEGATIVE_TOKEN_BALANCE, RESOURCES_EXIST resource/orderitem
S11.4 error message organization cannot be deleted yet: billing account[<id>] owes 50 tokens: contact support to settle the balance, then retry the delete
S12.1 check with an unknown org id permission_denied
S12.2 check with an unknown org id, as a platform superuser permission_denied (pre-existing)
S12.3 check on a deleted org, as a platform superuser permission_denied (pre-existing)
OFF1 check with the config off can_delete=true; no blockers
OFF2 delete with the config off ok
OFF3 org row kept and marked deleted 1

@vercel

vercel Bot commented Oct 22, 2026 •

Copy link
Copy Markdown

The latest updates on your projects. Learn more about Vercel for GitHub.

Project Deployment Actions Updated
frontier Ready Ready Preview Oct 23, 2026 3:31am UTC

@coderabbitai

coderabbitai Bot commented Oct 22, 2026 •

Copy link
Copy Markdown
Contributor

Review in Change Stack →

Warning

Review limit reached

You've used all free OSS reviews for now. Wait for the free limit to reset to keep reviewing this public repository.

Next included review available in 51 minutes.

Check out review usage here.

View limit details

Limit details: You’ve used all 2 included reviews currently available.

Learn how review limits work.

Review configuration:

⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 3997c73 and 71275d9.

📒 Files selected for processing: 4
  • core/deleter/service.go
  • core/deleter/service_test.go
  • internal/api/v1beta1connect/deleter_test.go
  • test/e2e/regression/api_test.go

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
📥 Commits

Reviewing files that changed from the base of the PR and between 2e14945 and 3997c73.

📒 Files selected for processing: 4
  • core/deleter/service.go
  • core/deleter/service_test.go
  • internal/api/v1beta1connect/deleter_test.go
  • test/e2e/regression/api_test.go

Included review availability: This review used your included allowance. Your plan provides up to 2 included reviews per hour; 1 remain after this review.

📝 Summary

Summary by CodeRabbit

  • New Features
    • Organization deletion checks now include resources that cannot be deleted, alongside billing blockers.
    • Blockers are grouped by resource namespace and include counts.
  • Bug Fixes
    • Organizations with undeletable resources are prevented from being deleted, helping preserve the organization and its resources until blockers are cleared.
    • Resource-listing errors are surfaced during deletion checks.

Organization deletion checks now include resource blockers alongside billing blockers. The deletion path returns resource-listing errors. Tests cover blocker aggregation, API precondition violations, and end-to-end organization deletion behavior.

Changes: Organization Deletion

Layer / File(s) Summary
Collect organization resource blockers
core/deleter/service.go, core/deleter/service_test.go
The deletion and check paths pass the organization ID to blocker collection. The collector appends organization-resource blockers after billing blockers and returns resource-collection errors. Unit tests cover blocker aggregation and deletion stopping when blockers or collection errors occur.
Expose resource blockers in API responses
internal/api/v1beta1connect/deleter_test.go
API tests verify that resource blockers appear in deletion errors and organization deletion-check results alongside billing blockers.
Verify organization deletion behavior
test/e2e/regression/api_test.go
The regression test configures resource deletion rules and checks that a non-deletable resource blocks organization deletion, while a deletable resource is not counted as a blocker.

Priority: ➖ Normal

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

Change: Feature

Suggested reviewers: whoabhisheksah

Merge Risk: ⚪ Minimal · up to 3997c

Organization deletion now refuses when resource blockers exist, in addition to billing blockers, and the change is tested at service, API and end-to-end level. No merge-blocking risk was found. Maintainers may want to confirm in practice that blockers in disabled projects can be cleared.

Pre-merge checks: Passed 2
Check name Status Explanation
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.
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

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.

@coveralls

coveralls commented Oct 22, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 39895381862

Coverage increased (+0.009%) to 55.729%

Details

  • Coverage increased (+0.009%) from the base build.
  • Patch coverage: 8 of 8 lines across 1 file are fully covered (100%).
  • No coverage regressions found.

Uncovered Changes

No uncovered changes found.

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 41921
Covered Lines: 23362
Line Coverage: 55.73%
Coverage Strength: 19.39 hits per line

💛 - Coveralls

Comment thread core/deleter/service_test.go Outdated
Comment thread core/deleter/service.go
Comment thread test/e2e/regression/api_test.go
@AmanGIT07
AmanGIT07 force-pushed the feat/check-project-delete branch from 8f2fc33 to fefd216 Compare October 23, 2026 03:22
@AmanGIT07
AmanGIT07 force-pushed the feat/org-delete-resource-blockers branch from 8670b16 to 8884f71 Compare October 23, 2026 03:22
Base automatically changed from feat/check-project-delete to main October 23, 2026 03:29
DeleteOrganization and CheckOrganizationDelete also report the resources the
deletion config says block an org delete, in the same list as the billing
blockers, judged by the organization rules over the org's own resources and
every project's. The delete checks them before anything is canceled or deleted.
The e2e test adds a resource created with CreateOrganizationResource and
checks that it blocks the org delete on its own. A test comment that the
subtest name already says is removed.
@AmanGIT07
AmanGIT07 force-pushed the feat/org-delete-resource-blockers branch from 8884f71 to 71275d9 Compare October 23, 2026 03:30
@AmanGIT07
AmanGIT07 merged commit eb8cb0f into main Oct 23, 2026
8 checks passed
@AmanGIT07
AmanGIT07 deleted the feat/org-delete-resource-blockers branch October 23, 2026 03:36

This branch was successfully deployed

1 active deployment
Preview — 71275d9c Deployed Oct 23, 2026 by vercel[bot]
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.

3 participants