feat(deploy): inline bootstrap into db-init — compose is the single source; DB_PORT everywhere
z_rls_role.sh is gone. The db-init service carries the bootstrap SQL
inline (the script's docker-exec/local-socket mode was its only reason
to exist as a file, and the no-clone deployment path could not use it:
docker bind-mounts of missing host paths auto-create empty DIRECTORIES,
so a compose-only install got '/z_rls_role.sh: no such file').
The inline form surfaced two silent bugs the script had masked:
- psql defaulted its DATABASE to the username (POSTGRES_DB is not a
libpq variable) — the ownership loop ran against 'postgres' and
no-opped on existing volumes. Fixed with PGDATABASE: bookhoard.
- PGPORT was never propagated: with a custom DB_PORT, db-init dialed
5432 while Postgres listened on the custom port. Fixed with
PGPORT: ${DB_PORT:-5432} — DB_PORT is now a variable in every
connection path (db listener, host mappings, app, db-init).
Also removed the db service's initdb.d mount: the app executes the full
schema as the runtime-role owner on every boot incl. first (pg_trgm is
a trusted extension; the db owner can create it), so the bootstrap SQL
was its only content.
Verified: dev two-boot against the existing 40-table volume (owners
converged, 0 off-owner tables/functions), custom-port throwaway
(DB_PORT=15432: db-init + app both on the custom port, healthy),
default-port throwaway (fresh volume, healthy), full Go suite green.
Manual repair path: docker compose run --rm db-init.
This commit is contained in:
1 parent
cf73eec2e4
commit
304ea9599f
4 files changed
+73
-90
No files matched your search
@@ -108,8 +108,8 @@ inside quoted attribute values (gotcha #1 below), so dynamic
|
|||||||
Per-user library visibility (`library_visibility` table, default-ALLOW:
|
Per-user library visibility (`library_visibility` table, default-ALLOW:
|
||||||
every user sees every library unless an admin hides one) is enforced by
|
every user sees every library unless an admin hides one) is enforced by
|
||||||
**Postgres RLS** — one source of truth: the labeled policy block in
|
**Postgres RLS** — one source of truth: the labeled policy block in
|
||||||
`database/schema/schema.sql` + the runtime role
|
`database/schema/schema.sql` + the runtime role provisioned by the
|
||||||
(`database/schema/z_rls_role.sh`).
|
`db-init` compose service.
|
||||||
|
|
||||||
- **The engine is the enforcement.** Every `media_items` read is
|
- **The engine is the enforcement.** Every `media_items` read is
|
||||||
filtered by the policy, which joins `library_visibility` on the
|
filtered by the policy, which joins `library_visibility` on the
|
||||||
|
|||||||
@@ -69,14 +69,42 @@ services:
|
|||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
container_name: bookhoard_db_init
|
container_name: bookhoard_db_init
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_DB: bookhoard
|
|
||||||
PGHOST: db
|
PGHOST: db
|
||||||
DBPASS: ${DBPASS}
|
PGUSER: postgres
|
||||||
|
PGDATABASE: bookhoard
|
||||||
|
PGPASSWORD: ${DBPASS}
|
||||||
DBSRVPASS: ${DBSRVPASS}
|
DBSRVPASS: ${DBSRVPASS}
|
||||||
volumes:
|
entrypoint: ["bash", "-c"]
|
||||||
- ./database/schema/z_rls_role.sh:/z_rls_role.sh:ro
|
command:
|
||||||
command: bash /z_rls_role.sh
|
- |
|
||||||
|
set -e
|
||||||
|
: "${DBSRVPASS:?DBSRVPASS required in .env}"
|
||||||
|
psql -v ON_ERROR_STOP=1 -v role_pass="$DBSRVPASS" <<'EOSQL'
|
||||||
|
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';
|
||||||
|
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;
|
||||||
|
EOSQL
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
@@ -1,75 +0,0 @@
|
|||||||
#!/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
|
|
||||||
+37
-7
@@ -11,7 +11,6 @@ services:
|
|||||||
PGPORT: ${DB_PORT:-5432}
|
PGPORT: ${DB_PORT:-5432}
|
||||||
volumes:
|
volumes:
|
||||||
- postgres_data:/var/lib/postgresql/data
|
- postgres_data:/var/lib/postgresql/data
|
||||||
- ./database/schema:/docker-entrypoint-initdb.d
|
|
||||||
ports:
|
ports:
|
||||||
- "127.0.0.1:${DB_PORT:-5432}:${DB_PORT:-5432}"
|
- "127.0.0.1:${DB_PORT:-5432}:${DB_PORT:-5432}"
|
||||||
- "[::1]:${DB_PORT:-5432}:${DB_PORT:-5432}"
|
- "[::1]:${DB_PORT:-5432}:${DB_PORT:-5432}"
|
||||||
@@ -28,18 +27,49 @@ services:
|
|||||||
# transfers object ownership to it (RLS prerequisite). Idempotent —
|
# transfers object ownership to it (RLS prerequisite). Idempotent —
|
||||||
# safe on every startup; the app waits for its success. Passwords
|
# safe on every startup; the app waits for its success. Passwords
|
||||||
# accept inline values or .env, like the rest of this file.
|
# accept inline values or .env, like the rest of this file.
|
||||||
|
# NOTE: plpgsql $$ tags are written $$$$ — compose eats one dollar
|
||||||
|
# per pair before the container sees the SQL.
|
||||||
db-init:
|
db-init:
|
||||||
image: postgres:15-alpine
|
image: postgres:15-alpine
|
||||||
container_name: bookhoard_db_init
|
container_name: bookhoard_db_init
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: postgres
|
|
||||||
POSTGRES_DB: bookhoard
|
|
||||||
PGHOST: db
|
PGHOST: db
|
||||||
DBPASS: ${DBPASS}
|
PGPORT: ${DB_PORT:-5432}
|
||||||
|
PGUSER: postgres
|
||||||
|
PGDATABASE: bookhoard
|
||||||
|
PGPASSWORD: ${DBPASS}
|
||||||
DBSRVPASS: ${DBSRVPASS}
|
DBSRVPASS: ${DBSRVPASS}
|
||||||
volumes:
|
entrypoint: ["bash", "-c"]
|
||||||
- ./database/schema/z_rls_role.sh:/z_rls_role.sh:ro
|
command:
|
||||||
command: bash /z_rls_role.sh
|
- |
|
||||||
|
set -e
|
||||||
|
: "${DBSRVPASS:?DBSRVPASS required in .env}"
|
||||||
|
psql -v ON_ERROR_STOP=1 -v role_pass="$DBSRVPASS" <<'EOSQL'
|
||||||
|
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';
|
||||||
|
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;
|
||||||
|
EOSQL
|
||||||
depends_on:
|
depends_on:
|
||||||
db:
|
db:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
|
|||||||
Reference in new issue
Block a user