Repository navigation
Commit 94842ba
feat(core,plugin-security)!: 18.x step 7 part 7a-2 — a grant applies only in its own organization; the organization-less grant readers retire (#24872)
Part of #24583 — step 7, part 7a-2: the organization-less readers of the
authorization resolver and plugin-security retire. The card stays open
for the other step-7 parts (7a-1, 7a-3, 7b-1, 7b-2, 7b-3, 7c).
Clause-②: yes (narrowing) — measured: a stored grant row with no
`organization_id` no longer applies in any resolution, and no longer
anchors platform standing.
## What changes
**`@objectstack/core` — `resolve-authz-context.ts`**
- `grantAppliesInTenant(org, tenantId)` is now `org !== null && org ===
tenantId`. A `sys_user_position` (§4) or `sys_user_permission_set` (§6)
row applies only in its own organization; with no active organization,
no stored grant applies. §4 now reads the row through
`grantOrganizationOf`, so a blank value is normalized to null like
everywhere else.
- `grantOrganizationOf` stays as the normalizer; `null` (NULL, absent or
blank) confers nothing.
- `grantAnchorsPlatformStanding` loses its organization-less arm: under
`single`, only the Default Organization's `admin_full_access` grant
anchors `platform_admin`. That is the grant the first-user promotion
already writes. It still applies in every organization, or with none
active. Walled postures are unchanged (no grant anchors there).
- `mayApply` / `applyingGrants` keep their shape and now read the
narrowed rule; their docblocks are corrected.
**`@objectstack/plugin-security`**
- `delegated-admin-gate.ts`: `holdingTakesEffectIn` counts a holding
only when it belongs to the caller's active organization. That is
exactly what the resolver grants, so an organization-less holding, or a
caller with no active organization, can no longer self-delegate. The
org-admin removal of an organization-less row no longer claims the row
"applies in every organization".
- `bootstrap-platform-admin.ts`: `isAnchorHumanHolder` matches the
Default Organization's grant only. Leg A asks once, for the Default
Organization's id as `organization_id`, never for `null`. When no
Default Organization can be named (a wall, or none to name), the holder
scan reads no grant at all. The walled "no owner email" error loses its
sentence about a legacy unscoped grant row, which can no longer be
found.
- `organization-scope.ts`: `resolveOwnOrganizationRow` returns `{ own
}`; the residue half is gone.
- `grant-permission-set-name.ts`: the dead export `grantOrganizationOf`
is deleted. It was not re-exported from the package and had no importer.
**Two text corrections this change made false** (a declared extension of
the claim's file surface, text only):
- `core/src/security/admin-standing-surface.ts`: the module docblock and
the `sys_user_permission_set` reason no longer name the
organization-less row as an anchor.
- `runtime/src/domains/automation.ts`: under `single`, the run-lifecycle
refusal named "an unscoped `admin_full_access` grant" as the remedy. It
now names the Default Organization's grant.
**Pins and fixtures.** Every pin that asserted the retired reading is
rewritten to the new rule, each with a positive control in the same
test. Every fixture that relied on an organization-less grant to
exercise something else is re-stamped into an organization (with the
resolution run in it) or into the Default Organization. No assertion is
loosened, no test is skipped, and no test is deleted. These live in 16
packages, because the readers' pins reach that far: core,
plugin-security, plugin-auth, runtime, rest, cloud-connection,
service-datasource, service-settings, service-automation,
plugin-hono-server, plugin-approvals and plugin-sharing (test files
only), and dogfood. Five suites gained a new negative pin, "an
organization-less grant confers / anchors nothing".
## Premise check — the retired readers are dead after certification
| tree | measurement | result |
|:--|:--|:--|
| fresh `single` install (`pnpm dev -- --fresh`, merged head) |
`sys_user_permission_set` / `sys_user_position` rows with NULL or blank
`organization_id` | 0 of 2 / 0 of 5; the one `admin_full_access` grant
is owned by the Default Organization |
| walled (`isolated`) `os serve`, real Organizations runtime | same
tables | 0 rows at all; no grant row is written under a wall |
| ceremonied 18.0 fixture (R2 harness) | `expectCertified`: zero NULL
`organization_id` rows; the fixture's admin then signs in and reads |
green |
| composition with no organization object (`no-organization-object`) |
does any shipped composition reach it? | no — `os serve` registers
SecurityPlugin only inside the AuthPlugin block (which ships
`sys_organization`); no example, template or `os init` output composes
plugin-security without it. In such an embedding, grants stay
organization-less and now confer nothing (the changeset says so). |
## Verification (head `21b172681`: `origin/main` at `81120caac` merged
in, carrying #24681 `12d14765c`, 7a-1 `7c2eab929`, 7c `bd6dab009`, 7a-3
`7cacad534` and 7b-3 `df8bb40a0`)
The full set below ran on `a62067ad4` (main with #24681 and 7a-1). The
two later merges (7c, 7a-3, 7b-3 and the commits around them) were
re-checked on the packages they touch: plugin-sharing, plugin-approvals,
plugin-auth, runtime, service-settings, plugin-hono-server and CLI unit,
the merged sharing and settings dogfood files, spec `check:generated`,
R1, R4 live, and the harness. All green on `21b172681`.
| reading | result |
|:--|:--|
| R1 — fresh `single` boot, random port | health 200, seeded-admin
sign-in 200, session carries `platform_admin`, 0 error lines |
| R2 + R3 — `upgrade-18x-harness` +
`organization-ownership-boot-refusal-order` dogfood | 2 files / 12
passed (on every merged head) |
| R4 — `isolated` `os serve`, throwaway app declaring the real
`@objectstack/organizations` | banner reached, `Organizations` mounted,
discovery 200, 0 error lines; a non-owner admin resolves `everyone` only
|
| R4 — step 4's 7 walled e2e cases (`OS_TEST_TIERS=nightly`) | 2 files /
7 passed |
| package suites, whole | core 2680 · plugin-security 4056 · plugin-auth
3264 · runtime 7594 · rest 6424 · plugin-approvals 1170 · plugin-sharing
1201 · service-automation 2604 · plugin-hono-server 422 ·
cloud-connection 614 · service-datasource 852 · service-settings 899 ·
organizations 166 · trigger-api 55 · trigger-record-change 114 ·
trigger-schedule 174 · CLI `--project unit` 4635 — 0 failed |
| dogfood, the 71 files that touch grant tables | 71 files / 696 passed,
1 skipped (one earlier full run went red once on
`showcase-crud-persona-matrix`: its probe picked a paid invoice. That is
order-dependent and unrelated, it passed alone, and main fixed it in
`08d201a80`) |
| `dispatch-gates --commands` | 79 derived / 79 run, each with its exit
code; 79 exit 0 (`check-plugin-teardown-shape --self-test` needed its
pinned commit fetched into this shallow clone, then exit 0) |
| typecheck of the touched packages | exit 0 for all 13 touched packages
(core, plugin-security, plugin-auth, runtime, rest, cloud-connection,
service-datasource, service-settings, service-automation,
plugin-hono-server, plugin-approvals, plugin-sharing, dogfood).
`runtime/test-typecheck-debt.json` was re-recorded: one pinned TS7053
signature was renamed by a fixture's added table; the count is unchanged
|
| `check-platform-checklist --changed-from origin/main` | OK — 12
anchors judged, 0 lost |
**Reverse verification (ablation), core resolver, source path (core's
suite imports its own `src`):**
- leg 1: `grantAppliesInTenant` back to `organizationId === null || …` →
8 failed / 145 passed across the three resolver suites. Exactly the
organization-less and no-tenant pins went red. Restore proven by blob
hash.
- leg 2: `grantAnchorsPlatformStanding` given back its `null` arm → 5
failed / 170 passed. Exactly the anchor pins went red. Restore proven by
blob hash.
## Acceptance notes
- **Size.** 100+ files and more than 3,000 changed lines
(`check-governed-merges --branch`: 3,341). Almost all of it is test
fixtures across the 16 packages that pin these readers. No governed path
is touched, but the size rule applies: this lands by an authorized
approval or the maintainer's merge.
- **Out-of-surface organization-less readers (not edited, for the
seat):**
- `plugin-auth/src/last-admin-guard.ts` `anchorOwnerTest` (about line
1028) still counts an organization-less `admin_full_access` grant as a
platform-admin holder;
- `plugin-auth/src/ensure-default-organization.ts` (about lines 505–509)
still asks `organization_id: null` when it picks the Default
Organization's owner;
- `plugin-security/src/explain-engine.ts` `collectGrantProvenance` reads
holdings with no organization filter, so it can annotate a row the
resolver no longer applies.
On a certified or fresh database none of them is reachable, because no
such row exists.
- **plugin-sharing** (7a-3, not touched): `assertResolvableAdminScope`
(`sharing-rule-service.ts`) is no longer reachable over HTTP by any
grant. Its comment still describes the old reading.
- **Posture source split (pre-existing):** the resolver reads the walled
posture from `OS_TENANCY_POSTURE`, while cloud-connection's install door
reads it from the `tenancy` service. The organization-less grant used to
mask a disagreement between them.
- **Two fixture-design changes worth a look:**
`service-automation/src/sys-user-default-row-scope.integration.test.ts`
binds its `'*'` reader to the `everyone` anchor, because its "no
organisation" legs cannot be reached by a stored grant any more.
`service-datasource/src/__tests__/admin-routes-tenancy-posture-admission.test.ts`
§4–§7 now pin that an organization-less API key passes the posture but
holds nothing (403 rather than the posture's 401).
## 维护者速读(草稿)
- **改了什么:** 授权解析器与 plugin-security
不再把「没有组织的授权行」当成全局授权:一行授权只在它自己的组织里生效;平台管理员身份只认 Default Organization 名下的
`admin_full_access` 授权。
- **为什么改:** 18.0 升级仪式已把所有无组织的行归属到组织并设为 NOT NULL,全新安装也不会再写这种行,旧的读法已是死代码(本
PR 在全新安装、隔离墙启动、仪式后数据库上实测为零行)。
- **风险与代价(含回滚):** 只影响「不注册组织对象却挂
plugin-security」的嵌入式组合——这类组合的授权将不再生效;官方组合(`os serve`)不受影响。改动大(3,341
行,几乎全是测试夹具改戳),回滚即 revert 本 PR。
- **席位意见:**
- **你要做的:** 审阅后批准(超 3,000 行,需授权批准或由你合并);#24583 的其余部分落地后再发 18.0.0。
---
_Generated by [Claude
Code](https://claude.ai/code/session_01QzqbxJk3oD6sqe47Q84ET8)_
---------
Co-authored-by: Claude <noreply@anthropic.com>1 parent 19484af commit 94842ba
102 files changed
Lines changed: 2240 additions & 1133 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
- objects
- plugin-sharing/src
- qa/dogfood/test
- 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 | + | |
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
267 | 267 | | |
268 | 268 | | |
269 | 269 | | |
270 | | - | |
| 270 | + | |
| 271 | + | |
| 272 | + | |
| 273 | + | |
| 274 | + | |
271 | 275 | | |
272 | 276 | | |
| 277 | + | |
273 | 278 | | |
274 | 279 | | |
275 | 280 | | |
276 | 281 | | |
277 | 282 | | |
278 | 283 | | |
279 | 284 | | |
280 | | - | |
| 285 | + | |
281 | 286 | | |
282 | 287 | | |
283 | 288 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
56 | 56 | | |
57 | 57 | | |
58 | 58 | | |
| 59 | + | |
| 60 | + | |
| 61 | + | |
59 | 62 | | |
60 | | - | |
| 63 | + | |
61 | 64 | | |
62 | 65 | | |
63 | 66 | | |
64 | 67 | | |
65 | 68 | | |
66 | | - | |
| 69 | + | |
| 70 | + | |
| 71 | + | |
67 | 72 | | |
68 | 73 | | |
69 | 74 | | |
70 | 75 | | |
71 | | - | |
| 76 | + | |
72 | 77 | | |
73 | 78 | | |
74 | 79 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
35 | 35 | | |
36 | 36 | | |
37 | 37 | | |
38 | | - | |
| 38 | + | |
39 | 39 | | |
| 40 | + | |
40 | 41 | | |
41 | | - | |
| 42 | + | |
42 | 43 | | |
43 | 44 | | |
44 | 45 | | |
| |||
95 | 96 | | |
96 | 97 | | |
97 | 98 | | |
98 | | - | |
| 99 | + | |
99 | 100 | | |
100 | 101 | | |
101 | 102 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
38 | 38 | | |
39 | 39 | | |
40 | 40 | | |
41 | | - | |
| 41 | + | |
42 | 42 | | |
43 | 43 | | |
44 | 44 | | |
45 | | - | |
| 45 | + | |
46 | 46 | | |
47 | | - | |
| 47 | + | |
48 | 48 | | |
49 | 49 | | |
50 | 50 | | |
| 51 | + | |
| 52 | + | |
51 | 53 | | |
52 | 54 | | |
53 | 55 | | |
54 | | - | |
| 56 | + | |
55 | 57 | | |
56 | 58 | | |
57 | 59 | | |
| |||
61 | 63 | | |
62 | 64 | | |
63 | 65 | | |
| 66 | + | |
64 | 67 | | |
65 | 68 | | |
66 | 69 | | |
67 | 70 | | |
68 | 71 | | |
69 | 72 | | |
70 | | - | |
| 73 | + | |
71 | 74 | | |
72 | | - | |
73 | | - | |
| 75 | + | |
| 76 | + | |
74 | 77 | | |
75 | 78 | | |
| 79 | + | |
76 | 80 | | |
77 | 81 | | |
78 | 82 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
55 | 55 | | |
56 | 56 | | |
57 | 57 | | |
58 | | - | |
| 58 | + | |
| 59 | + | |
| 60 | + | |
59 | 61 | | |
60 | 62 | | |
61 | 63 | | |
62 | 64 | | |
63 | | - | |
| 65 | + | |
| 66 | + | |
64 | 67 | | |
65 | 68 | | |
66 | | - | |
| 69 | + | |
67 | 70 | | |
68 | 71 | | |
69 | 72 | | |
| |||
| Original file line number | Diff line number | Diff line change | |
|---|---|---|---|
| |||
189 | 189 | | |
190 | 190 | | |
191 | 191 | | |
192 | | - | |
| 192 | + | |
| 193 | + | |
193 | 194 | | |
194 | 195 | | |
195 | 196 | | |
196 | 197 | | |
197 | | - | |
| 198 | + | |
198 | 199 | | |
199 | 200 | | |
200 | 201 | | |
| |||
0 commit comments