Skip to content

Commit e0d3160

Browse files
committed
chore(changeset): federated objects are no longer walled by a phantom organization_id (#7835)
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01WbtFaTwsEstJXYTN847jz9
1 parent a2c3047 commit e0d3160

1 file changed

Lines changed: 53 additions & 0 deletions

File tree

Lines changed: 53 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,53 @@
1+
---
2+
"@objectstack/plugin-security": patch
3+
---
4+
5+
fix(plugin-security): the tenant wall no longer scopes a federated object by a column it does not have (#7835)
6+
7+
A **federated** object (ADR-0015 — `external`, bound to a remote table) is
8+
registered like any other, which means the ObjectQL registry injects the
9+
platform's system anchors into it: `organization_id`, `owner_id`,
10+
`owning_business_unit_id` and the audit `*_by` lookups. But the platform issues
11+
no DDL for a federated object — `Engine.syncObjectSchema` returns early, because
12+
the remote schema is owned externally. Those columns therefore exist in the
13+
registered schema and **in no backing store**.
14+
15+
Layer 0 (the tenant wall, ADR-0095 D1) decides "is this a tenant object?" by
16+
asking whether the object carries `organization_id`, so it was answered yes about
17+
a phantom and AND-composed `organization_id = <active org>` onto every federated
18+
read under a walled (`isolated` / `group`) posture. Measured on the shipped
19+
showcase: the composed read filter for `showcase_ext_customer` was
20+
`{ organization_id: 'org_alpha' }`, and `GET /data/showcase_ext_customer`
21+
answered **HTTP 200 with zero rows**.
22+
23+
The symptom is dialect-dependent and the defect is not. On SQLite an identifier
24+
that resolves to no column is reinterpreted as a string literal, so the
25+
comparison is constant-false: no error, no rows, a success status. Postgres and
26+
MySQL raise `column "organization_id" does not exist` instead. Either way the
27+
wall isolates nothing while the federated catalog stops answering the moment a
28+
deployment turns the organization wall on.
29+
30+
Layer 0 now discounts an `organization_id` that is the **platform's injected
31+
anchor** on a federated object, so it contributes no predicate there. What is
32+
unchanged:
33+
34+
- **Local objects.** The platform provisions their `organization_id`, so the
35+
anchor is real and the wall is untouched.
36+
- **A federated object that DECLARES a real remote `organization_id`.** The test
37+
is provenance — identity against the shipped column definition the registry
38+
spreads — not "is this object federated", so an author who exposes a genuine
39+
remote tenant column keeps their wall. Any inexact match is read as "not the
40+
platform's anchor" and leaves the wall in place: the fail direction is toward
41+
isolation.
42+
- **Layer 1 (business RLS).** App-authored policies still reach the compiler
43+
untouched (ADR-0049), including on federated objects.
44+
45+
This is the plugin-security sibling of the engine-layer fix that withheld
46+
`DriverOptions.tenantId` for the same objects; that one cannot reach here,
47+
because Layer 0 is a `where` predicate composed into the query AST rather than a
48+
driver option.
49+
50+
Record-ownership scoping (`__readScope` `own`/`unit` lowered to an `owner_id`
51+
predicate) reaches federated objects through the same phantom column set and is
52+
**not** addressed here — it is produced in `@objectstack/plugin-sharing` and is
53+
tracked separately.

0 commit comments

Comments
 (0)