infra(db-roles): validated Option A — shared tracksolid_owner for migrators
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>
This commit is contained in:
parent
e1472adc3a
commit
e571eeabed
2 changed files with 128 additions and 168 deletions
|
|
@ -17,115 +17,99 @@ clients already`:
|
||||||
Giving each app a dedicated **NOSUPERUSER** role with a hard `CONNECTION LIMIT` fixes
|
Giving each app a dedicated **NOSUPERUSER** role with a hard `CONNECTION LIMIT` fixes
|
||||||
all three.
|
all three.
|
||||||
|
|
||||||
## The six connections (confirmed live)
|
## The six connections (confirmed live 2026-06-20)
|
||||||
|
|
||||||
| Service | Database | Current user | New role | Conn limit |
|
| Service | Database | Current user | New role | Conn limit | Notes |
|
||||||
|---|---|---|---|---|
|
|---|---|---|---|---|---|
|
||||||
| `webhook_receiver` | tracksolid_db | postgres | `webhook_app` | 10 |
|
| `webhook_receiver` | tracksolid_db | postgres | **`tracksolid_owner`** | 30 (shared) | runs migrations |
|
||||||
| `ingest_worker` | tracksolid_db | postgres | `ingest_app` | 10 |
|
| `ingest_worker` | tracksolid_db | postgres | **`tracksolid_owner`** | (shared) | runs migrations |
|
||||||
| `worker` | tracksolid_db | postgres | `worker_app` (read) | 5 |
|
| `worker` | tracksolid_db | postgres | **`tracksolid_owner`** | (shared) | = ingest_worker image; runs migrations |
|
||||||
| `dashboard_api` (prod backend) | tracksolid_db | postgres | `dashboard_app` (or reuse `dashboard_ro`) | 8 |
|
| `dashboard_api` (prod backend) | tracksolid_db | postgres | `dashboard_app` (read) | 8 | reader |
|
||||||
| `gateway` | **fleet_platform** | postgres | `gateway_app` | 15 |
|
| `gateway` | **fleet_platform** | postgres | `gateway_app` | 15 | migration TBD |
|
||||||
| `cron` | **fleet_platform** | postgres | `cron_app` | 5 |
|
| `cron` | **fleet_platform** | postgres | `cron_app` | 5 | migration TBD |
|
||||||
|
|
||||||
> Note `gateway`/`cron` use a **different database** (`fleet_platform`) on the same
|
> **Migrators share `tracksolid_owner`.** `webhook_receiver`, `ingest_worker`, and
|
||||||
> server — they still count against the shared 100-slot ceiling.
|
> `worker` all run `run_migrations.py` (DDL) and write telemetry. Because they ALTER
|
||||||
|
> objects, they must OWN them — so they connect as the single non-superuser
|
||||||
|
> `tracksolid_owner` (the role the repo already intends to own these schemas). One
|
||||||
|
> shared role = correct ownership, no app code change, one bounded connection cap.
|
||||||
|
> `gateway`/`cron` use a **different database** (`fleet_platform`) on the same server —
|
||||||
|
> still counted against the 100-slot ceiling; confirm whether they migrate before
|
||||||
|
> cutover (apply the same owner pattern if so).
|
||||||
|
|
||||||
### Connection budget (keep the sum < ~95, leaving 3 reserved + admin headroom)
|
### Connection budget (keep the sum < ~95, leaving 3 reserved + admin headroom)
|
||||||
|
|
||||||
```
|
```
|
||||||
webhook_app 10 + ingest_app 10 + worker_app 5 + dashboard_app 8 = 33 (tracksolid_db)
|
tracksolid_owner 30 (shared by 3 migrators) + dashboard_app 8 = 38 (tracksolid_db)
|
||||||
gateway_app 15 + cron_app 5 = 20 (fleet_platform)
|
gateway_app 15 + cron_app 5 = 20 (fleet_platform)
|
||||||
analytics_ro ~8 + dashboard_ro ~12 + grafana_ro ~5 + reporting_refresher ~3 = ~28 (existing)
|
analytics_ro ~8 + dashboard_ro ~12 + grafana_ro ~5 + reporting_refresher ~3 = ~28 (existing)
|
||||||
TOTAL ≈ 81 ✅
|
TOTAL ≈ 86 ✅
|
||||||
```
|
```
|
||||||
Tune the `CONNECTION LIMIT`s in the SQL to your real pool sizes; the point is the sum
|
Tune the `CONNECTION LIMIT`s to your real pool sizes; the point is the sum is now
|
||||||
is now **bounded and visible**, not open-ended superuser pools.
|
**bounded and visible**, not open-ended superuser pools.
|
||||||
|
|
||||||
## Step 1 — Discover what each app actually needs (do NOT skip)
|
## Step 1 — Discovery (DONE 2026-06-20)
|
||||||
|
|
||||||
The drafted grants are best-effort (ingestion = write telemetry; gateway/cron = RW
|
Confirmed live: `webhook_receiver`, `ingest_worker`, `worker` all start with
|
||||||
app state; worker/dashboard = read). Confirm before cutover:
|
`python run_migrations.py && …` → they run **DDL** and write telemetry (`worker` is
|
||||||
|
the same image as `ingest_worker`). Writes span `tracksolid`, `reporting`, `tickets`.
|
||||||
|
`dashboard_api` (prod backend) reads. `gateway`/`cron` are on `fleet_platform` and
|
||||||
|
write `state`; their migration behaviour is **not yet confirmed** (opaque
|
||||||
|
`entrypoint.sh`) — verify before cutover with:
|
||||||
|
|
||||||
```sql
|
```sql
|
||||||
-- (a) Which tables does each app WRITE? Reset stats, run the app for a bit, re-check:
|
-- re-run after a deploy to see writes; or set log_statement='ddl' on fleet_platform.
|
||||||
SELECT schemaname, relname, n_tup_ins, n_tup_upd, n_tup_del
|
SELECT schemaname, sum(n_tup_ins+n_tup_upd+n_tup_del) FROM pg_stat_user_tables GROUP BY 1;
|
||||||
FROM pg_stat_user_tables
|
|
||||||
WHERE n_tup_ins + n_tup_upd + n_tup_del > 0
|
|
||||||
ORDER BY 1,2;
|
|
||||||
|
|
||||||
-- (b) Does the app run DDL/migrations at deploy? Check its code/entrypoint for
|
|
||||||
-- CREATE/ALTER/DROP or a migrations runner (e.g. run_migrations.py, alembic).
|
|
||||||
-- If yes → it needs object OWNERSHIP, see Step 3.
|
|
||||||
```
|
```
|
||||||
Or temporarily set `log_statement = 'ddl'` (or `'mod'`) and watch one deploy cycle.
|
|
||||||
|
|
||||||
## Step 2 — Create the roles (no app impact yet)
|
## Step 2 — Create roles + reassign ownership (no app impact yet)
|
||||||
|
|
||||||
Generate a password per role (host-only, 0600), then apply the SQL as postgres:
|
The ownership reassignment in `app_roles_tracksolid_db.sql` is **safe to run while the
|
||||||
|
apps still connect as `postgres`** — superuser bypasses ownership, so nothing breaks
|
||||||
|
until you flip a `DATABASE_URL`. It is Timescale-aware (skips linked sequences, uses
|
||||||
|
`ALTER MATERIALIZED VIEW` for continuous aggregates, leaves `reporting.v_trips` with
|
||||||
|
`reporting_refresher`) and idempotent — validated in a rolled-back transaction against
|
||||||
|
the live DB.
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
for r in webhook_app ingest_app worker_app dashboard_app gateway_app cron_app; do
|
for r in tracksolid_owner dashboard_app gateway_app cron_app; do
|
||||||
[ -s ~/.$r.pw ] || ( umask 077; openssl rand -hex 24 > ~/.$r.pw )
|
[ -s ~/.$r.pw ] || ( umask 077; openssl rand -hex 24 > ~/.$r.pw )
|
||||||
done
|
done
|
||||||
DB=$(docker ps --filter name=timescale_db --format '{{.Names}}' | head -1)
|
DB=$(docker ps --filter name=timescale_db --format '{{.Names}}' | head -1)
|
||||||
|
|
||||||
|
# tracksolid_db: owner/migrator role + ownership reassignment + dashboard reader
|
||||||
docker exec -i "$DB" psql -U postgres -d tracksolid_db -v ON_ERROR_STOP=1 \
|
docker exec -i "$DB" psql -U postgres -d tracksolid_db -v ON_ERROR_STOP=1 \
|
||||||
-v webhook_pw="$(cat ~/.webhook_app.pw)" -v ingest_pw="$(cat ~/.ingest_app.pw)" \
|
-v owner_pw="$(cat ~/.tracksolid_owner.pw)" -v dash_pw="$(cat ~/.dashboard_app.pw)" \
|
||||||
-v worker_pw="$(cat ~/.worker_app.pw)" -v dash_pw="$(cat ~/.dashboard_app.pw)" \
|
|
||||||
< scripts/app_roles_tracksolid_db.sql
|
< scripts/app_roles_tracksolid_db.sql
|
||||||
|
|
||||||
|
# fleet_platform: gateway/cron roles (see that file's notes re: migrations)
|
||||||
docker exec -i "$DB" psql -U postgres -d fleet_platform -v ON_ERROR_STOP=1 \
|
docker exec -i "$DB" psql -U postgres -d fleet_platform -v ON_ERROR_STOP=1 \
|
||||||
-v gateway_pw="$(cat ~/.gateway_app.pw)" -v cron_pw="$(cat ~/.cron_app.pw)" \
|
-v gateway_pw="$(cat ~/.gateway_app.pw)" -v cron_pw="$(cat ~/.cron_app.pw)" \
|
||||||
< scripts/app_roles_fleet_platform.sql
|
< scripts/app_roles_fleet_platform.sql
|
||||||
```
|
```
|
||||||
|
|
||||||
## Step 3 — (Only if an app runs migrations) give its role object ownership
|
> If `gateway`/`cron` run migrations, they need the same owner treatment on
|
||||||
|
> `fleet_platform` (reassign its schemas to a `fleet_platform_owner` login role) — do
|
||||||
|
> that before cutting them over. Until confirmed, leave them on `postgres`.
|
||||||
|
|
||||||
All objects are owned by `postgres`, so a non-superuser role can write **rows** but
|
## Step 3 — Cut over one app at a time
|
||||||
not `ALTER`/`DROP` existing tables. If discovery showed an app issues DDL, reassign
|
|
||||||
the **app schemas** to the existing non-superuser owner role and add the app role to
|
Change each service's `DATABASE_URL` user/password (same host/port/dbname), redeploy
|
||||||
it. **Scope this to the app schemas — never `REASSIGN OWNED BY postgres` globally**
|
**just that one**, watch its logs for `permission denied` and the DB for the count:
|
||||||
(that would also try to move TimescaleDB/system objects).
|
|
||||||
|
|
||||||
```sql
|
|
||||||
-- tracksolid_db: make tracksolid_owner own the app objects, then add the ingestors.
|
|
||||||
DO $$
|
|
||||||
DECLARE r record;
|
|
||||||
BEGIN
|
|
||||||
FOR r IN
|
|
||||||
SELECT n.nspname, c.relname,
|
|
||||||
CASE c.relkind WHEN 'v' THEN 'VIEW' WHEN 'm' THEN 'MATERIALIZED VIEW' ELSE 'TABLE' END AS kind
|
|
||||||
FROM pg_class c JOIN pg_namespace n ON n.oid=c.relnamespace
|
|
||||||
WHERE n.nspname IN ('tracksolid','reporting') AND c.relkind IN ('r','p','v','m')
|
|
||||||
LOOP
|
|
||||||
EXECUTE format('ALTER %s %I.%I OWNER TO tracksolid_owner', r.kind, r.nspname, r.relname);
|
|
||||||
END LOOP;
|
|
||||||
END $$;
|
|
||||||
GRANT CREATE ON SCHEMA tracksolid, reporting TO tracksolid_owner;
|
|
||||||
GRANT tracksolid_owner TO webhook_app, ingest_app; -- they inherit ownership rights
|
|
||||||
```
|
```
|
||||||
(Do the analogous reassignment in `fleet_platform` to a `fleet_platform_owner` role
|
# the three migrators → the shared owner role:
|
||||||
if `gateway`/`cron` run migrations. Keep `reporting.v_trips` owned by
|
postgresql://tracksolid_owner:<owner_pw>@timescale_db:5432/tracksolid_db
|
||||||
`reporting_refresher` if that role refreshes it.)
|
# the dashboard backend → the reader:
|
||||||
|
postgresql://dashboard_app:<dash_pw>@timescale_db:5432/tracksolid_db
|
||||||
Test one deploy/migration as the new role **before** cutting over all apps.
|
```
|
||||||
|
|
||||||
## Step 4 — Cut over one app at a time
|
|
||||||
|
|
||||||
For each service, change its `DATABASE_URL` user/password from `postgres:…` to the new
|
|
||||||
role (same host/port/dbname), redeploy **just that one**, and watch its logs for
|
|
||||||
`permission denied` (→ widen the group grant) and the DB for connection count:
|
|
||||||
|
|
||||||
```bash
|
```bash
|
||||||
# in the app's env (Coolify secret or compose):
|
|
||||||
# tracksolid_db: postgresql://webhook_app:<pw>@timescale_db:5432/tracksolid_db
|
|
||||||
# fleet_platform: postgresql://gateway_app:<pw>@timescale_db:5432/fleet_platform
|
|
||||||
docker exec -i "$DB" psql -U postgres -d tracksolid_db -c \
|
docker exec -i "$DB" psql -U postgres -d tracksolid_db -c \
|
||||||
"SELECT usename, count(*) FROM pg_stat_activity GROUP BY 1 ORDER BY 2 DESC;"
|
"SELECT usename, count(*) FROM pg_stat_activity GROUP BY 1 ORDER BY 2 DESC;"
|
||||||
```
|
```
|
||||||
Order: start with the **lowest-risk reader** (`worker`/`dashboard_api`), then the
|
**Order:** `dashboard_api` (reader, lowest risk) first → confirm → then the migrators
|
||||||
ingestors, then `gateway`/`cron`.
|
one at a time (`ingest_worker`, then `worker`, then `webhook_receiver`), watching that
|
||||||
|
`run_migrations.py` succeeds and ingestion resumes after each.
|
||||||
|
|
||||||
## Rollback (instant)
|
## Rollback (instant)
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -1,119 +1,95 @@
|
||||||
-- app_roles_tracksolid_db.sql — dedicated NON-SUPERUSER login roles for the apps
|
-- app_roles_tracksolid_db.sql — get the tracksolid_db apps off the postgres SUPERUSER.
|
||||||
-- that currently connect to tracksolid_db as the `postgres` SUPERUSER.
|
|
||||||
-- ─────────────────────────────────────────────────────────────────────────────
|
-- ─────────────────────────────────────────────────────────────────────────────
|
||||||
-- WHY: six stack services connect to this Postgres server as the postgres superuser
|
-- DESIGN (validated against the live DB, 2026-06-20):
|
||||||
-- (webhook_receiver, ingest_worker, worker, the prod dashboard_api backend on
|
-- * webhook_receiver, ingest_worker, worker each run `run_migrations.py` (DDL) and
|
||||||
-- tracksolid_db; gateway + cron on fleet_platform — see the sibling file). That is
|
-- write telemetry. `worker` is a second copy of the ingest_worker image. Because
|
||||||
-- both a least-privilege problem AND the root of the "too many connections" error:
|
-- they run migrations, they need to OWN the objects they ALTER. They therefore
|
||||||
-- superuser sessions ignore per-role connection caps and can exhaust the 100-slot
|
-- connect as the shared, NON-SUPERUSER **tracksolid_owner** (the role the repo
|
||||||
-- ceiling (incl. the superuser-reserved slots). Dedicated roles let us pin a hard
|
-- already intends to own these schemas — see analytics_ro_role.sql default privs).
|
||||||
-- CONNECTION LIMIT and timeouts per app.
|
-- * the prod dashboard_api backend only reads → its own read role `dashboard_app`
|
||||||
|
-- (or reuse the existing dashboard_ro).
|
||||||
--
|
--
|
||||||
-- WHAT THIS DOES (run as the postgres SUPERUSER, on tracksolid_db):
|
-- This file is idempotent. Section 2 (ownership reassignment) is Timescale-aware:
|
||||||
-- * creates capability GROUP roles (NOLOGIN) for read vs. read-write,
|
-- it skips table-linked sequences, uses ALTER MATERIALIZED VIEW for continuous
|
||||||
-- * creates one LOGIN role per app, NOSUPERUSER, with a CONNECTION LIMIT and
|
-- aggregates, and leaves reporting.v_trips with reporting_refresher. Reassigning
|
||||||
-- bounded GUCs, as a member of the group it needs,
|
-- while the apps still run as postgres is SAFE — superuser bypasses ownership, so
|
||||||
-- * grants the groups SELECT / DML on the operational schemas.
|
-- nothing breaks until you flip each app's DATABASE_URL (see the runbook).
|
||||||
--
|
--
|
||||||
-- WHAT IT DOES *NOT* DO: change object ownership. All objects here are owned by
|
-- Run as the postgres SUPERUSER, on tracksolid_db:
|
||||||
-- `postgres`, so a non-superuser role can write ROWS but cannot ALTER/DROP existing
|
|
||||||
-- tables (i.e. run migrations). If an app runs DDL at deploy, see step 3 in
|
|
||||||
-- MIGRATE_APPS_OFF_SUPERUSER.md (reassign the app schemas to `tracksolid_owner` and
|
|
||||||
-- add the app role to it). Roles here INHERIT, so membership grants apply directly.
|
|
||||||
--
|
|
||||||
-- Idempotent. Passwords are supplied as psql vars (never stored in the repo):
|
|
||||||
-- docker exec -i <timescale_db> psql -U postgres -d tracksolid_db -v ON_ERROR_STOP=1 \
|
-- docker exec -i <timescale_db> psql -U postgres -d tracksolid_db -v ON_ERROR_STOP=1 \
|
||||||
-- -v webhook_pw="$(cat ~/.webhook_app.pw)" \
|
-- -v owner_pw="$(cat ~/.tracksolid_owner.pw)" \
|
||||||
-- -v ingest_pw="$(cat ~/.ingest_app.pw)" \
|
|
||||||
-- -v worker_pw="$(cat ~/.worker_app.pw)" \
|
|
||||||
-- -v dash_pw="$(cat ~/.dashboard_app.pw)" \
|
-- -v dash_pw="$(cat ~/.dashboard_app.pw)" \
|
||||||
-- < scripts/app_roles_tracksolid_db.sql
|
-- < scripts/app_roles_tracksolid_db.sql
|
||||||
|
|
||||||
\set ON_ERROR_STOP on
|
\set ON_ERROR_STOP on
|
||||||
|
|
||||||
-- ── 1. Capability groups (NOLOGIN; apps inherit privileges via membership) ──────
|
-- ── 1. tracksolid_owner: the shared owner/migrator login for the ingestion apps ──
|
||||||
DO $$
|
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
|
BEGIN
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='ts_app_read') THEN CREATE ROLE ts_app_read NOLOGIN; END IF;
|
FOR r IN
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='ts_app_write') THEN CREATE ROLE ts_app_write NOLOGIN; END IF;
|
SELECT n.nspname, c.relname, c.relkind,
|
||||||
END $$;
|
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$;
|
||||||
|
|
||||||
-- Read surface: telemetry + curated reporting layer.
|
DO $fns$
|
||||||
GRANT USAGE ON SCHEMA tracksolid, reporting TO ts_app_read;
|
DECLARE r record;
|
||||||
GRANT SELECT ON ALL TABLES IN SCHEMA tracksolid, reporting TO ts_app_read;
|
BEGIN
|
||||||
GRANT SELECT ON reporting.v_trips TO ts_app_read; -- matview (not in ALL TABLES)
|
FOR r IN SELECT p.oid::regprocedure AS sig
|
||||||
GRANT EXECUTE ON ALL FUNCTIONS IN SCHEMA reporting TO ts_app_read;
|
FROM pg_proc p JOIN pg_namespace n ON n.oid=p.pronamespace
|
||||||
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA tracksolid, reporting GRANT SELECT ON TABLES TO ts_app_read;
|
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$;
|
||||||
|
|
||||||
-- Write surface for ingestion: row DML on telemetry (NOT DDL — see header).
|
-- ── 3. dashboard_app: read-only role for the prod dashboard_api backend ──────────
|
||||||
GRANT ts_app_read TO ts_app_write; -- write implies read
|
-- (If that backend turns out to also WRITE app state, widen via a write group like
|
||||||
GRANT INSERT, UPDATE, DELETE ON ALL TABLES IN SCHEMA tracksolid TO ts_app_write;
|
-- the fleet_platform file; start read-only.)
|
||||||
GRANT USAGE, SELECT, UPDATE ON ALL SEQUENCES IN SCHEMA tracksolid TO ts_app_write;
|
|
||||||
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA tracksolid
|
|
||||||
GRANT INSERT, UPDATE, DELETE ON TABLES TO ts_app_write;
|
|
||||||
ALTER DEFAULT PRIVILEGES FOR ROLE postgres IN SCHEMA tracksolid
|
|
||||||
GRANT USAGE, SELECT, UPDATE ON SEQUENCES TO ts_app_write;
|
|
||||||
|
|
||||||
-- ── 2. Per-app LOGIN roles ──────────────────────────────────────────────────────
|
|
||||||
-- CONNECTION LIMIT is the hard budget cap (sum across all roles must stay < 100).
|
|
||||||
-- GUCs are belt-and-braces and tunable per app.
|
|
||||||
|
|
||||||
-- webhook_receiver — ingests Tracksolid webhooks (writes telemetry; may run migrations).
|
|
||||||
DO $$ BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='webhook_app') THEN
|
|
||||||
CREATE ROLE webhook_app LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE;
|
|
||||||
END IF; END $$;
|
|
||||||
ALTER ROLE webhook_app WITH LOGIN PASSWORD :'webhook_pw' CONNECTION LIMIT 10;
|
|
||||||
GRANT CONNECT ON DATABASE tracksolid_db TO webhook_app;
|
|
||||||
GRANT ts_app_write TO webhook_app;
|
|
||||||
ALTER ROLE webhook_app SET statement_timeout = '120s'; -- bulk inserts
|
|
||||||
ALTER ROLE webhook_app SET idle_in_transaction_session_timeout = '120s';
|
|
||||||
ALTER ROLE webhook_app SET idle_session_timeout = '10min';
|
|
||||||
ALTER ROLE webhook_app SET lock_timeout = '5s';
|
|
||||||
|
|
||||||
-- ingest_worker — background ingestion/normalisation (writes telemetry).
|
|
||||||
DO $$ BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='ingest_app') THEN
|
|
||||||
CREATE ROLE ingest_app LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE;
|
|
||||||
END IF; END $$;
|
|
||||||
ALTER ROLE ingest_app WITH LOGIN PASSWORD :'ingest_pw' CONNECTION LIMIT 10;
|
|
||||||
GRANT CONNECT ON DATABASE tracksolid_db TO ingest_app;
|
|
||||||
GRANT ts_app_write TO ingest_app;
|
|
||||||
ALTER ROLE ingest_app SET statement_timeout = '120s';
|
|
||||||
ALTER ROLE ingest_app SET idle_in_transaction_session_timeout = '120s';
|
|
||||||
ALTER ROLE ingest_app SET idle_session_timeout = '10min';
|
|
||||||
ALTER ROLE ingest_app SET lock_timeout = '5s';
|
|
||||||
-- If ingestion REFRESHes reporting.v_trips, add it to the existing refresher role:
|
|
||||||
-- GRANT reporting_refresher TO ingest_app; -- (uncomment after confirming)
|
|
||||||
|
|
||||||
-- worker — fleet_platform worker that also reads tracksolid_db. Assumed READ-ONLY
|
|
||||||
-- here; widen to ts_app_write only if it actually writes telemetry.
|
|
||||||
DO $$ BEGIN
|
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='worker_app') THEN
|
|
||||||
CREATE ROLE worker_app LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE;
|
|
||||||
END IF; END $$;
|
|
||||||
ALTER ROLE worker_app WITH LOGIN PASSWORD :'worker_pw' CONNECTION LIMIT 5;
|
|
||||||
GRANT CONNECT ON DATABASE tracksolid_db TO worker_app;
|
|
||||||
GRANT ts_app_read TO worker_app;
|
|
||||||
ALTER ROLE worker_app SET statement_timeout = '60s';
|
|
||||||
ALTER ROLE worker_app SET idle_in_transaction_session_timeout = '60s';
|
|
||||||
ALTER ROLE worker_app SET idle_session_timeout = '10min';
|
|
||||||
ALTER ROLE worker_app SET lock_timeout = '5s';
|
|
||||||
|
|
||||||
-- dashboard_api (PROD backend, currently postgres). If it only reads, prefer the
|
|
||||||
-- existing dashboard_ro. This role is for a backend that ALSO writes app state;
|
|
||||||
-- start read-only and widen per discovery.
|
|
||||||
DO $$ BEGIN
|
DO $$ BEGIN
|
||||||
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dashboard_app') THEN
|
IF NOT EXISTS (SELECT 1 FROM pg_roles WHERE rolname='dashboard_app') THEN
|
||||||
CREATE ROLE dashboard_app LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE;
|
CREATE ROLE dashboard_app LOGIN INHERIT NOSUPERUSER NOCREATEDB NOCREATEROLE;
|
||||||
END IF; END $$;
|
END IF; END $$;
|
||||||
ALTER ROLE dashboard_app WITH LOGIN PASSWORD :'dash_pw' CONNECTION LIMIT 8;
|
ALTER ROLE dashboard_app WITH LOGIN PASSWORD :'dash_pw' CONNECTION LIMIT 8;
|
||||||
GRANT CONNECT ON DATABASE tracksolid_db TO dashboard_app;
|
GRANT CONNECT ON DATABASE tracksolid_db TO dashboard_app;
|
||||||
GRANT ts_app_read 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 statement_timeout = '30s';
|
||||||
ALTER ROLE dashboard_app SET idle_in_transaction_session_timeout = '60s';
|
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 idle_session_timeout = '5min';
|
||||||
ALTER ROLE dashboard_app SET lock_timeout = '5s';
|
ALTER ROLE dashboard_app SET lock_timeout = '5s';
|
||||||
|
|
||||||
-- ── 3. Verify ───────────────────────────────────────────────────────────────────
|
-- ── 4. Verify ────────────────────────────────────────────────────────────────────
|
||||||
-- \du+ -- inspect roles, CONNECTION LIMIT, and memberships
|
-- \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;
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue