Oihana PHP Arango

PermissionSubjectResolver implements PermissionSubjectResolverInterface

Resolves a permission `subject` (e.g. `roles.permissions:list`) to the (`object`, `action`) couple Casbin actually enforces against.

The Casbin policy table stores (subject_user, domain, object, action, effect) — the permission subject from the seed (the human-readable label) is not carried into Casbin. To answer the question "does this user hold the permission identified by roles.permissions:list?", a translation step from the label to the (object, action) couple is required.

That translation is read from the ArangoDB permissions collection, where every row already exposes subject, object and action. The resolver loads the full table once and caches the result in Memcached so subsequent lookups are O(1) memory accesses.

Cache lifecycle:

  • Lazy: the map is built on the first resolve() call after a cold cache. No work is done at boot.
  • TTL safety net: a TTL (default 1 hour) prevents stale state if an explicit invalidation point is ever missed.
  • Surgical invalidation: the catalog only changes through three paths, and each one calls invalidate():
    1. php bin/console.php auth:materialize (and php bin/console.php auth:import)
    2. POST /permissions
    3. DELETE /permissions/{key}

The resolver is stateless per request beyond the in-process Memcached connection — it is safe to share a single instance across the container.

Tags
author

Marc Alcaraz

Table of Contents

Interfaces

PermissionSubjectResolverInterface

Constants

CACHE_KEY  : string = 'auth.permissions.subject_map'
Memcached key for the cached subject → (object, action) map.
DEFAULT_TTL  : int = 3600
Default cache TTL in seconds (1 hour). May be overridden via the constructor — typical override is 60s in dev to confirm invalidation paths during a chantier, or 0 to bypass the cache entirely (every lookup hits ArangoDB).

Properties

$cache  : Memcached
$logger  : LoggerInterface|null
$permissionsModel  : Documents
$ttl  : int

Methods

__construct()  : mixed
Creates a new PermissionSubjectResolver.
getMap()  : array<string, array{object: string, action: string}>
Returns the full subject → (object, action) map.
invalidate()  : void
Drops the cached map.
resolve()  : array{object: string, action: string}|null
Returns the `(object, action)` couple bound to a permission subject, or `null` when the subject is unknown.
loadFromDatabase()  : array<string, array{object: string, action: string}>
Reads the full `permissions` collection and projects it as a map.

Constants

CACHE_KEY

Memcached key for the cached subject → (object, action) map.

public string CACHE_KEY = 'auth.permissions.subject_map'

Hardcoded — the catalog is global and must not be partitioned per caller / per role / per anything. Exposing it as a config knob would only invite drift between writers (the controllers that invalidate) and readers (the resolver itself).

DEFAULT_TTL

Default cache TTL in seconds (1 hour). May be overridden via the constructor — typical override is 60s in dev to confirm invalidation paths during a chantier, or 0 to bypass the cache entirely (every lookup hits ArangoDB).

public int DEFAULT_TTL = 3600

Properties

Methods

__construct()

Creates a new PermissionSubjectResolver.

public __construct(Documents $permissionsModel, Memcached $cache[, int $ttl = self::DEFAULT_TTL ][, LoggerInterface|null $logger = null ]) : mixed
Parameters
$permissionsModel : Documents

The ArangoDB permissions collection model.

$cache : Memcached

The shared Memcached connection — same one used by JWKS / route caches.

$ttl : int = self::DEFAULT_TTL

Cache TTL in seconds. 0 disables the cache (debugging only).

$logger : LoggerInterface|null = null

Optional logger for hot-reload telemetry.

getMap()

Returns the full subject → (object, action) map.

public getMap() : array<string, array{object: string, action: string}>

Exposed for tests and for callers that need to enumerate the catalog (e.g. doctor commands, debug endpoints). Not intended for hot paths — the per-subject resolve() should be preferred since the map grows with the seed.

Return values
array<string, array{object: string, action: string}>

invalidate()

Drops the cached map.

public invalidate() : void

Called by the three points that mutate the permissions catalog — auth:materialize, auth:import, and PermissionsController writes. The next resolve() after invalidation triggers a fresh load.

resolve()

Returns the `(object, action)` couple bound to a permission subject, or `null` when the subject is unknown.

public resolve(string $subject) : array{object: string, action: string}|null

The first call after a cold cache materializes the full map; every subsequent call inside the TTL window is a Memcached lookup followed by an in-memory array dereference.

Parameters
$subject : string

The permission subject label, e.g. roles.permissions:list.

Return values
array{object: string, action: string}|null

loadFromDatabase()

Reads the full `permissions` collection and projects it as a map.

private loadFromDatabase() : array<string, array{object: string, action: string}>

On read failure the map is returned empty — every subsequent resolve() falls open (returns null), which the caller must interpret as "subject unknown" and translate into the safe default for the caller's policy (typically: deny the projection, since gating cannot be evaluated).

Return values
array<string, array{object: string, action: string}>
On this page

Search results