CasbinPolicySyncPolicyTrait
Casbin policy synchronization handlers for the `policies` collection.
Handles the policy branch of the live RBAC sync : the policy_has_permissions
propagation (when a permission is added to / removed from a policy, every
subject currently attached to that policy gets its Casbin tuples kept in
sync), and the cascade-aware cleanup that runs before a policy or
permission vertex is deleted (since the cascade edge purge bypasses
per-edge afterDelete signals).
The cleanup methods cover both direct subjects (role / user / service via
*_has_permissions) and the indirect "policy materialised on subject"
path (via policy_has_permissions × service_has_policies ×
role_has_policies).
The trait expects the consumer class to expose the following protected properties (already declared on CasbinPolicySync via constructor promotion) :
?Enforcer $enforcerstring $domain?Documents $permissionsModel?Documents $policiesModel?Edges $policyHasPermissions?Edges $roleHasPermissions?Edges $roleHasPolicies?Edges $userHasPermissions?Edges $serviceHasPermissions?Edges $serviceHasPolicies?LoggerInterface $logger
Plus the cross-trait helpers CasbinPolicySyncRoleTrait::resolveRoleSubject, CasbinPolicySyncUserTrait::resolveUserIdentifier, CasbinPolicySyncServiceTrait::resolveServiceSubject, CasbinPolicySyncServiceTrait::resolveServicesForPolicy, CasbinPolicySyncServiceTrait::removeServicePolicyPolicies and CasbinPolicySyncRoleTrait::removeRolePolicyPolicies.
Tags
Table of Contents
Methods
- cleanupPermissionDerivedPolicies() : void
- Removes every Casbin policy derived from a permission about to be deleted.
- cleanupPolicyDerivedPolicies() : void
- Removes every Casbin policy derived from attaching the given policy to a subject (service OR role), for every subject currently holding the policy.
- registerPermissionDelete() : void
- Registers cleanup of Casbin tuples when a permission vertex is about to be deleted.
- registerPolicyDelete() : void
- Registers cleanup of Casbin tuples when a policy vertex is about to be deleted.
- addPolicyPermissionPolicy() : void
- Propagates a `policy_has_permissions` insertion to every subject (service or role) currently attached to the policy.
- onPermissionDelete() : void
- Called before one or more permission vertices are deleted.
- onPolicyDelete() : void
- Called before one or more policy vertices are deleted.
- removePolicyPermissionPolicy() : void
- Propagates a `policy_has_permissions` deletion to every service AND every role currently attached to the policy.
- loadPolicyPermissionContext() : array{permission: object, serviceKeys: string[], roleKeys: string[], domain: string, object: string, action: string, effect: string}|null
- Loads the shared context used by `addPolicyPermissionPolicy` and `removePolicyPermissionPolicy` : looks up the permission, resolves every service AND role currently attached to the policy, and returns a precomputed bundle. Returns null when nothing can be done (no enforcer, missing permission, no attached subjects).
- purgePoliciesByEdges() : int
- Lists every edge of the given relation pointing to `$permissionId`, resolves each `_from` vertex into its Casbin subject through `$resolver`, and removes the matching `(subject, domain, object, action, effect)` policy via the Enforcer.
- purgePoliciesViaPolicy() : int
- Removes every Casbin policy derived from the permission via the indirect "policy materialised on subject" path: lists policies that hold the permission, then for each policy lists the services and roles that hold the policy, and removes the `(subject, domain, object, action, effect)` policy keyed on each of them.
- resolveRolesForPolicy() : array<string|int, string>
- Lists every role currently attached to the given policy via the `role_has_policies` edge.
Methods
cleanupPermissionDerivedPolicies()
Removes every Casbin policy derived from a permission about to be deleted.
public
cleanupPermissionDerivedPolicies(string $permissionKey) : void
Motivation — a permission is not itself a Casbin subject. It surfaces in
the (object, action, effect) tuple of every policy that references it,
keyed on a subject (role identifier, user identifier or service
identifier). When the permission vertex is deleted, the native cascade
purges the inbound edges (role_has_permissions,
user_has_permissions, service_has_permissions and
policy_has_permissions) via raw AQL REMOVE, which bypasses per-edge
afterDelete signals — so the corresponding removePolicy calls never
fire and policies survive in the rbac collection as orphan rows. Any
subject keyed on those rows (role / user / M2M service) keeps passing
Casbin checks against an object/action that no longer exists.
Strategy (approach B — targeted by edges) :
- Read the permission to capture
(domain, object, action, effect). - Walk each direct inbound edge type, resolve every subject's stable
identifier and call
removePolicy(subject, ...)once per match. - Walk
policy_has_permissionsto find policies that hold this permission, then for each policy walkservice_has_policies/role_has_policiesto find every subject that holds that policy, andremovePolicykeyed on the subject's identifier (this covers the indirect "policy materialised on subject" path that no direct edge expresses).
This method must be called before the permission vertex is removed from Arango — once the cascade fires, the inbound edges are gone and we can no longer enumerate the subjects to purge.
Parameters
- $permissionKey : string
-
The
_keyof the permission about to be deleted.
Tags
cleanupPolicyDerivedPolicies()
Removes every Casbin policy derived from attaching the given policy to a subject (service OR role), for every subject currently holding the policy.
public
cleanupPolicyDerivedPolicies(string $policyKey) : void
Motivation — policies are not Casbin subjects: when a policy is attached
to a service via service_has_policies (or to a role via
role_has_policies), the corresponding edge listener materialises one
Casbin policy per permission in the policy, keyed by the subject :
p, service:<service._key>, <domain>, <object>, <action>, <effect>
p, <roleIdentifier>, <domain>, <object>, <action>, <effect>
Deleting a policy triggers a cascade purge of the *_has_policies
edges via raw AQL REMOVE, which bypasses per-edge afterDelete
signals — so the derived policies would survive in the rbac
collection and M2M subjects would keep passing Casbin checks for
capabilities that no longer exist. Silent security gap.
This method must be called before the policy vertex is removed from
Arango (it reads policy.permissions and the current *_has_policies
edges, which both disappear once the cascade runs).
Parameters
- $policyKey : string
-
The
_keyof the policy about to be deleted.
Tags
registerPermissionDelete()
Registers cleanup of Casbin tuples when a permission vertex is about to be deleted.
public
registerPermissionDelete(Documents $permissionsModel) : void
Subscribes to the permissions model's beforeDelete signal (not
afterDelete like role / user / service): a permission is not itself
a Casbin subject, so the tuples to purge are keyed on the role / user /
service vertices that point to it. We must enumerate those subjects
via the inbound *_has_permissions and policy_has_permissions edges
before the cascade edge purge fires — once afterDelete runs,
the edges are gone and the subject set is unrecoverable.
Wired alongside the existing registerRoleDelete / registerUserDelete
/ registerServiceDelete so every delete path (HTTP controller, CLI
command, raw model call, future transitive cascade) automatically
cleans the rbac collection.
Parameters
- $permissionsModel : Documents
-
The permissions vertex model.
registerPolicyDelete()
Registers cleanup of Casbin tuples when a policy vertex is about to be deleted.
public
registerPolicyDelete(Documents $policiesModel) : void
Symmetric to registerPermissionDelete but for policies. Connects
to beforeDelete because the cleanup walks *_has_policies and
policy_has_permissions edges that the cascade purge wipes via raw
AQL (bypassing per-edge afterDelete signals).
Parameters
- $policiesModel : Documents
-
The policies vertex model.
addPolicyPermissionPolicy()
Propagates a `policy_has_permissions` insertion to every subject (service or role) currently attached to the policy.
protected
addPolicyPermissionPolicy(string $policyKey, string $permissionKey) : void
Walks both service_has_policies and role_has_policies for the
policy, then for each attached subject adds one Casbin row :
p, service:<service._key>, <domain>, <object>, <action>, <effect>
p, <roleIdentifier>, <domain>, <object>, <action>, <effect>
Without this propagation, an admin attaching a permission to a policy
would only update the rbac collection at the next full materialization
(php bin/console.php auth:materialize) — subjects already attached to the policy
would keep operating on a stale permission set until then.
Casbin's addPolicy is idempotent (returns false on duplicates) so a
subject already granted the same permission via another source (a
sibling policy or a direct *_has_permissions edge) keeps a single
row in rbac.
Parameters
- $policyKey : string
-
The policy ArangoDB
_key. - $permissionKey : string
-
The permission ArangoDB
_key.
Tags
onPermissionDelete()
Called before one or more permission vertices are deleted.
protected
onPermissionDelete(Payload $payload) : void
The payload context mirrors the $init array passed to
Documents::delete() — the keys to purge live in Arango::VALUE
(always normalised to an array by DocumentsControllerDeleteTrait).
Each key triggers the existing edge-walk + Enforcer purge.
Parameters
- $payload : Payload
onPolicyDelete()
Called before one or more policy vertices are deleted.
protected
onPolicyDelete(Payload $payload) : void
Same payload contract as onPermissionDelete: keys come from
context[Arango::VALUE]. Each key triggers the existing
service / role attached-subjects purge.
Parameters
- $payload : Payload
removePolicyPermissionPolicy()
Propagates a `policy_has_permissions` deletion to every service AND every role currently attached to the policy.
protected
removePolicyPermissionPolicy(string $policyKey, string $permissionKey) : void
Symmetric to addPolicyPermissionPolicy: removes the
(subject, domain, object, action, effect) row keyed on each
attached subject (the service's namespaced service:{_key} or
the role identifier). Subjects that grant the same permission
via a sibling policy or a direct *_has_permissions edge will
lose the row here — this is a known limitation shared with the
existing removeService* / removeRole* handlers (no overlap
audit). A full reseed (php bin/console.php auth:materialize) restores the
rows from any remaining source.
Parameters
- $policyKey : string
-
The policy ArangoDB
_key. - $permissionKey : string
-
The permission ArangoDB
_key.
Tags
loadPolicyPermissionContext()
Loads the shared context used by `addPolicyPermissionPolicy` and `removePolicyPermissionPolicy` : looks up the permission, resolves every service AND role currently attached to the policy, and returns a precomputed bundle. Returns null when nothing can be done (no enforcer, missing permission, no attached subjects).
private
loadPolicyPermissionContext(string $policyKey, string $permissionKey) : array{permission: object, serviceKeys: string[], roleKeys: string[], domain: string, object: string, action: string, effect: string}|null
Lifted out of the two propagation handlers to avoid duplicating the early-return + lookup + (domain, object, action, effect) extraction. Keeps both handlers down to a clean two-loop body.
Parameters
- $policyKey : string
-
The policy ArangoDB
_key. - $permissionKey : string
-
The permission ArangoDB
_key.
Tags
Return values
array{permission: object, serviceKeys: string[], roleKeys: string[], domain: string, object: string, action: string, effect: string}|nullpurgePoliciesByEdges()
Lists every edge of the given relation pointing to `$permissionId`, resolves each `_from` vertex into its Casbin subject through `$resolver`, and removes the matching `(subject, domain, object, action, effect)` policy via the Enforcer.
private
purgePoliciesByEdges(Edges $edges, string $permissionId, callable $resolver, string $domain, string $object, string $action, string $effect) : int
Used by cleanupPermissionDerivedPolicies for the three direct
inbound relations: role_has_permissions, user_has_permissions and
service_has_permissions.
Parameters
- $edges : Edges
-
The inbound edge relation.
- $permissionId : string
-
The permission's
_id(e.g.permissions/123). - $resolver : callable
-
Maps an Arango
_keyto its Casbin subject string, or null if the subject cannot be resolved. - $domain : string
- $object : string
- $action : string
- $effect : string
Tags
Return values
int —The number of policies actually removed.
purgePoliciesViaPolicy()
Removes every Casbin policy derived from the permission via the indirect "policy materialised on subject" path: lists policies that hold the permission, then for each policy lists the services and roles that hold the policy, and removes the `(subject, domain, object, action, effect)` policy keyed on each of them.
private
purgePoliciesViaPolicy(string $permissionId, string $domain, string $object, string $action, string $effect) : int
Used by cleanupPermissionDerivedPolicies. Direct edges
are already covered by purgePoliciesByEdges; this method
covers the case where the policy was materialised by the
service_policy / role_policy listener and no direct
service_has_permissions / role_has_permissions edge ever
existed.
Parameters
- $permissionId : string
- $domain : string
- $object : string
- $action : string
- $effect : string
Tags
Return values
int —The number of policies actually removed.
resolveRolesForPolicy()
Lists every role currently attached to the given policy via the `role_has_policies` edge.
private
resolveRolesForPolicy(string $policyKey) : array<string|int, string>
Parameters
- $policyKey : string
-
The policy ArangoDB
_key.
Return values
array<string|int, string> —Distinct role _keys — empty array on failure or
when no role is attached.