#!/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 on EVERY `docker compose up` via the one-shot `db-init` service # (idempotent — the app's depends_on gates on its success), AND on # fresh volumes via docker-entrypoint-initdb.d ('z' sorts after # 'schema.sql'). Manual equivalent for existing volumes: # docker exec -i -e DBSRVPASS=... 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)}" # Connection mode: local socket (docker exec into the db container) or # network (the db-init compose service, which sets PGHOST=db and needs # the bootstrap password for scram auth). PSQL_OPTS=(-v ON_ERROR_STOP=1 --username "$POSTGRES_USER" --dbname "$POSTGRES_DB") if [ -n "${PGHOST:-}" ]; then : "${DBPASS:?DBPASS is required when connecting over the network (PGHOST set)}" PSQL_OPTS+=(-h "$PGHOST") export PGPASSWORD="$DBPASS" fi psql "${PSQL_OPTS[@]}" \ -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