|
| 1 | +--- |
| 2 | +title: v15.0.0 |
| 3 | +description: Authorization kernel chain (ADR-0095) — tenant isolation becomes an independent Layer 0, a monotonic posture ladder derived from capabilities, cross-tenant write hardening, and record-grained access explanation. Breaking changes affect multi-org deployments only. |
| 4 | +--- |
| 5 | + |
| 6 | +**Authorization kernel hardening (ADR-0095) + supporting features (tracking framework#2920).** |
| 7 | +This release restructures the authorization hot path: tenant isolation moves out |
| 8 | +of the business-RLS pass into an independent, always-first **Layer 0**; principal |
| 9 | +tiering becomes an explicit, capability-derived **posture ladder**; and the tenant |
| 10 | +write wall is closed on the update path. Several **behavior changes affect |
| 11 | +multi-org (`tenancy.mode = 'multi'` + `@objectstack/organizations`) deployments |
| 12 | +only** — single-org and physically-isolated (env-per-database, ADR-0002) deployments |
| 13 | +are unaffected. |
| 14 | + |
| 15 | +Read this before upgrading a multi-org or enterprise-hierarchy deployment. |
| 16 | + |
| 17 | +## Breaking changes |
| 18 | + |
| 19 | +### Multi-org deployments only |
| 20 | + |
| 21 | +- **Cross-tenant read leak closed (Layer 0).** A permissive business RLS policy |
| 22 | + (e.g. `status == 'public'`) can no longer OR-widen tenant scope. Tenant isolation |
| 23 | + is now AND-composed as the outermost filter (`Layer0(tenant) AND Layer1(business RLS)`); |
| 24 | + a foreign-org row a business policy matched is now invisible. |
| 25 | +- **Member by-id writes narrow to owner-only.** The previous OR-merge silently |
| 26 | + widened `owner_only_writes` back to org-wide, so a member could by-id update/delete |
| 27 | + *any* record in their org. Writes are now owner-scoped as authored. **Migration:** |
| 28 | + if your deployment intentionally relied on members editing each other's records |
| 29 | + org-wide, grant an explicit per-object edit permission set (position-distributed) |
| 30 | + where that is wanted — the baseline `member_default` no longer permits it. |
| 31 | +- **Cross-tenant writes via `organization_id` blocked (insert + update).** A user |
| 32 | + could previously plant a row into another tenant by supplying a forged |
| 33 | + `organization_id` on insert, or by reassigning it on update. Both are now denied |
| 34 | + for user contexts (Layer 0 post-image check, symmetric across insert/update/bulk); |
| 35 | + `organization_id` is effectively immutable outside system context. Default writes |
| 36 | + (no `organization_id`) and system-context writes (import, migration, per-org seed |
| 37 | + replay) are unaffected. |
| 38 | +- **Org admins no longer cross the tenant wall on `private` objects.** The Layer 0 |
| 39 | + cross-tenant exemption is now gated on a genuine platform-admin capability |
| 40 | + (`manage_metadata` / `manage_platform_settings` / `studio.access` / `manage_users`), |
| 41 | + not on the `viewAllRecords` / `modifyAllRecords` super-bits that `organization_admin` |
| 42 | + also holds. On `access.default: 'private'` tenant objects an org admin is now walled |
| 43 | + to their own organization (previously unfiltered). Genuine platform admins are |
| 44 | + unaffected; the super-bits continue to drive only the in-org Layer 1 business-RLS |
| 45 | + short-circuit (TENANT_ADMIN semantics unchanged). |
| 46 | +- **No-active-org writes fail closed.** A write by a principal with no active |
| 47 | + organization on a tenant object is now denied. |
| 48 | +- **Seeded `tenant_isolation` policy retired.** The wildcard `tenant_isolation` |
| 49 | + RLS policy is removed from the `organization_admin` / `member_default` / |
| 50 | + `viewer_readonly` sets (isolation is enforced by Layer 0 instead). The |
| 51 | + `_self` / `_org` identity-table carve-outs and `owner_only_writes/deletes` are |
| 52 | + unchanged; customized seeded sets keep their overlays (ADR-0094). |
| 53 | + |
| 54 | +### Enterprise hierarchy RLS (`@objectstack/security-enterprise`) |
| 55 | + |
| 56 | +- **`unit` scope anchors to the position's business unit.** `unit` / `unit_and_below` |
| 57 | + now prefer `sys_user_position.business_unit_id` (the position's BU) over |
| 58 | + `sys_business_unit_member` membership. **Deployments that do not populate |
| 59 | + `sys_user_position.business_unit_id` see no change** (every user falls back to |
| 60 | + membership). Once anchors are populated, a user's `unit` visibility narrows to |
| 61 | + their position BU; multiple positions union; expired/inactive positions fall back |
| 62 | + to membership. |
| 63 | + |
| 64 | +## Non-breaking hardening (same effective visibility) |
| 65 | + |
| 66 | +- **RLS safety nets recognize canonical `==` (#2936).** The field-existence / |
| 67 | + tenancy-disabled nets were inert for `==`-form policies; now recognized. Only |
| 68 | + visible effect: on column-less system tables a member's by-id write is now an |
| 69 | + explicit fail-closed deny rather than a phantom-column filter — same result. |
| 70 | +- **Self-delegation position anchors are subtree-constrained.** A delegated position's |
| 71 | + `business_unit_id` must fall within the delegator's own effective anchor — closing a |
| 72 | + lateral-visibility escalation the position-anchor change would otherwise have opened. |
| 73 | +- **Hierarchy resolver queries are org-scoped (defense in depth).** Owner-id resolution |
| 74 | + now filters by organization directly, and private hierarchy-scoped objects that lack |
| 75 | + an `organization_id` column fail closed at boot. |
| 76 | + |
| 77 | +## New features (additive) |
| 78 | + |
| 79 | +- **A3** — the `sys_user` record page gains **Permission Sets** (direct grant) and |
| 80 | + **Business Units** assignment tabs. |
| 81 | +- **A4** — record-level provenance vocabulary unifies to **platform / package / admin** |
| 82 | + across `sys_capability` / `sys_permission_set` / `sys_position`, surfaced as a badge; |
| 83 | + legacy values self-heal at boot (no destructive migration). |
| 84 | +- **A5** — package-level capability declaration API (`defineCapability`): a package's |
| 85 | + own capabilities flow into the registry with package provenance. |
| 86 | +- **B2/B4** — an explicit, monotonic **posture ladder** |
| 87 | + (`PLATFORM_ADMIN > TENANT_ADMIN > MEMBER > EXTERNAL`) derived from capability grants |
| 88 | + (never the identity provider's admin designation); an additive, derived, explainable field. Enforcement is |
| 89 | + behavior-preserving. |
| 90 | +- **C2** — `security.explain` extends to **record granularity**: explain why a specific |
| 91 | + record is visible/invisible to a user, layer by layer (permission set → position → |
| 92 | + sharing → row rule → effective row filter), with Layer 0/1 and posture surfaced in the |
| 93 | + Studio Access panel. |
| 94 | +- **C1** — hierarchy RLS enterprise package reaches GA (subtree/reporting-chain caching, |
| 95 | + customer docs, license gate). |
| 96 | + |
| 97 | +## Upgrade checklist |
| 98 | + |
| 99 | +- **Multi-org:** review any workflow that relies on members editing each other's records |
| 100 | + org-wide (grant an explicit edit permission set) and any integration that supplied a |
| 101 | + cross-tenant `organization_id` on user-context writes (route it through system context |
| 102 | + or a dedicated endpoint). |
| 103 | +- **Enterprise hierarchy:** the upgrade itself is inert; plan BU-anchor data before you |
| 104 | + begin populating `sys_user_position.business_unit_id`, since `unit` visibility then |
| 105 | + narrows to the position BU. |
| 106 | +- **Before production:** a multi-org / hierarchy / tenant-write-guard **enterprise |
| 107 | + end-to-end smoke test in a licensed environment is still owed** — open-core CI cannot |
| 108 | + exercise the multi-org path. |
| 109 | + |
| 110 | +## References |
| 111 | + |
| 112 | +ADR-0095 (kernel chain) · ADR-0090 Addendum (position-anchor) · ADR-0094 (overlay |
| 113 | +preservation) · framework#2920 (tracking). |
0 commit comments