-- app_roles_tracksolid_db.sql — get the tracksolid_db apps off the postgres SUPERUSER. -- ───────────────────────────────────────────────────────────────────────────── -- DESIGN (validated against the live DB, 2026-06-20): -- * webhook_receiver, ingest_worker, worker each run `run_migrations.py` (DDL) and -- write telemetry. `worker` is a second copy of the ingest_worker image. Because -- they run migrations, they need to OWN the objects they ALTER. They therefore -- connect as the shared, NON-SUPERUSER **tracksolid_owner** (the role the repo -- already intends to own these schemas — see analytics_ro_role.sql default privs). -- * the prod dashboard_api backend only reads → its own read role `dashboard_app` -- (or reuse the existing dashboard_ro). -- -- This file is idempotent. Section 2 (ownership reassignment) is Timescale-aware: -- it skips table-linked sequences, uses ALTER MATERIALIZED VIEW for continuous -- aggregates, and leaves reporting.v_trips with reporting_refresher. Reassigning -- while the apps still run as postgres is SAFE — superuser bypasses ownership, so -- nothing breaks until you flip each app's DATABASE_URL (see the runbook). -- -- Run as the postgres SUPERUSER, on tracksolid_db: -- docker exec -i psql -U postgres -d tracksolid_db -v ON_ERROR_STOP=1 \ -- -v owner_pw="$(cat ~/.tracksolid_owner.pw)" \ -- -v dash_pw="$(cat ~/.dashboard_app.pw)" \ -- < scripts/app_roles_tracksolid_db.sql \set ON_ERROR_STOP on -- ── 1. tracksolid_owner: the shared owner/migrator login for the ingestion apps ── DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='tracksolid_owner') THEN CREATE ROLE tracksolid_owner LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE; END IF; END $$; -- LOGIN + password + a HARD connection cap (the real budget control). No -- statement_timeout: migrations (e.g. CREATE INDEX on a hypertable) can run long. ALTER ROLE tracksolid_owner WITH LOGIN PASSWORD :'owner_pw' CONNECTION LIMIT 30; ALTER ROLE tracksolid_owner SET idle_in_transaction_session_timeout = '5min'; ALTER ROLE tracksolid_owner SET idle_session_timeout = '10min'; ALTER ROLE tracksolid_owner SET lock_timeout = '10s'; GRANT CONNECT ON DATABASE tracksolid_db TO tracksolid_owner; GRANT USAGE, CREATE ON SCHEMA tracksolid, reporting, tickets, fuel TO tracksolid_owner; -- ── 2. Reassign the app objects to tracksolid_owner (Timescale-aware, idempotent) ─ DO $reassign$ DECLARE r record; k text; BEGIN FOR r IN SELECT n.nspname, c.relname, c.relkind, EXISTS (SELECT 1 FROM timescaledb_information.continuous_aggregates ca WHERE ca.view_schema=n.nspname AND ca.view_name=c.relname) AS is_cagg FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace WHERE n.nspname IN ('tracksolid','reporting','tickets','fuel') AND c.relkind IN ('r','p','v','m','S') AND pg_get_userbyid(c.relowner) <> 'tracksolid_owner' -- idempotent AND NOT (n.nspname='reporting' AND c.relname='v_trips') -- keep with refresher AND NOT (c.relkind='S' AND EXISTS ( -- skip linked seqs SELECT 1 FROM pg_depend d WHERE d.objid=c.oid AND d.deptype IN ('a','i'))) LOOP k := CASE WHEN r.is_cagg OR r.relkind='m' THEN 'MATERIALIZED VIEW' WHEN r.relkind='v' THEN 'VIEW' WHEN r.relkind='S' THEN 'SEQUENCE' ELSE 'TABLE' END; EXECUTE format('ALTER %s %I.%I OWNER TO tracksolid_owner', k, r.nspname, r.relname); END LOOP; END $reassign$; DO $fns$ DECLARE r record; BEGIN FOR r IN SELECT p.oid::regprocedure AS sig FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace WHERE n.nspname IN ('tracksolid','reporting','tickets','fuel') AND pg_get_userbyid(p.proowner) <> 'tracksolid_owner' LOOP EXECUTE format('ALTER FUNCTION %s OWNER TO tracksolid_owner', r.sig); END LOOP; END $fns$; -- ── 3. dashboard_app: read-only role for the prod dashboard_api backend ────────── -- (If that backend turns out to also WRITE app state, widen via a write group like -- the fleet_platform file; start read-only.) DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dashboard_app') THEN CREATE ROLE dashboard_app LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE; END IF; END $$; ALTER ROLE dashboard_app WITH LOGIN PASSWORD :'dash_pw' CONNECTION LIMIT 8; GRANT CONNECT ON DATABASE tracksolid_db TO dashboard_app; GRANT USAGE ON SCHEMA tracksolid, reporting, tickets, fuel TO dashboard_app; GRANT SELECT ON ALL TABLES IN SCHEMA tracksolid, reporting, tickets, fuel TO dashboard_app; GRANT SELECT ON reporting.v_trips TO dashboard_app; GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA reporting TO dashboard_app; ALTER DEFAULT PRIVILEGES FOR ROLE tracksolid_owner IN SCHEMA tracksolid, reporting, tickets, fuel GRANT SELECT ON TABLES TO dashboard_app; -- future objects (now owned by tracksolid_owner) ALTER ROLE dashboard_app SET statement_timeout = '30s'; ALTER ROLE dashboard_app SET idle_in_transaction_session_timeout = '60s'; ALTER ROLE dashboard_app SET idle_session_timeout = '5min'; ALTER ROLE dashboard_app SET lock_timeout = '5s'; -- ── 4. Verify ──────────────────────────────────────────────────────────────────── -- \du+ tracksolid_owner -- LOGIN + CONNECTION LIMIT 30 -- SELECT pg_get_userbyid(relowner), count(*) FROM pg_class -- WHERE relnamespace IN (SELECT oid FROM pg_namespace WHERE nspname='tracksolid') GROUP BY 1;