Skip to content

Commit be48c5e

Browse files
feat(spec)!: retire AdminScope.manageBindings and authorEnvironmentSets — delegation rights no door honoured (#23488) (#23601)
Fixes #23488 Clause-②: yes Executes ruling of record 6181988435 on #23488 (letter A, the maintainer's 「同意」): `AdminScope.manageBindings` and `AdminScope.authorEnvironmentSets` retire inside the 18 major by the `spec-property-retirement` route, the delegated-admin gate's strict-containment comparison stops reading them, and the text that described the removed gates is corrected. The changeset declares `Clause-②: yes (narrowing)`, BREAKING, ADR-0087 disposition `registered`. The ADR half (ADR-0090 D12 status line, ADR-0086 line 446) is a separate Tier H docs PR on branch `claude/issue-23488-adr-delegation-rights-retired`, as ruling item 5 requires. The objectui half (Studio's two switches, help strings, i18n keys, pin test) is objectstack-ai/objectui#12406, filed by the seat (ruling item 4); this PR does not touch objectui. ## What changes **1. The retirement kit (`@objectstack/spec`).** - Two `retiredKey()` tombstones on `AdminScopeSchema` (`security/permission.zod.ts`). Each prescription says what the ruling binds: delete the key; a delegate's scope is placement, assignment and the allowlist. Both close with the house `os migrate meta --from 17` sentence. - `DelegableAdminScope` (`contracts/security-service.ts`) drops both fields. - D2 conversion `permission-admin-scope-binding-authoring-removed` in `MAJOR_18_CONVERSIONS` (order 65, `retiredFromLoadPath: true`, `retiredAfter: '17.7.0'`). It strips both keys from every `permissions[].adminScope`, whatever they hold, one notice per key, and the notice path names the set. Both keys carried `.default(false)`, so a 17.x parse materialized them in every declared scope. The rehydration seams replay this entry over stored rows and built artifacts (the `email_template.isSystem` precedent; `acceptRetiredDefaultResidue` is not adopted). - `RETIRED_KEYS_BY_MAJOR[18]`: `security/AdminScope:manageBindings`, `security/AdminScope:authorEnvironmentSets`. - D3 semantic entry `permission-admin-scope-binding-authoring-retired`, which states the containment change, plus its `STEP18_RATIONALE` fragment (order 97). - The form's help text, the generated reference page, `authorable-surface/security.json` (two rows become RETIRED), `authorable-defaults/security.json` (two default rows go), the liveness note (`liveness/permission.json`, re-verified, still `live`), and the removal-version pin's JOIN row. **2. The containment change (`@objectstack/plugin-security`, ruling item 2).** - `definitionContainsStrictly` compares `includeSubtree`, `manageAssignments` and the allowlist only. `resolveHeldScopes` and `describeDelegableScope` drop both flags. - Case (ii) of reading 6172654646 becomes a refusal: a delegate whose own scope carries an inert `true` can no longer hand out a scope otherwise equal to its own. It answers `403 PERMISSION_DENIED`. - New pins in `delegated-admin-gate.test.ts`, under the describe `[#23488] the retired binding/authoring flags count on no containment axis`: - case (ii) refused, asserting `code` + `status` + the message's first clause; - a CONTROL: the same delegate grants a scope narrower on a real axis; - case (i) reversed: the granted side's retired `true` no longer out-reaches the delegate. - Mechanism assumption 1 holds: the gate fixture set both flags `true` on both sides (`EAST_SCOPE`), so these pins are new, not flipped assertions. The six other plugin-security fixtures drop the keys. **3. The legacy-row ceremony (`@objectstack/core`, ruling item 3) — measured, and the mapper drops the keys.** - The measurement used a scratch test, not committed, in `packages/metadata-protocol` at `43c50c843`. It took `permissionSetDefinitionFromRow` over a 17.x-shaped row (`admin_scope` carrying both keys as `false`) and passed the body to `ObjectStackProtocolImplementation.saveMetaItem`. That is the door `os migrate organization-ownership` composes (`packages/cli/src/commands/migrate/organization-ownership.ts:567-578`). - The door answered `{"ok":false,"code":"INVALID_METADATA","status":422}` at `adminScope.manageBindings`, with the tombstone's prescription. - CONTROL: the same row without the two keys saved, `{"ok":true,"success":true}`. - So the save does not replay the conversion. That matches `conversions/stored.ts`'s header: "The write path must NOT use this: `saveMetaItem` rejects off-spec bodies with the current schema". - Fix: `permission-set-row-definition.ts` drops both keys from the parsed `admin_scope` (`RETIRED_ADMIN_SCOPE_KEYS`). - New pin: the body without the keys passes `PermissionSetSchema` for `false`, `true` and a non-boolean value. A CONTROL shows the same body with the key kept is refused at `['adminScope','manageBindings']`. - `packages/cli/src/utils/organization-ownership-apply.ts` is read only, not edited (mechanism assumption 4 holds). **4. Position 3 text (docs accuracy, no contract change).** - `content/docs/permissions/delegated-administration.mdx`: the action-flag and strict-containment rows. - `content/docs/permissions/permission-sets.mdx`: the strict-containment sentence. - The showcase's seven sentences: five in `examples/app-showcase/src/security/permission-sets.ts` (`:26-27`, `:166-167`, `:357-362`, `:384-385`, and the two keys deleted) and two in `examples/app-showcase/src/docs/showcase_tour_security.md`. - The spec JSDoc (`AdminScopeSchema` header, `assignablePermissionSets`, `PermissionSet.adminScope`). - The ledger note. - The two plugin-security comments: `delegated-admin-gate.ts` `scopesCoverUser` and the placement comment, and `objects/sys-user-permission-set.object.ts`. - `packages/metadata-core/src/meta-write-capability.ts:184-185`: the security plugin's data door no longer asks the walled verdict. Measured: `git grep -n 'walledCatalogWriteVerdict(' -- packages` finds no caller under `packages/plugins`. - `docs/design/permission-model.md` (the delegated-admin bullets and the authoring sample). - `packages/platform-objects`' four `metadata-forms` bundles: - `en` was regenerated with `pnpm i18n:extract`; - the three translated `helpText` values (zh-CN, ja-JP, es-ES) were edited by hand, since AGENTS.md allows translated-locale values to be hand-written; - the `adminScope` path has no source-hash entry, so it is legacy-trusted. - `packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts` now asserts both keys are absent from the served definition. **5. Tree-scoped absence pin.** `packages/spec/src/security/admin-scope-binding-authoring-retirement.test.ts` (registered in `vitest.repo-tests.json`) pins: - the refusal at every door: schema, the `permission` write-door binding, and `defineStack` with `STACK_SCHEMA_INVALID` / 422; - the tsc channel (`@ts-expect-error`); - the D2 strip: stored-row rehydration, the artifact before and after, idempotence, and that it is retired from the load path; - the registration; - an absence walk over the five roots already declared for `@objectstack/spec` in `scripts/cross-package-test-inputs.mjs`. ## File surface vs the claim These are outside the claim's list, each with the reason: - `docs/design/permission-model.md`: the reading (Reading 5) lists it, and it named both keys as authorable. - `examples/app-showcase/src/docs/showcase_tour_security.md`: two of "the showcase's seven" (ruling item 5). - `packages/qa/dogfood/test/showcase-permission-zoo.dogfood.test.ts`: it asserted `manageBindings === false`. - `packages/plugins/plugin-security/src/*.test.ts` fixtures. - `packages/spec/src/shared/retired-key-removal-version.pin.test.ts`: its guard requires a JOIN row for every newest-stable retirement. - `packages/spec/vitest.repo-tests.json`. - The generated `authorable-*` and reference files. Mechanism assumption 3 does not hold: no generator wrote under `skills/**`, so this PR touches no governed surface. ## Verification **Pre-merge head `0221b165f`.** Each command ran through `scripts/pm/os-verify-lock.sh`, with the exit code captured before any pipe: - `pnpm --filter @objectstack/spec test`: 677 files passed, 1 skipped; 20729 tests passed. - `pnpm --filter @objectstack/spec test:repo`: 62 files, 1051 tests passed. - `pnpm --filter @objectstack/spec typecheck`: exit 0. That includes `check:test-typecheck`, so the two `@ts-expect-error` lines are compiled and used. - `pnpm --filter @objectstack/plugin-security typecheck`: exit 0. - `pnpm --filter @objectstack/core test`: exit 0. - `pnpm --filter @objectstack/platform-objects test`: exit 0. - plugin-security, 7 targeted files (`delegated-admin-gate*.test.ts`, `granted-by-writer-provenance`, `reserved-identity-names`, `can-write-object-admission`): 206 passed. - `pnpm --filter @objectstack/spec check:generated`: one stale artifact (`content/docs/references/**`), regenerated with `gen:docs`. Every other gate was green. **Reverse verification, at `0221b165f`, both restored and proved by blob hash and an empty `git diff HEAD`:** - **Gate.** `delegated-admin-gate.ts` at the branch point `fa6a6085c` was written over the file. Of the four selected cases, 2 failed (case (ii) and case (i) reversed) and 2 passed (the CONTROL and the existing equal-scope refusal). That is the expected direction. - **Mapper.** `scripts/ablation-replace.mjs` removed the `withoutRetiredAdminScopeKeys(...)` call (anchor 1 → 0, blob `03f32ba6f610` → `01d09ba3e389`). The mapper pin failed (1 failed, 12 passed), and the file was restored to the HEAD blob. **objectui at the pin (`b9814b334d`), measured by reproduction, not by a full objectui build.** - I typechecked a copy of `PermissionAdvancedFacets.tsx`'s typing against this branch's built `@objectstack/spec` dist. The copy covered the `AdminScope` import, `Partial` alias, `setScopeDetail`, the three switches' `scope[key]` and the `{ [key]: !!v } as Partial` write, plus `DelegableAdminScope`. It used objectui's compiler options (`strict`, `moduleResolution: bundler`) and TypeScript 6.0.3, and exited 0. - CONTROL: a literal `{ businessUnit: 'x', manageBindings: false }` typed as `AdminScope` fails `TS2322` (the retired mark), and `DelegableAdminScope['manageBindings']` fails `TS2339`. So the reproduction reads the new types. - A full objectui build at the pin is NOT MEASURED. **Merged head `3854d1403`** (`origin/main` merged through `scripts/pm/os-regen-merge.sh`): the closure rebuild, the derived gate families, the consumer typechecks and the dogfood zoo file were in progress when this PR opened. Their readings go into the `os-dev-report` comment on #23488, and CI's check-runs on this PR are authoritative. ## Acceptance notes - **Not edited:** `packages/plugins/plugin-security/src/__tests__/legacy-catalog-objects.testkit.ts:333-336`. It describes the retired `sys_permission_set.admin_scope` column as it was, both keys included. That is accurate for a model of a retired object; carrier: none. - **Left as they are (PROSPECTIVE, not false):** three sentences describe a door that ADR-0138 D3 item 4 assigns to the bind door when it is built: `packages/plugins/plugin-security/src/builtin-positions.ts:92`, `packages/core/src/security/resolve-authz-context.ts:1099-1100`, and `content/docs/permissions/permission-sets.mdx` "the wildcard ban is the stricter `guest` tier's rule". Carrier: none. - **Release-owned, not touched:** `content/docs/releases/v13.mdx:91` and the `CHANGELOG.md` entries. - **Out-of-repo stored rows and consumers:** NOT MEASURED, as the reading stated. --- _Generated by [Claude Code](https://claude.ai/code/session_01LFqdRsuM5nkqoJdHd5ztVk)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent bc0538a commit be48c5e

38 files changed

Lines changed: 1016 additions & 112 deletions

File tree

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
---
2+
'@objectstack/spec': minor
3+
'@objectstack/plugin-security': minor
4+
'@objectstack/core': patch
5+
'@objectstack/platform-objects': patch
6+
---
7+
8+
feat(spec)!: retire the delegated-admin scope's `manageBindings` and `authorEnvironmentSets` — rights no door honoured (#23488)
9+
10+
**BREAKING** — `manageBindings` and `authorEnvironmentSets` on a permission set's `adminScope` (`AdminScopeSchema`, ADR-0090 D12) are now refused at parse. They named the right to bind sets to positions and the right to author environment permission sets, and they gated the `sys_position_permission_set` and `sys_permission_set` rows, which ADR-0131 D3 retired. A position's sets are part of its definition and a permission set is a definition; both are written through the metadata door, which admits a `manage_metadata` holder and reads no `adminScope`. So a delegate who "may bind" or "may author" was granted nothing. Delete both keys: a delegate's scope is placement, assignment and the allowlist.
11+
12+
Clause-②: yes (narrowing)
13+
14+
Measured before removal, at `cdc42eb49`: no production author set either key to `true` (the showcase wrote the default `false`; the control key `manageAssignments: true` has four production authors), and no door that writes a position or a permission set consulted either key. Their one reader was the delegated-admin gate's strict-containment comparison, which changes below.
15+
16+
## FROM → TO
17+
18+
| you wrote (17.7 and earlier) | write instead |
19+
| --- | --- |
20+
| `adminScope: { …, manageBindings: true \| false }` | nothing — delete the key |
21+
| `adminScope: { …, authorEnvironmentSets: true \| false }` | nothing — delete the key |
22+
| a delegate who re-composes what a position distributes, or authors a set's body | a `manage_metadata` holder writes the position or set definition through the metadata door (ADR-0131 D3); the delegate assigns people to positions and grants allowlisted sets inside its subtree |
23+
| reading `scopes[].manageBindings` / `scopes[].authorEnvironmentSets` from `GET /api/v1/security/my-delegable-scope` (`DelegableAdminScope`) | nothing — the report carries `manageAssignments` as the scope's one action right |
24+
25+
**The one-line fix:** delete `manageBindings` and `authorEnvironmentSets` from every `adminScope`.
26+
27+
**What an author who still writes either sees.** `tsc` fails at the authoring site (`AdminScope` types both keys as retired), and the parse — `definePermissionSet()`, `defineStack({ permissions })`, `PUT /api/v1/meta/permission/:name` — refuses it at its own path with the prescription:
28+
29+
> `permission.adminScope.manageBindings` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0090 D12) — nothing honoured it once the position-to-permission-set binding rows it gated were retired: a position's sets are part of its definition (ADR-0131 D3), and that definition is written through the metadata door, which admits a `manage_metadata` holder and reads no adminScope. Delete the key; a delegate's scope is placement, assignment and the allowlist — `businessUnit` with `includeSubtree`, `manageAssignments`, and `assignablePermissionSets`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand.
30+
31+
## The containment change (`@objectstack/plugin-security`)
32+
33+
The delegated-admin gate judges a delegate who hands out a set that itself carries an `adminScope` — a `sys_user_position` assignment of a position that distributes it, or a direct `sys_user_permission_set` grant — by **strict containment**. That comparison read both retired flags. It now reads `includeSubtree`, `manageAssignments` and the allowlist only:
34+
35+
- a delegate whose own scope set either flag `true` can **no longer** hand out a scope otherwise EQUAL to its own — the inert flag used to count as the strictly-larger axis and let that lateral copy through the equal-scope refusal. It is refused `403 PERMISSION_DENIED` (`… would hand out an adminScope not strictly contained by yours`);
36+
- a granted scope that set either flag `true` no longer counts as out-reaching a delegate that did not.
37+
38+
`DelegatedAdminGate.describeDelegableScope()` (and so `GET /api/v1/security/my-delegable-scope`) no longer reports either flag.
39+
40+
## The retirement kit
41+
42+
- **Two `retiredKey()` tombstones** on `AdminScopeSchema`, a `strictObject` — so each refusal carries its prescription and `tsc` fails first. `RETIRED_KEYS_BY_MAJOR[18]`: `security/AdminScope:manageBindings`, `security/AdminScope:authorEnvironmentSets`. `DelegableAdminScope` (`@objectstack/spec/contracts`) drops both fields.
43+
- **The D2 conversion `permission-admin-scope-binding-authoring-removed`** (protocol 18, retired from the load path) deletes both keys from every `permissions[].adminScope`, one notice per key, whatever they hold. Both keys carried the default `false`, so a 17.x parse materialized them in every declared scope; the rehydration seams replay this conversion over stored `permission` rows and built artifacts, and `os migrate meta --stored` lists the rows that carry them before the upgrade.
44+
- **The D3 entry `permission-admin-scope-binding-authoring-retired`** records the containment change above: the judgement the strip cannot make for the upgrader.
45+
- **`@objectstack/core`:** `permissionSetDefinitionFromRow` — the mapping `os migrate`'s legacy-row ceremony saves a `sys_permission_set` row through — drops both keys from the row's `admin_scope`. That save goes through `saveMetaItem`, which replays no conversion: measured, a 17.x row's body answered `422 INVALID_METADATA` at `adminScope.manageBindings`, and the same body without the keys saved.
46+
- **`@objectstack/platform-objects`:** the permission-set form's `adminScope` help text (all four locales) names the scope's live keys only.
47+
- **No deprecation window**, per the project's startup-stage posture.
48+
49+
⚠️ **The out-of-repo consumer population is NOT MEASURED.** `@objectstack/spec` is published, so this is breaking for consumers no telemetry was consulted for. Studio's two switches for these keys retire in an objectui follow-up.
50+
51+
<!-- adr-0087: registered permission-admin-scope-binding-authoring-removed, permission-admin-scope-binding-authoring-retired -->

‎content/docs/permissions/delegated-administration.mdx‎

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -56,8 +56,8 @@ set, and the gate resolves the name in the security catalog. The gate's rules:
5656
| Tenant admins pass through | The superuser wildcard reaches the ordinary CRUD/RLS checks — delegation constrains *delegates*, not HQ |
5757
| Subtree anchoring | Assignments a delegate creates must be anchored (`sys_user_position.business_unit_id`) inside their subtree, and the target user must sit inside it |
5858
| Allowlist, no self-escalation | Every set reached by the write — bound to the assigned position, or granted directly — must be on `assignablePermissionSets`. Granting an un-allowlisted set is refused **for anyone, including the delegate themselves** |
59-
| Action flags | `manageAssignments` gates the assignment rows — `sys_user_position`, and direct `sys_user_permission_set` grants. `manageBindings` and `authorEnvironmentSets` gated writes to the binding rows and set rows of the retired `sys_position_permission_set` and `sys_permission_set` objects; neither object is registered any more, so neither flag admits a write. A position's sets are part of its definition and a permission set is a definition: both are written through the metadata door, which admits a `manage_metadata` holder (the platform operator under `group` or `isolated`) and reads no `adminScope` (ADR-0131 D3). The two flags still count in the strict-containment comparison below |
60-
| Strict containment | Granting or authoring a set that itself carries an `adminScope` requires a held scope that **strictly contains** it — handing your own exact scope to a peer is refused (no lateral propagation) |
59+
| Action flag | `manageAssignments` gates the assignment rows — `sys_user_position`, and direct `sys_user_permission_set` grants — and is the scope's only action right. A position's sets are part of its definition and a permission set is a definition: both are written through the metadata door, which admits a `manage_metadata` holder (the platform operator under `group` or `isolated`) and reads no `adminScope` (ADR-0131 D3). The scope's former `manageBindings` and `authorEnvironmentSets` named the retired `sys_position_permission_set` and `sys_permission_set` rows, admitted no write once those objects were gone, and are retired: the schema refuses both with the fix |
60+
| Strict containment | Granting a set that itself carries an `adminScope` — assigning a position that distributes it, or granting it directly — requires a held scope that **strictly contains** it: the same root, no wider subtree, `manageAssignments` only if yours has it, an allowlist inside yours, and smaller on at least one of those. Handing your own exact scope to a peer is refused (no lateral propagation) |
6161
| Single-row writes | Delegates write single rows by id only — a broad filter-write cannot be boundary-checked |
6262
| Audit stamp | Every assignment a delegate creates is `granted_by`-stamped automatically |
6363
| Tenant-level assets stay tenant-level | The `everyone`/`guest` audience anchors and security-domain publishes are untouchable from any delegated scope |

‎content/docs/permissions/permission-sets.mdx‎

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -336,9 +336,11 @@ export const EastSubsidiaryAdmin = definePermissionSet({
336336
The runtime gate enforces: assignments anchored inside the subtree only,
337337
allowlisted sets only (**to others and to themselves** — no self-escalation),
338338
single-row writes, `granted_by` audit stamping, and **strict containment**
339-
when granting or authoring a set that itself carries an `adminScope` (handing
340-
out your own exact scope is refused). Anchors and security publishes stay
341-
tenant-level.
339+
when granting a set that itself carries an `adminScope` (handing out your own
340+
exact scope is refused). A delegate's scope is placement, assignment and the
341+
allowlist: writing a set's body or a position's sets is a metadata write, judged
342+
by `manage_metadata`, never by an `adminScope`. Anchors and security publishes
343+
stay tenant-level.
342344

343345
## Provenance — package vs environment sets (ADR-0086)
344346

‎content/docs/references/security/permission.mdx‎

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -31,8 +31,8 @@ const result = AdminScopeSchema.parse(data);
3131
| **businessUnit** | `string` | ✅ | [ADR-0090 D12] Delegation boundary: sys_business_unit.name of the subtree root. Required and non-blank: an empty or whitespace-only value names no business unit and is refused. |
3232
| **includeSubtree** | `boolean` | optional (default: `true`) | Cover descendant business units too (default true) |
3333
| **manageAssignments** | `boolean` | optional (default: `false`) | Manage user↔position assignments within the subtree |
34-
| **manageBindings** | `boolean` | optional (default: `false`) | Manage position↔permission-set bindings within the subtree |
35-
| **authorEnvironmentSets** | `boolean` | optional (default: `false`) | Author environment-owned permission sets (single posture only: under group or isolated the platform operator authors the catalog, ADR-0131 D3) |
34+
| **manageBindings** | `never` | optional | [REMOVED] `permission.adminScope.manageBindings` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0090 D12) — nothing honoured it once the position-to-permission-set binding rows it gated were retired: a position's sets are part of its definition (ADR-0131 D3), and that definition is written through the metadata door, which admits a `manage_metadata` holder and reads no adminScope. Delete the key; a delegate's scope is placement, assignment and the allowlist — `businessUnit` with `includeSubtree`, `manageAssignments`, and `assignablePermissionSets`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
35+
| **authorEnvironmentSets** | `never` | optional | [REMOVED] `permission.adminScope.authorEnvironmentSets` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0090 D12) — nothing honoured it once permission sets stopped being rows: a permission set is an environment metadata definition (ADR-0131 D3), written through the metadata door, which admits a `manage_metadata` holder and reads no adminScope. Delete the key; a delegate's scope is placement, assignment and the allowlist — `businessUnit` with `includeSubtree`, `manageAssignments`, and `assignablePermissionSets`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
3636
| **assignablePermissionSets** | `string[]` | optional (default: `[]`) | Allowlist of permission-set names the delegate may hand out |
3737

3838

@@ -125,7 +125,7 @@ const result = AdminScopeSchema.parse(data);
125125
| **systemPermissions** | `string[]` | optional | System level capabilities |
126126
| **tabPermissions** | `Record<string, Enum<'visible' \| 'hidden' \| 'default_on' \| 'default_off'>>` | optional | App/tab visibility: visible, hidden, default_on (shown by default), default_off (available but hidden initially) |
127127
| **rowLevelSecurity** | `{ name: string; label?: string; description?: string; object: string; … }[]` | optional | Row-level security policies (see rls.zod.ts for full spec) |
128-
| **adminScope** | `{ businessUnit: string; includeSubtree?: boolean; manageAssignments?: boolean; manageBindings?: boolean; … }` | optional | [ADR-0090 D12] Scoped delegated-administration grant (BU subtree + assignable-set allowlist) |
128+
| **adminScope** | `{ businessUnit: string; includeSubtree?: boolean; manageAssignments?: boolean; assignablePermissionSets?: string[] }` | optional | [ADR-0090 D12] Scoped delegated-administration grant (BU subtree + assignable-set allowlist) |
129129
| **protection** | `{ lock: Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>; reason: string; docsUrl?: string }` | optional | Package author protection block — lock policy for this permission set. |
130130
| **_lock** | `Enum<'none' \| 'no-overlay' \| 'no-delete' \| 'full'>` | optional | Item-level lock — controls overlay & delete (ADR-0010). |
131131
| **_lockReason** | `string` | optional | Human-readable reason shown when a write is refused by _lock. |
@@ -182,8 +182,8 @@ const result = AdminScopeSchema.parse(data);
182182
| **businessUnit** | `string` | ✅ | [ADR-0090 D12] Delegation boundary: sys_business_unit.name of the subtree root. Required and non-blank: an empty or whitespace-only value names no business unit and is refused. |
183183
| **includeSubtree** | `boolean` | optional (default: `true`) | Cover descendant business units too (default true) |
184184
| **manageAssignments** | `boolean` | optional (default: `false`) | Manage user↔position assignments within the subtree |
185-
| **manageBindings** | `boolean` | optional (default: `false`) | Manage position↔permission-set bindings within the subtree |
186-
| **authorEnvironmentSets** | `boolean` | optional (default: `false`) | Author environment-owned permission sets (single posture only: under group or isolated the platform operator authors the catalog, ADR-0131 D3) |
185+
| **manageBindings** | `never` | optional | [REMOVED] `permission.adminScope.manageBindings` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0090 D12) — nothing honoured it once the position-to-permission-set binding rows it gated were retired: a position's sets are part of its definition (ADR-0131 D3), and that definition is written through the metadata door, which admits a `manage_metadata` holder and reads no adminScope. Delete the key; a delegate's scope is placement, assignment and the allowlist — `businessUnit` with `includeSubtree`, `manageAssignments`, and `assignablePermissionSets`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
186+
| **authorEnvironmentSets** | `never` | optional | [REMOVED] `permission.adminScope.authorEnvironmentSets` was removed in @objectstack/spec 18 (ADR-0049 enforce-or-remove, ADR-0090 D12) — nothing honoured it once permission sets stopped being rows: a permission set is an environment metadata definition (ADR-0131 D3), written through the metadata door, which admits a `manage_metadata` holder and reads no adminScope. Delete the key; a delegate's scope is placement, assignment and the allowlist — `businessUnit` with `includeSubtree`, `manageAssignments`, and `assignablePermissionSets`. Run `os migrate meta --from 17` to list the mechanical edits for existing sources; `--write` applies the ones it can prove, and you apply the rest by hand. |
187187
| **assignablePermissionSets** | `string[]` | optional (default: `[]`) | Allowlist of permission-set names the delegate may hand out |
188188

189189
### Nested Shape: `PermissionSet.protection`

‎docs/design/permission-model.md‎

Lines changed: 11 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -294,7 +294,7 @@ itself is a scoped grant*. Example: the 华东 subsidiary's IT admin receives an
294294
that says:
295295

296296
- *where*: the 华东 business-unit subtree only;
297-
- *what*: assign users to positions, bind sets to positions — inside that subtree;
297+
- *what*: assign users to positions, and grant allowlisted sets directly — inside that subtree;
298298
- *which*: an **allowlist** of sets they may hand out (say, the CRM and HR self-service sets).
299299

300300
They can onboard their own staff and reshuffle their own positions, but they can never grant
@@ -303,19 +303,21 @@ things (the `everyone`/`guest` anchors, security publishes). Headquarters keeps
303303
the same explain engine answers "who *could have* granted this", not just "who did".
304304

305305
**How it is authored (landed in P3).** An admin scope is a field on an ordinary permission set
306-
(`adminScope: { businessUnit, includeSubtree, manageAssignments, manageBindings,
307-
authorEnvironmentSets, assignablePermissionSets[] }`), so it is distributed via positions and
308-
audited like every other grant. The same set should also carry plain CRUD on the RBAC link
309-
tables (`sys_user_position`, `sys_position_permission_set`, `sys_user_permission_set`) — the
310-
scope authorizes *what* may be administered, the CRUD bits let the requests through at all.
306+
(`adminScope: { businessUnit, includeSubtree, manageAssignments, assignablePermissionSets[] }`),
307+
so it is distributed via positions and audited like every other grant. The same set should also
308+
carry plain CRUD on the assignment tables (`sys_user_position`, `sys_user_permission_set`) — the
309+
scope authorizes *what* may be administered, the CRUD bits let the requests through at all. A
310+
position's sets and a set's body are environment definitions written through the metadata door by
311+
a `manage_metadata` holder (ADR-0131 D3), so binding and set authoring are not delegable: the
312+
scope's former `manageBindings` and `authorEnvironmentSets` retired with the binding and set rows
313+
they gated.
311314
Runtime rules enforced by the `DelegatedAdminGate` (plugin-security):
312315

313316
- assignments a delegate creates must be **anchored** (`sys_user_position.business_unit_id`)
314317
inside their subtree, and are `granted_by`-stamped automatically;
315318
- every set reached by the write — bound to the assigned position, or granted directly — must be
316-
in the allowlist; re-composing a position (bindings) requires every current holder to sit
317-
inside the subtree;
318-
- granting or authoring a set that itself carries an `adminScope` requires a held scope that
319+
in the allowlist;
320+
- granting a set that itself carries an `adminScope` requires a held scope that
319321
**strictly contains** it (handing your own exact scope to a peer is refused — no lateral
320322
propagation);
321323
- delegates write **single rows by id** only (a broad filter-write cannot be boundary-checked);

0 commit comments

Comments
 (0)