Discovery (live) corrected the design: webhook_receiver, ingest_worker, and worker all run run_migrations.py (DDL) and write telemetry — worker is the same image as ingest_worker, not a reader. Because they ALTER objects they must own them, so all three connect as the shared non-superuser tracksolid_owner (the role the repo already intends to own these schemas). dashboard_api backend stays a reader (dashboard_app). - app_roles_tracksolid_db.sql rewritten: tracksolid_owner LOGIN + CONNECTION LIMIT 30 + GUCs + USAGE/CREATE; Timescale-aware ownership reassignment (skips table-linked sequences, ALTER MATERIALIZED VIEW for continuous aggregates, leaves reporting.v_trips with reporting_refresher, reassigns functions); dashboard_app read role. - Reassignment validated in a rolled-back transaction on the live DB: reassigns the 31-chunk position_history hypertable + the v_mileage_daily_cagg continuous aggregate, and as tracksolid_owner can ALTER the hypertable and create/drop tables. - Runbook updated: discovery marked done, ownership folded into the apply (safe while apps still run as postgres — superuser bypasses ownership), corrected cutover order. Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
95 lines
5.7 KiB
SQL
95 lines
5.7 KiB
SQL
-- 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 <timescale_db> 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;
|