| title | Explain Engine |
|---|---|
| description | Ask the runtime why a decision came out the way it did — every pipeline layer, with contributor attribution, over the kernel service or REST (ADR-0090 D6). |
Authorization here is a nine-layer pipeline, and a nine-layer pipeline nobody can interrogate is a support ticket generator. The explain engine (ADR-0090 D6) is the first-class answer to "why can 张三 PATCH 李四's leave_request?" — it walks the same code paths the enforcement middleware runs (shared permission-set resolution, evaluator, FLS mask, RLS composition), so the report is explained by construction, never a parallel re-implementation that can drift.
Each report carries the final verdict, the resolved principal, and one entry per pipeline layer, in enforcement order:
principal → required_permissions → object_crud → fls → owd_baseline
→ depth → sharing → vama_bypass → rls
| Field | Meaning |
|---|---|
allowed |
The overall decision for (object, operation, principal) |
principal |
Resolved identity: userId, positions[], permissionSets[], optional principalKind / onBehalfOf |
layers[] |
One entry per layer: layer, verdict, human-readable detail, contributors[] |
readFilter |
For reads: the composed row filter actually applied — the machine artifact |
Layer verdicts are grants / denies / narrows / widens / neutral /
not_applicable. Contributor attribution names the permission set that
produced the verdict and how the caller holds it (position binding, additive
everyone baseline, direct grant) — e.g. the showcase auditor's read of
another user's private note reports:
{
"layer": "vama_bypass",
"verdict": "widens",
"detail": "View/Modify All Data bypass held via [showcase_auditor] — ownership and sharing checks are skipped.",
"contributors": [{ "kind": "permission_set", "name": "showcase_auditor", "via": "direct grant" }]
}Kernel service (in-process, plugins/tests):
const security = kernel.getService('security');
const decision = await security.explain(
{ object: 'showcase_private_note', operation: 'read', userId: targetUserId },
callerContext,
);REST — same contract, two transports (ExplainRequestSchema):
# GET, query-string form
curl -H "Authorization: Bearer $TOKEN" \
"$BASE/api/v1/security/explain?object=showcase_private_note&operation=read&userId=usr_123"
# POST, body form
curl -X POST -H "Authorization: Bearer $TOKEN" -H 'Content-Type: application/json' \
-d '{"object":"showcase_private_note","operation":"read","userId":"usr_123"}' \
"$BASE/api/v1/security/explain"operation is one of read | create | update | delete | transfer | restore | purge | export (defaults to read); omit userId to explain yourself.
export is the one operation whose answer you cannot infer from read. Since
the export axis
is opt-in, a user can be fully able to read an object and still be refused a
bulk copy of it — so when someone hits 403 EXPORT_NOT_PERMITTED, ask with
operation: 'export'. Asking with read will answer allowed and tell you
nothing, because reading is precisely what they may still do. The object_crud
layer reports the conjunction (read ∧ the export grant) and names the set that
supplies it; every other layer is computed as the find the export performs, so
the readFilter you get back is the real row filter the export would stream
through.
The endpoint is authenticated-only — even on requireAuth: false
deployments (an access report is sensitive even about oneself). Beyond that,
authorization lives in the service, so REST and in-process callers share one
rule:
- Yourself — always allowed.
- Another user — requires the
manage_userscapability or a delegatedadminScopewhose business-unit subtree covers that user (ADR-0090 D12): an administrator who can rewire a user's grants may read why they resolve the way they do — and only a covering administrator. Anything else → 403.
A deployment without @objectstack/plugin-security answers 501.
- The admin simulator — Studio's Access pillar builds its "why can this user access?" panel on this endpoint.
- The access-matrix snapshot gate — the publish-time matrix is the same evaluation run over representative (permission set × object) pairs; explain is the per-decision zoom lens.
- Delegated-administration audits — via contributor attribution, each layer's verdict names the permission set behind it and how the caller holds it — including a position held via delegation (delegated administration).
- Authorization Architecture — the pipeline the layers mirror
- Access-Matrix Snapshot Gate
- Delegated Administration
- Reference: Explain schema