Repository navigation
Commit 7d45c8e
fix(plugin-security): the shipped sets stop copying the identity objects' declared row scopes (#24761)
Fixes #24484
Clause-②: no
The follow-up that ruling E′ item 3 (comment 6250748677 on #24164)
names, as #24340's body records it. `organization_admin` (and so the
derived `organization_admin_no_bypass`), `member_default` and
`viewer_readonly` no longer carry copies of the row scope that thirteen
identity objects now declare as their own `defaultRowScope` (declared by
#24341). The thirteen are the seven SCIM projection tables,
`sys_verification` and `sys_jwks`, and the `select` self scope of
`sys_account`, `sys_team_member`, `sys_oauth_access_token` and
`sys_oauth_refresh_token`. The security plugin's collection applies an
object's default wherever the caller's held sets carry no policy for
that object and operation, so rows do not move. Explain now names the
object's default (`via: 'object_default'`) where it named a set's copy,
as triage (6261582931) recorded the ruling accepts. No schema, `via`
value, object declaration, `core` code or `packages/spec` file changes.
It runs after #24136 (landed as b27f8de), which edited the same file;
this branch starts on the tree that holds it.
## Per-copy byte-identity readings (taken on `origin/main` 4a2ca57)
Each shipped set's policy for each candidate object, compared field by
field (`name`, `object`, `operation`, `using`, `check`, `positions`,
`enabled`) with the object's declared `defaultRowScope` plus `object`.
Sets read: `organization_admin`, `organization_admin_no_bypass`,
`member_default`, `viewer_readonly`.
| Family | Objects | Copies read | Identical | Disposition |
|---|---|---|---|---|
| SCIM | `sys_scim_user`, `sys_scim_subject`,
`sys_scim_projection_grant`, `sys_scim_identity_tombstone`,
`sys_scim_group`, `sys_scim_group_member`, `sys_scim_connection_binding`
| 28 | 28 | dropped |
| credential | `sys_verification`, `sys_jwks` | 8 | 8 | dropped |
| `_self`, `select` | `sys_account`, `sys_team_member`,
`sys_oauth_access_token`, `sys_oauth_refresh_token` | 16 | 16 | dropped
|
| ledger | `sys_audit_log` (`sys_audit_log_org`) | 4 | 4 | **kept**,
measured (below) |
56 of 56 copies were byte-identical. The only `'*'` policies in any
shipped set are `member_default`'s `owner_only_writes` (`update`) and
`owner_only_deletes` (`delete`), so after the drop each holder's
`select` collection on the thirteen objects is empty and collects the
default. On the head, the same reading finds only the four ledger copies
left among the candidates, and the must-keep rows unchanged.
## What stays, and why
- **Pointer 6280763190's must-keep list**, untouched: `sys_user_self`
(`all`) in `member_default`, and every `all` self scope (`sys_session`,
`sys_two_factor`, `sys_user_preference`, `sys_api_key`,
`sys_device_code`, `sys_oauth_consent`, `sys_oauth_application`,
`sys_organization`). A member's `update` and `delete` collections hold
the `'*'` ownership floor, so the default is never collected there. Also
kept, outside the pointer's may-drop list: `sys_user`'s two `select`
policies in `organization_admin` and `viewer_readonly` (they differ from
the merged `sys_user` declaration), and `viewer_readonly`'s `select`
spellings of objects whose default is `all` (they differ by operation).
- **`sys_audit_log_org`: kept, although it is byte-identical and on the
pointer's may-drop list.** I measured it before dropping it. When no
organization wall is enforced, `collectRLSPolicies` strips the
platform's own tenant policies by provenance (`isPlatformTenantPolicy`).
`platform-tenant-policies.ts` builds those provenance keys from the
shipped sets' policies. With no set carrying the copy, the ledger's
identical default is no longer recognised as the platform's. It survives
the `single` strip as an authored tenant policy and narrows the ledger
read. Ablation A3 below measures it under `single`: the viewer, both
organization-admin variants and their agents went from all three ledger
rows to one. `sys-audit-log-row-scope.test.ts` already pins the
provenance half (the shipped copies are non-empty and each is a platform
tenant policy). The helper's comment now says why the copy stays.
## Rows: the before/after served-ids run
New pin:
`packages/plugins/plugin-security/src/objects/shipped-sets-identity-default-row-scope.test.ts`,
modelled on `default-row-scope-declarations.test.ts`. It runs on the
real `SecurityPlugin` + `ObjectQL` + `SqlDriver`, with the real
declarations and a security catalog of the shipped sets. It covers three
deployment shapes: walled with the platform baseline, walled with **no**
platform baseline (`fallbackPermissionSet: null`, each principal
resolves only the set it names), and `single`. The personas are a
`member_default` holder, a `viewer_readonly` holder, both
organization-admin variants, a user in no organization, delegated MCP
agents for the member, the viewer and the organization admin (ruling B,
6278784335), and the platform administrator as a control. Each persona's
served ids are asserted against the ids written out per family, on all
14 touched objects. They are also asserted equal to what the same
deployment serves with the dropped copies spread back into the sets, on
those objects plus `sys_user` and the eight `all` objects.
| Tree | Served-ids block (9 tests: 3 shapes x listed ids, copies
restored, control) | After-state pins (explain, census, authored policy,
no copy in any set) |
|---|---|---|
| `origin/main` 4a2ca57, sets unchanged | **9/9 pass** (plus the two
keep pins and the private-object control) | 20 red, by design: the
copies are still there |
| head e12d958 | **9/9 pass** | all pass: 32/32 in the file |
## Explain moves, as ruled
Pinned per family against the copies-restored tree:
- SCIM: a member's record-grained explain of its own `sys_scim_user` row
gives the rule `sys_scim_user_self` with `via: 'object_default'`. The
tree with the copy gives `business_rls` and no `via`. The object-level
read of `sys_scim_group` names `object default row scope
'sys_scim_group_none'` as its source, for all four set holders.
- `_self`: the same for `sys_account` / `sys_account_self`.
- credential: the object-level reads of `sys_verification` and
`sys_jwks` name `sys_verification_none` / `sys_jwks_none`. The verdict
is unchanged.
- `sys_audit_log_org`: **not moved**, because the copy is kept (above).
- Census: every human persona collects the object default on exactly the
thirteen dropped objects' `select`, and on no other object or operation.
With the copies restored, none. A delegated agent's own leg collects
none (ruling B, whole).
## An authored policy still wins
A member who also holds an application set authoring `sys_account` `id
== null` reads no `sys_account` row, and its collection is exactly that
policy. The control is the same member without the set, who reads its
own row under the default. Measured change: on 4a2ca57 this holder
read its own row, because `member_default`'s copy was OR-ed in. An
authored scope that left out the holder's own rows was widened back to
them. Now the authored policy alone decides on that object and
operation. This only ever narrows. No package or example in this tree
authors a set policy on any of the thirteen objects (`git grep` over
`packages`, `examples` and `apps`, tests excluded: 0 hits).
## Ablations (each pin red under the forbidden change, then restored)
Mutations went through `scripts/ablation-replace.mjs`, which checks the
anchor count, the replacement count and the blob change on disk.
Restores used `git checkout HEAD -- PATH` and were proven by the blob
hash equalling HEAD's and by an empty `git diff HEAD`. No build was
involved: the pin imports the sets from source. All four ran on
48e8f6d.
| Leg | Forbidden change | Red | Green, as expected |
|---|---|---|---|
| A1b | drop `member_default`'s `sys_oauth_application_self` (`all`) |
keep pin (member write collection), census | served ids |
| A2 | drop `member_default`'s `sys_user_self` (`all`) | keep pin (write
collection), `sys_user_self` stays `all` | served ids |
| A3 | drop `sys_audit_log_org` from all three sets | served ids under
`single` (viewer, org admins and their agents: 3 ledger rows to 1),
census, ledger-copy pin | served ids walled, both shapes |
| A4b | re-add `sys_account_self` (`select`) to `member_default` |
no-copy pin, `_self` explain pin, census, authored-policy pin | served
ids |
The first A1 and A4 attempts never ran. The tool refused both before any
test, because their replacement overlapped their anchor. They were
re-anchored as A1b and A4b.
## Also in this diff
- **Existing pins restated** where they asserted the copies stay:
`default-row-scope-declarations.test.ts` (the thirteen declare the
literal written out and no set carries it; the behaviour-neutrality
comparison covers the ten declarers the sets still carry; the census
expects the thirteen `select` defaults),
`object-default-row-scope.test.ts` (`sys_scim_user` moves out of its
byte-for-byte block into a block showing the declaration is the scope),
and `rbac-objects.test.ts` (the `member_default` roster).
- **`private-credential-row-scope.test.ts`**: the header says where the
scope now lives, and the `agent_x` persona (pointer 6280763190, last
bullet) now has the shape its one producer emits: `userId` is the
delegating member, `onBehalfOf.principalKind: 'human'`.
`scim-projection-row-scope.test.ts` gets the same header correction, and
its agent comment now states ruling B.
- **Outside the claim's declared file surface, comment-only, declared
here:** the 13 `packages/platform-objects/src/identity/*.object.ts`
comments above each `defaultRowScope` said "the shipped sets still carry
it". That is false after this change. Only `//` lines changed; no
declaration byte moved, and the declaration pin reads each value.
- **Deliberate correction of a pending release note:**
`.changeset/24341-identity-objects-default-row-scope.md`. Three
sentences stated that the shipped sets carry each policy, which this
change makes false before that note ships. They now say each object's
policy is either still carried or collected as the identical default.
`check-empty-changeset` is red on purpose: its own text says a
deliberate correction is confirmed on the PR, not routed around.
**Please confirm this correction.** The other #24341 note sentences are
about that change's own effect and stay true.
- New changeset:
`.changeset/24484-sets-stop-copying-identity-defaults.md`,
`@objectstack/plugin-security` `patch`.
## Tests and gates (head e12d958)
- `pnpm --filter @objectstack/plugin-security test`: 191 files, 3926
passed, 45 skipped, exit 0.
- `pnpm --filter @objectstack/plugin-security typecheck` (tsc, scripts,
test layer): exit 0. File-scoped `eslint --no-inline-config` on the
seven touched plugin-security files: exit 0.
- `pnpm --filter @objectstack/platform-objects test`: 1195 passed.
`typecheck`: exit 0.
- Closure build `pnpm --filter '@objectstack/plugin-security^...'
build`: exit 0, run on the merged tree.
- `node scripts/pm/dispatch-gates.mjs --commands --repo
objectstack-ai/objectstack` derived 67 families. Each was run locally
with its exit captured before any pipe. 64 exited 0.
`check-empty-changeset --base origin/main` exited 1: the deliberate
correction above. `check:dual-build-cjs-loads` and `check:i18n` exited
3, PREREQUISITE NOT MET (they need a whole-repo build), so they are NOT
MEASURED here and CI's `Lint & Repo Gates` measures them. `--ran`
reconciliation: 67 accounted for, 65 run, 2 NOT MEASURED, 0 UNRUN.
- `test:repo` was not run locally; CI's `Test Core` runs it.
## Acceptance notes
- **Provenance reads the sets, not the declarations.**
`platform-tenant-policies.ts` keys "this is the platform's tenant
policy" off the shipped sets. The ledger's copy therefore cannot go
until provenance also recognises an object's declared default. Dormant
today, because the copy stays. Carrier: none.
- Agents on `private` objects: a read ceiling's `'*'` does not reach
`sys_verification`, `sys_jwks`, `sys_oauth_access_token` or
`sys_oauth_refresh_token`, so the agent personas are refused there
before any row scope applies. That is identical on both trees, and the
pin lists it.
---
_Generated by [Claude
Code](https://claude.ai/code/session_011TdZ8GjixNZFLz1xJD9zzK)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 74f7815 commit 7d45c8e
22 files changed
Lines changed: 850 additions & 385 deletions
File tree
- .changeset
- packages
- platform-objects/src/identity
- plugins/plugin-security/src
- objects
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
7 | 7 | | |
8 | 8 | | |
9 | 9 | | |
10 | | - | |
| 10 | + | |
11 | 11 | | |
12 | | - | |
| 12 | + | |
13 | 13 | | |
14 | 14 | | |
15 | 15 | | |
| |||
19 | 19 | | |
20 | 20 | | |
21 | 21 | | |
22 | | - | |
| 22 | + | |
23 | 23 | | |
24 | 24 | | |
25 | 25 | | |
| |||
| 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
115 | 115 | | |
116 | 116 | | |
117 | 117 | | |
118 | | - | |
119 | | - | |
120 | | - | |
121 | | - | |
122 | | - | |
123 | | - | |
124 | | - | |
| 118 | + | |
| 119 | + | |
| 120 | + | |
| 121 | + | |
| 122 | + | |
| 123 | + | |
| 124 | + | |
125 | 125 | | |
126 | 126 | | |
127 | 127 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
40 | 40 | | |
41 | 41 | | |
42 | 42 | | |
43 | | - | |
44 | | - | |
| 43 | + | |
| 44 | + | |
45 | 45 | | |
46 | 46 | | |
47 | | - | |
48 | | - | |
49 | | - | |
| 47 | + | |
| 48 | + | |
| 49 | + | |
50 | 50 | | |
51 | 51 | | |
52 | 52 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
39 | 39 | | |
40 | 40 | | |
41 | 41 | | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
48 | | - | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
| 48 | + | |
49 | 49 | | |
50 | 50 | | |
51 | 51 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
42 | | - | |
43 | | - | |
44 | | - | |
45 | | - | |
46 | | - | |
47 | | - | |
| 41 | + | |
| 42 | + | |
| 43 | + | |
| 44 | + | |
| 45 | + | |
| 46 | + | |
| 47 | + | |
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
52 | 52 | | |
53 | 53 | | |
54 | 54 | | |
55 | | - | |
56 | | - | |
| 55 | + | |
| 56 | + | |
57 | 57 | | |
58 | 58 | | |
59 | | - | |
60 | | - | |
61 | | - | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
62 | 62 | | |
63 | 63 | | |
64 | 64 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
58 | 58 | | |
59 | 59 | | |
60 | 60 | | |
61 | | - | |
62 | | - | |
| 61 | + | |
| 62 | + | |
63 | 63 | | |
64 | 64 | | |
65 | | - | |
66 | | - | |
67 | | - | |
| 65 | + | |
| 66 | + | |
| 67 | + | |
68 | 68 | | |
69 | 69 | | |
70 | 70 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
51 | 51 | | |
52 | 52 | | |
53 | 53 | | |
54 | | - | |
55 | | - | |
| 54 | + | |
| 55 | + | |
56 | 56 | | |
57 | 57 | | |
58 | | - | |
59 | | - | |
60 | | - | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
61 | 61 | | |
62 | 62 | | |
63 | 63 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
47 | 47 | | |
48 | 48 | | |
49 | 49 | | |
50 | | - | |
51 | | - | |
| 50 | + | |
| 51 | + | |
52 | 52 | | |
53 | 53 | | |
54 | | - | |
55 | | - | |
56 | | - | |
| 54 | + | |
| 55 | + | |
| 56 | + | |
57 | 57 | | |
58 | 58 | | |
59 | 59 | | |
| |||
0 commit comments