AuthzDispatcher
in package
Transparent REST/gRPC authz transport selection (CONTRACT.md §1, D-03, SC#3).
checkAccess()/can()/batchCheck() ALWAYS work: with no grpc PECL extension (or
restOnly: true) they route over POST /api/v1/authz/check[/batch] via
AuthzRestClient (FND-04). When the extension IS present and restOnly is
false, the SAME public methods transparently upgrade to
AuthzGrpcClient — callers never need to know which transport
actually ran.
PITFALL 4 / T-22-16 (high severity, mitigate): \Axiam\Sdk\Grpc\AuthzGrpcClient
(which extends \Grpc\BaseStub) is referenced ONLY inside the
!$this->restOnly && extension_loaded('grpc') branches below and inside
self::grpcClient(), which those branches are the only callers of. On a
REST-only runtime (no grpc PECL extension — the actual condition in this SDK's own
CI/dev sandbox) that class is never autoloaded, so no "Class 'Grpc\BaseStub' not
found" fatal is possible — AuthzDispatcherFallbackTest proves
this empirically. The typed property self::$grpcClient and
self::grpcClient()'s return type both name AuthzGrpcClient, but PHP resolves
class names used only in type declarations lazily (confirmed empirically: a typed
property/return-type naming a nonexistent class does not fatal a class that never
assigns/invokes it) — the guard is what matters, not merely avoiding the class name in
source text.
D-06: the gRPC path shares Session's single-flight refresh/token —
$tokenAccessor is expected to be Session::accessToken(...) (or an equivalent live
reader), never a second independent refresh mechanism.
Table of Contents
Methods
- __construct() : mixed
- batchCheck() : array<int, bool>
- `batchCheck` (CONTRACT.md §1) — results preserve input order, mirroring {@see AuthzRestClient::batchCheck()}'s contract exactly regardless of transport.
- can() : bool
- `can` (CONTRACT.md §1) — the browser/UI-scenario alias for {@see self::checkAccess()}.
- checkAccess() : bool
- `checkAccess` (CONTRACT.md §1).
- getUserInfo() : UserInfo
- `getUserInfo` (CONTRACT.md §1.1) — the gRPC-ONLY OIDC-style userinfo operation, the low-latency counterpart of the server's REST `GET /oauth2/userinfo`. Unlike {@see self::checkAccess()}/{@see self::batchCheck()} it has NO REST fallback: §1.1.6 explicitly forbids substituting the REST endpoint, so on a runtime without the `grpc` PECL extension (or with `restOnly: true`) this raises a {@see NetworkError} rather than silently degrading.
Methods
__construct()
public
__construct(AuthzRestClient $restClient[, bool $restOnly = false ][, string|null $grpcTarget = null ][, string|null $tenantId = null ][, callable(): Array $tokenAccessor = null ][, callable(): string $subjectIdAccessor = null ][, string|null $customCaPem = null ][, string|null $clientCertPem = null ][, Sensitive|null $clientKey = null ][, callable(): void $refreshAccessor = null ]) : mixed
Parameters
- $restClient : AuthzRestClient
- $restOnly : bool = false
- $grpcTarget : string|null = null
- $tenantId : string|null = null
- $tokenAccessor : callable(): Array = null
-
Reads the CURRENT access token live (D-06) — required only for the gRPC path; ignored entirely when
restOnlyis true or thegrpcextension is absent. - $subjectIdAccessor : callable(): string = null
-
Reads the current authenticated user's subject UUID — the gRPC wire contract (unlike REST) requires
subject_idexplicitly, cross-validated server-side against the verified JWT (SEC-003); required only for the gRPC path. - $customCaPem : string|null = null
- $clientCertPem : string|null = null
-
§6.1 (mTLS): PEM client-certificate chain the gRPC channel presents for mutual TLS;
nullleaves the channel using bearer-token auth only. Ignored on the REST path (Guzzle receives its own file-based copy). - $clientKey : Sensitive|null = null
-
§6.1/§7 (mTLS): the matching private key, wrapped in Sensitive so it never leaks; revealed only when building the gRPC channel credentials. Must be present iff
$clientCertPemis. - $refreshAccessor : callable(): void = null
-
Drives the shared §9 single-flight token refresh (expected to be
Session::refreshIfNeeded()->wait()or equivalent) — used ONLY by self::getUserInfo() to refresh-and-retry once on a gRPCUNAUTHENTICATED(CONTRACT.md §1.1.4), exactly as REST does on a 401. Never a second, independent refresh mechanism (D-06);nulldisables the retry (the UNAUTHENTICATEDAuthErrorthen surfaces directly).
batchCheck()
`batchCheck` (CONTRACT.md §1) — results preserve input order, mirroring {@see AuthzRestClient::batchCheck()}'s contract exactly regardless of transport.
public
batchCheck(array<int, array{action: string, resourceId: string, scope?: string|null}> $checks) : array<int, bool>
Parameters
- $checks : array<int, array{action: string, resourceId: string, scope?: string|null}>
Return values
array<int, bool>can()
`can` (CONTRACT.md §1) — the browser/UI-scenario alias for {@see self::checkAccess()}.
public
can(string $resource, string $action) : bool
Parameters
- $resource : string
- $action : string
Return values
boolcheckAccess()
`checkAccess` (CONTRACT.md §1).
public
checkAccess(string $action, string $resourceId[, string|null $scope = null ][, string|null $subjectId = null ]) : bool
Parameters
- $action : string
- $resourceId : string
- $scope : string|null = null
- $subjectId : string|null = null
-
Additive, optional (CONTRACT.md §11.2.2 — declarative authorization helpers): when given, the check is evaluated for THIS subject rather than whichever identity the dispatcher's own configured session represents.
null(the default) preserves the pre-§11 behavior exactly on both transports — REST omitssubject_idfrom the wire body (server derives it from the verified JWT) and gRPC falls back to self::currentSubjectId() (the dispatcher's own session subject), exactly as before this parameter was added.
Return values
boolgetUserInfo()
`getUserInfo` (CONTRACT.md §1.1) — the gRPC-ONLY OIDC-style userinfo operation, the low-latency counterpart of the server's REST `GET /oauth2/userinfo`. Unlike {@see self::checkAccess()}/{@see self::batchCheck()} it has NO REST fallback: §1.1.6 explicitly forbids substituting the REST endpoint, so on a runtime without the `grpc` PECL extension (or with `restOnly: true`) this raises a {@see NetworkError} rather than silently degrading.
public
getUserInfo() : UserInfo
Behavior (mirrors the checkAccess gRPC path + the REST 401 refresh path exactly):
- §1.1.3 precondition: with no current access token, raises AuthError client-side WITHOUT any wire call.
- §1.1.2 metadata:
authorization: Bearer <token>+x-tenant-idon the RPC, via the same UserInfoGrpcClient channel machinery AuthzGrpcClient uses. - §1.1.4 auth-failure: a gRPC
UNAUTHENTICATED(surfaced as AuthError) drives the shared §9 single-flight refresh ($refreshAccessor) and retries the RPC exactly once; a second failure propagates (no loop, §9.3). - §1.1.5 return shape: a typed UserInfo —
sub/tenantId/orgIdalways set,email/preferredUsernamepresent only when the token carried the "email"/"profile" scope (the server gates them; an absent optional is surfaced asnull).