diff --git a/.changeset/adr-0095-d1-tenant-layer-0.md b/.changeset/adr-0095-d1-tenant-layer-0.md index bef1902c23..8b4465a836 100644 --- a/.changeset/adr-0095-d1-tenant-layer-0.md +++ b/.changeset/adr-0095-d1-tenant-layer-0.md @@ -1,5 +1,5 @@ --- -"@objectstack/plugin-security": minor +"@objectstack/plugin-security": major --- ADR-0095 D1: tenant isolation is now **Layer 0** — an independent, always-first, diff --git a/.changeset/authz-2937-insert-tenant-guard.md b/.changeset/authz-2937-insert-tenant-guard.md index a85ea8dccd..7a732917e2 100644 --- a/.changeset/authz-2937-insert-tenant-guard.md +++ b/.changeset/authz-2937-insert-tenant-guard.md @@ -1,5 +1,5 @@ --- -'@objectstack/plugin-security': minor +'@objectstack/plugin-security': major --- fix(plugin-security): #2937 — Layer 0 insert post-image 租户检查(伪造 organization_id 的用户 insert 现被拒) diff --git a/.changeset/authz-tenant-write-wall-fix.md b/.changeset/authz-tenant-write-wall-fix.md index 0ee5bed420..9aff4f054e 100644 --- a/.changeset/authz-tenant-write-wall-fix.md +++ b/.changeset/authz-tenant-write-wall-fix.md @@ -1,5 +1,5 @@ --- -'@objectstack/plugin-security': minor +'@objectstack/plugin-security': major --- fix(plugin-security): 堵跨租户 UPDATE 写 + org_admin 越 private 租户对象墙(security) diff --git a/content/docs/releases/meta.json b/content/docs/releases/meta.json index b790063320..f5ffa640f6 100644 --- a/content/docs/releases/meta.json +++ b/content/docs/releases/meta.json @@ -1,5 +1,5 @@ { "title": "Releases", "icon": "Tag", - "pages": ["index", "v14", "v13", "v12", "v9", "implementation-status"] + "pages": ["index", "v15", "v14", "v13", "v12", "v9", "implementation-status"] } diff --git a/content/docs/releases/v15.mdx b/content/docs/releases/v15.mdx new file mode 100644 index 0000000000..b51ffd14a8 --- /dev/null +++ b/content/docs/releases/v15.mdx @@ -0,0 +1,113 @@ +--- +title: v15.0.0 +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. +--- + +**Authorization kernel hardening (ADR-0095) + supporting features (tracking framework#2920).** +This release restructures the authorization hot path: tenant isolation moves out +of the business-RLS pass into an independent, always-first **Layer 0**; principal +tiering becomes an explicit, capability-derived **posture ladder**; and the tenant +write wall is closed on the update path. Several **behavior changes affect +multi-org (`tenancy.mode = 'multi'` + `@objectstack/organizations`) deployments +only** — single-org and physically-isolated (env-per-database, ADR-0002) deployments +are unaffected. + +Read this before upgrading a multi-org or enterprise-hierarchy deployment. + +## Breaking changes + +### Multi-org deployments only + +- **Cross-tenant read leak closed (Layer 0).** A permissive business RLS policy + (e.g. `status == 'public'`) can no longer OR-widen tenant scope. Tenant isolation + is now AND-composed as the outermost filter (`Layer0(tenant) AND Layer1(business RLS)`); + a foreign-org row a business policy matched is now invisible. +- **Member by-id writes narrow to owner-only.** The previous OR-merge silently + widened `owner_only_writes` back to org-wide, so a member could by-id update/delete + *any* record in their org. Writes are now owner-scoped as authored. **Migration:** + if your deployment intentionally relied on members editing each other's records + org-wide, grant an explicit per-object edit permission set (position-distributed) + where that is wanted — the baseline `member_default` no longer permits it. +- **Cross-tenant writes via `organization_id` blocked (insert + update).** A user + could previously plant a row into another tenant by supplying a forged + `organization_id` on insert, or by reassigning it on update. Both are now denied + for user contexts (Layer 0 post-image check, symmetric across insert/update/bulk); + `organization_id` is effectively immutable outside system context. Default writes + (no `organization_id`) and system-context writes (import, migration, per-org seed + replay) are unaffected. +- **Org admins no longer cross the tenant wall on `private` objects.** The Layer 0 + cross-tenant exemption is now gated on a genuine platform-admin capability + (`manage_metadata` / `manage_platform_settings` / `studio.access` / `manage_users`), + not on the `viewAllRecords` / `modifyAllRecords` super-bits that `organization_admin` + also holds. On `access.default: 'private'` tenant objects an org admin is now walled + to their own organization (previously unfiltered). Genuine platform admins are + unaffected; the super-bits continue to drive only the in-org Layer 1 business-RLS + short-circuit (TENANT_ADMIN semantics unchanged). +- **No-active-org writes fail closed.** A write by a principal with no active + organization on a tenant object is now denied. +- **Seeded `tenant_isolation` policy retired.** The wildcard `tenant_isolation` + RLS policy is removed from the `organization_admin` / `member_default` / + `viewer_readonly` sets (isolation is enforced by Layer 0 instead). The + `_self` / `_org` identity-table carve-outs and `owner_only_writes/deletes` are + unchanged; customized seeded sets keep their overlays (ADR-0094). + +### Enterprise hierarchy RLS (`@objectstack/security-enterprise`) + +- **`unit` scope anchors to the position's business unit.** `unit` / `unit_and_below` + now prefer `sys_user_position.business_unit_id` (the position's BU) over + `sys_business_unit_member` membership. **Deployments that do not populate + `sys_user_position.business_unit_id` see no change** (every user falls back to + membership). Once anchors are populated, a user's `unit` visibility narrows to + their position BU; multiple positions union; expired/inactive positions fall back + to membership. + +## Non-breaking hardening (same effective visibility) + +- **RLS safety nets recognize canonical `==` (#2936).** The field-existence / + tenancy-disabled nets were inert for `==`-form policies; now recognized. Only + visible effect: on column-less system tables a member's by-id write is now an + explicit fail-closed deny rather than a phantom-column filter — same result. +- **Self-delegation position anchors are subtree-constrained.** A delegated position's + `business_unit_id` must fall within the delegator's own effective anchor — closing a + lateral-visibility escalation the position-anchor change would otherwise have opened. +- **Hierarchy resolver queries are org-scoped (defense in depth).** Owner-id resolution + now filters by organization directly, and private hierarchy-scoped objects that lack + an `organization_id` column fail closed at boot. + +## New features (additive) + +- **A3** — the `sys_user` record page gains **Permission Sets** (direct grant) and + **Business Units** assignment tabs. +- **A4** — record-level provenance vocabulary unifies to **platform / package / admin** + across `sys_capability` / `sys_permission_set` / `sys_position`, surfaced as a badge; + legacy values self-heal at boot (no destructive migration). +- **A5** — package-level capability declaration API (`defineCapability`): a package's + own capabilities flow into the registry with package provenance. +- **B2/B4** — an explicit, monotonic **posture ladder** + (`PLATFORM_ADMIN > TENANT_ADMIN > MEMBER > EXTERNAL`) derived from capability grants + (never the identity provider's admin designation); an additive, derived, explainable field. Enforcement is + behavior-preserving. +- **C2** — `security.explain` extends to **record granularity**: explain why a specific + record is visible/invisible to a user, layer by layer (permission set → position → + sharing → row rule → effective row filter), with Layer 0/1 and posture surfaced in the + Studio Access panel. +- **C1** — hierarchy RLS enterprise package reaches GA (subtree/reporting-chain caching, + customer docs, license gate). + +## Upgrade checklist + +- **Multi-org:** review any workflow that relies on members editing each other's records + org-wide (grant an explicit edit permission set) and any integration that supplied a + cross-tenant `organization_id` on user-context writes (route it through system context + or a dedicated endpoint). +- **Enterprise hierarchy:** the upgrade itself is inert; plan BU-anchor data before you + begin populating `sys_user_position.business_unit_id`, since `unit` visibility then + narrows to the position BU. +- **Before production:** a multi-org / hierarchy / tenant-write-guard **enterprise + end-to-end smoke test in a licensed environment is still owed** — open-core CI cannot + exercise the multi-org path. + +## References + +ADR-0095 (kernel chain) · ADR-0090 Addendum (position-anchor) · ADR-0094 (overlay +preservation) · framework#2920 (tracking).