PR #23 (migration 0050, src/rls.rs) made row-level security provably enforceable, but only partially adopted. This issue closes the remainder.
What already works
FORCE ROW LEVEL SECURITY on all 11 org-scoped tables.
- Restricted role
ione_app — non-SUPERUSER, non-BYPASSRLS.
src/rls.rs::org_scoped_tx sets app.current_org_id per transaction.
tests/rls_enforcement_integration.rs proves isolation by reading broker_credentials with no org_id predicate, so RLS is demonstrably the only filter.
Migrated: WorkspacePeerCredentialRepo::{get,list_for_workspace,delete}, WorkspacePeerDelegationRepo::{get,delete}, BrokerCredentialRepo::{create_pending,list_for_user,find_for_user,store_tokens,delete}.
What remains
1. The deployment everyone runs still bypasses RLS. DATABASE_URL, docker-compose.yml, CI and the dev loop connect as ione, which is SUPERUSER + BYPASSRLS. Postgres lets such a role past row security unconditionally — FORCE does not apply to it. On that connection the application-layer WHERE org_id = $n is still the only guard.
2. ione_app is not yet a supported runtime role. Under it, unmigrated paths fail closed, not open — but they do fail:
- a fresh connection sees
NULL and matches no rows;
- a recycled pooled connection sees
'', and ''::uuid raises 22P02 rather than returning NULL.
No leak in either shape (both pinned by the test), but an unmigrated write would silently affect zero rows. Cutting over requires finishing (3) first.
3. Repos and methods that never set the org context.
- No org id in hand, so they structurally cannot:
WorkspacePeerCredentialRepo::{upsert,secret_for}, WorkspacePeerDelegationRepo::{upsert_tokens,update_refreshed,material_for,begin_pending,consume_pending}, BrokerCredentialRepo::{find_by_state,consume_by_state,find_user_provider}. secret_for/material_for are reached from src/services/peer_tokens.rs and need the caller to thread an org id.
- Entirely unmigrated: peers,
workspace_peer_bindings, service_account_tokens, auto_exec_policies, interaction_events, MFA tables, identity-audit writers. Their tables carry FORCE; their queries never set context.
service_account_tokens is called out separately in md/design/headless-provisioning.md (HP-M4).
4. Consider current_setting(..., true)::uuid handling. The '' → 22P02 shape is fail-closed but noisy. A NULLIF(current_setting(...), '')::uuid form would fail closed quietly. Deliberately not changed in #23 to keep that PR surgical.
Acceptance
- Every repository touching an org-scoped table sets the context, or is documented as intentionally exempt with a reason.
- The app can run as
ione_app end-to-end with the full suite green.
md/design/identity-broker.md AC-15 can be marked satisfied without qualification.
Context: PR #23, md/design/identity-broker.md "RLS activation".
PR #23 (migration
0050,src/rls.rs) made row-level security provably enforceable, but only partially adopted. This issue closes the remainder.What already works
FORCE ROW LEVEL SECURITYon all 11 org-scoped tables.ione_app— non-SUPERUSER, non-BYPASSRLS.src/rls.rs::org_scoped_txsetsapp.current_org_idper transaction.tests/rls_enforcement_integration.rsproves isolation by readingbroker_credentialswith noorg_idpredicate, so RLS is demonstrably the only filter.Migrated:
WorkspacePeerCredentialRepo::{get,list_for_workspace,delete},WorkspacePeerDelegationRepo::{get,delete},BrokerCredentialRepo::{create_pending,list_for_user,find_for_user,store_tokens,delete}.What remains
1. The deployment everyone runs still bypasses RLS.
DATABASE_URL,docker-compose.yml, CI and the dev loop connect asione, which isSUPERUSER+BYPASSRLS. Postgres lets such a role past row security unconditionally —FORCEdoes not apply to it. On that connection the application-layerWHERE org_id = $nis still the only guard.2.
ione_appis not yet a supported runtime role. Under it, unmigrated paths fail closed, not open — but they do fail:NULLand matches no rows;'', and''::uuidraises22P02rather than returningNULL.No leak in either shape (both pinned by the test), but an unmigrated write would silently affect zero rows. Cutting over requires finishing (3) first.
3. Repos and methods that never set the org context.
WorkspacePeerCredentialRepo::{upsert,secret_for},WorkspacePeerDelegationRepo::{upsert_tokens,update_refreshed,material_for,begin_pending,consume_pending},BrokerCredentialRepo::{find_by_state,consume_by_state,find_user_provider}.secret_for/material_forare reached fromsrc/services/peer_tokens.rsand need the caller to thread an org id.workspace_peer_bindings,service_account_tokens,auto_exec_policies,interaction_events, MFA tables, identity-audit writers. Their tables carryFORCE; their queries never set context.service_account_tokensis called out separately inmd/design/headless-provisioning.md(HP-M4).4. Consider
current_setting(..., true)::uuidhandling. The''→22P02shape is fail-closed but noisy. ANULLIF(current_setting(...), '')::uuidform would fail closed quietly. Deliberately not changed in #23 to keep that PR surgical.Acceptance
ione_append-to-end with the full suite green.md/design/identity-broker.mdAC-15 can be marked satisfied without qualification.Context: PR #23,
md/design/identity-broker.md"RLS activation".