Repository navigation
Description
Edition: ObjectOS Enterprise (licensed, self-managed)
Version: 18.2.0 in production. The virtual-object route is evaluated against 18.3.0 (origin/main 0b2b309c).
Background
#27130 (closed) was answered with the virtual-object recipe: a plugin driver behind an external object (content/docs/data-modeling/virtual-objects.mdx, reference test packages/runtime/src/plugin-driver-external-object.spike.test.ts). We are migrating our production-scheduling gantt to that recipe. Our deployment is multi-organization: several companies share one instance, and each must see only its own projects, plans and work orders.
The current endpoint scopes every read to the caller's organization (we shipped a P0 fix for a cross-company leak there). The recipe gives us no way to keep that scoping.
What happens
Read on origin/main 0b2b309c:
- No Layer-0 wall on a virtual object. An object with an
externalblock whoseorganization_idis the platform-injected definition is treated as having no tenant column:hasPhantomTenantAnchor(packages/plugins/plugin-security/src/federated-phantom-anchors.ts:131) makesobjectHasOrgIdFieldfalse (packages/plugins/plugin-security/src/tenant-layer.ts:112), so the verdict isnone. Noorganization_idcondition reaches the driver'swhere, under any posture. - The driver is not told who is asking.
find(object, query, options?)gets the object name, the query andDriverOptions. For a federated objecttenantId/tenantIdsare withheld on purpose (packages/objectql/src/engine-driver-options.ts:131,tenantObject = … && !isFederated). There is no user field inDriverOptionseither. - So a driver written as the guide shows reads its source with a system identity, and every organization sees every row the driver returns. Nothing refuses it and nothing warns.
The guide and the reference test do not cover this: the spike runs under the default single posture, and the guide does not mention organization scoping.
What we found as a workaround (unverified)
If the virtual object declares its own organization_id field (any definition that differs from TENANT_SCOPE_FIELD_DEF), hasPhantomTenantAnchor returns false, the Layer-0 wall applies, and { organization_id: <org> } (or the deny filter) arrives in the driver's where. The driver then has to return organization_id on every row and push that condition down.
This depends on the exact-equality check in equalsShippedDef and is not documented, so we would rather not build on it without a platform answer.
Request
One of the following, or both:
- (a) Document and pin the workaround. State in the virtual-objects guide that a multi-organization deployment must declare its own
organization_idon the virtual object and return it from the driver. Add anisolated/groupposture case to the reference test that proves the wall reaches the driver'swhere, and that a caller of org A never reads org B's rows. - (b) Give a plugin driver the caller's scope. For a plugin-published driver (not a built-in SQL driver federating a remote database), pass
tenantId/tenantIdsinDriverOptions, so the driver can scope its own source. The reason external-datasource-federated-read: the platform injects its org-scoping predicate onto a federated remote table that has no organization_id column #7738 withholds them from federated SQL objects (a remote table has no tenant column) does not apply to code that computes its rows.
Either way, a validate warning for "external object on a plugin driver, walled posture, no declared organization_id" would catch the leak before it ships. #27332 added the same kind of warning for the sharing model.
Related
- object-gantt can no longer write back to a same-origin endpoint after #23082 — a measured producer of the retired view-level
apisource (read + write) #27130 (the original report), [Decision] Ontology over external systems: virtual objects on ADR-0015 federation — two driver tiers (plugin code · declarative OpenAPI), pushdown/identity/catalog/cache rules, Studio and AI drafts (2026 ontology research + deep design) #27261 (virtual-objects epic), objectql: virtual-object correctness floor —applyDriverQueryInMemoryhelper, per-datasource row cap with a loud refusal,supportshonoured for plugin drivers, refusal-envelope mapping for driver failures (epic #27261 P1) #27277 (engine floor), access: an external object on the default sharing model refuses every member edit (403) because the ownership checks readowner_id/created_by, which no remote row carries —os validatesays nothing (epic #27261, found by the P0 spike) #27332 (owner-unmapped warning), external-datasource-federated-read: the platform injects its org-scoping predicate onto a federated remote table that has no organization_id column #7738 / plugin-security injects org- and owner-shaped predicates onto federated external objects too — the #7738 defect on a layer outside objectql #7835 (tenant scope withheld on federated objects)
Activity
objectstack-fleet commentedon Nov 3, 2026
security#1 · os-justin · session_01QSeKYn5CmmdShCJifm6WYj
Thank you for the report. A security item is with the maintainer, and the work continues there privately; this card is closed on the maintainer's decision.