Skip to content

Latest commit

 

History

History
119 lines (95 loc) · 5.15 KB

File metadata and controls

119 lines (95 loc) · 5.15 KB
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).

Explain Engine

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.

What a decision looks like

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" }]
}

Calling it

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.

Who may ask

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_users capability or a delegated adminScope whose 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.

What it powers

  • 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).

See also