refactor(group): move group-delete cleanup into membership package#1615
Conversation
|
The latest updates on your projects. Learn more about Vercel for GitHub.
|
|
Warning Rate limit exceeded
You’ve run out of usage credits. Purchase more in the billing tab. ⌛ How to resolve this issue?After the wait time has elapsed, a review can be triggered using the We recommend that you space out your commits to avoid hitting the rate limit. 🚦 How do rate limits work?CodeRabbit enforces hourly rate limits for each developer per organization. Our paid plans have higher rate limits than the trial, open-source and free plans. In all cases, we re-allow further reviews after a brief timeout. Please see our FAQ for further information. ℹ️ Review info⚙️ Run configurationConfiguration used: Organization UI Review profile: CHILL Plan: Pro Run ID: 📒 Files selected for processing (13)
📝 WalkthroughWalkthroughIntroduce MembershipService cleanup APIs and call them from the cascade deleter: add RemoveAllGroupMembers, OnGroupDeleted, and removeGroupAsPrincipalPolicies; replace group.Service.Delete with DeleteModel and wire membership cleanup into the cascade deleter. ChangesGroup deletion refactoring
Estimated code review effort🎯 4 (Complex) | ⏱️ ~45 minutes Possibly related PRs
Suggested reviewers
🚥 Pre-merge checks | ✅ 2✅ Passed checks (2 passed)
✏️ Tip: You can configure your own custom pre-merge checks in the settings. 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. Comment |
Coverage Report for CI Build 25907737689Coverage increased (+0.08%) to 42.311%Details
Uncovered Changes
Coverage RegressionsNo coverage regressions found. Coverage Stats
💛 - Coveralls |
There was a problem hiding this comment.
Actionable comments posted: 2
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: 1aaa74b4-fcef-4791-b54f-6a2ef06dd697
📒 Files selected for processing (13)
cmd/serve.gocore/deleter/mocks/group_service.gocore/deleter/mocks/membership_service.gocore/deleter/service.gocore/deleter/service_test.gocore/group/service.gocore/membership/service.gocore/membership/service_test.gointernal/api/v1beta1connect/group.gointernal/api/v1beta1connect/group_test.gointernal/api/v1beta1connect/interfaces.gointernal/api/v1beta1connect/mocks/cascade_deleter.gointernal/api/v1beta1connect/mocks/group_service.go
💤 Files with no reviewable changes (1)
- internal/api/v1beta1connect/mocks/group_service.go
- core/group: use schema.GroupNamespace (semantic constant for the group-as-resource Object) instead of schema.GroupPrincipal in DeleteModel's wildcard relation delete. Both values are the same string today, but GroupNamespace is the correct constant for this call site. - core/membership.RemoveAllGroupMembers: split into two passes. First pass attempts every policy delete and tracks principals with any failure. Second pass clears direct relations only for principals whose policies were all deleted. This avoids leaving a surviving policy without its matching SpiceDB tuples when a per-policy delete fails mid-loop — the previous code could strip relations after one policy succeeded for a principal even though a later policy delete for the same principal failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
There was a problem hiding this comment.
Actionable comments posted: 1
ℹ️ Review info
⚙️ Run configuration
Configuration used: Path: .coderabbit.yaml
Review profile: CHILL
Plan: Pro
Run ID: e53ac88a-4d76-4836-bd01-b6c8cbf1f29b
📒 Files selected for processing (3)
core/group/service.gocore/membership/service.gocore/membership/service_test.go
🚧 Files skipped from review as they are similar to previous changes (1)
- core/membership/service_test.go
Manual Testing ResultsTested group deletion scenarios with fresh org
Setup
Verified Behaviors
|
- core/group: use schema.GroupNamespace (semantic constant for the group-as-resource Object) instead of schema.GroupPrincipal in DeleteModel's wildcard relation delete. Both values are the same string today, but GroupNamespace is the correct constant for this call site. - core/membership.RemoveAllGroupMembers: split into two passes. First pass attempts every policy delete and tracks principals with any failure. Second pass clears direct relations only for principals whose policies were all deleted. This avoids leaving a surviving policy without its matching SpiceDB tuples when a per-policy delete fails mid-loop — the previous code could strip relations after one policy succeeded for a principal even though a later policy delete for the same principal failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
245e293 to
8641d84
Compare
Mirrors the project deletion pattern (handler → deleter → DeleteModel + membership cleanup): - core/membership.RemoveAllGroupMembers: deletes every policy on the group and every per-principal owner/member relation. No min-owner check; the group itself is being destroyed. - core/group.Service.DeleteModel: minimal entity teardown — Object-side relations on the group + repo delete + audit. Replaces the previous combined Delete method that mixed membership cleanup into the entity service. - core/deleter.DeleteGroup: orchestrator that calls RemoveAllGroupMembers then DeleteModel. Wired into the DeleteOrganization cascade and exposed via the CascadeDeleter interface. - DeleteGroup handler routes through deleterService instead of groupService directly. group.RemoveUsers / removeUsers stay — they serve the single-user remove path in deleter.DeleteUser (cascade where the user is being destroyed system-wide). Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
DeleteModel only clears Object-side relations on the group, which misses the inverse hierarchy tuple created in OnGroupCreated: organization#member@group#member (Object is the org, Subject is the group — pre-existing leak in the old group.Service.Delete; surfaced during this refactor's audit.) Adds membership.OnGroupDeleted as the symmetric counterpart to OnGroupCreated: bundles RemoveAllGroupMembers + unlinkGroupFromOrg so both hierarchy directions plus member policies/relations are cleaned up in one call. deleter.DeleteGroup now invokes it before DeleteModel. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
OnGroupDeleted previously only deleted policies where the group was the *resource* (memberships into the group). Policies where the group is the *principal* on some other resource — e.g. CreatePolicy granting a group access to a project — were leaked when the group was destroyed, leaving orphaned bindings. Adds removeGroupAsPrincipalPolicies which queries policies by principal and deletes them, called from OnGroupDeleted alongside the existing member + hierarchy cleanup. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
- core/group: use schema.GroupNamespace (semantic constant for the group-as-resource Object) instead of schema.GroupPrincipal in DeleteModel's wildcard relation delete. Both values are the same string today, but GroupNamespace is the correct constant for this call site. - core/membership.RemoveAllGroupMembers: split into two passes. First pass attempts every policy delete and tracks principals with any failure. Second pass clears direct relations only for principals whose policies were all deleted. This avoids leaving a surviving policy without its matching SpiceDB tuples when a per-policy delete fails mid-loop — the previous code could strip relations after one policy succeeded for a principal even though a later policy delete for the same principal failed. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
Wildcard Object-side relation sweep now lives in membership.OnGroupDeleted alongside the policy/member/hierarchy cleanup. group.DeleteModel is reduced to entity-only cleanup (repo delete + audit log) so all SpiceDB writes are owned by a single package. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
The sweep was a workaround for a leak in policy.Delete (resource-side grant tuple not cleaned up — tracked in #1617). It doesn't belong in the membership package: every relation and policy created during a group's lifetime is now matched 1:1 with an explicit delete via the same service primitive. The leak will be addressed at policy.Delete. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
8641d84 to
2b30c2a
Compare
Mirrors
project's deletion pattern (handler → deleter orchestrator →DeleteModel+ membership cleanup) and centralises every SpiceDB write performed during a group's lifetime in one package.What we create vs. what we delete (service-level audit)
A group's lifetime touches SpiceDB through exactly two service primitives. From that abstraction, every write must be matched by an explicit delete on the same primitive.
relation.Servicewrites (outsidepolicy.AssignRole)linkGroupToOrggroup#org@organizationunlinkGroupFromOrglinkGroupToOrgorganization#member@group#memberunlinkGroupFromOrgSetGroupMemberRole(add path)group#owner@user/group#member@userRemoveAllGroupMembers→removeRelationspolicy.ServicewritesSetGroupMemberRoleresource=group, principal=userRemoveAllGroupMembers→policyService.Deleteprincipal=group, resource=<other>(group granted role elsewhere)removeGroupAsPrincipalPolicies→policyService.DeleteEverything created via the group/membership code path is matched 1:1 with an explicit delete via the same primitive. No wildcard sweeps, no defensive cleanup.
Orchestration
core/deleter.DeleteGroup(id)is the new orchestrator:membership.OnGroupDeleted(id)— tears down all SpiceDB state above. Symmetric counterpart toOnGroupCreated.group.Service.DeleteModel(id)— drops the entity row + emits audit. Pure entity cleanup, no SpiceDB writes.RemoveAllGroupMembersuses a two-pass approach: delete every policy first, track which principals had a failure, then clean up direct relations only for principals whose policies were all deleted. Prevents stripping a principal's relations while a surviving policy still references them; a retry can finish the job.Known leak (separate, broader)
policy.AssignRolewrites three SpiceDB tuples butpolicy.Deleteonly cleans two of them — the resource→rolebinding grant tuple leaks on every policy removal across the codebase (org / project / group member removal all hit it). The historical wildcardDelete(Object: <resource>)sweep ingroup.DeleteModel/project.DeleteModelwas a workaround that only covered the same-resource case. Tracked separately in #1617 — not in scope here.Other pieces
DeleteGrouphandler routes throughdeleterServiceinstead of callinggroupServicedirectly.group.RemoveUsers/removeUsersstay — they serve the single-user cascade indeleter.DeleteUser(user is being destroyed system-wide), same dual-track as org.Test plan
go build ./...go test ./core/membership/... ./core/group/... ./core/deleter/... ./internal/api/v1beta1connect/...RemoveAllGroupMembers(principal-dedupe, partial-failure leaves relations for the failed principal intact) andOnGroupDeleted(full three-way cleanup, group-not-found). Existing deleter cascade test updated.