You signed in with another tab or window. Reload to refresh your session.You signed out in another tab or window. Reload to refresh your session.You switched accounts on another tab or window. Reload to refresh your session.Dismiss alert
The Layer 0 cross-tenant exemption gate in computeLayeredRlsFilter now
reads the carried ctx.posture rung (#2956) as authoritative: crossing the
tenant wall requires PLATFORM_ADMIN. The hasPlatformAdminPosture capability
probe demotes to a fallback for resolver-less contexts (delegated-admin
bridge, sharing service, getReadFilter consumers), where behavior is
byte-for-byte unchanged. Read and write (insert/update post-image) tenant
checks share the one decision, so they cannot drift. A probe/rung
disagreement logs an [authz/ADR-0099] defect breadcrumb and enforces the
narrower rung verdict. Retires the stale 'posture not plumbed' comment.
Security narrowing (multi-org / @objectstack/organizations only, the G1-
adjudicated deltas): a scoped admin_full_access grant (a) and a piecemeal
platform-capability grant (b) resolve below PLATFORM_ADMIN and are now
walled to their own org on private / platform-global / better-auth objects,
where the posture-blind probe used to exempt them. Fail-safe (rung is a
strict subset of the probe, ADR-0099 I3); recover with an unscoped
admin_full_access grant. Single-org / env-per-database unaffected.
authz-matrix-gate.test.ts: new ADR-0099 P1 describe (narrowing read/write,
PLATFORM_ADMIN stays exempt, TENANT_ADMIN stays walled per I1, resolver-less
fallback preserved, defect-breadcrumb on disagreement / silence on
agreement). dogfood multi-tenant-exemption-posture ledger note updated.
Docs: authorization.mdx records the unscoped-grant rule.
Verified: plugin-security 503 passed, dogfood 296 passed, build 71/71, tsc +
check:role-word + check:doc-authoring clean.
Refs #3211 · ADR-0099 / #3109 · #2946 Finding 2 · #2956
Claude-Session: https://claude.ai/code/session_01DAHp4K7FvyMPBY1DPNkmRu
Co-authored-by: Claude <noreply@anthropic.com>
ADR-0099 P1 (#3211 M2): the Layer 0 cross-tenant exemption gate now reads the carried `ctx.posture` rung (#2956) as authoritative, with the platform-admin capability probe demoted to a fallback for resolver-less contexts (delegated-admin bridge, sharing service, `getReadFilter`). The read and write (insert/update post-image) tenant checks share one decision (`computeLayeredRlsFilter`), so they cannot drift. A probe↔rung disagreement logs a defect breadcrumb and enforces the narrower rung verdict.
6
+
7
+
**Behavior change (security narrowing, multi-org / `@objectstack/organizations` only):** a principal whose carried rung is not `PLATFORM_ADMIN` no longer crosses the tenant wall on private / platform-global / better-auth-managed objects, even when its resolved permission sets carry a platform-exclusive capability. Two shapes are affected: (a) a **scoped**`admin_full_access` grant (`sys_user_permission_set.organization_id` non-null), and (b) a custom set granting a platform capability (e.g. `studio.access`) piecemeal alongside a superuser bit. Both are now walled to their own org — the fail-safe direction (the carried rung is a strict subset of the probe). Single-org / env-per-database deployments are unaffected (Layer 0 is inert).
8
+
9
+
**Upgrade check:** before upgrading, scan `sys_user_permission_set` for `admin_full_access` rows with a non-null `organization_id`, and custom permission sets whose `systemPermissions` intersect `{manage_metadata, manage_platform_settings, studio.access, manage_users}`. To restore cross-tenant operator access for such a principal, grant the **unscoped**`admin_full_access` instead. The `[authz/ADR-0099]` warn log names any principal hitting the divergence at runtime.
enforcement: 'plugin-security/security-plugin.ts step 3.7 — computeWriteTenantCheckFilter (reuses computeLayeredRlsFilter\'s Layer 0) matched against the write post-image (fail-closed) for BOTH insert and update; enterprise auto-stamp authoritatively overwrites a user-context organization_id (@objectstack/organizations Middleware A)',
54
54
note: 'INSERT has no pre-image and UPDATE\'s pre-image (step 2.7) validates only the OLD organization_id, so the AND-composed Layer 0 wall never inspected the NEW value: a member could INSERT a forged cross-tenant organization_id (#2937) or UPDATE a row to RE-POINT it into a victim tenant (Finding 1, BLOCKER). A supplied cross-tenant organization_id is now DENIED on both paths — organization_id is effectively immutable in non-platform user contexts (platform-admin posture on a posture-permitting object + single-mode exempt, same rule as the read side). Unit-proven in plugin-security/authz-matrix-gate.test.ts ([#2937] insert + [Finding 1 / #2937] update post-image tenant guard). Multi-org is enterprise-only so it is not in the open-core dogfood boot; see ADR-0095 D1.'},
55
55
{id: 'multi-tenant-exemption-posture',summary: 'Layer 0 cross-tenant exemption requires the PLATFORM_ADMIN posture (Finding 2 — org_admin does not cross the wall)',state: 'enforced',
56
-
enforcement: 'plugin-security/security-plugin.ts hasPlatformAdminPosture (platform-exclusive systemPermissions) gates the tenant-layer.ts Layer 0 exemption; the superuser bit (viewAllRecords/modifyAllRecords) governs only the Layer 1 business-RLS short-circuit',
57
-
note: 'An organization_admin holds the superuser bit via its `*` wildcard, so it used to also get the Layer 0 exemption and read/write EVERY tenant\'s rows on private tenant objects. The exemption now requires a platform-exclusive capability (manage_metadata/manage_platform_settings/studio.access/manage_users), which org_admin deliberately lacks — a SECURITY NARROWING: org admin is walled to its own org, a true platform admin still crosses, the better-auth carve-out is untouched. Unit-proven in plugin-security/authz-matrix-gate.test.ts ([Finding 2 / #2937] Layer 0 cross-tenant exemption requires the platform posture).'},
56
+
enforcement: 'plugin-security/security-plugin.ts computeLayeredRlsFilter reads the carried ctx.posture rung (ADR-0099 D1 / #2956) to gate the tenant-layer.ts Layer 0 exemption — PLATFORM_ADMIN crosses, everything below is walled; the hasPlatformAdminPosture capability probe is the resolver-less fallback; the superuser bit (viewAllRecords/modifyAllRecords) governs only the Layer 1 business-RLS short-circuit',
57
+
note: 'An organization_admin holds the superuser bit via its `*` wildcard, so it used to also get the Layer 0 exemption and read/write EVERY tenant\'s rows on private tenant objects. Crossing now requires the carried PLATFORM_ADMIN rung (ADR-0099 P1), which derives only from an unscoped admin_full_access grant — org_admin resolves to TENANT_ADMIN and a scoped grant / piecemeal platform capability to MEMBER, so all are walled to their own org (SECURITY NARROWING; a true platform admin still crosses, the better-auth carve-out is untouched). Before P1 the gate keyed on a platform-exclusive capability probe, which a scoped admin_full_access grant or a piecemeal studio.access grant could satisfy — the divergence class the equivalence gate pinned and P1 closed (invariant I1: TENANT_ADMIN never crosses). Unit-proven in plugin-security/authz-matrix-gate.test.ts ([Finding 2 / #2937] platform-posture exemption + "ADR-0099 P1 — Layer 0 exemption reads the carried rung").'},
0 commit comments