Found while measuring #13255 (reachability + consequence of a swallowed execution-context resolution at the package-management door). ⛔ No severity is asserted here; this records what was driven and what came back, for triage.
The seam
tryFind in packages/core/src/security/resolve-authz-context.ts wraps every permission-store read:
try {
let rows = await ql.find(object, { where, limit, context });
...
} catch {
return [];
}
resolveUserAuthzGrants issues all of its reads through it (sys_user_permission_set, sys_permission_set, sys_member, sys_user_position, sys_position, ...), and resolveAuthzContext's own docblock states the contract this creates: "Always resolves — never throws."
⇒ when the permission store is unreachable, the resolution does not fail. It SUCCEEDS, with an authenticated principal whose capability set is empty.
What was measured
Driven through the real RestServer (constructor seams only, no private replaced) and the real registerPackageRoutes, wired exactly as rest-api-plugin.ts wires it. Full harness and controls: packages/rest/src/package-door-execctx-fault-reachability.test.ts.
| wiring |
resolved context |
GET /api/v1/packages |
healthy store granting manage_metadata + studio.access |
userId: u_admin, those two capabilities |
200 |
| the same store, every read throws |
userId: u_admin, systemPermissions: [] |
403 FORBIDDEN |
| the same store, reachable and genuinely empty |
userId: u_admin, systemPermissions: [] |
403 FORBIDDEN |
The last two responses are compared with JSON.stringify and are equal. Positive control on the same comparison: the healthy 200 and the refusal are not equal, so the comparison discriminates.
The refusal message the caller and the operator both see is
Reading packages requires the studio.access or setup.access capability.
An authenticated administrator is told they lack a capability, during an outage of the store that holds the capability.
Why this is not #13255's seam
#13255 is about resolveExecCtx's .catch(() = undefined ) and the computeExecCtx swallow behind it, whose degrade is CONTEXT LOST — the whole context becomes undefined and the anonymous floor answers 401. This one is GRANTS LOST: identity survives, only the aggregation is empty, and the refusal comes from a different clause with a different status. It travels through neither the .catch nor computeExecCtx's catch. A repair aimed at either of those leaves this shape exactly as it is.
Not ruled here
Whether tryFind should distinguish "no rows" from "the read failed" is a behaviour change on the shared authorization resolver — every transport that authorizes through resolveAuthzContext inherits it, not just REST. Recorded for triage, deliberately unruled.
Refs
Found while measuring #13255 (reachability + consequence of a swallowed execution-context resolution at the package-management door). ⛔ No severity is asserted here; this records what was driven and what came back, for triage.
The seam
tryFindinpackages/core/src/security/resolve-authz-context.tswraps every permission-store read:resolveUserAuthzGrantsissues all of its reads through it (sys_user_permission_set,sys_permission_set,sys_member,sys_user_position,sys_position, ...), andresolveAuthzContext's own docblock states the contract this creates: "Always resolves — never throws."⇒ when the permission store is unreachable, the resolution does not fail. It SUCCEEDS, with an authenticated principal whose capability set is empty.
What was measured
Driven through the real
RestServer(constructor seams only, no private replaced) and the realregisterPackageRoutes, wired exactly asrest-api-plugin.tswires it. Full harness and controls:packages/rest/src/package-door-execctx-fault-reachability.test.ts.manage_metadata+studio.accessuserId: u_admin, those two capabilitiesuserId: u_admin,systemPermissions: []userId: u_admin,systemPermissions: []The last two responses are compared with
JSON.stringifyand are equal. Positive control on the same comparison: the healthy 200 and the refusal are not equal, so the comparison discriminates.The refusal message the caller and the operator both see is
An authenticated administrator is told they lack a capability, during an outage of the store that holds the capability.
Why this is not #13255's seam
#13255 is about
resolveExecCtx's.catch(() =undefined)and thecomputeExecCtxswallow behind it, whose degrade is CONTEXT LOST — the whole context becomesundefinedand the anonymous floor answers 401. This one is GRANTS LOST: identity survives, only the aggregation is empty, and the refusal comes from a different clause with a different status. It travels through neither the.catchnorcomputeExecCtx'scatch. A repair aimed at either of those leaves this shape exactly as it is.Not ruled here
Whether
tryFindshould distinguish "no rows" from "the read failed" is a behaviour change on the shared authorization resolver — every transport that authorizes throughresolveAuthzContextinherits it, not just REST. Recorded for triage, deliberately unruled.Refs
.catch(() => undefined)把执行上下文解析失败静默降级为「无上下文」— 该行为在包管理门上可达什么错误状态,未测 #13255 — the card this fell out ofresolveExecutionContextseam is TEST-ONLY —resolveExecCtxisprivate asyncbehind.catch(() => undefined), so no production throw leaves it #12537 — the sibling reading at the same door