You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
Several write paths don't fully reconcile the relational store (policies, relations, roles, permissions) with the SpiceDB datastore, so authorization data drifts over time: access that lingers after a delete, roles that grant more than they define, and stray permissions in the schema. PAT-specific gaps are tracked separately. Tracking here — I'll work on these.
1. Role deletion doesn't cascade to rolebindings / policies
core/roleDelete removes the role's own permission relations (app/role:<id>#<perm>) but leaves the rolebindings that reference it (app/rolebinding:<id>#role@app/role:<id>) and the policies rows using it. A deleted role can keep granting access via surviving rolebindings, invisible/unremovable through the API. Fix: on role delete, remove dependent policies (and their rolebinding tuples) first.
2. Role migration is add-only / doesn't prune
core/roleUpdate correctly prunes removed permission relations, but the boot-time predefined-role migration in internal/bootstrap (a) rewrites predefined roles without removing dropped permission relations, and (b) never deletes roles removed from the schema. Result: narrowed roles still grant old permissions; removed roles persist. Fix: during role migration, diff existing vs desired and delete removed permission relations and removed roles.
3. Invitations don't fully remove their SpiceDB relations
core/invitation creates two relations on create (invitation#user@user and invitation#org@organization) but Delete removes only the org relation — the user relation is never deleted, and accept/expire don't clean up either. Fix: delete both relations on accept / expire / delete.
4. No database backstop for one role per (resource, principal)
Membership APIs enforce one role per principal per resource (replace-not-add), but the policies unique constraint includes role_id, so the generic policy-create path / direct writes can still create multiple roles for the same principal on the same resource. Fix: tighten the policies unique constraint to (resource_id, resource_type, principal_id, principal_type) (after de-duplicating) and route writes through an upsert.
5. Ad-hoc permissions can pollute the compiled schema
internal/bootstrap builds the SpiceDB schema partly from the permissions table, so permissions created outside the intended resource definitions (e.g. via the create-permission API) get compiled into the schema and leave app/role relations/tuples not in the canonical base + generated schema. Nothing reconciles this. Fix: reconcile the permissions table against the canonical resource definitions; drop permissions not in the compiled schema and prune relations no longer defined. (Resource instances are unaffected — only stray permission definitions.)
6. permission delete is a no-op
core/permissionDelete removes only the DB row (with a comment acknowledging it doesn't clean relations), leaving every app/role:<id>#<perm> relation that referenced it — roles keep granting a permission that no longer exists. Fix: remove the role↔permission relations on permission delete (or block delete while in use).
core/resourceUpdate is DB-only. Changing a resource's project (transfer) or owner updates the row but not SpiceDB — the old resource#project@project:<old> / resource#owner@<old> stays and the new one isn't added, so the resource is linked to both projects / has stale owners. Fix: reconcile #project and #owner relations on update (delete old, add new).
8. Decide & document disable-vs-revoke
Disabling an org / project / group / user sets state=disabled but leaves all SpiceDB tuples/policies; access is gated by the app's disabled-state check, not by SpiceDB. If any check reads SpiceDB directly, disabled entities still authorize. Fix: decide whether disable should suspend tuples; document the chosen behavior and add a test.
Namespace has no delete path; removing one from config would leave dangling relations.
Platform admins removed from the admin config between boots aren't pruned.
The raw relation API (CreateRelation) is admin-only and has no lifecycle owner by design.
A consistency check between the relational store and SpiceDB (policies/relations/roles ↔ tuples) would catch this class of drift early — consider a maintenance command or a test-time invariant.
Each fix should ship with a test that performs the original action (delete a role, narrow/remove a predefined role, accept an invitation, attempt a duplicate policy, transfer a resource, boot with a stray permission) and asserts no orphaned tuples / duplicates remain.
Checklist
1. Cascade role delete → dependent rolebindings + policies — done in fix(role): reject deletion of a role still in use by policies #1683. Shipped as a protective block rather than a cascade: role.Delete is rejected ("role in use") while any policy still references it (gated by the policies → roles FK). Remove the policies first; the role's own perm tuples are cleaned once it's actually deletable.
4. One-role policies unique constraint + upsert writes — not done. The membership APIs already enforce one role per principal per resource; the constraint-level backstop in this item is still open.
5. Reconcile permissions table ↔ compiled schema; prune undefined relations — deferred, no safe auto-guard. A boot-time prune would delete valid permissions created at runtime via the create-permission API (not in any config file). The safe per-permission delete shipped in fix(permission): clean up leftover access in SpiceDB when a permission is deleted #1685; clearing existing stray rows is a one-off data-cleanup, not an automatic guard.
8. Decide & document disable-vs-revoke semantics — done in docs(authz): document disable vs delete semantics #1688. Decided to keep tuples on disable (so re-enable restores access); documented as intentional, gated by the read-time disabled-state check.
Several write paths don't fully reconcile the relational store (
policies,relations,roles,permissions) with the SpiceDB datastore, so authorization data drifts over time: access that lingers after a delete, roles that grant more than they define, and stray permissions in the schema. PAT-specific gaps are tracked separately. Tracking here — I'll work on these.1. Role deletion doesn't cascade to rolebindings / policies
core/roleDeleteremoves the role's own permission relations (app/role:<id>#<perm>) but leaves the rolebindings that reference it (app/rolebinding:<id>#role@app/role:<id>) and thepoliciesrows using it. A deleted role can keep granting access via surviving rolebindings, invisible/unremovable through the API.Fix: on role delete, remove dependent policies (and their rolebinding tuples) first.
2. Role migration is add-only / doesn't prune
core/roleUpdatecorrectly prunes removed permission relations, but the boot-time predefined-role migration ininternal/bootstrap(a) rewrites predefined roles without removing dropped permission relations, and (b) never deletes roles removed from the schema. Result: narrowed roles still grant old permissions; removed roles persist.Fix: during role migration, diff existing vs desired and delete removed permission relations and removed roles.
3. Invitations don't fully remove their SpiceDB relations
core/invitationcreates two relations on create (invitation#user@userandinvitation#org@organization) butDeleteremoves only the org relation — the user relation is never deleted, and accept/expire don't clean up either.Fix: delete both relations on accept / expire / delete.
4. No database backstop for one role per (resource, principal)
Membership APIs enforce one role per principal per resource (replace-not-add), but the
policiesunique constraint includesrole_id, so the generic policy-create path / direct writes can still create multiple roles for the same principal on the same resource.Fix: tighten the
policiesunique constraint to(resource_id, resource_type, principal_id, principal_type)(after de-duplicating) and route writes through an upsert.5. Ad-hoc permissions can pollute the compiled schema
internal/bootstrapbuilds the SpiceDB schema partly from thepermissionstable, so permissions created outside the intended resource definitions (e.g. via the create-permission API) get compiled into the schema and leaveapp/rolerelations/tuples not in the canonical base + generated schema. Nothing reconciles this.Fix: reconcile the
permissionstable against the canonical resource definitions; drop permissions not in the compiled schema and prune relations no longer defined. (Resource instances are unaffected — only stray permission definitions.)6.
permissiondelete is a no-opcore/permissionDeleteremoves only the DB row (with a comment acknowledging it doesn't clean relations), leaving everyapp/role:<id>#<perm>relation that referenced it — roles keep granting a permission that no longer exists.Fix: remove the role↔permission relations on permission delete (or block delete while in use).
7.
resourceupdate doesn't reconcile#project/#ownercore/resourceUpdateis DB-only. Changing a resource's project (transfer) or owner updates the row but not SpiceDB — the oldresource#project@project:<old>/resource#owner@<old>stays and the new one isn't added, so the resource is linked to both projects / has stale owners.Fix: reconcile
#projectand#ownerrelations on update (delete old, add new).8. Decide & document disable-vs-revoke
Disabling an org / project / group / user sets
state=disabledbut leaves all SpiceDB tuples/policies; access is gated by the app's disabled-state check, not by SpiceDB. If any check reads SpiceDB directly, disabled entities still authorize.Fix: decide whether disable should suspend tuples; document the chosen behavior and add a test.
Lower-priority / by-design notes
CreateRelation) is admin-only and has no lifecycle owner by design.Each fix should ship with a test that performs the original action (delete a role, narrow/remove a predefined role, accept an invitation, attempt a duplicate policy, transfer a resource, boot with a stray permission) and asserts no orphaned tuples / duplicates remain.
Checklist
role.Deleteis rejected ("role in use") while any policy still references it (gated by thepolicies → rolesFK). Remove the policies first; the role's own perm tuples are cleaned once it's actually deletable.role.Update, which rewrites the full set (adds new perm tuples, prunes dropped ones).policiesunique constraint + upsert writes — not done. The membership APIs already enforce one role per principal per resource; the constraint-level backstop in this item is still open.permissionstable ↔ compiled schema; prune undefined relations — deferred, no safe auto-guard. A boot-time prune would delete valid permissions created at runtime via the create-permission API (not in any config file). The safe per-permission delete shipped in fix(permission): clean up leftover access in SpiceDB when a permission is deleted #1685; clearing existing stray rows is a one-off data-cleanup, not an automatic guard.permissiondelete removes role↔permission relations — done in fix(permission): clean up leftover access in SpiceDB when a permission is deleted #1685. Delete now removes the role→permission grant tuples and the permission from each role's list (or the row stays if removal fails, logged).resourceupdate reconciles#project/#owner— on hold, design first (Resource update: reconcile SpiceDB #project/#owner, but first decide project-move + AuthZ + billing behaviour #1703). The reconcile code is written and tested, but the update path also runs an AuthZ check and a billing-entitlement check that were never designed for a project move (especially a cross-org move): the AuthZ check only verifiesupdateon the resource, not any right on the target project, and the entitlement check runs against the target project's plan. PR fix(resource): reconcile #project/#owner relations on update #1686 is closed pending that decision.