The role split previously required a one-time manual script run on existing volumes (initdb.d only fires on fresh ones). This folds the bootstrap into the compose file the way split-account deployments standardly do it: a one-shot db-init service runs z_rls_role.sh (idempotent, network mode via PGHOST=db + PGPASSWORD from DBPASS) on every 'compose up', and the app gates on db-init: condition: service_completed_successfully. - Zero manual steps for fresh AND existing deployments: update compose, add DBSRVPASS to .env, 'compose pull && up -d'. - Security posture improves: the superuser password is now held ONLY by the init service — the app container connects exclusively as the non-superuser runtime role. - z_rls_role.sh gains dual connection modes (local socket for docker exec, network for the init service) with the DBPASS requirement enforced only in network mode. - Verified on the dev stack: init runs and exits clean, app gates on it and comes up healthy, second 'up' re-converges idempotently, invariant test passes through the new dependency chain. The initdb.d mount stays as-is (harmless double coverage on fresh volumes; both paths are idempotent).
76 lines
3.2 KiB
Bash
Executable File
76 lines
3.2 KiB
Bash
Executable File
#!/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
|