Skip to content

Commit dd19298

Browse files
os-zhuangclaude
andauthored
docs(adr-0086): re-evaluate authz metadata/config boundary against latest code (#2557) (#3011)
ADR-0086 was written the day #2557 opened; its full plan has since shipped and three later ADRs (0090, 0094, 0095) built on it. Flip status Proposed → Accepted (substantially implemented) and add a code-grounded Re-evaluation (2026-07-16) addendum: - Per-decision/phase status table (D1-D7, P0-P2) with file-level landing points: D3 packageId+provenance, D5 bootstrapDeclaredPermissions, P0 objectui slice-merge (#2505/#2508), D6/D7 two doors — all implemented. - Flag the two mechanisms the original text describes that later work overtook: P0's shared-data-record slice-merge premise -> ADR-0094 pure projection + metadata overlay; D4's Salesforce-muting subtract layer -> ADR-0005 first-class overlay (ADR-0094 D5 revised). - Restate the D1 classification table in current ADR-0090 vocabulary: Position (not Role, flat/no parent), Profile removed, everyone/guest anchors, renamed + time-boxed (ADR-0091) assignment tables, adminScope (ADR-0090 D12). - Net: no decision reversed; thesis holds. Remaining open items are the ones already marked deferred/evidence-gated. Co-authored-by: Claude <noreply@anthropic.com>
1 parent a23c32e commit dd19298

1 file changed

Lines changed: 85 additions & 1 deletion

File tree

docs/adr/0086-authz-metadata-config-boundary-and-cross-package-composition.md

Lines changed: 85 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
# ADR-0086: Authorization metadata↔config boundary + cross-package composition — DEFINITION travels, ASSIGNMENT stays, packages ship their own sets
22

3-
**Status**: Proposed (2026-07-04)
3+
**Status**: Accepted (2026-07-04)**substantially implemented**; parts refined/superseded by [ADR-0090](./0090-permission-model-v2-concept-convergence.md) and [ADR-0094](./0094-sys-permission-set-pure-projection.md). See the **Re-evaluation (2026-07-16)** addendum for the current, code-grounded status of every decision.
44
**Deciders**: ObjectStack Protocol Architects
55
**Builds on**: [ADR-0066](./0066-unified-authorization-model.md) (three-way separation: Capability / Assignment / Requirement — this ADR operationalizes its **D5** for the packaging axis), [ADR-0056](./0056-permission-model-landing-verification.md) (whole-model enforce/prove; **D7** app-declared default profile), [ADR-0057](./0057-erp-authorization-core-business-units-and-scope-depth.md) (`readScope`/`writeScope` depth), [ADR-0005](./0005-metadata-customization-overlay.md) (per-org overlay = the subtract/adjust layer), [ADR-0028](./0028-metadata-naming-and-namespace-isolation.md) / [ADR-0048](./0048-cross-package-metadata-collision.md) (namespaced api names = collision-proof keys), [ADR-0078](./0078-no-silently-inert-metadata.md) (no declarable-but-never-seeded metadata), [ADR-0016](./0016-studio-package-authoring-and-publish.md) / [ADR-0033](./0033-ai-assisted-metadata-authoring.md) (draft/publish), [ADR-0084](./0084-application-builder-information-architecture.md) (Access pillar = matrix; defers composition), [ADR-0025](./0025-plugin-package-distribution.md) (manifest `permissions` = install-consent scopes, **not** RBAC grants), [ADR-0003](./0003-package-as-first-class-citizen.md) / [ADR-0070](./0070-package-first-authoring.md) (package-first), [ADR-0006](./0006-project-environment-split.v4.md) (environment split)
66
**Consumers**: `@objectstack/spec` (`security/permission.zod.ts`, `stack.zod.ts`, `system/metadata-persistence.zod.ts`), `@objectstack/plugin-security` (`bootstrap-declared-*`, `permission-evaluator`), `@objectstack/core` (`resolve-authz-context`), `@objectstack/metadata` (overlay), `../objectui` (`AccessPillar`, `PermissionMatrixEditor`, `metadata-admin`)
@@ -352,3 +352,87 @@ each door only writes what it owns.
352352
- **Lifecycle context**: this ADR covers the package-authoring/composition slice of the authz
353353
surface; the whole-lifecycle gap map (P0–P3, package dev → production) is tracked in
354354
umbrella issue #2561.
355+
356+
---
357+
358+
## Re-evaluation (2026-07-16) — code-grounded status; what landed, what later ADRs refined
359+
360+
This ADR was written the day issue #2557 opened (2026-07-04). In the two weeks since, its
361+
whole plan **shipped**, and three later ADRs (0090, 0094, and — transitively — 0095) built on
362+
top of it and refined two of its parts. This addendum re-grounds every decision against the
363+
current code (HEAD 2026-07-16) so the ADR stops reading as an unimplemented proposal. **The
364+
central thesis holds unchanged**: authorization *definitions* travel with the package
365+
(metadata, on the `managedBy` provenance axis + `packageId`); *subject bindings* stay
366+
environment config; cross-package grants compose conflict-free by the namespaced-key UNION.
367+
Two mechanisms named in the original text have been overtaken by a better realization and are
368+
flagged inline below.
369+
370+
### Status of each decision / phase
371+
372+
| Item | Status | Where it landed / how it changed |
373+
|---|---|---|
374+
| **D1** classification on the provenance axis | **Adopted**; table vocabulary now stale — see "revised table" below | Principle intact. Rows renamed/removed by ADR-0090 (Profile gone, role→position). |
375+
| **D2** DEFINITION↔ASSIGNMENT principle | **Adopted, reinforced** | ADR-0090 D2 leans on it to justify removing Profile; ADR-0091 dates the *assignment* rows only (`valid_from`/`valid_until`), confirming they are env data. |
376+
| **D3** `packageId` + provenance `managedBy` on the set | **Implemented** | `spec/security/permission.zod.ts:167` (`packageId`, tagged `[ADR-0086 D3]`) + `:178` (`managedBy` enum); record fields `package_id`/`managed_by`/`customized` + index on `sys-permission-set.object.ts:217-266`. |
377+
| **D4** Shape B (packages ship own sets; union; shared slices env-only) | **Adopted, reinforced; subtract layer upgraded** | ADR-0090 D9 / Alt #6 reject package-written shared/builtin sets for the same reasons. Composition is now anchored at the **position** ("bind several packages' sets to one position"), ADR-0094 D5. **Subtract layer changed** — see ⚠️ below. |
378+
| **D5** `bootstrapDeclaredPermissions` | **Implemented; `isDefault` semantics refined by ADR-0090 D5** | `plugin-security/src/bootstrap-declared-permissions.ts` (invoked at `kernel:ready`, `security-plugin.ts`); seeds `managed_by:'package'` + `package_id`, idempotent/upgrade-aware; closes the ADR-0078 inert-metadata smell the D5 header calls out. `isDefault` no longer means "profile fallback": app-level default sets **auto-bind to the `everyone` position**; package-level default sets materialize a `sys_audience_binding_suggestion` an admin confirms (new object). |
379+
| **D6** package Access door under draft/publish | **Implemented** | objectui writes the package door as `mode:'draft', packageId` (`PermissionMatrixEditor.tsx`); env door stays live and, per ADR-0094 D3, that "live" save is itself redirected into a metadata overlay. |
380+
| **D7** two doors | **Implemented; gate narrowed by ADR-0094 D5** | Package door edits the package slice (scoped, draft); env-admin door keeps the cross-package matrix + assignment (live). ADR-0094 narrows the two-doors *gate* to what stays structurally true: the admin door can never **forge** package provenance, and package-row lifecycle ops with no overlay translation stay refused — but an env edit of a package set is now a first-class overlay, not a flat 403. |
381+
| **P0** objectui scope + slice-merge | **Done** (objectui #2505/#2508) | `permission-slice.ts` + `mergePermissionSlice`; matrix loads `client.list('object', { packageId })`, fields lazy per-object; save re-reads a fresh layered record and merges only the package slice, byte-for-byte preserving other packages. ⚠️ realized against the ADR-0094 store — see below. |
382+
| **P1** framework `packageId` + seeder | **Done** | = D3 + D5 above. |
383+
| **P2** two doors + overlay/subtract | **Largely done** | Two doors shipped (D6/D7). Subtract layer landed as **ADR-0005 first-class overlay** (ADR-0094 D5), not the Salesforce-muting/explicit-deny path this ADR sketched. The ADR-0066 precedence-step-4 explicit-deny layer remains deferred and **evidence-gated** — the whole-document overlay-wins customization covers today's need. |
384+
385+
### ⚠️ Two mechanisms in the original text are superseded by a better realization
386+
387+
1. **P0's "slice-merge the shared *data record*" premise → ADR-0094 projection + overlay.**
388+
The original Context/P0 assumed grants live inline on **one writable `sys_permission_set`
389+
record** and the fix was to slice-merge that record on save. **ADR-0094 relocated the
390+
authoritative store to the metadata layer**: the `sys_permission_set` row is now a **pure
391+
projection** written only by an awaited mutation projector, and every data-door write is
392+
redirected (write-through) into a metadata save/overlay. The data-loss trap this ADR
393+
worried about is now closed **structurally** (there is no second writable authority to
394+
clobber), and the shipped P0 correctly slice-merges the **layered effective** body, not a
395+
raw shared row. *Note the inline `objects`/`fields` dict shape survived — ADR-0094 moved
396+
the store, not the body shape — so this ADR's load-bearing claim that namespaced keys make
397+
a package's slice unambiguous still holds and underpins both slice-merge and overlay.*
398+
399+
2. **D4's "Salesforce muting / clone-to-customize" subtract layer → ADR-0005 whole-document
400+
overlay.** ADR-0094 D5 (revised 2026-07-14) makes an environment overlay of a
401+
package-owned set a **first-class ADR-0005 customization** (overlay-wins, `managed_by:
402+
'package'` + `package_id` preserved; a data-door "delete" is an overlay reset to the shipped
403+
declaration). This is strictly better than muting/forking: the customization keeps receiving
404+
the vendor's baseline security tightenings and retains the code-vs-overlay diff. The trade
405+
it accepts (an overlay pins a name against a later vendor tightening until reset) is the same
406+
trade every overlayable type makes and is covered by ADR-0091 recertification.
407+
408+
### D1 classification table — restated in current (ADR-0090) vocabulary
409+
410+
Superseding the D1 table only where vocabulary changed; the **class** column is unchanged
411+
except where noted:
412+
413+
| Primitive (current name) | Class | What changed since 2026-07-04 |
414+
|---|---|---|
415+
| **Position** (was "Role") definition | **metadata** | Renamed `role``position` (ADR-0090 D3); "role" is now a reserved-forbidden word. |
416+
| ~~Role hierarchy `parent`~~ | **removed** | Positions are **flat, no `parent`** (ADR-0090 D3); the visibility hierarchy is the **Business Unit tree** (ADR-0057), which carries its own metadata/config split. The old "hybrid parent" row no longer exists. |
417+
| **PermissionSet** definition | **metadata** | **Profile removed** (ADR-0090 D2 — `isProfile` deleted); `permission_set` is the sole capability container. |
418+
| ObjectPermission / FieldPermission / `systemPermissions` / `tabPermissions` | **metadata** | Unchanged — ride the set definition, own objects/fields only. |
419+
| RLS policy / Sharing rule / OWD `sharingModel` | metadata / hybrid / metadata | Unchanged in class. `sharingModel` default flipped to `private` for custom objects (ADR-0090 D1) and gained `externalSharingModel` (D11). `sys_sharing_rule` is **record-authoritative** (seed-not-clobber), per the ADR-0094 addendum. |
420+
| **`everyone` / `guest`** built-in positions | **config** (admin-confirmed) | NEW default-grant mechanism (ADR-0090 D5/D9) that replaces the `member_default` fallback + ADR-0056 D7 default-profile. Packages *suggest* bindings; admins confirm. High-privilege bits on `everyone`/`guest` are lint-blocked. |
421+
| **Assignment** `sys_user_position` / `sys_position_permission_set` (was `sys_user_permission_set` / `sys_role_permission_set`) | **config / data** | Tables renamed (ADR-0090 D3); still the universal DEFINITION↔ASSIGNMENT line. ADR-0091 adds `valid_from`/`valid_until` — assignment is now *time-boxed* config. |
422+
| Delegated-admin **`adminScope`** on a set | **metadata (definition) + config (grant)** | NEW (ADR-0090 D12): admin capability itself is scoped (BU subtree + assignable-set allowlist + no self-escalation); `authorEnvironmentSets` keeps package-managed rows publish-only per this ADR. |
423+
| Organization / tenancy / actual users & teams | **config / data** | Unchanged. Tenancy is now an explicit kernel service (ADR-0093) and tenant isolation an always-first Layer 0 (ADR-0095). Teams *receive* sharing, never carry capability (ADR-0090 D8). |
424+
425+
### Net assessment
426+
427+
ADR-0086 is **done and correct**, not pending. Its provenance-axis classification and Shape-B
428+
composition became the load-bearing assumption of the whole permission-model-v2 wave: ADR-0090
429+
cites "ADR-0086 D3 made machine-checkable" as the reason Profile could be removed and
430+
uninstall-by-`packageId` works; the companion `docs/design/permission-model.md` documents D3/D5
431+
as the operative package/platform isolation boundary. The only corrections a reader needs are
432+
(1) the two superseded *mechanisms* above (store = ADR-0094 projection/overlay, not a raw shared
433+
record; subtract = ADR-0005 overlay, not muting) and (2) the ADR-0090 vocabulary (Position not
434+
Role, no Profile, `everyone`/`guest`). No decision here was reversed. Remaining genuinely-open
435+
items are the ones this ADR already marked deferred and evidence-gated: the ADR-0066 explicit-deny
436+
precedence layer, permission-set **groups** as a bundling primitive over Shape-B sets, and
437+
OWD/`sharingModel` env-tightening riding the overlay path (ADR-0005 still marks `security` types
438+
non-org-overridable by default; ADR-0094's overlay is the explicit, gated exception).

0 commit comments

Comments
 (0)