Skip to content

Commit 6029cc1

Browse files
os-zhuangclaude
andauthored
fix(security): explain resolves the ONE authorization aggregation (#6352) (#6764)
`buildContextForUser` was a hand-written second implementation of `@objectstack/core`'s `resolveAuthzContext` aggregation, kept in step by two comments claiming it mirrored the resolver. Measured over identical rows it did not: it dropped the `sys_member` role positions, every position-bound permission set (`sys_position_permission_set`), the `everyone` anchor's bound sets, the `platform_admin` position projection, `systemPermissions`, the posture rung, `email` and the `ai_seat` synthesis. Permission sets resolve BY NAME from `positions` + `permissions`, and a position-carried set only becomes a name inside the resolver — so any user granted through a position was explained as holding nothing, and the panel reported a denial enforcement never made. It now calls `resolveUserAuthzGrants` and adds presentation only: the ADR-0091 expired / `delegated_from` row annotations the resolver correctly discards, and `hasPlatformAdminGrant`, read back off the resolver's own posture verdict. Pinned by a parity suite running both implementations over the same fixture rows with per-case expected output, so it cannot pass by both sides resolving to nothing. `check:authz-resolver`'s ALLOW entry is narrowed to what still trips its heuristic — the explain-only provenance pass — without touching its remit. Claude-Session: https://claude.ai/code/session_01BM1tNf5U3nEbHKR4fo5qVQ Co-authored-by: Claude <noreply@anthropic.com>
1 parent 486d526 commit 6029cc1

4 files changed

Lines changed: 499 additions & 165 deletions

File tree

Lines changed: 45 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,45 @@
1+
---
2+
'@objectstack/plugin-security': minor
3+
---
4+
5+
Explain and enforcement now resolve ONE authorization aggregation (#6352).
6+
7+
`buildContextForUser()` — the explain API's reconstruction of an arbitrary user's
8+
context, behind `explain(request, callerContext)` and the `userId` parameter — was
9+
a hand-written second implementation of `@objectstack/core`'s `resolveAuthzContext`
10+
aggregation. Its agreement with enforcement was guaranteed by two comments saying
11+
it mirrored the resolver ("mirroring the runtime resolver's semantics", "we compute
12+
it here with the IDENTICAL rule") and by nothing else: no assertion anywhere in the
13+
repo compared the two.
14+
15+
It did not agree. Measured over identical rows, the mirror dropped:
16+
17+
| input | resolver | explain mirror |
18+
|---|---|---|
19+
| `sys_member` role positions (ADR-0095 D3) | `org_admin`, … ||
20+
| position-bound permission sets (`sys_position_permission_set`) | resolved ||
21+
| the `everyone` anchor's bound sets (ADR-0090 D5) | resolved ||
22+
| `platform_admin` position projection (ADR-0068 D2) | projected ||
23+
| `systemPermissions` / `posture` / `email` / `ai_seat` | resolved ||
24+
25+
The user-visible consequence: permission sets are resolved BY NAME from
26+
`context.positions ∪ context.permissions`, and a set carried by a POSITION only
27+
becomes a name inside the resolver. So for any user whose grants arrive through a
28+
position — the ordinary way an org grants access — the explain panel resolved fewer
29+
sets than enforcement and reported a denial the runtime never made. A security UI
30+
that says "you have no access" about access you have is worse than no panel.
31+
32+
`buildContextForUser` now calls `resolveUserAuthzGrants` (core's userId-driven
33+
resolver core, already the same entry point `runAs:'user'` automation runs use) and
34+
adds presentation only: the ADR-0091 expired-grant and `delegated_from` annotations
35+
the resolver correctly discards, and `hasPlatformAdminGrant`, which is now read
36+
back off the resolver's own posture verdict instead of recomputed. The returned
37+
context additionally carries `systemPermissions`, `org_user_ids`, `posture`,
38+
`tabPermissions` and `email` — additive; no field was removed or renamed.
39+
40+
Pinned by a parity suite that runs both implementations over the same fixture rows
41+
(org role projection, position-bound sets, the `everyone` anchor, both
42+
`platform_admin` polarities, `organization_admin``TENANT_ADMIN`, ADR-0091
43+
windows) and asserts each case's concrete expected output, so the pin cannot pass
44+
by both sides resolving to nothing. Restoring the mirror turns 9 of those cases
45+
red.

0 commit comments

Comments
 (0)