feat(db): dedicated non-superuser runtime role for RLS
The app connected as 'postgres' — the bootstrap SUPERUSER — which bypasses row-level security unconditionally (FORCE RLS does not apply to superusers), making any future policy a silent no-op. This is the prerequisite commit for the RLS visibility work (plan ~/.opencode/plan/server-rls-library-visibility.md REV 2, §4.0). What changed: - database/schema/z_rls_role.sh: creates the bookhoard_srv role (LOGIN, non-superuser, non-bypassrls) and transfers ownership of every public table/sequence/function + schema + database to it. Ownership (not mere grants) because executeSchema re-runs the whole schema.sql on EVERY boot and needs owner DDL. Idempotent, catalog- only — no user data is read or written. - docker-compose.yml app service + docker-compose.dev.yml tests service: DATABASE_USER postgres -> bookhoard_srv, DATABASE_PASSWORD DBPASS -> DBSRVPASS (new .env key, generated locally). - POSTGRES_USER: postgres stays as the container bootstrap account; on FRESH volumes z_rls_role.sh runs automatically via docker-entrypoint-initdb.d AFTER schema.sql (alphabetical: z > s). Verified on the dev stack: role has rolsuper=f, rolbypassrls=f, owns all 40 tables and every public function; single boot as bookhoard_srv executed schema.sql cleanly (CREATE EXTENSION IF NOT EXISTS pg_trgm skips fine as non-superuser when the extension exists); health + authenticated smoke pass. Behavior otherwise unchanged — no policies yet.
This commit is contained in:
1 parent
3dcb794032
commit
cb258b6d20
3 files changed
+78
-4
No files matched your search
Executable
+69
@@ -0,0 +1,69 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
# RLS role bootstrap — plan §4.0 (~/.opencode/plan/server-rls-library-visibility.md REV 2).
|
||||||
|
#
|
||||||
|
# Creates the NON-SUPERUSER runtime role `bookhoard_srv` and makes it the
|
||||||
|
# OWNER of everything in the public schema, so the app can keep running
|
||||||
|
# executeSchema's per-boot DDL while being subject to RLS (FORCE).
|
||||||
|
#
|
||||||
|
# The bootstrap superuser (POSTGRES_USER, "postgres") is UNTOUCHED and
|
||||||
|
# remains the cluster/database bootstrap account; the app never connects
|
||||||
|
# as it.
|
||||||
|
#
|
||||||
|
# Runs AUTOMATICALLY on fresh volumes: docker-entrypoint-initdb.d executes
|
||||||
|
# initdb.d entries in alphabetical order — "z_rls_role.sh" sorts AFTER
|
||||||
|
# "schema.sql" (digits/uppercase sort before lowercase 's'), so the tables
|
||||||
|
# exist before ownership transfers. It runs as the postgres superuser with
|
||||||
|
# the db container's env (DBSRVPASS comes from .env via compose env_file).
|
||||||
|
#
|
||||||
|
# EXISTING volumes (initdb.d never re-runs): apply manually ONCE, as the
|
||||||
|
# superuser, BEFORE switching the app's DATABASE_USER:
|
||||||
|
# docker exec -i bookhoard_db bash -s < database/schema/z_rls_role.sh
|
||||||
|
#
|
||||||
|
# IDEMPOTENT: safe to re-run (role creation is guarded; ownership ALTERs
|
||||||
|
# and the password set are idempotent).
|
||||||
|
#
|
||||||
|
# DATA SAFETY: every statement here is catalog-only. No user row is read,
|
||||||
|
# rewritten, or deleted — reading progress, annotations, highlights,
|
||||||
|
# bookmarks, collections, and media rows are untouched.
|
||||||
|
set -euo pipefail
|
||||||
|
: "${DBSRVPASS:?DBSRVPASS must be set in the db container env (.env via env_file)}"
|
||||||
|
: "${POSTGRES_USER:?POSTGRES_USER must be set (the image sets it)}"
|
||||||
|
: "${POSTGRES_DB:?POSTGRES_DB must be set (the image sets it)}"
|
||||||
|
|
||||||
|
psql -v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB" \
|
||||||
|
-v role_pass="$DBSRVPASS" <<'EOSQL'
|
||||||
|
\echo '== RLS role bootstrap: creating/refreshing bookhoard_srv =='
|
||||||
|
|
||||||
|
-- 1. The runtime role: LOGIN, NO superuser, NO bypassrls, NO createrole.
|
||||||
|
DO $$
|
||||||
|
BEGIN
|
||||||
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname = 'bookhoard_srv') THEN
|
||||||
|
CREATE ROLE bookhoard_srv LOGIN;
|
||||||
|
END IF;
|
||||||
|
END $$;
|
||||||
|
ALTER ROLE bookhoard_srv LOGIN PASSWORD :'role_pass';
|
||||||
|
|
||||||
|
-- 2. Ownership transfer — the runtime role must own the objects so
|
||||||
|
-- executeSchema (schema.sql re-runs on EVERY app boot) keeps working,
|
||||||
|
-- and so FORCE ROW LEVEL SECURITY can make the owner subject to the
|
||||||
|
-- policies. Catalog-only: flips ownership pointers, touches no rows.
|
||||||
|
DO $$
|
||||||
|
DECLARE r RECORD;
|
||||||
|
BEGIN
|
||||||
|
FOR r IN SELECT tablename FROM pg_tables WHERE schemaname = 'public' LOOP
|
||||||
|
EXECUTE format('ALTER TABLE public.%I OWNER TO bookhoard_srv', r.tablename);
|
||||||
|
END LOOP;
|
||||||
|
FOR r IN SELECT sequencename FROM pg_sequences WHERE schemaname = 'public' LOOP
|
||||||
|
EXECUTE format('ALTER SEQUENCE public.%I OWNER TO bookhoard_srv', r.sequencename);
|
||||||
|
END LOOP;
|
||||||
|
FOR r IN SELECT p.oid::regprocedure AS fn FROM pg_proc p
|
||||||
|
JOIN pg_namespace n ON p.pronamespace = n.oid
|
||||||
|
WHERE n.nspname = 'public' AND p.prokind = 'f' LOOP
|
||||||
|
EXECUTE format('ALTER FUNCTION %s OWNER TO bookhoard_srv', r.fn);
|
||||||
|
END LOOP;
|
||||||
|
END $$;
|
||||||
|
ALTER SCHEMA public OWNER TO bookhoard_srv;
|
||||||
|
ALTER DATABASE bookhoard OWNER TO bookhoard_srv;
|
||||||
|
|
||||||
|
\echo '== done: bookhoard_srv owns the public schema =='
|
||||||
|
EOSQL
|
||||||
@@ -22,10 +22,12 @@ services:
|
|||||||
container_name: bookhoard_tests
|
container_name: bookhoard_tests
|
||||||
environment:
|
environment:
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
|
# Runtime role = bookhoard_srv (non-superuser OWNER, subject to RLS
|
||||||
|
# via FORCE) — same role the app service uses.
|
||||||
DATABASE_HOST: db
|
DATABASE_HOST: db
|
||||||
DATABASE_PORT: ${DB_PORT:-5432}
|
DATABASE_PORT: ${DB_PORT:-5432}
|
||||||
DATABASE_USER: postgres
|
DATABASE_USER: bookhoard_srv
|
||||||
DATABASE_PASSWORD: ${DBPASS}
|
DATABASE_PASSWORD: ${DBSRVPASS}
|
||||||
DATABASE_NAME: bookhoard
|
DATABASE_NAME: bookhoard
|
||||||
COOKIE_SECURE: false
|
COOKIE_SECURE: false
|
||||||
|
|
||||||
|
|||||||
+5
-2
@@ -35,10 +35,13 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
environment:
|
environment:
|
||||||
# Database Configuration
|
# Database Configuration
|
||||||
|
# Runtime role = bookhoard_srv (non-superuser OWNER, subject to RLS
|
||||||
|
# via FORCE). POSTGRES_USER above is the bootstrap account ONLY —
|
||||||
|
# the app must NEVER connect as it (superusers bypass RLS).
|
||||||
DATABASE_HOST: db
|
DATABASE_HOST: db
|
||||||
DATABASE_PORT: ${DB_PORT:-5432}
|
DATABASE_PORT: ${DB_PORT:-5432}
|
||||||
DATABASE_USER: postgres
|
DATABASE_USER: bookhoard_srv
|
||||||
DATABASE_PASSWORD: ${DBPASS}
|
DATABASE_PASSWORD: ${DBSRVPASS}
|
||||||
DATABASE_NAME: bookhoard
|
DATABASE_NAME: bookhoard
|
||||||
|
|
||||||
# Application Configuration
|
# Application Configuration
|
||||||
|
|||||||
Reference in new issue
Block a user