Repository navigation
Commit 2e10c9a
feat(core,plugin-security,plugin-auth)!: the authorization resolver reads a grant's permission set by name (ADR-0131 D4, C2 stage S5a) (#22495)
Part of #15196
Clause-②: no (narrowing)
C2 stage **S5a** (`domain:engine`): the authorization resolver reads
which permission set a user grant holds from the grant's **name**,
`sys_user_permission_set.permission_set` (ADR-0131 D4). The card stays
open for S5c, S8, S9 and S10.
## What changes
**The resolver** (`packages/core/src/security/resolve-authz-context.ts`,
`resolveUserAuthzGrants` §6 and §6b):
- A user grant's set row is found by its name: the grant's own
organization's row, otherwise the organization-less row
(`readGrantSetRowsByName`). The grant's `permission_set_id` is no longer
read here.
- A grant that names nothing, or whose name resolves only to another
organization's set, confers nothing.
- **Platform-admin anchor.** An unscoped grant reads the
organization-less `admin_full_access` row only. Deactivation is still
read from the row. Under walled postures the anchor stays retired.
- **Position-bound sets** are still reached through the junction's id.
That relation belongs to C3 (`KEPT-C3`, untouched).
- The by-name read is issued beside the `sys_position` read, so it adds
no sequential leg.
- `ADMIN_STANDING_SURFACE` declares the columns the resolver now reads:
`sys_user_permission_set.permission_set` in place of the id, and
`sys_permission_set.organization_id`.
**Prerequisite 2: the S4a name hook applies the S4b rule at write time**
(`grant-permission-set-name.ts`).
- Measured first, at the write door: a tenant-less system writer
inserting an organization-less grant whose id named another
organization's set stored the name `qa_b_only`, that set's name.
- Now a name is taken only from a set row of the grant's own
organization, or from an organization-less row.
- Another organization's set is never named after:
- a supplied name is refused for every caller, system included, with the
hook's existing `400 VALIDATION_FAILED` and `invalid_value` at
`permission_set`;
- no name is stamped;
- an update that re-points the grant at such a set, or moves the grant
to an organization its set does not belong to, clears the name.
**Prerequisite 1: the upgrade-boot window, accounted for in S5a.**
- The backfill runs at `kernel:bootstrapped`. Both kernels await every
`kernel:bootstrapped` handler before `kernel:listening`, where HTTP
servers open their socket. So no request reaches the resolver while the
grants the backfill can name are still unnamed.
- Only boot code running at `kernel:ready` sees them unnamed. Pinned on
a real `LiteKernel` boot.
- Route C (moving the backfill ahead of the `kernel:ready` readers) was
not taken. S4b measured that a `kernel:ready` placement misses
late-registered sets (its ablation A6). Under a by-name resolver those
grants would then confer nothing for the whole process lifetime.
- `security-plugin.ts` is not touched.
**The S4b carrier: a grant whose id names another organization's set
confers nothing.** This follows the seat's ruling: a set resolves by
name, in its own organization's row and otherwise the organization-less
row.
**Break-glass guard** (`plugin-auth`, `last-admin-guard.ts`). The
resolver now reads `sys_permission_set.organization_id`, so the guard's
correspondence gate asks for a disposition. The guard now:
- treats the column as a standing column;
- counts only the organization-less `admin_full_access` row as the
anchor, matching the resolver;
- refuses moving that row into an organization when no administrator
would remain.
## Read sites (census rows 31–35, located by symbol on `35ef501e1`)
| Site | Leg | Moved or kept | Why |
|:--|:--|:--|:--|
| `sys_user_position` read (Leg 1) | assignment by position name | kept
| already a name (`ASSIGN`) |
| `sys_user_permission_set` read (Leg 1) | which set a grant holds |
**moved**: `permission_set_id` → `permission_set` | `REWRITE-C2` |
| same | grant organization, validity window | kept | unchanged grant
rule |
| `sys_position` read (§6a) | position existence and `active` | kept |
`KEPT-C3` id bridge, `OPEN-ACTIVE`; `REG` is S8a |
| `sys_position_permission_set` read (§6a) | binding | kept | `KEPT-C3`
|
| `sys_permission_set` read (§6b) | user-grant leg | **moved**: by id,
installation-wide → by name, own organization's row else the
organization-less one | `REWRITE-C2` and the S4b carrier |
| same | position-bound leg | kept, by the junction's id | `KEPT-C3`
(id→name bridge) |
| same | `active`, capability body | kept on the row | Q2 = A
(`OPEN-ACTIVE`); `REG(body)` is S8a |
| platform-admin anchor (§6b) | unscoped `admin_full_access` grant |
**moved**: by id → by name, on the organization-less row | it read a
grant by id (`unscopedUserPsIds`) |
| `platform-admin.ts` (§6b-config) | declared owner email | untouched |
reads no grant |
`@objectstack/core` cannot import `plugin-security`. It keeps its own
internal copy of the rule (`grantSetNameOf`, not exported), the shape
S5b set ("one copy per package, internal"). No new dependency edge, no
cycle.
## Pins, each reverse-verified (one-time runs, quoted in the report)
- **(a) Goldens per principal, base-recorded.**
`resolve-authz-grant-set-by-name.golden.test.ts` covers the platform
admin, the organization admin, a member and an agent, in `single`,
`group` and `isolated`. Each records the envelope inside the
organization and outside it, plus `hasPlatformAdminStanding`.
- Recorded on `61765bfb`: goldens only, production code at the base.
- Unchanged with the change.
- S5b's and S4a's goldens are unchanged too.
- **(b) The census ablation.** One grant's name was pointed at another
set, with the name hooks unbound, via `ablation-replace`.
- With the change, each leg turns its golden red in every posture that
holds that grant: member 3/3, agent 3/3, organization admin `single` 1/3
and walled 2/3, platform admin `single` 1/3 (no grant row under a wall).
- On the base resolver, all five re-points at once leave the goldens
green, 3/3.
- **(c) A grant whose id names another organization's set confers
nothing.** Pinned in core (recording double) and in plugin-security (SQL
driver, real hooks). Red on the base resolver.
- **(d) An unnamed grant confers nothing, and its id still restricts.**
Pinned in core and in plugin-security: the organization-admin reconcile
still revokes an unnamed grant through its id. Red on the base resolver.
- **(e) The upgrade boot.** On a real `LiteKernel` boot, a grant stored
before the name column is unnamed at `kernel:ready` and confers at
`kernel:listening`.
- The `kernel:ready` leg is red on the base resolver.
- Unwiring the backfill turns the `kernel:listening` leg red.
- **(f) Write-time rule in the hook.** Four pins: stored unnamed,
supplied name refused, a re-point clears the name, an organization move
clears the name. All four are red with the hook at the base; the control
stays green.
- **Guard.** Moving the organization-less `admin_full_access` into an
organization is refused. Red with the guard at the base; the two
controls stay green.
## Fixture triage
The resolver no longer reads the id, so test grants written without the
name hooks now carry the name every platform writer stores beside the
id. Hand-written engine doubles now read an absent column as NULL, as
SQL does, and answer a `sys_permission_set` read by name.
- The core batch-equivalence **query** goldens move for the
`sys_permission_set` entries of the three fixtures that hold a user
grant. The move is written down in the test file, as its header
requires.
- Every other recorded read, and every **envelope**, is unchanged.
- The leg count drops from 3 to 2 for two fixtures.
- S4b's "no principal's grants change" pin now states the new truth:
before the backfill, unnamed grants confer nothing; after it, the golden
holds.
## Cross-lane paths (`domain:services`, beyond the claim's file surface)
- `packages/plugins/plugin-security/src/grant-permission-set-name.ts`,
as claimed.
- `packages/plugins/plugin-auth/src/last-admin-guard.ts`, not in the
claim. This is the guard's half of the surface this stage changed: its
correspondence gate is red without it.
- Tests in plugin-security: `grant-permission-set-name.test.ts`,
`grant-permission-set-name-backfill.test.ts`,
`resolve-authz-grant-set-by-name.test.ts` and
`resolve-authz-grant-set-by-name.golden.test.ts` (both new), and the
fixtures of `explain-enforce-parity`, `explain-engine`,
`explain-positions-name-authority`, `explain-removed-member-principal`,
`orgless-position-name-fold` and `security-plugin`.
- Tests in plugin-auth: `last-admin-guard.test.ts` and the fixtures of
10 test files.
- Test fixtures only: rest (11 files), runtime (11), plugin-hono-server
(5), cloud-connection (5), plugin-sharing (1), plugin-approvals (2),
service-automation (1), service-datasource (2), service-settings (1).
- No `security-plugin.ts`, no `packages/spec`, no id-column change, no
deletion, no driver arm, no junction read.
## Verification at `2e2d3bec`
`origin/main` `35ef501e1` was merged in clean.
- **Suites:**
- core: 2278 passed, typecheck 0;
- plugin-security: 3958 passed, 45 skipped, typecheck 0;
- plugin-auth: 2731 passed, 10 skipped, typecheck 0.
- **Grant-fixture test files in the consumer packages:** rest 240,
runtime 247, plugin-hono-server 43, client 50, cloud-connection 514
(full), service-datasource 763 (full), plugin-sharing 35,
plugin-approvals 24, service-automation 20, service-settings 18, mcp 21,
organizations 38, objectql 173, lint 147, triggers 23, verify 2. All
passed.
- **Dogfood:** 40 files that read or write grant rows; 427 passed, 1
skipped.
- **Gates:** `dispatch-gates --commands` derived 71; all 71 exit 0.
`--ran` answers "71 derived, 71 run, 0 NOT-MEASURED (a DERIVED zero)".
Two needed a prerequisite before they ran: a fixture commit fetched for
`check-plugin-teardown-shape --self-test`, and seven unrelated packages
built for `check:dual-build-cjs-loads`.
- **`check:durability-log-level`:** exit 0.
- **Lint (narrowed):** `eslint --no-inline-config --format json` over
the 73 changed TS files: 73 linted, 0 errors, 0 warnings, none ignored.
`eslint.config.mjs` enables no type-aware linting, so untouched files
are invariant.
- **Integration layer:** the `cli` integration test (a write through the
real data door, stamped by the hook) and `http-conformance` are left to
CI.
## Acceptance notes
- **Unnamed grants on a real upgrade.** The backfill leaves some grants
unnamed for good: an id with no set row, an id on another organization's
set, or a name the catalog does not resolve. After this stage those
grants confer nothing. That is the ruled fail-closed answer, and they
are listed in the backfill's boot report.
- A deployment holding row-only permission sets from before the set
write-through would see their grants stay unnamed (catalog-unresolved)
and stop conferring. This is inference from source; no such deployment
was measured.
- **The backfill's failure line** in `security-plugin.ts` still says
unnamed grants "keep no name until a later boot runs it". With this
stage they also confer nothing until then.
- Carrier: the next stage that holds `security-plugin.ts`. Not edited
here.
- **Three copies of the by-name rule** now exist: core, plugin-security
and plugin-auth, each internal. Core is upstream of both plugins, so one
exported copy in core is possible. Recorded as an open question, not
built.
---
_Generated by [Claude
Code](https://claude.ai/code/session_01W6VxfDKkkpAFgax4onpHt8)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent e148ca9 commit 2e10c9a
75 files changed
Lines changed: 1884 additions & 261 deletions
File tree
- .changeset
- packages
- cloud-connection/src
- core/src/security
- __tests__
- plugins
- plugin-approvals/src
- plugin-auth/src
- plugin-hono-server/src
- plugin-security/src
- plugin-sharing/src
- rest/src
- runtime/src
- domains
- security
- services
- service-automation/src
- service-datasource/src/__tests__
- service-settings/src
Some content is hidden
Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
| 1 | + | |
| 2 | + | |
| 3 | + | |
| 4 | + | |
| 5 | + | |
| 6 | + | |
| 7 | + | |
| 8 | + | |
| 9 | + | |
| 10 | + | |
| 11 | + | |
| 12 | + | |
| 13 | + | |
| 14 | + | |
| 15 | + | |
| 16 | + | |
| 17 | + | |
| 18 | + | |
| 19 | + | |
| 20 | + | |
| 21 | + | |
| 22 | + | |
| 23 | + | |
| 24 | + | |
| 25 | + | |
| 26 | + | |
| 27 | + | |
| 28 | + | |
| 29 | + | |
| 30 | + | |
Lines changed: 1 addition & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
53 | 53 | | |
54 | 54 | | |
55 | 55 | | |
56 | | - | |
| 56 | + | |
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| |||
Lines changed: 4 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
134 | 134 | | |
135 | 135 | | |
136 | 136 | | |
137 | | - | |
| 137 | + | |
| 138 | + | |
| 139 | + | |
| 140 | + | |
138 | 141 | | |
139 | 142 | | |
140 | 143 | | |
| |||
Lines changed: 4 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
111 | 111 | | |
112 | 112 | | |
113 | 113 | | |
114 | | - | |
| 114 | + | |
| 115 | + | |
| 116 | + | |
| 117 | + | |
115 | 118 | | |
116 | 119 | | |
117 | 120 | | |
| |||
Lines changed: 3 additions & 1 deletion
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
89 | 89 | | |
90 | 90 | | |
91 | 91 | | |
92 | | - | |
| 92 | + | |
93 | 93 | | |
94 | 94 | | |
95 | 95 | | |
| |||
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
| 128 | + | |
| 129 | + | |
128 | 130 | | |
129 | 131 | | |
130 | 132 | | |
| |||
Lines changed: 9 additions & 4 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
108 | 108 | | |
109 | 109 | | |
110 | 110 | | |
| 111 | + | |
| 112 | + | |
| 113 | + | |
| 114 | + | |
| 115 | + | |
111 | 116 | | |
112 | 117 | | |
113 | 118 | | |
| |||
143 | 148 | | |
144 | 149 | | |
145 | 150 | | |
146 | | - | |
147 | | - | |
148 | | - | |
149 | | - | |
| 151 | + | |
| 152 | + | |
| 153 | + | |
| 154 | + | |
150 | 155 | | |
151 | 156 | | |
152 | 157 | | |
| |||
Lines changed: 9 additions & 7 deletions
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
| 61 | + | |
| 62 | + | |
61 | 63 | | |
62 | 64 | | |
63 | 65 | | |
| |||
253 | 255 | | |
254 | 256 | | |
255 | 257 | | |
256 | | - | |
257 | | - | |
258 | | - | |
259 | | - | |
260 | | - | |
| 258 | + | |
| 259 | + | |
| 260 | + | |
| 261 | + | |
| 262 | + | |
261 | 263 | | |
262 | 264 | | |
263 | 265 | | |
| |||
279 | 281 | | |
280 | 282 | | |
281 | 283 | | |
282 | | - | |
| 284 | + | |
283 | 285 | | |
284 | 286 | | |
285 | 287 | | |
| |||
330 | 332 | | |
331 | 333 | | |
332 | 334 | | |
333 | | - | |
| 335 | + | |
334 | 336 | | |
335 | 337 | | |
336 | 338 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
94 | 94 | | |
95 | 95 | | |
96 | 96 | | |
| 97 | + | |
| 98 | + | |
97 | 99 | | |
98 | 100 | | |
99 | 101 | | |
| |||
165 | 167 | | |
166 | 168 | | |
167 | 169 | | |
168 | | - | |
| 170 | + | |
169 | 171 | | |
170 | 172 | | |
171 | 173 | | |
| |||
241 | 243 | | |
242 | 244 | | |
243 | 245 | | |
244 | | - | |
| 246 | + | |
245 | 247 | | |
246 | 248 | | |
247 | 249 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
116 | 116 | | |
117 | 117 | | |
118 | 118 | | |
119 | | - | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
120 | 124 | | |
121 | 125 | | |
122 | 126 | | |
123 | 127 | | |
| 128 | + | |
| 129 | + | |
124 | 130 | | |
125 | 131 | | |
126 | 132 | | |
| |||
138 | 144 | | |
139 | 145 | | |
140 | 146 | | |
141 | | - | |
| 147 | + | |
| 148 | + | |
| 149 | + | |
142 | 150 | | |
143 | 151 | | |
144 | | - | |
145 | | - | |
| 152 | + | |
146 | 153 | | |
147 | 154 | | |
148 | 155 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
43 | 43 | | |
44 | 44 | | |
45 | 45 | | |
46 | | - | |
| 46 | + | |
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
| |||
0 commit comments