From fd9a40cc183925dfb74b453536ec0c9c753c5eed Mon Sep 17 00:00:00 2001 From: Hassan Abdel-Rahman Date: Thu, 4 Jun 2026 20:54:11 -0400 Subject: [PATCH] Grant pg_read_all_stats to the Claude read-only DB user Lets the read-only DB role see the query and state columns of every other backend in pg_stat_activity (and the other system statistics views) instead of , so stuck/slow queries can be diagnosed during indexing incidents. pg_read_all_stats is a read-only stats-visibility grant only; it confers no data-write or table-read privilege. The migration is gated to staging and production, where the role exists, and no-ops elsewhere. Co-Authored-By: Claude Opus 4.8 (1M context) --- ...57_schema.sql => 1780620712404_schema.sql} | 0 ...grant-claude-readonly-pg-read-all-stats.js | 63 +++++++++++++++++++ 2 files changed, 63 insertions(+) rename packages/host/config/schema/{1780412666457_schema.sql => 1780620712404_schema.sql} (100%) create mode 100644 packages/postgres/migrations/1780620712404_grant-claude-readonly-pg-read-all-stats.js diff --git a/packages/host/config/schema/1780412666457_schema.sql b/packages/host/config/schema/1780620712404_schema.sql similarity index 100% rename from packages/host/config/schema/1780412666457_schema.sql rename to packages/host/config/schema/1780620712404_schema.sql diff --git a/packages/postgres/migrations/1780620712404_grant-claude-readonly-pg-read-all-stats.js b/packages/postgres/migrations/1780620712404_grant-claude-readonly-pg-read-all-stats.js new file mode 100644 index 0000000000..ff6a9908f3 --- /dev/null +++ b/packages/postgres/migrations/1780620712404_grant-claude-readonly-pg-read-all-stats.js @@ -0,0 +1,63 @@ +// Grants the Claude read-only DB user the built-in `pg_read_all_stats` role. +// +// Without this grant, `claude_readonly_user` sees `` +// for the `query` and `state` columns of every other backend in +// `pg_stat_activity` (and the other system statistics views). That masking +// hides the SQL a worker backend is actually running, which is exactly the +// information needed to diagnose a stuck or slow query during an indexing +// incident. `pg_read_all_stats` is a read-only stats-visibility grant: it +// lifts the masking on the statistics views without conferring any +// data-write or table-read privilege. +// +// The grant target reuses CLAUDE_DB_USER (the same source the +// claude-readonly-db-user migration created the role from), so the GRANT +// always lands on the role that exists in the deployed environment. + +const username = process.env.CLAUDE_DB_USER; + +// Conservative PostgreSQL identifier check. The username is interpolated +// into raw SQL for GRANT / REVOKE below, so anything outside this +// character set is rejected before it can become a SQL injection or a +// silent-corruption hazard. PostgreSQL itself allows up to NAMEDATALEN-1 +// (63) characters in an identifier; this regex matches that limit and +// disallows the leading-digit / quoted-identifier variants we don't use. +const VALID_PG_IDENTIFIER = /^[A-Za-z_][A-Za-z0-9_]{0,62}$/; + +function ensureProvisioningEnv() { + if (!username) { + throw new Error( + 'CLAUDE_DB_USER must be set in staging/production. The infra side ' + + 'surfaces it from SSM into the pg-migration ECS task; if the ' + + 'migration is running without it set, that wiring has not landed yet.', + ); + } + if (!VALID_PG_IDENTIFIER.test(username)) { + throw new Error( + `CLAUDE_DB_USER (${JSON.stringify(username)}) does not match the ` + + 'allowed PostgreSQL identifier pattern [A-Za-z_][A-Za-z0-9_]{0,62}. ' + + 'Refusing to interpolate it into a GRANT / REVOKE statement.', + ); + } +} + +exports.up = (pgm) => { + if ( + !['staging', 'production'].includes(process.env.REALM_SENTRY_ENVIRONMENT) + ) { + return; + } + ensureProvisioningEnv(); + + pgm.sql(`GRANT pg_read_all_stats TO "${username}"`); +}; + +exports.down = (pgm) => { + if ( + !['staging', 'production'].includes(process.env.REALM_SENTRY_ENVIRONMENT) + ) { + return; + } + ensureProvisioningEnv(); + + pgm.sql(`REVOKE pg_read_all_stats FROM "${username}"`); +};