Oihana PHP Arango

CasbinPolicySync uses CasbinPolicySyncEdgeTrait, CasbinPolicySyncPolicyTrait, CasbinPolicySyncRoleTrait, CasbinPolicySyncServiceTrait, CasbinPolicySyncUserTrait, EnforcerTrait, LoggerTrait, PermissionsModelTrait, PoliciesModelTrait, PolicyHasPermissionsTrait, RoleHasPermissionsTrait, RoleHasPoliciesTrait, RolesModelTrait, ServiceHasPermissionsTrait, ServiceHasPoliciesTrait, ServicesModelTrait, UserHasPermissionsTrait, UsersModelTrait

Synchronizes Casbin policies in real-time when edge relations are modified.

Coordinator class : every actual sync logic lives in one of the per-domain traits (edge dispatcher, role, user, service, policy). This class carries the dependencies (Enforcer + Arango models + edge collections + logger) by composing the canonical XxxModelTrait, XxxEdgesTrait, EnforcerTrait and LoggerTrait families, then wires them through the standard $init / $container init pattern shared by every other auth-side service.

Tags
author

Marc Alcaraz

Table of Contents

Constants

DOMAIN  : string = 'domain'
Initialization key for the Casbin domain (e.g. the active API identifier such as `my-api`). Forwarded to every implicit-permission lookup performed by the per-domain sync traits.
PERMISSIONS_MODEL  : string = 'permissionsModel'
Initialization key for the permissions Documents model.
POLICIES_MODEL  : string = 'policiesModel'
Initialization key for the policies Documents model.
POLICY_HAS_PERMISSIONS  : string = 'policyHasPermissions'
Initialization key for the policy_has_permissions Edges model.
ROLE_HAS_PERMISSIONS  : string = 'roleHasPermissions'
Initialization key for the role_has_permissions Edges model.
ROLE_HAS_POLICIES  : string = 'roleHasPolicies'
Initialization key for the role_has_policies Edges model.
ROLES_MODEL  : string = 'rolesModel'
Initialization key for the roles Documents model.
SERVICE_HAS_PERMISSIONS  : string = 'serviceHasPermissions'
Initialization key for the service_has_permissions Edges model.
SERVICE_HAS_POLICIES  : string = 'serviceHasPolicies'
Initialization key for the service_has_policies Edges model.
SERVICES_MODEL  : string = 'servicesModel'
Initialization key for the services Documents model.
USER_HAS_PERMISSIONS  : string = 'userHasPermissions'
Initialization key for the user_has_permissions Edges model.
USERS_MODEL  : string = 'usersModel'
Initialization key for the users Documents model.

Properties

$domain  : string
The Casbin domain (= the active API identifier) the policies live in.
$permissionsModel  : Documents|null
The permissions Documents model.
$policiesModel  : Documents|null
The policies Documents model.
$policyHasPermissions  : Edges|null
The policy_has_permissions Edges model.
$roleHasPermissions  : Edges|null
The role_has_permissions Edges model.
$roleHasPolicies  : Edges|null
The role_has_policies Edges model.
$rolesModel  : Documents|null
The roles Documents model.
$serviceHasPermissions  : Edges|null
The service_has_permissions Edges model.
$serviceHasPolicies  : Edges|null
The service_has_policies Edges model.
$servicesModel  : Documents|null
The services Documents model.
$userHasPermissions  : Edges|null
The user_has_permissions Edges model.
$usersModel  : Documents|null
The users Documents model.

Methods

__construct()  : mixed
Creates a new CasbinPolicySync instance.
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.
register()  : void
Registers this sync on an edge model's insert/delete signals.
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.
registerRoleDelete()  : void
Registers cleanup of Casbin policies / groupings when a role vertex is deleted.
registerServiceDelete()  : void
Registers cleanup of Casbin policies / groupings when a service vertex is deleted.
registerUserDelete()  : void
Registers cleanup of Casbin policies / groupings when a user vertex is deleted.
addPolicyPermissionPolicy()  : void
Propagates a `policy_has_permissions` insertion to every subject (service or role) currently attached to the policy.
addRolePermissionPolicy()  : void
Adds: p, roleIdentifier, domain, object, action, effect
addRolePolicyPolicies()  : void
Adds policies for every permission of a policy attached to a role.
addServicePermissionPolicy()  : void
Adds a direct permission policy for a service.
addServicePolicyPolicies()  : void
Adds Casbin policies for every permission of a policy attached to a service.
addUserPermissionPolicy()  : void
Adds: p, userId (identifier), domain, object, action, effect
addUserRoleGrouping()  : void
Adds: g, userIdentifier, roleIdentifier, domain
initializePermissionsModel()  : static
Initializes the permissions model dependency from the $init array.
initializePoliciesModel()  : static
Initializes the policies model dependency from the $init array.
initializePolicyHasPermissions()  : static
Initializes the policy_has_permissions edges dependency from the $init array.
initializeRoleHasPermissions()  : static
Initializes the role_has_permissions edges dependency from the $init array.
initializeRoleHasPolicies()  : static
Initializes the role_has_policies edges dependency from the $init array.
initializeRolesModel()  : static
Initializes the roles model dependency from the $init array.
initializeServiceHasPermissions()  : static
Initializes the service_has_permissions edges dependency from the $init array.
initializeServiceHasPolicies()  : static
Initializes the service_has_policies edges dependency from the $init array.
initializeServicesModel()  : static
Initializes the services model dependency from the $init array.
initializeUserHasPermissions()  : static
Initializes the user_has_permissions edges dependency from the $init array.
initializeUsersModel()  : static
Initializes the users model dependency from the $init array.
onEdgeDelete()  : void
Called when an edge is deleted.
onEdgeInsert()  : void
Called when a new edge is inserted.
onPermissionDelete()  : void
Called before one or more permission vertices are deleted.
onPolicyDelete()  : void
Called before one or more policy vertices are deleted.
onRoleDelete()  : void
Called when a role vertex is deleted — wipes every Casbin trace of that role.
onServiceDelete()  : void
Called when a service vertex is deleted — wipes every Casbin trace.
onUserDelete()  : void
Called when a user vertex is deleted — wipes every Casbin trace of that user.
removePolicyPermissionPolicy()  : void
Propagates a `policy_has_permissions` deletion to every service AND every role currently attached to the policy.
removeRolePermissionPolicy()  : void
Removes: p, roleIdentifier, domain, object, action, effect
removeRolePolicyPolicies()  : void
Removes policies for every permission of a policy detached from a role.
removeServicePermissionPolicy()  : void
Removes a direct permission policy from a service.
removeServicePolicyPolicies()  : void
Removes Casbin policies for every permission of a policy detached from a service.
removeUserPermissionPolicy()  : void
Removes: p, userId (identifier), domain, object, action, effect
removeUserRoleGrouping()  : void
Removes: g, userIdentifier, roleIdentifier, domain
resolveRoleSubject()  : string|null
Resolves the Casbin subject for a role.
resolveServicesForPolicy()  : array<string|int, string>
Returns the list of service `_key`s currently linked to the given policy via the `service_has_policies` edge collection.
resolveServiceSubject()  : string
Resolves the namespaced Casbin subject for a service.
resolveUserIdentifier()  : string|null
Resolves a user's identifier (Zitadel ID) from their ArangoDB _key.
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.

Constants

DOMAIN

Initialization key for the Casbin domain (e.g. the active API identifier such as `my-api`). Forwarded to every implicit-permission lookup performed by the per-domain sync traits.

public string DOMAIN = 'domain'

PERMISSIONS_MODEL

Initialization key for the permissions Documents model.

public string PERMISSIONS_MODEL = 'permissionsModel'

POLICIES_MODEL

Initialization key for the policies Documents model.

public string POLICIES_MODEL = 'policiesModel'

POLICY_HAS_PERMISSIONS

Initialization key for the policy_has_permissions Edges model.

public string POLICY_HAS_PERMISSIONS = 'policyHasPermissions'

ROLE_HAS_PERMISSIONS

Initialization key for the role_has_permissions Edges model.

public string ROLE_HAS_PERMISSIONS = 'roleHasPermissions'

ROLE_HAS_POLICIES

Initialization key for the role_has_policies Edges model.

public string ROLE_HAS_POLICIES = 'roleHasPolicies'

ROLES_MODEL

Initialization key for the roles Documents model.

public string ROLES_MODEL = 'rolesModel'

SERVICE_HAS_PERMISSIONS

Initialization key for the service_has_permissions Edges model.

public string SERVICE_HAS_PERMISSIONS = 'serviceHasPermissions'

SERVICE_HAS_POLICIES

Initialization key for the service_has_policies Edges model.

public string SERVICE_HAS_POLICIES = 'serviceHasPolicies'

SERVICES_MODEL

Initialization key for the services Documents model.

public string SERVICES_MODEL = 'servicesModel'

USER_HAS_PERMISSIONS

Initialization key for the user_has_permissions Edges model.

public string USER_HAS_PERMISSIONS = 'userHasPermissions'

USERS_MODEL

Initialization key for the users Documents model.

public string USERS_MODEL = 'usersModel'

Properties

$domain

The Casbin domain (= the active API identifier) the policies live in.

protected string $domain = ''

Methods

__construct()

Creates a new CasbinPolicySync instance.

public __construct([array<string|int, mixed> $init = [] ][, Container|null $container = null ]) : mixed
Parameters
$init : array<string|int, mixed> = []

Init array.

$container : Container|null = null

Optional DI container — strings in $init are resolved via $container->get().

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface

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

register()

Registers this sync on an edge model's insert/delete signals.

public register(Edges $edges, string $type) : void
Parameters
$edges : Edges

The edge model to listen to.

$type : string

The edge type — one of EdgeSyncType's constants.

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.

registerRoleDelete()

Registers cleanup of Casbin policies / groupings when a role vertex is deleted.

public registerRoleDelete(Documents $rolesModel) : void

The cascade edge purge wired on the Roles model (role_has_permissions, user_has_roles via RolesController::CASCADE_EDGES) removes the edges with a raw AQL query, which bypasses per-edge afterDelete signals. As a consequence the normal edge-level Casbin sync is never triggered for those cascaded deletes and policies/groupings would leak.

This listener subscribes to the role model's afterDelete signal and calls Enforcer::deleteRole($name) for every deleted role — which wipes both the p, <roleName>, ... policies and the g, <userId>, <roleName>, ... groupings in one shot.

Parameters
$rolesModel : Documents

The roles vertex model.

registerServiceDelete()

Registers cleanup of Casbin policies / groupings when a service vertex is deleted.

public registerServiceDelete(Documents $servicesModel) : void

The cascade edge purge wired on the Services model (service_has_policies, service_has_permissions) removes the edges with a raw AQL REMOVE, which bypasses per-edge afterDelete signals — so the normal edge-level Casbin sync is never triggered for those cascaded deletes and policies would leak. This handler subscribes to the service model's afterDelete signal and calls Enforcer::deleteUser($subject) keyed on the service's namespaced subject (service:{_key}).

Parameters
$servicesModel : Documents

The services vertex model.

registerUserDelete()

Registers cleanup of Casbin policies / groupings when a user vertex is deleted.

public registerUserDelete(Documents $usersModel) : void

Symmetric to CasbinPolicySyncRoleTrait::registerRoleDelete but for users. The cascade edge purge wired on the Users model (user_has_roles, user_has_permissions via UsersController::CASCADE_EDGES) removes the edges with a raw AQL query, which bypasses per-edge afterDelete signals — so the edge-level Casbin sync never fires and any g, <userIdentifier>, <roleIdentifier>, ... grouping or p, <userIdentifier>, ... direct policy would survive the deletion as orphaned data (silent security gap on M2M flows that rely on the user's identifier).

This listener subscribes to the users model's afterDelete signal and calls Enforcer::deleteUser($identifier) which purges both the user's groupings AND any direct user→permission policy in one shot.

Parameters
$usersModel : Documents

The users 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

addRolePermissionPolicy()

Adds: p, roleIdentifier, domain, object, action, effect

protected addRolePermissionPolicy(string $roleKey, string $permissionKey) : void

The Casbin subject for role-level policies is the role's stable identifier (Zitadel role key), never the mutable name. This way a PATCH rename never has to rewrite policies — name drifts but identifier is set once at POST time and pinned forever.

Legacy fallback: for roles created before the identifier pin convention landed (typically the 3 seeded ones: admin/guest/superadmin when not yet backfilled) we fall back to $role->name, which is their current Zitadel key. Backfill command: php bin/console.php auth:roles:backfill-identifiers.

Parameters
$roleKey : string
$permissionKey : string
Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

addRolePolicyPolicies()

Adds policies for every permission of a policy attached to a role.

protected addRolePolicyPolicies(string $roleKey, string $policyKey) : void

When a policy vertex is attached to a role via the role_has_policies edge, every permission contained in the policy must be materialised in Casbin keyed on the role's stable identifier — otherwise getImplicitPermissionsForUser will not walk through policy_has_permissions and the role's effective permission set silently misses the policy's permissions.

Without this method, the M2M branch (service → policy) is the only path Casbin sees, and self-service callers whose effective permissions depend on a role-attached policy fail validatePolicyAttachmentPermissions even though they have the permission "on paper" (in the role's policies tab).

Parameters
$roleKey : string

The role ArangoDB _key.

$policyKey : string

The policy ArangoDB _key.

Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

addServicePermissionPolicy()

Adds a direct permission policy for a service.

protected addServicePermissionPolicy(string $serviceKey, string $permissionKey) : void
Parameters
$serviceKey : string

The service ArangoDB _key.

$permissionKey : string

The permission ArangoDB _key.

Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

addServicePolicyPolicies()

Adds Casbin policies for every permission of a policy attached to a service.

protected addServicePolicyPolicies(string $serviceKey, string $policyKey) : void

Subject : namespaced service:{_key} — see the trait header for why the prefix is load-bearing.

Parameters
$serviceKey : string

The service ArangoDB _key.

$policyKey : string

The policy ArangoDB _key.

Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

addUserPermissionPolicy()

Adds: p, userId (identifier), domain, object, action, effect

protected addUserPermissionPolicy(string $userKey, string $permissionKey) : void
Parameters
$userKey : string
$permissionKey : string
Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

addUserRoleGrouping()

Adds: g, userIdentifier, roleIdentifier, domain

protected addUserRoleGrouping(string $userKey, string $roleKey) : void
Parameters
$userKey : string
$roleKey : string
Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

initializePermissionsModel()

Initializes the permissions model dependency from the $init array.

protected initializePermissionsModel(array<string|int, mixed> $init, Container|null $container) : static
Parameters
$init : array<string|int, mixed>

The initialization array.

$container : Container|null

The DI container.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
static

initializePoliciesModel()

Initializes the policies model dependency from the $init array.

protected initializePoliciesModel(array<string|int, mixed> $init, Container|null $container) : static
Parameters
$init : array<string|int, mixed>

The initialization array.

$container : Container|null

The DI container.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
static

initializePolicyHasPermissions()

Initializes the policy_has_permissions edges dependency from the $init array.

protected initializePolicyHasPermissions(array<string|int, mixed> $init, Container|null $container) : static
Parameters
$init : array<string|int, mixed>

The initialization array.

$container : Container|null

The DI container.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
static

initializeRoleHasPermissions()

Initializes the role_has_permissions edges dependency from the $init array.

protected initializeRoleHasPermissions(array<string|int, mixed> $init, Container|null $container) : static
Parameters
$init : array<string|int, mixed>

The initialization array.

$container : Container|null

The DI container.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
static

initializeRoleHasPolicies()

Initializes the role_has_policies edges dependency from the $init array.

protected initializeRoleHasPolicies(array<string|int, mixed> $init, Container|null $container) : static
Parameters
$init : array<string|int, mixed>

The initialization array.

$container : Container|null

The DI container.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
static

initializeRolesModel()

Initializes the roles model dependency from the $init array.

protected initializeRolesModel(array<string|int, mixed> $init, Container|null $container) : static
Parameters
$init : array<string|int, mixed>

The initialization array.

$container : Container|null

The DI container.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
static

initializeServiceHasPermissions()

Initializes the service_has_permissions edges dependency from the $init array.

protected initializeServiceHasPermissions(array<string|int, mixed> $init, Container|null $container) : static
Parameters
$init : array<string|int, mixed>

The initialization array.

$container : Container|null

The DI container.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
static

initializeServiceHasPolicies()

Initializes the service_has_policies edges dependency from the $init array.

protected initializeServiceHasPolicies(array<string|int, mixed> $init, Container|null $container) : static
Parameters
$init : array<string|int, mixed>

The initialization array.

$container : Container|null

The DI container.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
static

initializeServicesModel()

Initializes the services model dependency from the $init array.

protected initializeServicesModel(array<string|int, mixed> $init, Container|null $container) : static
Parameters
$init : array<string|int, mixed>

The initialization array.

$container : Container|null

The DI container.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
static

initializeUserHasPermissions()

Initializes the user_has_permissions edges dependency from the $init array.

protected initializeUserHasPermissions(array<string|int, mixed> $init, Container|null $container) : static
Parameters
$init : array<string|int, mixed>

The initialization array.

$container : Container|null

The DI container.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
static

initializeUsersModel()

Initializes the users model dependency from the $init array.

protected initializeUsersModel(array<string|int, mixed> $init, Container|null $container) : static
Parameters
$init : array<string|int, mixed>

The initialization array.

$container : Container|null

The DI container.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
Return values
static

onEdgeDelete()

Called when an edge is deleted.

protected onEdgeDelete(Payload $payload, string $type) : void
Parameters
$payload : Payload
$type : string
Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

onEdgeInsert()

Called when a new edge is inserted.

protected onEdgeInsert(Payload $payload, string $type) : void
Parameters
$payload : Payload
$type : string
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

onRoleDelete()

Called when a role vertex is deleted — wipes every Casbin trace of that role.

protected onRoleDelete(Payload $payload) : void

The payload data is the OLD document (or list of documents) returned by the ArangoDB REMOVE query, so role names are still accessible even though the vertex is gone.

Parameters
$payload : Payload

onServiceDelete()

Called when a service vertex is deleted — wipes every Casbin trace.

protected onServiceDelete(Payload $payload) : void

The payload data is the OLD document (or list of documents) returned by the ArangoDB REMOVE query, so the service's _key is still accessible even though the vertex is gone.

Enforcer::deleteUser($subject) purges every p, <subject>, <domain>, <object>, <action>, <effect> policy keyed on the service's namespaced subject — covering both service_policy derived policies and service_permission direct policies in a single call.

Parameters
$payload : Payload

onUserDelete()

Called when a user vertex is deleted — wipes every Casbin trace of that user.

protected onUserDelete(Payload $payload) : void

The payload data is the OLD document (or list of documents) returned by the ArangoDB REMOVE query, so the Zitadel identifier is still accessible even though the vertex is gone.

Enforcer::deleteUser($subject) purges in one shot:

  • every g, <subject>, *, * grouping (role assignments removed when the cascade-purged user_has_roles edges silently bypass per-edge afterDelete signals) ;
  • every p, <subject>, *, *, *, * direct user→permission policy (created via user_has_permissions, equally silent under cascade).
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

removeRolePermissionPolicy()

Removes: p, roleIdentifier, domain, object, action, effect

protected removeRolePermissionPolicy(string $roleKey, string $permissionKey) : void

Same identifier-vs-name convention as addRolePermissionPolicy.

Parameters
$roleKey : string
$permissionKey : string
Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

removeRolePolicyPolicies()

Removes policies for every permission of a policy detached from a role.

protected removeRolePolicyPolicies(string $roleKey, string $policyKey) : void

Mirror of addRolePolicyPolicies.

Parameters
$roleKey : string

The role ArangoDB _key.

$policyKey : string

The policy ArangoDB _key.

Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

removeServicePermissionPolicy()

Removes a direct permission policy from a service.

protected removeServicePermissionPolicy(string $serviceKey, string $permissionKey) : void
Parameters
$serviceKey : string

The service ArangoDB _key.

$permissionKey : string

The permission ArangoDB _key.

Tags
throws
ContainerExceptionInterface
NotFoundExceptionInterface
ReflectionException
ArangoException
BindException

removeServicePolicyPolicies()

Removes Casbin policies for every permission of a policy detached from a service.

protected removeServicePolicyPolicies(string $serviceKey, string $policyKey) : void
Parameters
$serviceKey : string

The service ArangoDB _key.

$policyKey : string

The policy ArangoDB _key.

Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

removeUserPermissionPolicy()

Removes: p, userId (identifier), domain, object, action, effect

protected removeUserPermissionPolicy(string $userKey, string $permissionKey) : void
Parameters
$userKey : string
$permissionKey : string
Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

removeUserRoleGrouping()

Removes: g, userIdentifier, roleIdentifier, domain

protected removeUserRoleGrouping(string $userKey, string $roleKey) : void
Parameters
$userKey : string
$roleKey : string
Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException

resolveRoleSubject()

Resolves the Casbin subject for a role.

protected resolveRoleSubject(string $roleKey) : string|null

Roles created since the Zitadel pin convention carry their stable identifier (Zitadel role key = Arango _key of the role vertex). Legacy roles (admin / guest / superadmin seeded before the pin landed, not yet backfilled) fall back to name, which is their current Zitadel key — safe for Casbin. Once the backfill command php bin/console.php auth:roles:backfill-identifiers has run, every role has an identifier and the fallback becomes dead code.

Parameters
$roleKey : string

The ArangoDB _key of the role vertex.

Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException
Return values
string|null

null if the role vertex is gone.

resolveServicesForPolicy()

Returns the list of service `_key`s currently linked to the given policy via the `service_has_policies` edge collection.

protected resolveServicesForPolicy(string $policyKey) : array<string|int, string>

Used by the policy-permission propagation handlers when a perm is added to / removed from a policy : every service holding that policy needs its Casbin tuples kept in sync.

Parameters
$policyKey : string

The policy ArangoDB _key.

Return values
array<string|int, string>

Deduplicated service _keys.

resolveServiceSubject()

Resolves the namespaced Casbin subject for a service.

protected resolveServiceSubject(string $serviceKey) : string

The service: prefix partitions Service Account RBAC tuples from raw user identifiers — a security load-bearing invariant since a leaked human token must never match a Service Account's RBAC bundle. The middleware writes the same prefix into ATTR_USER_ID for incoming JWTs (see CheckJwtAuthentication::process()).

Parameters
$serviceKey : string

The service ArangoDB _key.

Return values
string

Casbin-safe subject : service:{_key} (passed through safeSubject for the digit-only quirk).

resolveUserIdentifier()

Resolves a user's identifier (Zitadel ID) from their ArangoDB _key.

protected resolveUserIdentifier(string $userKey) : string|null
Parameters
$userKey : string
Tags
throws
BindException
ContainerExceptionInterface
ArangoException
NotFoundExceptionInterface
ReflectionException
Return values
string|null

null if the user has no identifier set.

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