AccessEnforcer
in package
The single CONTRACT.md §11 ("Declarative Authorization Helpers") enforcement implementation, shared by BOTH framework bridges ({@see \Axiam\Sdk\Symfony\AxiamAccessAttributeListener} and {@see \Axiam\Sdk\Laravel\AxiamAccessMiddleware}) so the `#[RequireAuth]` / `#[RequireAccess]` / `#[RequireRole]` semantics can never drift between the two integrations. Neither bridge re-implements resource resolution, subject propagation, or the error-mapping table below — both call into this class exclusively.
Composition with the §10 guard (CONTRACT.md §11.2.1): this class NEVER extracts
or verifies a token itself. It only ever reads the identity already injected by the
CONTRACT.md §10 guard (the axiam_user request attribute populated by
AxiamMiddleware / AxiamAuthSubscriber),
passed in by the caller as $identity. A null identity is always a 401.
Subject propagation (CONTRACT.md §11.2.2): the authorization check is made for
the REQUEST's authenticated end user, never for the shared AxiamClient's own
session (which, in a framework bridge, is typically a service account, or not
authenticated at all). self::enforceAccess() always passes
subjectId: $identity['user_id'] to AxiamClient::checkAccess() — the additive
$subjectId parameter that method gained specifically so this class can do so
without checking the wrong identity's permissions.
Resource resolution (CONTRACT.md §11.2.3), in order of precedence:
- RequireAccess::$resourceId — a static UUID literal, when set;
- RequireAccess::$resourceParam — a route/path parameter name, looked up in
the
$routeParamsmap the caller supplies; - the
$resolvercallback the caller supplies, consulted when the configured route parameter is absent from$routeParams(or whenresourceParamitself isnull, signaling "resolve some other way"). A missing or non-UUID-shaped resource value at every step is a 400invalid_request— never a silent allow, never an empty/nil-UUID fallback.
Error mapping (CONTRACT.md §11.2.5), identical JSON body shape
{ "error": ..., "message": ... } on every failure path:
- no identity -> 401
authentication_failed - unresolvable resource id -> 400
invalid_request checkAccessdenies, or the server 403s -> 403authorization_denied- the calling client's OWN session 401s mid-check, or a transport-level
NetworkError occurs -> 503
authz_unavailable(fail closed: deny, never allow, on any transport/availability failure — anAuthErrorhere reflects a problem with the shared client's own credentials, not a verdict about the end user, so it is treated the same as "the authz service could not be reached" rather than manufacturing a false "authenticate again" prompt for a request whose OWN identity was already verified by the §10 guard)
No decision caching (CONTRACT.md §11.2.6): every self::enforceAccess()
call performs a fresh checkAccess round-trip; nothing here memoizes an allow/deny
outcome across requests.
Redaction (CONTRACT.md §11.2.8): no method on this class ever logs or echoes a
token or credential value. A denied/errored check is logged at debug level only,
carrying just the action and resolved resourceId — never the identity's raw
claims or any bearer token.
Table of Contents
Methods
- __construct() : mixed
- enforceAccess() : JsonResponse|null
- `require_access` (CONTRACT.md §11.1): the endpoint requires the authenticated caller to pass an AXIAM authorization check for `$attribute->action` on a resource resolved from the request. This is the ONE codepath both framework bridges call for `#[RequireAccess]` enforcement — see this class's own docblock for the full resource-resolution and error-mapping tables.
- enforceAuth() : JsonResponse|null
- `require_auth` (CONTRACT.md §11.1): the endpoint requires an authenticated AXIAM identity — nothing more. Pure sugar over the CONTRACT.md §10 guard for a route where that guard is applied per-endpoint rather than globally.
- enforceRole() : JsonResponse|null
- `require_role` (CONTRACT.md §11.1, MAY-level): a LOCAL check (no server round-trip, CONTRACT.md §11.2.9) that the identity's already-verified `roles` intersect with at least one of {@see RequireRole::$roles}. Requires an authenticated identity first (a role check needs somewhere to read roles from), so an unauthenticated caller still gets 401, not 403.
Methods
__construct()
public
__construct(AxiamClient $client[, LoggerInterface|null $logger = null ]) : mixed
Parameters
- $client : AxiamClient
-
The shared client used for the actual
checkAccessround-trip (REST by default, gRPC when the client is so configured — CONTRACT.md §11.2.7: no new transport code, the existingcheckAccesssurface is reused as-is). - $logger : LoggerInterface|null = null
-
Injectable logger (diagnostic-only:
action- resolved
resourceIdon a deny/error, NEVER a token/credential value — CONTRACT.md §11.2.8). Defaults to a silent NullLogger.
- resolved
enforceAccess()
`require_access` (CONTRACT.md §11.1): the endpoint requires the authenticated caller to pass an AXIAM authorization check for `$attribute->action` on a resource resolved from the request. This is the ONE codepath both framework bridges call for `#[RequireAccess]` enforcement — see this class's own docblock for the full resource-resolution and error-mapping tables.
public
enforceAccess(array{user_id: string, tenant_id: string, roles: list}|null $identity, RequireAccess $attribute[, array<string, mixed> $routeParams = [] ][, callable(): Array|null $resolver = null ]) : JsonResponse|null
Parameters
-
$identity
: array{user_id: string, tenant_id: string, roles: list
}|null -
The identity the CONTRACT.md §10 guard already injected, or
null. - $attribute : RequireAccess
-
The
#[RequireAccess(...)]attribute instance. - $routeParams : array<string, mixed> = []
-
The inbound request's route/path parameters (e.g. Symfony's
$request->attributes->get('_route_params')or Laravel's$route->parameters()), consulted when RequireAccess::$resourceParam is set. - $resolver : callable(): Array|null = null
-
A language-idiomatic resolver callback (CONTRACT.md §11.2.3c) for resource identifiers that live somewhere other than a route parameter (a body field, a header, a composite lookup) — consulted only when the configured RequireAccess::$resourceParam is
nullor absent from$routeParams.
Return values
JsonResponse|null —null when the check is allowed (caller proceeds); a
401/400/403/503 JsonResponse otherwise (see this class's docblock
for the exact mapping).
enforceAuth()
`require_auth` (CONTRACT.md §11.1): the endpoint requires an authenticated AXIAM identity — nothing more. Pure sugar over the CONTRACT.md §10 guard for a route where that guard is applied per-endpoint rather than globally.
public
enforceAuth(array{user_id: string, tenant_id: string, roles: list}|null $identity) : JsonResponse|null
Parameters
-
$identity
: array{user_id: string, tenant_id: string, roles: list
}|null -
The identity the CONTRACT.md §10 guard already injected (the
axiam_userrequest attribute), ornullwhen no such identity is present (unauthenticated, or the §10 guard is not installed on this route).
Return values
JsonResponse|null —null when authenticated (caller proceeds); a 401
authentication_failed JsonResponse otherwise.
enforceRole()
`require_role` (CONTRACT.md §11.1, MAY-level): a LOCAL check (no server round-trip, CONTRACT.md §11.2.9) that the identity's already-verified `roles` intersect with at least one of {@see RequireRole::$roles}. Requires an authenticated identity first (a role check needs somewhere to read roles from), so an unauthenticated caller still gets 401, not 403.
public
enforceRole(array{user_id: string, tenant_id: string, roles: list}|null $identity, RequireRole $attribute) : JsonResponse|null
Role names are tenant-defined; this check is intentionally coarse and is NOT a substitute for self::enforceAccess(), which is the authoritative, server-verified check (CONTRACT.md §11.2.9).
Parameters
-
$identity
: array{user_id: string, tenant_id: string, roles: list
}|null -
The identity the CONTRACT.md §10 guard already injected, or
null. - $attribute : RequireRole
-
The
#[RequireRole(...)]attribute instance carrying the acceptable role list.
Return values
JsonResponse|null —null when the identity holds at least one of the
required roles (caller proceeds); a 401 authentication_failed or 403
authorization_denied JsonResponse otherwise.