|
| 1 | +--- |
| 2 | +'@objectstack/spec': minor |
| 3 | +'@objectstack/plugin-security': minor |
| 4 | +'@objectstack/plugin-auth': minor |
| 5 | +'@objectstack/core': minor |
| 6 | +'@objectstack/types': minor |
| 7 | +'@objectstack/lint': minor |
| 8 | +'@objectstack/platform-objects': minor |
| 9 | +'@objectstack/rest': patch |
| 10 | +'@objectstack/runtime': patch |
| 11 | +'@objectstack/mcp': patch |
| 12 | +'@objectstack/plugin-hono-server': patch |
| 13 | +'@objectstack/plugin-approvals': patch |
| 14 | +'@objectstack/cli': patch |
| 15 | +--- |
| 16 | + |
| 17 | +ADR-0105 Phase 0 + Phase 1: group tenancy posture; organization scope as a |
| 18 | +first-class authorization dimension. |
| 19 | + |
| 20 | +> This release carries BREAKING spec removals (see "Enforce-or-remove" below) |
| 21 | +> but is recorded as `minor`: every publishable package is in the Changesets |
| 22 | +> lockstep group, so one `major` would promote the whole monorepo. Breaking |
| 23 | +> changes ship as `minor` during the launch window — the migration notes below |
| 24 | +> are what reach consumers in `CHANGELOG.md`. |
| 25 | +
|
| 26 | +## Tenancy is now a spectrum (D1) |
| 27 | + |
| 28 | +`single | group | isolated`, resolved by the `tenancy` service and selected with |
| 29 | +the new `OS_TENANCY_POSTURE` env var. Existing deployments are unchanged: |
| 30 | +`OS_TENANCY_POSTURE` unset derives the posture from `OS_MULTI_ORG_ENABLED` |
| 31 | +(`true` ⇒ `isolated`, else `single`). An unrecognized value throws at boot |
| 32 | +rather than silently landing in a posture with no organization wall. |
| 33 | + |
| 34 | +- `single` — no wall (unchanged). |
| 35 | +- `group` — **new.** Organizations are membership boundaries over one shared |
| 36 | + dataset; Layer 0 becomes `organization_id IN accessible_org_ids` (union / MOAC |
| 37 | + semantics). Enforced by the OPEN engine. |
| 38 | +- `isolated` — today's `multi`, renamed. Behavior, enterprise `org-scoping` |
| 39 | + probe and degraded-boot handling all unchanged. |
| 40 | + |
| 41 | +## Organization scope is a first-class context field (D2) |
| 42 | + |
| 43 | +`ExecutionContext.accessible_org_ids` — every organization the caller holds a |
| 44 | +currently-valid membership in (ADR-0091 validity windows) — is resolved once by |
| 45 | +`resolveAuthzContext` and carried by every transport. The `group` wall reads it |
| 46 | +directly; RLS policies may reference it as |
| 47 | +`organization_id IN (current_user.accessible_org_ids)`. An empty or absent set |
| 48 | +fails the wall closed. |
| 49 | + |
| 50 | +Only the Layer 0 PREDICATE widens. Composition is untouched: the wall is still |
| 51 | +computed independently of the RLS compiler, AND-composed outermost, and |
| 52 | +crossable only by a true `PLATFORM_ADMIN` on a posture-permitting object — so |
| 53 | +ADR-0095's W1/W2 invariants hold in every posture. |
| 54 | + |
| 55 | +## Two P0 correctness fixes (D3, D4) — behavior changes |
| 56 | + |
| 57 | +**D3 — app-authored org-scoped RLS policies are no longer silently dropped** |
| 58 | +(finding F1, framework#3539). `collectRLSPolicies` used to strip any policy whose |
| 59 | +`using` contained the substring `current_user.organization_id` when isolation was |
| 60 | +inactive, which swallowed app-authored policies as well as the platform's own. |
| 61 | +Stripping is now decided by PROVENANCE (identity against the shipped |
| 62 | +declaration). **Upgrade impact:** in a deployment with no organization wall, an |
| 63 | +app-authored policy referencing the active organization is now RETAINED and |
| 64 | +fails closed (zero rows) with a one-time warning, where it previously vanished |
| 65 | +and the object read unscoped. `getReadFilter` shared the defect, so analytics and |
| 66 | +raw-SQL consumers were affected too. If a policy was only ever meant for |
| 67 | +multi-org, delete it or install `@objectstack/organizations`. |
| 68 | + |
| 69 | +**D4 — `viewAllRecords`/`modifyAllRecords` never cross an organization |
| 70 | +boundary** (finding F2, framework#3540). Under a wall-less posture nothing |
| 71 | +bounded the wildcard superuser bits `organization_admin` carries, so a |
| 72 | +deployment that accumulated organizations (personal orgs on signup) made every |
| 73 | +owner/admin an environment-wide superuser. `auto-org-admin-grant` now grants a |
| 74 | +de-VAMA'd `organization_admin_no_bypass` variant when no wall is enforced, and |
| 75 | +revokes the superseded variant whenever the posture changes. **Upgrade impact:** |
| 76 | +in `single` posture an org owner/admin keeps full CRUD but loses the blanket |
| 77 | +ownership/sharing/RLS bypass. Deliberate deployment-wide visibility remains |
| 78 | +available through `admin_full_access` or an explicitly authored permission set — |
| 79 | +it just stops being a side effect of a better-auth membership role. |
| 80 | + |
| 81 | +## Engine-owned organization stamping (D5) |
| 82 | + |
| 83 | +Under any wall-enforcing posture the engine stamps `organization_id` from the |
| 84 | +caller's active organization on an insert that omits it, and validates every |
| 85 | +supplied value against the wall. Idempotent with the enterprise auto-stamp |
| 86 | +(neither overwrites a supplied value). This also closes a real hole: the |
| 87 | +pre-existing post-image check required a non-array payload, so a BULK insert |
| 88 | +could carry a forged `organization_id` per row. One forged row now denies the |
| 89 | +whole write. |
| 90 | + |
| 91 | +## Group structure, extension fields and red-line lints (D6, D7) |
| 92 | + |
| 93 | +- `sys_organization` gains `parent_organization_id` and `sort_order` — a |
| 94 | + **reporting dimension only**. |
| 95 | +- New lint `validateOrgAxisRedLines` (`org-axis-permission-inheritance`, |
| 96 | + `org-axis-cross-org-bu-grant`), wired into `os lint` / `os compile` / |
| 97 | + `os validate`: an RLS policy or sharing rule that walks the org tree is an |
| 98 | + error, as is a business-unit grant on a platform-global object. |
| 99 | +- Extension fields on better-auth-managed objects ride the existing ADR-0092 |
| 100 | + whitelist. A new guard derives better-auth's real field surface from |
| 101 | + `getAuthTables()` at the pinned version and fails the build on any name |
| 102 | + collision, so a library upgrade cannot silently take ownership of a column. |
| 103 | + |
| 104 | +## Enforce-or-remove (D11) — BREAKING |
| 105 | + |
| 106 | +Both removals are of surface that had **zero runtime consumers**, so no |
| 107 | +behavior changes; authoring them is now a no-op instead of a lint warning. |
| 108 | + |
| 109 | +- **`PermissionSet.contextVariables` — REMOVED.** The RLS compiler never read |
| 110 | + it. FROM → TO: a set a policy needs as `field IN (current_user.<key>)` is now |
| 111 | + supplied by a registered membership resolver (below); a constant belongs in |
| 112 | + the policy itself as a literal (`status = 'published'`). |
| 113 | +- **`Territory` / `TerritoryModel` / `TerritoryType` (`security/territory.zod.ts`) |
| 114 | + — REMOVED.** No runtime object, stack field or resolver existed. FROM → TO: |
| 115 | + matrix requirements are served by multi-position × business-unit anchoring; a |
| 116 | + generalized dimension-security module will arrive with its own ADR. |
| 117 | +- **`ExecutionContext.rlsMembership` — PRODUCTIZED.** The bag the compiler has |
| 118 | + merged since ADR-0056 finally has a producer: register an |
| 119 | + `IRlsMembershipResolver` (`@objectstack/spec/contracts`) under the |
| 120 | + `rls-membership-resolver` service, declaring the keys it owns. Fail-closed by |
| 121 | + construction — an unresolved key makes its policies drop out. Kernel-owned |
| 122 | + keys (`accessible_org_ids`, `org_user_ids`, …) are reserved and cannot be |
| 123 | + overwritten from this seam. |
| 124 | + |
| 125 | +## Edition boundary (D12) |
| 126 | + |
| 127 | +The `group` posture's enforcement primitives ship OPEN — the union wall, |
| 128 | +`accessible_org_ids` resolution, D5 stamping/validation, the D3/D4 correctness |
| 129 | +fixes and the D6 lints — because the correctness of a wall is never a paid |
| 130 | +feature (cloud ADR-0016 铁律「强制免费、治理收费」). `isolated` keeps its existing |
| 131 | +enterprise `org-scoping` probe, so the current commercial boundary for |
| 132 | +legal-entity isolation is unchanged by this release. |
0 commit comments