Oihana PHP Arango

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 $enforcer
  • string $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
author

Marc Alcaraz

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) :

  1. Read the permission to capture (domain, object, action, effect).
  2. Walk each direct inbound edge type, resolve every subject's stable identifier and call removePolicy(subject, ...) once per match.
  3. Walk policy_has_permissions to find policies that hold this permission, then for each policy walk service_has_policies / role_has_policies to find every subject that holds that policy, and removePolicy keyed 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 _key of the permission about to be deleted.

Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

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 _key of the policy about to be deleted.

Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

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
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

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
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

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
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException
Return values
array{permission: object, serviceKeys: string[], roleKeys: string[], domain: string, object: string, action: string, effect: string}|null

purgePoliciesByEdges()

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 _key to its Casbin subject string, or null if the subject cannot be resolved.

$domain : string
$object : string
$action : string
$effect : string
Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException
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
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException
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.

On this page

Search results