What happens
Removing a user from an organization with the RemoveOrganizationMember RPC deletes their org, project, and group policies and membership relations. But their policies on custom resources (for example resource/aoi) in the org's projects are not touched. The policy row and its SpiceDB rolebinding tuple stay live.
Why
The handler calls membership.RemoveOrganizationMember, and the membership cascade only covers org, project, and group scopes. Custom-resource policy cleanup lives in resource.RemovePrincipalAccess (#1800), which is only called from the deleter during user deletion (DeleteUser). The org-removal path never reaches it.
Consequence
- An ex-member keeps a live policy on a resource inside an org they were removed from. If they rejoin, or if the permission check does not require org membership, the old access still applies.
- If the user is later deleted with
DeleteUser, the policy row is orphaned: the deleter cleans resource policies per org the user is still a member of, and the user is no longer in any. The user row and SpiceDB tuples are deleted, but the policy row stays in Postgres pointing at a deleted user.
Reproduce (verified on a local server built from #1800)
- Create an org, a project, and a custom resource (
resource/aoi) in the project.
- Add a user to the org. Create a custom role with
resource_aoi_* permissions and a policy for the user on the resource.
- Call
RemoveOrganizationMember for the user.
- The user's org and project policies are gone. The resource policy row and its
app/rolebinding tuple remain.
- Call
DeleteUser for the same user. The user row and tuples are gone; the resource policy row remains, orphaned.
Possible fix
Have the org-removal path clean the member's resource policies too, for example by calling resource.RemovePrincipalAccess with the org's project IDs from membership.RemoveOrganizationMember (or from the handler). Separately, DeleteUser could delete any remaining policies by principal id as a final sweep so no orphan rows survive.
What happens
Removing a user from an organization with the
RemoveOrganizationMemberRPC deletes their org, project, and group policies and membership relations. But their policies on custom resources (for exampleresource/aoi) in the org's projects are not touched. The policy row and its SpiceDB rolebinding tuple stay live.Why
The handler calls
membership.RemoveOrganizationMember, and the membership cascade only covers org, project, and group scopes. Custom-resource policy cleanup lives inresource.RemovePrincipalAccess(#1800), which is only called from the deleter during user deletion (DeleteUser). The org-removal path never reaches it.Consequence
DeleteUser, the policy row is orphaned: the deleter cleans resource policies per org the user is still a member of, and the user is no longer in any. The user row and SpiceDB tuples are deleted, but the policy row stays in Postgres pointing at a deleted user.Reproduce (verified on a local server built from #1800)
resource/aoi) in the project.resource_aoi_*permissions and a policy for the user on the resource.RemoveOrganizationMemberfor the user.app/rolebindingtuple remain.DeleteUserfor the same user. The user row and tuples are gone; the resource policy row remains, orphaned.Possible fix
Have the org-removal path clean the member's resource policies too, for example by calling
resource.RemovePrincipalAccesswith the org's project IDs frommembership.RemoveOrganizationMember(or from the handler). Separately,DeleteUsercould delete any remaining policies by principal id as a final sweep so no orphan rows survive.