Skip to content

Commit 06cc45d

Browse files
fix(security)!: under a wall the security catalog is the platform operator's — tenants are refused catalog writes and may only assign (ADR-0131 D3) (#22895)
Part of #15204 Clause-②: yes (narrowing) Stage W of #15204 (claim amendment 6112750171): the walled catalog refusal. This PR builds the refusal ADR-0131 D3 names. The C3 card's seeder and catalog-object retirement stays open on #15204 and is not part of this PR. ## The ruling this executes ADR-0131 D3, verbatim: "Shared-DB multi-tenant (`group` / `isolated`): tenant admins cannot create or edit catalog items; the operator defines the catalog for every tenant (managed packages, Studio). Creation through Setup or the API is refused with a message naming the posture and the capability." ADR-0131 D7 adds that the catalog pages are "read-only under a wall". The server refuses whatever the UI shows. This is the refusal D3 names, not a new gate. Catalog writes stay with `manage_metadata` holders under `single` (#22621 → A, unchanged). Under a wall the authority is the platform operator's (#22801 → A). ## Measured first, on `main` at `40bc51f` The harness was a booted `examples/app-crm` stack (`bootStack`, `multiTenant: 'posture-only'`, which gives a real non-degraded posture). It had two organizations created over `POST /auth/organization/create`. Organization A's owner was granted a set carrying `manage_metadata`, scoped to organization A. That owner is not the platform operator (`isPlatformAdmin: false`). Organization B's member held an environment set, `w_shared`, scoped to organization B. | Door | `isolated` | `group` | |:---|:---|:---| | A: `PUT /meta/permission/w_shared` (adds `manage_users`) | 200 | 200 | | B's next resolution (`resolveUserAuthzGrants`) | **gains `manage_users`** | **gains `manage_users`** | | A: `PUT /meta/position/:name` and `PUT /meta/permission/:name` (create) | 200 | 200 | | A: `DELETE /meta/permission/:name` | 200 | 200 | | A: `PUT /meta/capability/:name` | 403 (`capability` is code-only) | 403 | | A: `POST /security/_activation/permission/w_shared` | 403 (operator-only already) | 403 | | A with CRUD on `sys_permission_set`: `POST /data/sys_permission_set` | 201, lands in the **environment ledger** | n/a | | A with CRUD on `sys_position`: `POST /data/sys_position` | 201, an organization row with no definition | n/a | | A: `POST /data/sys_user_permission_set` (assignment) | 201 | n/a | **Cross-tenant write: yes.** One organization's `PUT /meta` changed what another organization's members resolve. The precondition is that a principal below the operator rung holds `manage_metadata`. A stock `organization_admin` does not hold it, and the anti-escalation deny in `default-permission-sets.ts` refuses that admin the catalog tables at the data door. The data door's edit of an existing environment set answered 404, because the wall hides the organization-less projection row. So the data door created environment sets but did not reach another organization's set. ## What changed - **One predicate:** `walledCatalogWriteVerdict` in `@objectstack/metadata-core`, beside `metaWriteCapabilityVerdict`. Under `group` / `isolated`, a write of a `position`, `permission` or `capability` is admitted only for `isSystem` or the `PLATFORM_ADMIN` posture rung. The activation gate already reads that rung for the same reason. It is derived from the platform-admin anchors, never from a held capability. Under `single`, or with no `tenancy` service, the predicate is inert. URL spellings are folded through `canonicalMetaUrlType`, the same fold `saveMetaItem` applies, so `/meta/permissions/x` is judged as `permission`. - **The `/meta` doors** ask it right after the capability gate. On `RestServer` that is save, reset, publish and rollback (`walledCatalogRefusal`), answering `403 FORBIDDEN`. On the runtime dispatcher it is `PUT`, answering `403 PERMISSION_DENIED`. The posture is read only for a catalog type. The dispatcher reads it through `readEffectiveTenancyPosture`, factored out of the activation gate unchanged. REST reads it through the existing `resolveAnonymousFormTenancy` wiring, so the posture and the protocol come from one kernel. A tenancy service that is registered but cannot answer is a 503, never a permit. - **The data door:** `walled-catalog-data-door.ts` (`plugin-security`), a middleware on `sys_position`, `sys_permission_set` and `sys_capability`. It runs inside the security middleware and before the permission-set write-through, so a refused write is never translated into a metadata save. Assignments (`sys_user_position`, `sys_user_permission_set`) are not judged. - **ADR-0087:** the semantic entry `walled-catalog-tenant-writes-retired`. The changeset carries FROM → TO. - **Docs:** system-context census row 55b, with the counts regenerated. ### Why not `saveMetaItem` The amendment named "the metadata door's write path, so every door that saves through `saveMetaItem` inherits it". Measured: `saveMetaItem` receives no caller principal, only an `actor` string. It cannot tell the operator rung from a tenant without a contract change on every caller. So the predicate sits where the caller is known: each door, after its capability gate. That is the activation gate's two-step shape (`refuseUngrantedActivationAuthoring`, then `refuseUngrantedActivationWrite`). It is one predicate with one sentence, and no door restates it. ## The clone door (#22881, stage CD) CD landed first (`45e206e4`), and this PR merged it in (`d2a6934e5`). The clone door creates environment permission sets, so this PR adds the same call there: - `permission-set-clone.ts` asks `walledCatalogWriteVerdict({ type: 'permission', ... })` right after `metaWriteCapabilityVerdict`, with the posture read through `readEffectiveTenancyPosture`. - The call comes before any catalog read, so a refused caller learns nothing about the source or the target name. The answer is `403 PERMISSION_DENIED`, the dispatcher `PUT`'s envelope. - Pins: - `permission-set-clone.test.ts`: `isolated` and `group` are refused with nothing saved; the operator rung is admitted; a `single` control; the capability gate still runs first. - Two dogfood legs: organization A's clone answers 403 with no definition saved, and an operator control answers 200. ## Re-judged pins - `position-write-through.test.ts`: the old walled pin was "an organization administrator's create lands its organization row, and no definition". That is the behaviour D3 refuses, so it now pins the refusal. The promotion pin seeds the pre-existing row-only position with a system write: that population is #15196 Q3 = A's. - `builtin-positions.boot.test.ts`: under a wall, the golden's "control: create a position row" is now `403 PERMISSION_DENIED`. The assignment cases do not move. ## Tests (first round at `de89dd413`; patch round 1 at code head `26c70abf8`; every later commit up to `637f1269b` is text only: docs, the changeset, the spec entry's strings, one `.describe()` string and its regenerated reference page) - `metadata-core` 17 files / 386 tests, plus 8 new predicate cases. `plugin-security` 198 files / 4057 passed. `rest` 278 files / 5542 passed. `runtime` 355 files / 5831 passed. `spec` `src/migrations` 205 passed. Build and typecheck green for metadata-core, rest, runtime and plugin-security. - New pins: - `wall-catalog-refusal.dogfood.test.ts`: `isolated` and `group`, plus a `single` control, 13 tests. - `walled-catalog-data-door.test.ts`: real `ObjectQL` and `SecurityPlugin`, 12 tests. - `meta-walled-catalog-refusal.test.ts`: the dispatcher, 11 tests. - **Ablation, REST door:** `walledCatalogRefusal` was forced to admit, `rest` rebuilt, and the marker confirmed in `dist/` by `ablation-dist-preflight`. The 4 `/meta` dogfood pins went red, on both postures. The data-door and control pins stayed green, as expected. Restored to the `HEAD` blob and rebuilt; the marker is absent from `dist/`. - **Ablation, data door:** the middleware registration was removed. 6 walled refusal pins went red, and the assignment, operator and `single` controls stayed green. Restored to the `HEAD` blob. - `dispatch-gates --ran` on `5354b1601`: 125 derived, 124 run green, 1 not measured. `check:dual-build-cjs-loads` exited 3 (PREREQUISITE NOT MET) because packages outside this diff are unbuilt here. - **Ablation, clone door:** the `if (!walled.allowed)` refusal was made unreachable. The `isolated` and `group` clone refusal pins went red (2 failed, 20 passed). Restored to the `HEAD` blob. - `permission-set-clone.test.ts` with `meta-walled-catalog-refusal.test.ts`: 33 passed. The dogfood files `wall-catalog-refusal` and `permission-set-clone-door`: 21 passed. `check:system-context-census`: OK, 129 sites (the clone door's `isSystem` read moved it from 128). - Dogfood suite in full on the merged head: 251 files passed, 1 skipped (the report of patch round 1 on #15204, 6114447595). ## Acceptance notes - **Not filed (no reach measured):** under a wall, an organization-scoped `manage_metadata` holder also writes the environment's objects, views and flows through `/meta`. D3 governs only the catalog. No card is filed, because only the catalog was measured. - **Not covered here:** under a wall, ADR-0090 D12's delegated `authorEnvironmentSets` scope can no longer author environment sets at the data door. D3 refuses tenants catalog creation, and a delegate is a tenant. The delegated assignment and binding paths are untouched. `sys_position_permission_set` is not judged: under ADR-0131 D3 its rows bind nothing, and it retires with the catalog objects. --- _Generated by [Claude Code](https://claude.ai/code/session_015JgoXpZUGtNxnMDvRZsKzP)_ --------- Co-authored-by: Claude <noreply@anthropic.com>
1 parent 85b4ac6 commit 06cc45d

24 files changed

Lines changed: 1207 additions & 35 deletions
Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
---
2+
'@objectstack/metadata-core': minor
3+
'@objectstack/rest': minor
4+
'@objectstack/runtime': minor
5+
'@objectstack/plugin-security': minor
6+
'@objectstack/spec': minor
7+
---
8+
9+
Under a walled posture the security catalog is the platform operator's: tenants are refused creating, changing or deleting positions, permission sets and capabilities, and may only assign them
10+
11+
Clause-②: yes (narrowing)
12+
13+
<!-- adr-0087: registered walled-catalog-tenant-writes-retired -->
14+
15+
**BREAKING** accept-set narrowing, shipped as `minor` under the repo's launch-window convention for breaking changes (ADR-0131 D3: "Shared-DB multi-tenant (`group` / `isolated`): tenant admins cannot create or edit catalog items; the operator defines the catalog for every tenant … Creation through Setup or the API is refused with a message naming the posture and the capability.").
16+
17+
**Why.** The catalog is environment-level, and every organization resolves it by name. Measured on a booted `isolated` and `group` stack: an organization owner granted `manage_metadata` inside their own organization saved `PUT /meta/permission/:name` over a permission set another organization's member holds, and that member's next resolution carried the change. One organization's write changed what another organization's members resolve.
18+
19+
**FROM → TO**, on a `group` or `isolated` deployment, for a caller below the platform operator (the `PLATFORM_ADMIN` rung: the deployment's declared `OS_PLATFORM_OWNER_EMAIL` owner):
20+
21+
- `PUT /meta/:type/:name`, `DELETE /meta/:type/:name`, `POST /meta/:type/:name/publish` and `POST /meta/:type/:name/rollback` on `@objectstack/rest`, and `PUT /meta/:type/:name` on the runtime dispatcher, for `position` or `permission`: **FROM** admitted to any `manage_metadata` holder **TO** `403 FORBIDDEN` (the dispatcher's `PUT` answers `403 PERMISSION_DENIED`). The message names the posture and the capability.
22+
- The same doors for `capability`: admitted where they were admitted. `capability` is a code-only type, so these doors already refused its writes before this change; only the sentence the caller reads changes, to the posture refusal. For `capability`, the accept set narrows at the data door on `sys_capability`, below.
23+
- `POST /security/permission-sets/:name/clone` (the permission-set clone door): **FROM** admitted to any `manage_metadata` holder **TO** `403 PERMISSION_DENIED`, before the source is read.
24+
- A data-door insert, update or delete on `sys_position`, `sys_permission_set` or `sys_capability`: **FROM** admitted to any caller whose grants admit the table (a `sys_permission_set` create landed in the environment ledger; a `sys_position` create landed as an organization row with no definition) **TO** `403 PERMISSION_DENIED`, before anything is written. This includes an ADR-0090 D12 delegate whose `adminScope` carries `authorEnvironmentSets`: under a wall it can no longer author environment permission sets at the data door. Its assignment and binding paths are untouched.
25+
26+
**Unchanged.** Assignments: `sys_user_position` and `sys_user_permission_set` rows stay organization rows that an organization's administrators write for their own members. The platform operator and the engine's own writes (seeders, installers, migrations) write the catalog as before. Every other metadata type is untouched. Under `single` (and with no `tenancy` service), catalog writes stay with `manage_metadata` holders, exactly as before. The catalog activation door already required the platform operator under a wall.
27+
28+
**The remedy.** A tenant that needs a position or permission set asks the platform operator to define it in Studio, or through a managed package. The tenant's administrators then assign it to their members. `@objectstack/spec` registers the change as the ADR-0087 semantic entry `walled-catalog-tenant-writes-retired`. `@objectstack/metadata-core` exports the one predicate every door asks, `walledCatalogWriteVerdict`, beside `isSecurityCatalogType`, `SECURITY_CATALOG_TYPES` and `PLATFORM_OPERATOR_POSTURE`. A custom door that writes catalog items asks it after `metaWriteCapabilityVerdict`.

‎content/docs/permissions/authorization.mdx‎

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -242,7 +242,9 @@ one of two doors, each writing only what it owns:
242242
CRUD) is transparently redirected into an env-scope metadata save
243243
(`saveMetaItem`), and the data record is a **pure projection** the platform
244244
re-derives on every metadata mutation (awaited — no staleness window) and at
245-
boot. Deleting an artifact-backed set through this door **resets** it to its
245+
boot. Under a `group` or `isolated` tenancy posture that Setup write is the
246+
platform operator's: anyone else is refused `403 PERMISSION_DENIED` before the
247+
redirect (ADR-0131 D3). Deleting an artifact-backed set through this door **resets** it to its
246248
declared body rather than removing it; renaming through the data door is
247249
rejected (the name is the metadata identity — clone instead). Subject
248250
assignments remain plain config rows, unchanged.

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

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -58,7 +58,7 @@ Writes to the governed tables (`sys_user_position`,
5858
| Tenant admins pass through | The superuser wildcard reaches the ordinary CRUD/RLS checks — delegation constrains *delegates*, not HQ |
5959
| 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 |
6060
| 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** |
61-
| Action flags | `manageAssignments` / `manageBindings` / `authorEnvironmentSets` gate their table classes independently; re-composing a position (`manageBindings`) additionally requires every current holder to sit inside the subtree |
61+
| Action flags | `manageAssignments` / `manageBindings` / `authorEnvironmentSets` gate their table classes independently; re-composing a position (`manageBindings`) additionally requires every current holder to sit inside the subtree. Under a `group` or `isolated` tenancy posture `authorEnvironmentSets` admits nothing at the data door: the platform operator authors the security catalog there (ADR-0131 D3). It still applies under `single` |
6262
| 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) |
6363
| Single-row writes | Delegates write single rows by id only — a broad filter-write cannot be boundary-checked |
6464
| Audit stamp | Every assignment a delegate creates is `granted_by`-stamped automatically |

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

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -417,7 +417,9 @@ it was copied from, so changes to the source never reach the copy.
417417
- **The copy is held by nobody** until you add it to a position or assign it.
418418
- **Who may call it** is who may save a permission set through the metadata
419419
API: a holder of `manage_metadata`. Anyone else is refused `403`
420-
`PERMISSION_DENIED`.
420+
`PERMISSION_DENIED`. Under a `group` or `isolated` tenancy posture the clone
421+
is the platform operator's alone (ADR-0131 D3): a `manage_metadata` holder
422+
inside one organization is refused `403` `PERMISSION_DENIED` too.
421423

422424
### Declared ≠ enforced — diagnosing a frozen package set
423425

‎content/docs/permissions/system-context.mdx‎

Lines changed: 12 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@ the seed loader replaying package fixtures, a plugin's boot reconciler, a
1010
service self-write, a migration.
1111

1212
This page is **the authority** for what that flag actually does. It exists
13-
because the flag is not one concept: it is a single boolean read at **125
13+
because the flag is not one concept: it is a single boolean read at **130
1414
distinct sites across 20 packages**, and knowing three of those behaviours gives
1515
no hint that the other hundred-and-four exist. Every documented app-side bug
1616
traced to `isSystem` had the same shape — the metadata was complete and correct,
@@ -143,7 +143,7 @@ that silently does not happen.
143143

144144
### 3. Sharing (`plugin-sharing`)
145145

146-
The largest single consumer — **17 of the 125 sites**.
146+
The largest single consumer — **17 of the 130 sites**.
147147

148148
| # | Behaviour when `isSystem` | What you get / what you lose | Anchor |
149149
|:--|:---|:---|:---|
@@ -182,6 +182,7 @@ The largest single consumer — **17 of the 125 sites**.
182182
| 53 | Package REST route capability gate bypassed | rest | Get: a marketplace publish over REST (`POST /packages/publish`, the one route the REST registrar mounts since #14503) without `manage_metadata`; the package read cohort (`studio.access` / `setup.access`) is enforced by the dispatcher `/packages` domain's own read gate, where the reads are served | `packages/rest/src/package-routes.ts#refusePackageRequest` |
183183
| 54 | Package domain capability gates bypassed | runtime | Get: package management and package-inventory reads without the capability | `packages/runtime/src/domains/packages.ts#requireManageMetadata`, `#requireReadCapability` |
184184
| 55 | Activation write / authoring refusals do not fire | runtime | Get: activation artifacts writable and authorable without the activation-authoring capability | `packages/runtime/src/domains/activation-gate.ts#refuseUngrantedActivationWrite`, `#refuseUngrantedActivationAuthoring` |
185+
| 55b | Walled catalog refusal does not fire | metadata-core, rest, plugin-security | Get: under `isolated` / `group`, a create, change or delete of a position, a permission set or a capability, at the `/meta` write doors (save, reset, publish, rollback), at the permission-set clone door, and at the data door on `sys_position`, `sys_permission_set` and `sys_capability`. Every other caller below the platform-operator rung is refused `403` there (ADR-0131 D3), because one organization's write would change what every organization resolves by name. The seeders, the package door and migrations write the catalog this way. Under `single` the refusal is inert for every caller | `packages/metadata-core/src/meta-write-capability.ts#walledCatalogWriteVerdict`, `packages/rest/src/rest-server.ts#walledCatalogRefusal`, `packages/plugins/plugin-security/src/walled-catalog-data-door.ts#createWalledCatalogDataDoorRefusal` |
185186
| 56 | Automation run-state read, flow-authoring write, the paused-run caller gate (screen read and resume), the two operator run-lifecycle writes and the elevated self-triggered start at every door that starts a flow by name all pass | runtime | Get: run state, flow writes, a paused run's screen and its resume with no grant and without being the run's starter — and cancelling or restoring a suspension without the platform-operator rung. The screen read and the resume ask one predicate, so this is one bypass for both. The lifecycle bypass is the in-process owner's door: plugin-approvals' revise-window recall (ADR-0044) cancels on behalf of a decision it already authorized and recorded. And starting a flow declared `runAs: 'system'` whose type is self-triggered (`autolaunched`, `record_change`, `schedule`) through any door that starts a flow by name — the trigger route, an action of `type: 'flow'` (REST `/actions` and the MCP `run_action` bridge) and a declared endpoint of `type: 'flow'` — which every other caller, the guest principal included, is refused `403 PERMISSION_DENIED` (the maintainer's rulings, letters B and A). The three doors ask one predicate, so this is one bypass for all of them, and it is the in-process caller's, such as a job: inbound HTTP never carries the flag | `packages/runtime/src/domains/automation.ts#mayReadRunState`, `#refuseUngrantedFlowWrite`, `#isRunStarterOrRunStateReader`, `#refuseUngrantedRunLifecycleWrite`, `packages/runtime/src/flow-start-admission.ts#refusesElevatedSelfTriggeredStart` |
186187
| 57 | Audience-binding suggestion authority gate passes | plugin-security | Get: a system caller lists, accepts and dismisses suggestions without `manage_metadata` and, under a wall, without the platform-operator rung | `packages/plugins/plugin-security/src/suggested-audience-bindings.ts#assertDeploymentAuthority` |
187188
| 58 | Email-template organization door passes; webhook provenance stamp skipped | plugin-email, plugin-webhooks | Get (`sys_email_template`): a create or update of a template row. The door refuses every other write that names a caller, `403 PERMISSION_DENIED` (ADR-0131 D6); the plugin itself no longer writes the table (templates resolve from the registry), so the v18 migration ceremony's promotion is the one remaining writer. Lose (`sys_webhook`): the row is not marked as an admin customization | `packages/plugins/plugin-email/src/email-template-door.ts#isOrganizationWrite`, `packages/plugins/plugin-webhooks/src/webhook-provenance.ts#bindWebhookProvenanceStamp` |
@@ -286,7 +287,7 @@ Ownership injection, `readonly` bypass and sharing materialisation are
286287
independent decisions, and a seed loader plausibly wants the first two but not
287288
the third. The concept is nevertheless **staying as one boolean**:
288289

289-
- **Shipped semantics.** `isSystem` is a published contract with 125 read sites
290+
- **Shipped semantics.** `isSystem` is a published contract with 130 read sites
290291
in 20 packages. Splitting it is a breaking contract change across all of them.
291292
(The ruling was taken when the census read 80 sites in 18 packages; the count
292293
has grown, which strengthens rather than weakens the argument.)
@@ -358,19 +359,19 @@ still holds equal to the census on every pull request:
358359
| — in tests | 1013 | — |
359360
| — in non-test sources | 798 | — |
360361
| Appearances of the bare identifier `isSystem` in non-test sources | 813 | — |
361-
| — parsed as a declaration | 28 | ✅ |
362+
| — parsed as a declaration | 29 | ✅ |
362363
| — parsed as an object-literal / type key (producers and option objects) | 310 | — |
363-
| — parsed as a property **read** | 130 | ✅ |
364+
| — parsed as a property **read** | 135 | ✅ |
364365
| — parsed in some other syntactic position (a local, a cast, a conditional) | 9 | ✅ |
365366
| — the remainder: text inside comments and string literals | 358 | — |
366367
| Of those reads: reads of one of the unrelated metadata fields | 5 | ✅ |
367-
| Of those reads: reads of `ExecutionContext.isSystem` | **125** | ✅ |
368-
| — behaviour-bearing (rows 1–61 above) | 122 | ✅ |
368+
| Of those reads: reads of `ExecutionContext.isSystem` | **130** | ✅ |
369+
| — behaviour-bearing (rows 1–61 above) | 127 | ✅ |
369370
| — carry the flag onward only (rows 62–64 above) | 3 | ✅ |
370371
| Packages containing at least one elevation read | **20** | ✅ |
371-
| Files containing at least one elevation read | 58 | ✅ |
372-
| — the distinct symbols those reads live in — what this page anchors | 107 | ✅ |
373-
| — of those files, the ones holding more than one read in one symbol | 8 | ✅ |
372+
| Files containing at least one elevation read | 59 | ✅ |
373+
| — the distinct symbols those reads live in — what this page anchors | 110 | ✅ |
374+
| — of those files, the ones holding more than one read in one symbol | 9 | ✅ |
374375

375376
The six rows marked — are a **dated decomposition, not a live claim**: they were
376377
measured on 2026-08-29 at `ca1965f2b5` and CI does not re-derive them. They count
@@ -433,7 +434,7 @@ same resolver, and the same registration shape, that holds `docs/adr/**`.
433434
Renaming a symbol is now a loud red instead of a silent misdirection.
434435

435436
⚠️ **The precision that costs, priced here rather than buried.** A symbol anchor
436-
cannot say WHICH read inside a function it means, and **8** of the **58**
437+
cannot say WHICH read inside a function it means, and **9** of the **59**
437438
anchored files hold more than one read inside a single symbol. So the population
438439
check runs per file at symbol granularity: every file the census finds a read in
439440
must be anchored, and the set of symbols this page cites into that file must

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

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -32,7 +32,7 @@ const result = AdminScopeSchema.parse(data);
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 |
3434
| **manageBindings** | `boolean` | optional (default: `false`) | Manage position↔permission-set bindings within the subtree |
35-
| **authorEnvironmentSets** | `boolean` | optional (default: `false`) | Author environment-owned permission sets |
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) |
3636
| **assignablePermissionSets** | `string[]` | optional (default: `[]`) | Allowlist of permission-set names the delegate may hand out |
3737

3838

@@ -183,7 +183,7 @@ const result = AdminScopeSchema.parse(data);
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 |
185185
| **manageBindings** | `boolean` | optional (default: `false`) | Manage position↔permission-set bindings within the subtree |
186-
| **authorEnvironmentSets** | `boolean` | optional (default: `false`) | Author environment-owned permission sets |
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) |
187187
| **assignablePermissionSets** | `string[]` | optional (default: `[]`) | Allowlist of permission-set names the delegate may hand out |
188188

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

0 commit comments

Comments
 (0)