Skip to content

Add Row-Level Security (RLS) for per-user isolation - #47

Merged
Pino de Candia (pinodeca) merged 1 commit into
mainfrom
pinodeca/rls
Mar 10, 2026
Merged

Pino de Candia (pinodeca) merged 1 commit into
mainfrom
pinodeca/rls

Conversation

@pinodeca

@pinodeca Pino de Candia (pinodeca) commented Mar 9, 2026

Copy link
Copy Markdown
Contributor

Summary

Implements Phase 1 of the RLS spec (docs/rls.md) to enforce per-user isolation at the PostgreSQL table level, with hardened grant model.

Changes

Core RLS & Grants (src/lib.rs):

  • Enable RLS on df.instances and df.nodes with instances_user_isolation and nodes_user_isolation policies (submitted_by = current_user::regrole)
  • Auto-GRANT to PUBLIC: SELECT, INSERT on instances/nodes, column-level UPDATE (status, updated_at) on instances (for df.cancel()), full DML on vars, schema usage, and function execution
  • No full UPDATE — identity columns (submitted_by, login_role) and structural columns (root_node) are protected from user modification
  • No DELETE on instances or nodes
  • No FORCE ROW LEVEL SECURITY — superusers bypass RLS (standard PostgreSQL convention)

Ownership checks (src/dsl.rs):

  • df.cancel(): SPI ownership check via RLS before calling duroxide client, then SPI UPDATE on status (column-level grant)
  • df.signal(): Same ownership check before raise_external_event()
  • Both error with "Instance not found or access denied" for non-owned instances

Monitoring function rework (src/monitoring.rs):

  • list_instances(): Queries df.instances via SPI first (RLS-filtered), then only fetches duroxide info for user's own instance IDs
  • instance_info(): RLS-based ownership check — returns empty for non-owned instances
  • instance_executions(): RLS-based ownership check — returns empty for non-owned instances
  • instance_nodes(): No changes needed (already SPI-filtered)
  • metrics(): No changes needed (aggregate-only, no per-instance data)

E2E tests (tests/e2e/sql/):

  • Updated 00_setup_playground.sql_e2e_grant_df_to_e2e_user() is now a no-op (grants are automatic)
  • Updated 27_user_isolation.sql — removed redundant explicit GRANTs
  • Added 36_rls.sql — 9 tests: instance isolation, node isolation, cancel/signal denial, monitoring RLS, superuser bypass, UPDATE denial on identity columns, DELETE denial, cross-user UPDATE denial

Docs:

  • Updated docs/rls.md Decision 8 — column-level UPDATE grant model
  • Updated README.md — added Multi-User Setup section
  • Updated USER_GUIDE.md — updated Privilege Grants, Cross-Instance Visibility, Security Best Practices sections

Security model

Table SELECT INSERT UPDATE DELETE
df.instances ✅ (RLS) ✅ (RLS) (status, updated_at) only (RLS)
df.nodes ✅ (RLS) ✅ (RLS)
df.vars

Users cannot modify submitted_by, login_role, root_node, or any column on df.nodes. RLS restricts all operations to the user's own rows.

Not included (deferred per spec)

  • Variables scoping (df.vars RLS) — Phase 2 follow-up PR

Test results

  • 75 unit tests pass
  • 36 E2E tests pass (including 36_rls with 9 sub-tests)

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

Implements Phase 1 of per-user isolation using PostgreSQL Row-Level Security (RLS) in the pg_durable extension, plus accompanying privilege hardening, monitoring updates, and E2E coverage.

Changes:

  • Enable RLS on df.instances and df.nodes and auto-grant a hardened set of privileges to PUBLIC during CREATE EXTENSION.
  • Add RLS-based ownership checks to df.cancel() / df.signal(), and update monitoring functions to only query duroxide for instance IDs visible via SPI (RLS-filtered).
  • Update E2E setup/user-isolation tests and add a dedicated RLS E2E suite; refresh docs describing the new security model and multi-user setup.

Reviewed changes

Copilot reviewed 12 out of 12 changed files in this pull request and generated 6 comments.

Show a summary per file
File Description
src/lib.rs Adds RLS enablement + policies and default GRANT model during extension install.
src/dsl.rs Adds ownership checks for cancel/signal and updates cancel’s status update path.
src/monitoring.rs Reworks instance listing/info/executions to respect RLS visibility before calling duroxide APIs.
tests/e2e/sql/36_rls.sql New E2E coverage validating RLS isolation, denials, and superuser bypass.
tests/e2e/sql/00_setup_playground.sql Removes manual grants helper logic (now relying on extension auto-grants).
tests/e2e/sql/27_user_isolation.sql Removes redundant GRANTs and aligns with new auto-grant model.
scripts/test-e2e-local.sh Runs 36_rls as superuser due to session authorization switching.
docs/rls.md Adds the RLS design/spec and grant strategy documentation.
docs/user-isolation.md Updates identity capture narrative to match current implementation (nodes inserted in df.start()).
USER_GUIDE.md Documents RLS behavior, default grants, and how to restrict access from PUBLIC.
README.md Adds “Multi-User Setup” section describing RLS + default grants.
.github/copilot-instructions.md Updates E2E invocation examples to include --verbose.

Comment thread src/monitoring.rs
Comment thread tests/e2e/sql/00_setup_playground.sql Outdated
Comment thread docs/rls.md
Comment thread src/lib.rs
Comment thread README.md Outdated
Comment thread USER_GUIDE.md
- Enable RLS on df.instances and df.nodes with user isolation policies
- Auto-GRANT permissions to PUBLIC (UPDATE on 2 columns, no DELETE)
- Add ownership checks to df.cancel() and df.signal()
- Rework list_instances(), instance_info(), instance_executions() to
  query df.instances via SPI first (RLS-filtered) before duroxide client
- Update E2E setup to reflect automatic grants
- New 37_rls.sql E2E test covers instance/node isolation, cancel/signal
  denial, monitoring RLS, superuser bypass, and DELETE denial
@pinodeca
Pino de Candia (pinodeca) merged commit 021e081 into main Mar 10, 2026
5 checks passed
@pinodeca
Pino de Candia (pinodeca) deleted the pinodeca/rls branch March 10, 2026 22:14
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants