Skip to content

[finding] current_user.accessible_org_ids is reserved and ADR-0105 D11 calls it core-resolved, but nothing resolves it into the RLS variable bag — every predicate using it fails closed #16518

Description

@os-sam

Found while building an app against the documented RLS surface (objectstack-ai/ats, whose whole employer-side isolation model is authored on this placeholder). Reporting it here because the app cannot fix it, and neither can any app.

The contradiction

Three facts that cannot all be right:

  1. ADR-0105 D2 makes accessible_org_ids a first-class, core-resolved union org scope, carried on ExecutionContext.
  2. RESERVED_RLS_MEMBERSHIP_KEYS (packages/spec/src/contracts/rls-membership-resolver.ts:89) lists accessible_org_ids, and packages/plugins/plugin-security/src/security-plugin.ts:6074 rejects it from an app's membership resolver. ADR-0105 D11 states the reason outright: "accessible_org_ids (D2) is core-resolved, not an app resolver."
  3. RLSUserContext (packages/plugins/plugin-security/src/rls-compiler.ts:43) declares exactly id, organization_id, positions, org_user_ids, email, plus the index signature for the rlsMembership bag. accessible_org_ids is not among them, and nothing copies it from the execution context.

Every accessible_org_ids reference in plugin-security is Layer 0 tenancy-wall machinery — callerHasOrganizationScope, the posture wall log, the ADR-0123 D2 write refusal, and accessibleOrgIds passed to the wall at security-plugin.ts:5817. None feeds RLS predicate compilation.

So the key is reserved on the grounds that core resolves it, and core does not resolve it — for RLS. An app is blocked from supplying the one thing nobody supplies.

What it costs

A predicate such as employer_org IN (current_user.accessible_org_ids) compiles to an unresolved variable, every applicable policy is dropped, and RLS_DENY_FILTER returns zero rows, silently. No error, no failed request — the reader simply sees an empty list. This is the "declared but unenforced" shape ADR-0049 forbids, in the surface where the consequence is invisible.

In the app that hit it, that is ten policies across ats_employer, ats_job, ats_application, ats_interview, ats_offer and ats_employer_member — the entire multi-tenant isolation model, inert, with all three static gates green.

Evidence boundary

The three source facts above I verified directly in this tree at origin/main. The runtime symptom is from a two-persona rig on the consuming app, which I did not re-run here; it reported the context carrying accessible_org_ids: ['org_acme'] while the engine logged:

[RLS] DENY (fail closed): policy 'employer_admin_members' on 'ats_employer_member' …
variable "current_user.accessible_org_ids" is undefined

and compileCelToFilter reproducing unresolved-variable for canonical and SQL-bridge spellings alike, then compiling to {$in: ['org_acme']} once the variable is present. The last part is the useful one: the compiler handles the value correctly; only the plumbing is missing.

Two coherent resolutions

  • Populate it. Copy context.accessible_org_ids into RLSUserContext alongside org_user_ids, which is the closest existing precedent — a core-resolved membership set, pre-resolved so the compiler needs no subquery. This is what D2 and D11 read as promising, and it appears to be a small change.
  • Or withdraw the promise. If a single-posture install is deliberately not meant to expose it, say so in ADR-0105 and remove it from RESERVED_RLS_MEMBERSHIP_KEYS — so an app can supply the set itself through rlsMembership under its own key. Reserving a key that nothing populates is the worst of both.

Either is fine for a consuming app; the current state is not, because no app-side spelling can work.

Relationship to #16119

#16119 covers the reporting half — that an un-pre-resolved current_user.* variable (and an RLS predicate naming a non-existent field) is flagged by nothing and fails closed at runtime. That is real and I hit both halves of it in the same app.

This issue is the separate question underneath: whether this particular variable should have been resolvable at all. Fixing #16119 would have saved the app a week of invisible breakage; it would not make the predicate work.

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

Labels

Type

Projects

No projects

    Milestone

    No milestone

    Relationships

    None yet

    Development

    No branches or pull requests

    Issue actions