Skip to content

Use RLS in Postgres instead of app-level filtering - #165

Draft
prvnsmpth wants to merge 3 commits into
masterfrom
feat/rls
Draft

Use RLS in Postgres instead of app-level filtering#165
prvnsmpth wants to merge 3 commits into
masterfrom
feat/rls

Conversation

@prvnsmpth

@prvnsmpth prvnsmpth commented Apr 17, 2026

Copy link
Copy Markdown
Contributor

Move document permission filtering from application code into PostgreSQL row-level security (RLS). documents, embeddings, and content_blobs are now protected at the database level, so all read paths (search, typeahead, direct reads, AI tools, aggregates) get the same authorization automatically.

High-level changes

  • RLS policies on documents/embeddings/content_blobs with fail-closed semantics; permissions evaluated from the existing {public, users, groups} document ACL.
  • Two runtime logins (also the roles): omni_user for all user-facing services (web, searcher, AI) and omni_system for background/indexing/storage work. No SET ROLE switching — a user-facing service holds no membership in omni_system, so there's no path to escalate to the privileged role.
  • Transaction-local context: requests set the viewer email + public/user scope on their connection; background operations use omni_system.
  • Removed admin-scoped API keys (org-wide read access via API key was a security hole). Existing admin keys are deactivated; keys are now public (public docs only) or user (owner permissions).
  • Trust boundary: searcher requires an internal service token on identity-bearing endpoints; identity is resolved from the DB and unknown/inactive users fail closed.
  • Deployment: migration 112 creates the roles; the migrator provisions their passwords. New secrets documented in docs/document-rls-rollout.md.

Validation

  • 45 AI integration tests pass against a real ParadeDB, including personal vs org-agent scoping with a live searcher.
  • Fresh-DB migration test verifies deny-by-default, omni_user cannot SET ROLE omni_system, and correct public/user/system access.
  • Rust compile checks, unit tests, Prettier, and compose config all pass.

@prvnsmpth prvnsmpth changed the title Use RLS instead of explicit user-based filtering Use RLS in Postgres instead of app-level filtering Apr 17, 2026
@prvnsmpth prvnsmpth added this to the Sync & search hardening milestone Apr 26, 2026
@prvnsmpth prvnsmpth added the area:infra Deploy, CI, infrastructure label Apr 26, 2026
@prvnsmpth
prvnsmpth marked this pull request as draft April 26, 2026 12:58
@prvnsmpth prvnsmpth moved this from Todo to In Progress in Omni Roadmap Apr 26, 2026
Replace application-side permission predicates with PostgreSQL RLS on
documents, embeddings, and content_blobs. Requests run in transaction-local
context as omni_documents_user with the viewer email/scope, while background
services use a privileged omni_documents_system role.

- Migration 112: RLS + FORCE policies, fail-closed helper functions,
  hardened document roles; deactivates admin-scoped API keys and limits
  new keys to public/user scope
- run-migrations.sh: provisions separate user/system runtime logins so a
  user-facing service can never assume the system document role
- shared: DatabasePool gains a system pool and begin_document_user/system
  helpers; DocumentRepository scopes reads by role
- searcher: user/public/system access scopes, internal-token requirement on
  identity-bearing endpoints, system-scoped storage/typeahead reads
- AI: user/system connection contexts; embedding and storage repositories
  use the system pool; system scope requires the internal token
- web: per-request RLS-scoped db, public/user API-key scopes only,
  RLS-filtered aggregates and document reads
- tests: RLS permission/role-separation integration tests, permission-filter
  and API-key scope unit tests, CI security job; AI integration tests updated
  to seed real group memberships (RLS resolves groups from the DB)
- docs: deployment/rollout guide
CI wiring for the RLS regression tests will be introduced later; keep the
tests in the repo but do not run them in CI yet.
Merge the login and role: user-facing services log in as omni_user and
background services as omni_system directly, instead of a login role that
switches to a NOLOGIN role via SET ROLE. This removes the SET ROLE switching
entirely and makes the privilege boundary structural (omni_user holds no
membership in omni_system).

- migration 112: create omni_user/omni_system as LOGIN roles with their
  table grants; policies and current_user guards use the new names
- run-migrations.sh: only provisions passwords (privileges live in the
  migration)
- shared pool: drop OMNI_DOCUMENT_DATABASE_ROLE and system-pool SET ROLE;
  begin_document_user/system keep SET LOCAL ROLE as a no-op for prod and a
  switch for owner connections in tests
- AI connection.py, web hooks, indexing status: updated role names
- compose/env/docs/tests: renamed references
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:infra Deploy, CI, infrastructure

Projects

Status: In Progress

Development

Successfully merging this pull request may close these issues.

1 participant