feat(fase-13): server_admins + admin_actions audit log model
Migration 024 introduces the orthogonal global role and the append-only audit log. server_admins is a separate table (not a column on users) so that promote/revoke don't require a JWT re-issue; is_server_admin() is a STABLE SECURITY DEFINER helper matching the shape of is_admin/is_member. admin_actions has only a SELECT policy (admin-only) — no INSERT/UPDATE/ DELETE policies, so only the SECURITY DEFINER RPCs in migration 025 can write to it. actor_id FK uses RESTRICT so the audit trail outlives admins. Bootstrap via infra/db-init/10-server-admin-seed.sh on first volume init (reads SERVER_ADMIN_EMAIL); supabase/seed.sql also pre-seeds Ana for dev/test because docker-entrypoint-initdb.d does not re-fire on long-lived dev volumes. Documented in .env.erosi.example. 20 new pgTAP assertions cover schema shape, RLS posture, FK behaviour, and the SECURITY DEFINER + STABLE attributes on is_server_admin(). Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
70
infra/db-init/10-server-admin-seed.sh
Executable file
70
infra/db-init/10-server-admin-seed.sh
Executable file
@@ -0,0 +1,70 @@
|
||||
#!/bin/bash
|
||||
# Fase 13.1.2 — server admin bootstrap.
|
||||
#
|
||||
# Runs once on first volume init (docker-entrypoint-initdb.d). Reads
|
||||
# SERVER_ADMIN_EMAIL from the environment. If the env var is set AND the
|
||||
# `public.server_admins` table is empty AND there's a matching `public.users`
|
||||
# row, promotes that user to server admin.
|
||||
#
|
||||
# Idempotent + safe to re-run:
|
||||
# * If SERVER_ADMIN_EMAIL is unset → no-op (logged).
|
||||
# * If the table already has at least one admin → no-op (logged). This is the
|
||||
# guard the plan calls "second invocation with a different email shouldn't
|
||||
# touch previous rows".
|
||||
# * If the email is set but no matching user exists yet (first prod boot:
|
||||
# volume is fresh, nobody has signed in via Keycloak yet) → log a warning
|
||||
# and exit cleanly. The operator must re-run this script (or insert
|
||||
# manually) after the first login. Documented in docs/deployment.md.
|
||||
#
|
||||
# Order: the leading digit (10-) puts this AFTER 00-role-passwords.sh so all
|
||||
# the supporting role + schema bootstrap has happened.
|
||||
#
|
||||
# Caveat: `docker-entrypoint-initdb.d` only fires on a FRESH volume. On a
|
||||
# long-lived dev volume it never runs again after the first init. We rely on
|
||||
# supabase/seed.sql to also insert the seed dev admin (Ana) so the integration
|
||||
# + e2e suites have a deterministic admin without depending on initdb timing.
|
||||
|
||||
set -e
|
||||
|
||||
if [ -z "${SERVER_ADMIN_EMAIL:-}" ]; then
|
||||
echo "==> server-admin-seed: SERVER_ADMIN_EMAIL not set, skipping"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
echo "==> server-admin-seed: looking up ${SERVER_ADMIN_EMAIL}…"
|
||||
|
||||
# Bash expands ${SERVER_ADMIN_EMAIL} inside the unquoted heredoc tag below.
|
||||
# The PL/pgSQL `$$ ... $$` dollar-quoting needs to be escaped (\$\$) so bash
|
||||
# doesn't try to interpolate it. Same idea as 00-role-passwords.sh.
|
||||
psql -v ON_ERROR_STOP=1 --username postgres --dbname postgres <<EOSQL
|
||||
DO \$\$
|
||||
DECLARE
|
||||
v_email text := '${SERVER_ADMIN_EMAIL}';
|
||||
v_user_id uuid;
|
||||
v_existing int;
|
||||
BEGIN
|
||||
SELECT count(*) INTO v_existing FROM public.server_admins;
|
||||
IF v_existing > 0 THEN
|
||||
RAISE NOTICE 'server-admin-seed: server_admins already populated (% row(s)), skipping', v_existing;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
SELECT id INTO v_user_id
|
||||
FROM public.users
|
||||
WHERE lower(email) = lower(v_email);
|
||||
|
||||
IF v_user_id IS NULL THEN
|
||||
RAISE WARNING 'server-admin-seed: no public.users row matches email %, skipping (re-run after first login)', v_email;
|
||||
RETURN;
|
||||
END IF;
|
||||
|
||||
INSERT INTO public.server_admins (user_id)
|
||||
VALUES (v_user_id)
|
||||
ON CONFLICT (user_id) DO NOTHING;
|
||||
|
||||
RAISE NOTICE 'server-admin-seed: promoted user % (email %)', v_user_id, v_email;
|
||||
END
|
||||
\$\$;
|
||||
EOSQL
|
||||
|
||||
echo "==> server-admin-seed: done"
|
||||
Reference in New Issue
Block a user