Skip to content

feat(deleter): delete disabled projects and groups with the org - #1976

Merged
rohilsurana merged 20 commits into
mainfrom
feat/org-delete-refuses-disabled-children
Oct 13, 2026
Merged

rohilsurana merged 20 commits into
mainfrom
feat/org-delete-refuses-disabled-children

Conversation

@rohilsurana

@rohilsurana rohilsurana commented Oct 8, 2026 •

Copy link
Copy Markdown
Member

What changes

  • DeleteOrganization now also deletes the org's disabled projects and disabled groups. It lists them in one query that includes disabled ones, and runs the same per-project and per-group delete on each.
  • Deleting a disabled group used to answer not_found, because two places read the group through a lookup that skips disabled groups. Both now find disabled groups: the group deleted hook in membership (one GetByIDs call with IncludeDisabled), and the group service delete. Group and project List now honor IncludeDisabled. The shared lookups used by other code are unchanged.
  • A disabled project already deleted fine. Only the org level list skipped it.
  • This changes the public DeleteGroup call too. Deleting a disabled group used to answer not_found. It now deletes the group, the same as a disabled project.

Why

The deleter listed projects and groups without the disabled ones, so it never visited them. Until now this was caught by accident. The final org row delete failed on the foreign key from projects or groups, and left the org with no owner.

The earlier PR in this stack makes the org row delete soft, so that foreign key never fires again. Without this change the org delete would succeed and leave the disabled projects and groups live, with their policies, under a deleted org.

Depends on

This branch is stacked on the PR that makes the org row delete soft. Ship them together.

Tests

  • Unit: the org delete visits disabled projects and disabled groups, and the org row is still last. An org with only enabled children behaves as before.
  • Unit: the group delete hook and the group service delete find a disabled group.
  • e2e: an org with enabled and disabled projects and groups is deleted. The policies of the disabled project and group are gone, and the org is gone. The test fails if the deleter lists only enabled children.

Closes #1978

@vercel

vercel Bot commented Oct 8, 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 13, 2026 7:56am UTC

@coderabbitai

coderabbitai Bot commented Oct 8, 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 33 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
  • Configuration used: Repository: raystack/frontier/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2ff6bf25-fefc-4721-b2b9-206fda635b12
📥 Commits

Reviewing files that changed from the base of the PR and between 705dfb3 and 6b36b82.

📒 Files selected for processing (14)
  • core/deleter/service.go
  • core/deleter/service_test.go
  • core/group/service.go
  • core/group/service_test.go
  • core/membership/group.go
  • core/membership/group_test.go
  • core/membership/mocks/group_service.go
  • core/membership/service.go
  • core/project/filter.go
  • internal/store/postgres/group_repository.go
  • internal/store/postgres/group_repository_test.go
  • internal/store/postgres/project_repository.go
  • internal/store/postgres/project_repository_test.go
  • test/e2e/regression/api_test.go

No actionable comments were generated in the recent review. 🎉

ℹ️ Recent review info
⚙️ Run configuration
  • Configuration used: Repository: raystack/frontier/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 2bd7d42e-2f61-4d50-96c6-c0f076b1e518
📥 Commits

Reviewing files that changed from the base of the PR and between caef5be and 705dfb3.

📒 Files selected for processing (14)
  • core/deleter/service.go
  • core/deleter/service_test.go
  • core/group/service.go
  • core/group/service_test.go
  • core/membership/group.go
  • core/membership/group_test.go
  • core/membership/mocks/group_service.go
  • core/membership/service.go
  • core/project/filter.go
  • internal/store/postgres/group_repository.go
  • internal/store/postgres/group_repository_test.go
  • internal/store/postgres/project_repository.go
  • internal/store/postgres/project_repository_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; 0 remain after this review.

📝 Summary

Summary by CodeRabbit

  • Bug Fixes

    • Deleting an organization now also removes its disabled projects and groups, along with their associated policies and relationships.
    • Deleting a disabled group now completes its cleanup correctly; missing groups continue to return a not-found error.
  • New Features

    • Project and group listings can include disabled records when explicitly requested. By default, disabled records remain excluded unless a state filter is specified.

Organization deletion now lists disabled projects and groups as well as enabled ones. Project and group list paths support including disabled records, and group deletion and membership cleanup can retrieve disabled groups. Tests cover list filtering, deletion order and errors, and end-to-end organization deletion.

Changes: Disabled child deletion

Layer / File(s) Summary
Include disabled records in lists
core/project/filter.go, internal/store/postgres/*_repository.go, internal/store/postgres/*_repository_test.go
Project and group repository lists include disabled records when requested. Tests verify that default lists still exclude them.
Retrieve disabled groups for deletion
core/membership/service.go, core/membership/mocks/group_service.go, core/group/service.go, core/group/service_test.go, core/membership/group.go, core/membership/group_test.go
Group deletion and membership cleanup retrieve groups with disabled records included. The lookup returns ErrNotExist when it finds no group.
Delete all organization children
core/deleter/service.go, core/deleter/service_test.go, test/e2e/regression/api_test.go
The organization cascade lists disabled projects and groups before deleting the organization. Tests cover child deletion, ordering, failure handling, and the end-to-end deletion flow.

Priority: ➖ Normal

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

Change: Bug fix · Severity of issue fixed: Medium

Suggested reviewers: whoabhisheksah

Merge Risk: 🔵 Low · up to 705df

Organization deletion now also removes disabled projects and groups, which prevents the organization from becoming inaccessible when those children block deletion of the organization row. The author reports that concurrent deletes of one organization may return internal errors even though the final state is correct. The change looks mergeable, but the author should confirm that behavior and ship the PR together with the organization soft-delete change it depends on.

Pre-merge checks · Passed 2
Passed Passed checks (2 passed)
Check name Status Explanation
Linked Issues check Passed Direct issue #1978 requires organization deletion to include disabled projects and groups. DeleteOrganization now lists both child types with disabled records included and sends each child through t…
Out of Scope Changes check Passed The changes stay within issue #1978. IncludeDisabled list behavior enables the cascade to find disabled children. The group service and membership lookup changes enable the existing DeleteGroup pa…
✨ Finishing Touches 💡 1
🛠️ Fix failing CI checks 💡
  • Commit to this branch
  • Create a new PR
  • 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 8, 2026 •

Copy link
Copy Markdown

Coverage Report for CI Build 38363489979

Coverage increased (+0.03%) to 54.79%

Details

  • Coverage increased (+0.03%) from the base build.
  • Patch coverage: 3 uncovered changes across 2 files (27 of 30 lines covered, 90.0%).
  • No coverage regressions found.

Uncovered Changes

File Changed Covered %
core/membership/group.go 10 8 80.0%
internal/store/postgres/project_repository.go 4 3 75.0%
Total (5 files) 30 27 90.0%

Coverage Regressions

No coverage regressions found.


Coverage Stats

Coverage Status
Relevant Lines: 41535
Covered Lines: 22757
Line Coverage: 54.79%
Coverage Strength: 18.87 hits per line

💛 - Coveralls

@rohilsurana rohilsurana changed the title feat(deleter): refuse an org delete while it has disabled projects or groups feat(deleter): delete disabled projects and groups with the org Oct 8, 2026
@rohilsurana
rohilsurana force-pushed the feat/org-row-soft-delete branch from e777a73 to 6b1fbfd Compare October 9, 2026 10:23
@rohilsurana
rohilsurana force-pushed the feat/org-delete-refuses-disabled-children branch from 17ac65f to 4da8268 Compare October 9, 2026 10:23
@rohilsurana
rohilsurana marked this pull request as ready for review October 9, 2026 10:23
@rohilsurana
rohilsurana force-pushed the feat/org-delete-refuses-disabled-children branch from 4da8268 to 8ff2e7c Compare October 9, 2026 10:42
@rohilsurana

Copy link
Copy Markdown
Member Author

Local sandbox test report for the organization soft delete stack

An independent run against a real local stack (Postgres, SpiceDB, the Frontier server). Nothing was fixed during the run, so it shows the stack as it is.

Code under test: the tip of this stack (#1971, #1972, #1973, #1976) merged with #1974.
Control: the same cases on main, to compare with the old behavior.
How fixtures were made: every org, project, group, role, policy, domain, invitation, PAT and service user was created through the API as a real logged in user, so the SpiceDB tuples are real. SQL was used only to read rows.

Summary

Result Cases
Pass 25
Fail 2 (the same issue, also present on main, see F1)
Not run 1 (billing views, billing is not configured in the sandbox)
Control runs on main 3 (the old behavior was reproduced)

After every delete these were checked: the API answers, the org row and all child rows, live policies and relations, SpiceDB tuples for every id of the org, the audit records, and the org lists (owner, admin, search, KYC).

What passed

Area Case Result
Baseline Owner deletes an org with projects, groups, members, a custom role, policies, an invitation, a domain and KYC Row kept with deleted_at, no live children, no live policies, no SpiceDB tuples, audit written
Baseline A superuser deletes the same kind of org Same
Disabled children Disabled project only Project gone, its policy gone, no tuples left
Disabled children Disabled group only Group gone, its policies and tuples gone, member no longer sees the org
Disabled children 2 enabled + 2 disabled projects and groups, including a disabled group as the principal of a policy All 8 children and all 19 policies gone, no tuples
Disabled children DeleteGroup called directly on a disabled group Works now (it answered not found before this PR)
Domains Several domains: pending, verified, removed before the org delete, re-added after removal Live ones get deleted_at and one domain.deleted record each. Already removed ones are untouched with no second record
Domains The same domain name in two orgs, one org deleted Only the deleted org's domain is removed, the other org keeps it
Domains Re-adding the deleted org's domain names to another org Accepted
PATs 5 PATs on the org (org scope, project scope, expiring, already deleted) and one PAT in another org All 5 stop working and are soft deleted with their policies gone. The PAT in the other org still works
Service users Org with service users, credentials and a JWK (regression check) No live service user or credential left, their auth fails
Org variants Disabled org Owner is blocked with failed_precondition, a superuser can delete it, row keeps state=disabled and gets deleted_at
Org variants Org with a pending invitation Invitation and its tuples are gone
Org variants Owner who is also a member of another org The other org is untouched
Retries SpiceDB stopped before the delete, then restarted The attempt fails with internal, nothing changes, the retry succeeds
Retries SpiceDB killed in the middle of the delete (151 projects), with and without domains The attempt fails part way, the org stays owned and readable, domains, PAT and invitation survive, the retry finishes clean with one organization.delete record
Lists Admin org list, search, CSV export, KYC list, user org lists Deleted orgs never appear (23 deleted and 9 live orgs, the export has exactly the 9 live names)
End of run SQL sweep over the whole database No live domain, PAT, project, group, role, service user, invitation or org policy under any of the 23 deleted orgs. The relations table matches SpiceDB tuple for tuple (779 = 779)

Name reuse after a soft delete

Case Result
Name reused after a baseline owner delete New org with a new id. It is fully usable: a project, a group and the old domain name can be created in it, and none of the old data shows up
Name reused after an org that had disabled projects and groups Same
Name reused after an org that had domains and PATs Same, the new org has no PATs
Name reused after a superuser delete Same
Create the name a third time after deleting the reused org Works
A second LIVE org with a live org's name Still refused with already_exists
Admin lists The name shows once, for the live org only

Failures and surprises

# What Caused by this stack? Notes
F1 A policy on a custom resource (for example compute/order) stays live after the org delete, with its rolebinding tuples. Seen for enabled and disabled projects No, the same happens on main DeleteProject lists only policies on the project itself, and the resource purge does not remove policies on the resource. Tracked separately
F2 After a delete, the owner, a platform admin and the bootstrap service account get permission_denied (not not_found) when they call by the deleted org's id. Only a lookup by name gives not_found No, main behaves the same after its hard delete The org's platform tuple goes with the delete, so superuser access goes too
F3 Three concurrent deletes of one org: one succeeds, the other two answer internal New The end state is correct every time: one organization.delete record, no leftovers. Only the error code and the log lines for the losers are unfriendly
F4 DeleteOrganization with a random uuid answers permission_denied, also for superusers Likely not, same mechanism as F2 By name it is not_found

Found along the way and not related to deletes: SearchOrganizations answers 500 for a name filter (#1979), and removing a member leaves policies on disabled projects and groups (#1980).

Control on main (old behavior)

Case What happened
Org with a disabled project The delete fails with a foreign key error and internal. The owner policy is already gone, so the org is left ownerless and stuck: the owner gets permission_denied, a retry fails
Normal org Hard delete. The row is gone, the database cascade removes domains and PATs
Policy on a custom resource Left behind, same as F1

Not tested

Item Why
Billing views and billing blockers Billing is not configured in the sandbox
DNS domain verification Needs real DNS. One domain was set to verified with SQL instead
KYC list skipping deleted orgs (#1974) beyond the trivial check The deleter removes the KYC row before the org row, so no API path leaves a deleted org with a KYC row
A failure injected exactly inside the domain delete step Not possible without code changes. The failure before that step shows the domains survive and go on the retry

@rohilsurana
rohilsurana force-pushed the feat/org-row-soft-delete branch from 144374a to 614bec4 Compare October 12, 2026 10:18
@rohilsurana
rohilsurana force-pushed the feat/org-delete-refuses-disabled-children branch from 7799eef to 194537b Compare October 12, 2026 10:18
Comment thread core/membership/group.go Outdated
Comment on lines +240 to +244
grp, err := s.groupService.Get(ctx, groupID)
if !errors.Is(err, group.ErrNotExist) {
return grp, err
}
disabled, listErr := s.groupService.List(ctx, group.Filter{GroupIDs: []string{groupID}, State: group.Disabled})

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.

Can be replaced by GetByIDs. It accepts filter Filter{IncludeDisabled: true}

Comment thread core/deleter/service.go Outdated
Comment on lines +549 to +556
enabled, err := d.projService.List(ctx, project.Filter{OrgID: orgID})
if err != nil {
return nil, err
}
disabled, err := d.projService.List(ctx, project.Filter{OrgID: orgID, State: project.Disabled})
if err != nil {
return nil, err
}

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.

Groups already have Filter.IncludeDisabled; GetByIDs honors it, but List ignores it. Can we make group List honor it, add the same field to project.Filter, and have the deleter make one list call per type instead of two? One query also means a project whose state changes between the two reads can't be missed or listed twice.

@AmanGIT07

Copy link
Copy Markdown
Contributor

This fixes #1978 (an org delete with a disabled project fails and leaves the org without owners). Can we add Closes #1978 to the description, so the issue closes when this lands?

Base automatically changed from feat/org-row-soft-delete to main October 13, 2026 07:05
@rohilsurana
rohilsurana merged commit 4785002 into main Oct 13, 2026
8 checks passed
@rohilsurana
rohilsurana deleted the feat/org-delete-refuses-disabled-children branch October 13, 2026 08:03

This branch was successfully deployed

1 active deployment
Preview — 6b36b822 Deployed Oct 13, 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.

Deleting an organization that has a disabled project fails and leaves it without owners

3 participants