#!/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