From c282f38170cd55933bf3a711807915ad3dd4b62e Mon Sep 17 00:00:00 2001 From: Marcel Date: Fri, 22 May 2026 17:20:35 +0200 Subject: [PATCH] feat(observability): own grafana_reader password via repeatable migration MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit V68 used to set the role's password in a versioned migration, which Flyway applies exactly once per database. Rotating GRAFANA_DB_PASSWORD therefore had no effect on the DB role — operators would need a manual ALTER ROLE or a `flyway repair` that nobody documented. The shape conflated two lifecycles: schema migration (one-shot, immutable) and credential provisioning (rotatable). Split into: - V68 (versioned, immutable): creates the role and applies SELECT grants on audit_log, documents, transcription_blocks. - R__grafana_reader_password.sql (repeatable): issues ALTER ROLE … PASSWORD with the placeholder. Flyway computes the checksum on the resolved content, so any change to GRAFANA_DB_PASSWORD changes the checksum and re-applies the migration on the next boot. Rotation becomes "bump env var + restart backend". Co-Authored-By: Claude Opus 4.7 --- .../db/migration/R__grafana_reader_password.sql | 14 ++++++++++++++ .../db/migration/V68__add_grafana_reader_role.sql | 12 ++++++------ 2 files changed, 20 insertions(+), 6 deletions(-) create mode 100644 backend/src/main/resources/db/migration/R__grafana_reader_password.sql diff --git a/backend/src/main/resources/db/migration/R__grafana_reader_password.sql b/backend/src/main/resources/db/migration/R__grafana_reader_password.sql new file mode 100644 index 00000000..a4e63037 --- /dev/null +++ b/backend/src/main/resources/db/migration/R__grafana_reader_password.sql @@ -0,0 +1,14 @@ +-- Repeatable migration: sets the grafana_reader role's password from the +-- ${grafanaDbPassword} placeholder (resolved by FlywayConfig from the +-- GRAFANA_DB_PASSWORD environment variable). Flyway computes the checksum on +-- the resolved migration content, so any change to GRAFANA_DB_PASSWORD changes +-- the checksum and re-applies this migration on the next boot. That makes +-- password rotation a "change env var + restart" operation — no manual psql. +-- +-- V68 created the role itself (without a usable password). This file owns the +-- password lifecycle; nothing else writes it. +DO $$ +BEGIN + EXECUTE format('ALTER ROLE grafana_reader WITH PASSWORD %L', '${grafanaDbPassword}'); +END +$$; diff --git a/backend/src/main/resources/db/migration/V68__add_grafana_reader_role.sql b/backend/src/main/resources/db/migration/V68__add_grafana_reader_role.sql index ffb185fa..eb276b77 100644 --- a/backend/src/main/resources/db/migration/V68__add_grafana_reader_role.sql +++ b/backend/src/main/resources/db/migration/V68__add_grafana_reader_role.sql @@ -1,13 +1,13 @@ -- Read-only role used by the Grafana PostgreSQL datasource for the PO Overview --- dashboard (issue #651). Password is injected at migration time via the Flyway --- placeholder ${grafanaDbPassword}, supplied by FlywayConfig from the --- GRAFANA_DB_PASSWORD environment variable. +-- dashboard (issue #651). The role is created here without a usable password +-- (LOGIN-capable but no password set); R__grafana_reader_password.sql sets the +-- password from GRAFANA_DB_PASSWORD on every boot, so rotation is just "bump +-- the env var and restart the backend" — see docs/adr/024-* and the rotation +-- runbook in docs/DEPLOYMENT.md. DO $$ BEGIN IF NOT EXISTS (SELECT 1 FROM pg_catalog.pg_roles WHERE rolname = 'grafana_reader') THEN - EXECUTE format('CREATE ROLE grafana_reader WITH LOGIN PASSWORD %L', '${grafanaDbPassword}'); - ELSE - EXECUTE format('ALTER ROLE grafana_reader WITH LOGIN PASSWORD %L', '${grafanaDbPassword}'); + CREATE ROLE grafana_reader WITH LOGIN; END IF; END $$;