Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -195,7 +195,7 @@ GRANT pg_durable_user TO app_backend, etl_service;
> **Note:** `GRANT EXECUTE ON ALL FUNCTIONS` only applies to functions that exist when the grant runs. After upgrading pg_durable with `ALTER EXTENSION pg_durable UPDATE`, re-run `df.grant_usage('role')` (or re-issue the manual grants) so new functions are accessible.

**Key points:**
- The background worker role (`pg_durable.worker_role` GUC, default: `azuresu`) **must be a superuser** — it bypasses RLS to manage all users' instances
- The background worker role (`pg_durable.worker_role` GUC, default: `postgres`) **must be a superuser** — it bypasses RLS to manage all users' instances
- Users get `SELECT` + `INSERT` on `df.instances` / `df.nodes`, column-level `UPDATE (status, updated_at)` on instances for `df.cancel()`
- Identity column (`submitted_by`) cannot be modified by users
- **`df.vars` uses per-user scoping** — each user has their own variable namespace via an `owner` column and RLS. Superusers bypass RLS but DSL functions still scope to the calling user via explicit filters. Avoid storing secrets in plain text
Expand Down
4 changes: 2 additions & 2 deletions docs/rls.md
Original file line number Diff line number Diff line change
Expand Up @@ -206,7 +206,7 @@ Superusers bypass all permission checks in PostgreSQL, including RLS. This means
- No additional GRANTs to the worker role needed
- Consistent with the existing trust model: the extension is installed by a superuser, and the worker is trusted code

The worker role is configured via `pg_durable.worker_role` GUC (defaults to `azuresu`). The extension should document that this role must be a superuser.
The worker role is configured via `pg_durable.worker_role` GUC (defaults to `postgres`). The extension should document that this role must be a superuser.

### Decision 7: `df.cancel()` and `df.signal()` ownership checks

Expand Down Expand Up @@ -298,7 +298,7 @@ The worker role must be a superuser (see Decision 6). Superusers bypass RLS auto

```sql
-- No RLS bypass configuration needed.
-- The worker role (pg_durable.worker_role GUC, default: azuresu) must be a superuser.
-- The worker role (pg_durable.worker_role GUC, default: postgres) must be a superuser.
-- Superusers bypass all permission checks including RLS.
```

Expand Down
2 changes: 1 addition & 1 deletion docs/security-review/threat-model.dfd-lite.yaml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ model:
- "Single-tenant PostgreSQL deployment — one organization per database instance"
- "Extension installed by superuser (trusted extension model)"
- "pg_hba.conf configured for trust or peer auth on localhost for background worker connections"
- "Background worker role (azuresu) is a PostgreSQL superuser"
- "Background worker role (postgres) is a PostgreSQL superuser"
- "No external services — everything runs inside the PostgreSQL server process"
- "Users have database role accounts with appropriate PostgreSQL RBAC"
externalDependencies:
Expand Down
6 changes: 3 additions & 3 deletions docs/security-review/workbook-data.md
Original file line number Diff line number Diff line change
Expand Up @@ -16,14 +16,14 @@ pg_durable does not use token-based authentication. All identity is PostgreSQL r
|---|---|---|---|---|
| session_user (login_role) | pg_hba.conf authentication | `GetSessionUserId()` C API | df.instances.login_role, df.nodes.login_role | Connection authentication for per-user SQL execution |
| current_user (submitted_by) | PostgreSQL SET ROLE / default | `GetOuterUserId()` C API | df.instances.submitted_by, df.nodes.submitted_by | SET ROLE target for privilege isolation; RLS policy column |
| worker_role | GUC `pg_durable.worker_role` | Configuration (default: "azuresu") | postgresql.conf | Background worker sqlx pool authentication |
| worker_role | GUC `pg_durable.worker_role` | Configuration (default: "postgres") | postgresql.conf | Background worker sqlx pool authentication |

### Least Privilege Assessment

| Identity | Privileges | Minimum Required | Delta |
|---|---|---|---|
| Database user | EXECUTE on all df.* functions, SELECT/INSERT on df.tables, USAGE on df schema | EXECUTE on needed df.* functions only; no df.http() unless needed | Too broad — PUBLIC has EXECUTE on all functions including df.http() |
| Worker role (azuresu) | SUPERUSER (bypasses RLS, connects as any role) | BYPASSRLS + CREATEROLE or trust-auth connect-as capability | Could explore non-superuser with BYPASSRLS if PostgreSQL supports connect-as without superuser |
| Worker role (postgres) | SUPERUSER (bypasses RLS, connects as any role) | BYPASSRLS + CREATEROLE or trust-auth connect-as capability | Could explore non-superuser with BYPASSRLS if PostgreSQL supports connect-as without superuser |
| Per-user SQL connection | User's own RBAC (login_role + SET ROLE submitted_by) | Exactly what the user has outside durable functions | ✅ Correct — no privilege amplification |

---
Expand Down Expand Up @@ -115,7 +115,7 @@ pg_durable does not use token-based authentication. All identity is PostgreSQL r

| GUC | Default | Scope | Security Relevance |
|---|---|---|---|
| `pg_durable.worker_role` | "azuresu" | Postmaster | Determines background worker's PostgreSQL identity; must be superuser |
| `pg_durable.worker_role` | "postgres" | Postmaster | Determines background worker's PostgreSQL identity; must be superuser |
| `pg_durable.database` | "postgres" | Postmaster | Target database for extension operations |
| `df.in_workflow` | unset | Session | Custom GUC set on worker connections; prevents variable mutation during execution |

Expand Down
4 changes: 2 additions & 2 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ use std::ffi::CString;
// ============================================================================

pub static WORKER_ROLE: GucSetting<Option<CString>> =
GucSetting::<Option<CString>>::new(Some(c"azuresu"));
GucSetting::<Option<CString>>::new(Some(c"postgres"));

pub static DATABASE: GucSetting<Option<CString>> =
GucSetting::<Option<CString>>::new(Some(c"postgres"));
Expand Down Expand Up @@ -532,7 +532,7 @@ DECLARE
BEGIN
wrole := pg_catalog.current_setting('pg_durable.worker_role', true);
IF wrole IS NULL OR wrole OPERATOR(pg_catalog.=) '' THEN
wrole := 'azuresu';
wrole := 'postgres';
END IF;

SELECT rolsuper INTO is_super FROM pg_catalog.pg_roles WHERE rolname OPERATOR(pg_catalog.=) wrole;
Expand Down
4 changes: 2 additions & 2 deletions src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -20,12 +20,12 @@ use uuid::Uuid;
// ============================================================================

/// Get the worker role from the `pg_durable.worker_role` GUC.
/// Falls back to `"azuresu"` if the GUC is not set.
/// Falls back to `"postgres"` if the GUC is not set.
pub fn get_worker_role() -> String {
crate::WORKER_ROLE
.get()
.map(|cs: CString| cs.to_string_lossy().into_owned())
.unwrap_or_else(|| "azuresu".to_string())
.unwrap_or_else(|| "postgres".to_string())
}

/// Get the database from the `pg_durable.database` GUC.
Expand Down
Loading