Skip to content

feat(reporting): TAM-6862: Tamanu manages reporting/raw db roles#10082

Open
dannash100 wants to merge 30 commits into
mainfrom
feat/TAM-6862/tamanu-manages-reporting-roles
Open

feat(reporting): TAM-6862: Tamanu manages reporting/raw db roles#10082
dannash100 wants to merge 30 commits into
mainfrom
feat/TAM-6862/tamanu-manages-reporting-roles

Conversation

@dannash100

@dannash100 dannash100 commented Jun 17, 2026

Copy link
Copy Markdown
Contributor

Changes

Tamanu now creates and manages the reporting/raw database roles itself instead of relying on manually-provisioned login users with credentials in config. Database credentials collapse to the single core ("tamanu") connection, and reporting is always on (the reportSchemas.enabled config flag is removed).

ops:

Includes the DATABASE_URL change: feat(database): TAM-6862: support a single DATABASE_URL connection string (#10106) was merged into this branch rather than landing on its own, so this PR also carries the collapse to a single DATABASE_URL core connection string (the matching infra change is ops#237).

How it works

  • On startup Tamanu provisions the tamanu_reporting / tamanu_raw roles — CREATE ROLE, password, schema, GRANT SELECT, and ALTER DEFAULT PRIVILEGES so tables materialised later stay readable. Idempotent and self-healing.
  • The reporting connections log in AS the unprivileged role (not SET ROLE).
  • The role passwords derive (HMAC) from a random per-server secret, generated once and stored in local_system_secrets (see Secret storage & read isolation below) — not synced. So they're real, instance-unique secrets regardless of the core db auth method (trust, peer or password).
  • Report routes key their schema behaviour off whether the reporting connections are present, so there's no separate enable flag.
  • Removed the per-connection username/password config and the redundant createMockReportingSchemaAndRoles test helper.

Why log in as the role, not SET ROLE
SET ROLE is reversible (the authenticated user stays the privileged core user), so report SQL could RESET ROLE — or end the surrounding transaction with COMMIT and continue in autocommit — to write as core. Authenticating as the unprivileged role removes the escape: the session has no write grants and is a member of no other role. Covered by tests that run COMMIT; RESET ROLE; INSERT … through the report runner and a trailing write through the central verifyQuery path, both rejected with no data written.

Secret storage & read isolation
The raw role is granted SELECT on all of public, so any secret stored there is readable by report SQL. To prevent that:

  • Server secrets — the per-server reporting secret and the device key — now live in a new local_system_secrets table (mirrors local_system_facts; DO_NOT_SYNC; excluded from the sync-tick + changelog triggers so values never reach logs.changes). A DDL migration creates it and a DML migration moves the two rows out of local_system_facts; dbt source model added with nil masking on value.
  • The raw role is revoked from local_system_secrets and from the credential/token tables (one_time_logins, portal_one_time_tokens, refresh_tokens, signers/signers_historical), so reports can't read the device private key, auth tokens or signing keys. It still reads local_system_facts (sync ticks etc.) normally.
  • LocalSystemSecret also offers opt-in encrypted accessors (setSecret/getSecret, AES via config.crypto.keyFile) for external credentials that shouldn't be plaintext even behind the grant (e.g. a future sync_host password). Self-generated internal values (reporting secret, device key) stay on the plain path, so the always-on startup path keeps no key-file dependency.

Concurrency
startAll brings up the api, fhir workers and tasks runner concurrently, each provisioning/opening the reporting connections, so two safeguards keep them from racing on the cluster-global roles:

  • role DDL runs inside a transaction-scoped advisory lock (CREATE/ALTER ROLE would otherwise hit "tuple concurrently updated");
  • openDatabase caches the in-flight connection promise, so concurrent opens of the shared reporting connection key share one connection instead of throwing.

Startup ordering
Reading the per-server secret needs a migrated db, so facility opens its reporting connections after migrate via a new initReportingStores() (called from the start subcommands) rather than in init(). Central already inits after migrate.

⚠️ Deployment notes

  • The core db user needs CREATEROLE to provision the roles.
  • The tamanu_reporting / tamanu_raw roles need pg_hba entries permitting them to authenticate (they log in with the derived password). Both are handled by the ops PR for ansible single-servers; k8s gets CREATEROLE via the cluster initSql.
  • No settings PSK or extra secret config required — the reporting secret is self-generated into local_system_secrets, so this works on trust/peer/minimal servers too. (The opt-in encrypted accessors need config.crypto.keyFile, but nothing in this PR uses them; on k8s the key file is already provisioned.)
  • Existing deployments: drop the old reportSchemas.connections.*.username/password and reportSchemas.enabled from config (now removed/ignored); startup re-aligns any pre-existing roles' password/grants.

Deferred / considered (kept simple on purpose)

  • Provision in a migration (to avoid the per-boot GRANT/CREATEROLE cost): reverted — roles are cluster-global, so a migration running in every parallel test/replica DB raced on the shared role catalogue. Startup provisioning + the advisory lock avoids the race. Revisit if the per-boot GRANT ON ALL TABLES is measured to hurt at scale.
  • Secret in settings (encrypted, scoped): considered — but settings are PULL_FROM_CENTRAL (central-owned), which fights a self-bootstrapping per-server credential, and secret settings need a PSK that minimal/trust/peer servers don't have. local_system_secrets (self-generated per server, like the device key) sidesteps both with no extra config.

Testing: ReportSchemaRoles (facility) and verifyQuery reporting-path (central) suites pass, including the sandbox-escape cases. Also smoke-tested a real local central boot (offline upgrade → provision → serve): HTTP 200, both roles log in, secret persisted. The local_system_secrets move + raw revoke were verified live on a k8s deploy (raw can read local_system_facts but not local_system_secrets/token tables); database build + lint clean, dbt-check-todos passes.

Auto-Deploy

  • Deploy
Options
  • Synthetic test
  • Generate fake data
  • More data (20Gi)
  • No facility servers (central-only)
  • No sync (facility tasks scaled to zero)
  • AMD64 architecture (default is arm64)
  • Skip mobile build
  • Always build mobile
  • Stay up for 8 hours
  • Stay up for 24 hours
  • Stay up (no TTL)
  • Build images only (don't deploy)
  • Pause this deploy

Tests

  • Run E2E tests
  • Run DAST scan

Review Hero

  • Run Review Hero
  • Auto-fix review suggestions Wait for Review Hero to finish, resolve any comments you disagree with or want to fix manually, then check this to auto-fix the rest.
  • Auto-fix CI failures Check this to auto-fix lint errors, test failures, and other CI issues.
  • Auto-merge upstream Check this to merge the base branch into this PR, with AI conflict resolution if needed.
  • Save suppressions Check this to capture 👎 reactions on Review Hero comments as suppression rules in .github/review-hero/suppressions.yml. Also runs automatically at the end of any auto-fix run.

Remember to...

  • ...write or update tests
  • ...add UI screenshots and testing notes to the Linear issue
  • ...add any manual upgrade steps to the Linear issue
  • ...update the config reference, settings reference, or any relevant runbook(s)
  • ...call out additions or changes to config files for the deployment team to take note of

@dannash100 dannash100 requested a review from a team as a code owner June 17, 2026 23:51

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using default effort and found 1 potential issue.

Fix All in Cursor

❌ Bugbot Autofix is OFF. To automatically fix reported issues with cloud agents, enable autofix in the Cursor dashboard.

Want higher recall? High effort reviews run extra passes and find more bugs. A team admin can switch effort levels in the Cursor dashboard.

Comment @cursor review or bugbot run to trigger another review on this PR

Reviewed by Cursor Bugbot for commit 35ca9f0. Configure here.

Comment thread packages/database/src/services/reportQuery.js Outdated
@review-hero

review-hero Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦸 Review Hero (could not post inline comments — showing here instead)

packages/database/src/services/reporting.js:41

[BES Requirements] suggestion

ALTER DEFAULT PRIVILEGES IN SCHEMA sets default grants for tables created by CURRENT_USER in future. This runs on every startup and is idempotent, but it only covers tables created by the exact same database user that ran this statement. If FHIR materialisation or other code creates reporting-schema tables while connected as a different role (e.g. after SET ROLE), those tables won't inherit these defaults and will be invisible to tamanu_reporting until the next GRANT SELECT ON ALL TABLES call. The existing GRANT SELECT ON ALL TABLES covers tables that already exist at startup time, so there's a window between table creation and the next restart. Since ALTER DEFAULT PRIVILEGES is per-user, consider explicitly re-running GRANT SELECT ON ALL TABLES on reporting schema tables after materialisation completes, or document this limitation clearly.


packages/database/src/services/reporting.js:16

[Design & Architecture] suggestion

ensureReportingRole re-runs GRANT SELECT ON ALL TABLES IN SCHEMA, ALTER DEFAULT PRIVILEGES, and GRANT "${role}" TO CURRENT_USER on every server startup. GRANT ON ALL TABLES scans pg_class and briefly locks every relation in the schema — on a large schema (e.g. the materialised reporting schema) this could meaningfully slow startup and generate noise in pg_stat_activity. It also requires the core DB user to have CREATEROLE on every restart, not just the first one. The role provisioning (CREATE ROLE, GRANT TO CURRENT_USER, schema creation) belongs in a database migration that runs once; the ALTER DEFAULT PRIVILEGES for newly-materialised tables is the only part that legitimately needs to run at startup or on each materialisation cycle. Splitting these concerns would reduce the privilege requirement at steady-state and keep startup fast.


packages/central-server/__tests__/admin/reports/reportRoutes.test.js:400

[Integration tests] suggestion

The existing verifyQuery tests (lines 400-427) only exercise the non-reporting-connection path — they pass { store: ctx.store } with no reportSchemaStores, so the new runReadOnlyReportQuery branch added in packages/central-server/app/admin/reports/utils.js is never hit. Add a test that uses createTestContext({ enableReportInstances: true }) and calls verifyQuery with a populated reportSchemaStores to cover the reporting connection path and confirm it rejects writes (analogous to the sandbox-escape test added in packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js). Without this, the security guarantee provided by wrapping EXPLAIN in a read-only transaction on reporting connections has no central-server integration coverage.


packages/database/src/services/reportQuery.js:17

[Performance] suggestion

Every report execution now costs 5 sequential DB round-trips (BEGIN + SET TRANSACTION READ ONLY + SELECT 1 + actual query + COMMIT) instead of 1. The same security guarantee can be achieved at connection time instead of per-query: add SET default_transaction_read_only = on to the afterConnect hook in database.js for reporting connections (alongside the existing SET ROLE). This makes every transaction on the connection read-only by default without any per-query overhead, and eliminates runReadOnlyReportQuery entirely. The SELECT 1 lock-in trick is only needed because SET TRANSACTION READ ONLY can be reversed in an empty transaction — session-level default_transaction_read_only has no such loophole.

@review-hero

review-hero Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦸 Review Hero Summary
15 agents reviewed this PR | 0 critical | 4 suggestions | 0 nitpicks | Filtering: consensus 3 voters, 14 below threshold, 1 suppressed

Below consensus threshold (14 unique issues not confirmed by majority)
Location Agent Severity Comment
packages/central-server/app/admin/reports/utils.js:40 BES Requirements suggestion verifyQuery silently falls back to store.sequelize when reportSchemaStores is configured but the given dbSchema key is absent (i.e. reportingSequelize is undefined). In contrast, `Repor...
packages/database/src/models/ReportDefinitionVersion.ts:150 Design & Architecture nitpick The two-variable pattern (reportingSequelize + instance) is harder to follow than necessary. instance is always equal to reportingSequelize when reportSchemaStores is truthy, so the secon...
packages/database/src/services/database.js:138 Performance nitpick When both assumeRole and searchPath are set (the normal case for the reporting connection), afterConnect fires two sequential queries per physical connection instead of one. These could be combined...
packages/database/src/services/reporting.js:16 BES Requirements suggestion The parameter existingStore (used across ensureReportingRole, initReportStore, and initReporting) doesn't convey what this store is relative to the reporting stores being created. Per cod...
packages/database/src/services/reporting.js:16 Design & Architecture nitpick ensureReportingRole issues six separate DDL statements with no wrapping transaction. PostgreSQL DDL is transactional, so wrapping in a single sequelize.transaction() call would make the provisi...
packages/database/src/services/reporting.js:59 Design & Architecture suggestion ...config.db spreads the entire core database config (including fields like migrateOnStartup, verbose, alwaysCreateConnection, and any future additions) into the reporting connection overri...
packages/database/src/services/reporting.js:63 Bugs & Correctness suggestion pool is destructured from the per-connection config (e.g., { pool } = {} defaults to undefined when pool is absent). The overrides then spread config.db (which includes config.db.pool) and ...
packages/database/src/services/reporting.js:74 Bugs & Correctness suggestion The reduce with async callbacks starts both initReportStore calls concurrently (the second iteration begins before the first await initReportStore resolves, because reduce calls the callbac...
packages/database/src/services/reportQuery.js:1 BES Requirements nitpick The file-level comment block is 13 lines across 3 paragraphs. Project coding rules say to write no comments unless the WHY is non-obvious, and limit them to one short line. The security invariant h...
packages/database/src/services/reportQuery.js:17 BES Requirements critical Pool contamination: PostgreSQL's RESET ROLE (and SET ROLE NONE) is not transactional — the role change persists after the transaction commits or rolls back. The afterConnect hook that sets SET ROLE...
packages/database/src/services/reportQuery.js:18 BES Requirements nitpick instance.transaction() uses Sequelize's CLS-bound managed transaction (per project convention). Because the reporting connection has disableChangesAudit: true, this is safe. However the functio...
packages/database/src/services/reportQuery.js:18 Bugs & Correctness suggestion The instance.transaction() call shares the global CLS namespace (set via Sequelize.useCLS() in database.js) across all Sequelize instances. If runReadOnlyReportQuery is ever invoked from with...
packages/database/src/services/reportQuery.js:19 Bugs & Correctness critical The read-only sandbox can be escaped via COMMIT. The existing sandbox test (ReportSchemaRoles.test.js:197) confirms that multi-statement queries execute in simple query protocol on this connection ...
packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js:207 BES Requirements nitpick The assertion queries WHERE id = 99 against a varchar(255) column (id in reporting_test_table). Postgres will implicitly cast the integer literal, but using the string literal '99' would ...
Local fix prompt (copy to your coding agent)

Fix these issues identified on the pull request. One commit per issue fixed.


packages/database/src/services/reporting.js:41: ALTER DEFAULT PRIVILEGES IN SCHEMA sets default grants for tables created by CURRENT_USER in future. This runs on every startup and is idempotent, but it only covers tables created by the exact same database user that ran this statement. If FHIR materialisation or other code creates reporting-schema tables while connected as a different role (e.g. after SET ROLE), those tables won't inherit these defaults and will be invisible to tamanu_reporting until the next GRANT SELECT ON ALL TABLES call. The existing GRANT SELECT ON ALL TABLES covers tables that already exist at startup time, so there's a window between table creation and the next restart. Since ALTER DEFAULT PRIVILEGES is per-user, consider explicitly re-running GRANT SELECT ON ALL TABLES on reporting schema tables after materialisation completes, or document this limitation clearly.


packages/database/src/services/reporting.js:16: ensureReportingRole re-runs GRANT SELECT ON ALL TABLES IN SCHEMA, ALTER DEFAULT PRIVILEGES, and GRANT "${role}" TO CURRENT_USER on every server startup. GRANT ON ALL TABLES scans pg_class and briefly locks every relation in the schema — on a large schema (e.g. the materialised reporting schema) this could meaningfully slow startup and generate noise in pg_stat_activity. It also requires the core DB user to have CREATEROLE on every restart, not just the first one. The role provisioning (CREATE ROLE, GRANT TO CURRENT_USER, schema creation) belongs in a database migration that runs once; the ALTER DEFAULT PRIVILEGES for newly-materialised tables is the only part that legitimately needs to run at startup or on each materialisation cycle. Splitting these concerns would reduce the privilege requirement at steady-state and keep startup fast.


packages/central-server/__tests__/admin/reports/reportRoutes.test.js:400: The existing verifyQuery tests (lines 400-427) only exercise the non-reporting-connection path — they pass { store: ctx.store } with no reportSchemaStores, so the new runReadOnlyReportQuery branch added in packages/central-server/app/admin/reports/utils.js is never hit. Add a test that uses createTestContext({ enableReportInstances: true }) and calls verifyQuery with a populated reportSchemaStores to cover the reporting connection path and confirm it rejects writes (analogous to the sandbox-escape test added in packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js). Without this, the security guarantee provided by wrapping EXPLAIN in a read-only transaction on reporting connections has no central-server integration coverage.


packages/database/src/services/reportQuery.js:17: Every report execution now costs 5 sequential DB round-trips (BEGIN + SET TRANSACTION READ ONLY + SELECT 1 + actual query + COMMIT) instead of 1. The same security guarantee can be achieved at connection time instead of per-query: add SET default_transaction_read_only = on to the afterConnect hook in database.js for reporting connections (alongside the existing SET ROLE). This makes every transaction on the connection read-only by default without any per-query overhead, and eliminates runReadOnlyReportQuery entirely. The SELECT 1 lock-in trick is only needed because SET TRANSACTION READ ONLY can be reversed in an empty transaction — session-level default_transaction_read_only has no such loophole.

@dannash100 dannash100 force-pushed the feat/TAM-6862/tamanu-manages-reporting-roles branch from 35ca9f0 to de077a9 Compare June 18, 2026 00:15
Comment thread packages/constants/src/reports.ts
Comment thread packages/database/src/services/reporting.js Outdated
Comment thread packages/database/src/services/reporting.js Outdated
@review-hero

review-hero Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦸 Review Hero Summary
9 agents reviewed this PR | 1 critical | 2 suggestions | 0 nitpicks | Filtering: consensus 3 voters, 9 below threshold

Below consensus threshold (9 unique issues not confirmed by majority)
Location Agent Severity Comment
packages/central-server/__tests__/utilities.js:25 Integration tests suggestion The central-server test utility wires up initReporting only when config.db.reportSchemas.enabled is true, but central-server/config/test.json5 keeps enabled: false. This means the new role-provisio...
packages/database/src/services/reporting.js:27 BES Requirements suggestion config.db.password ?? '' silently falls back to an empty-string HMAC key when the core DB connection has no password (e.g. peer auth or socket connections). The derived reporting role passwords w...
packages/database/src/services/reporting.js:36 Bugs & Correctness suggestion The CREATE ROLE ... LOGIN inside the DO block only catches duplicate_object. On first run — if the core DB user lacks CREATEROLE — the CREATE ROLE will throw a insufficient_privilege error ...
packages/database/src/services/reporting.js:36 Bugs & Correctness suggestion There is a brief window between CREATE ROLE ... LOGIN (no password) and the separate ALTER ROLE ... PASSWORD call where the role exists as a passwordless LOGIN role. If pg_hba.conf allows trust...
packages/database/src/services/reporting.js:59 Bugs & Correctness suggestion ALTER DEFAULT PRIVILEGES IN SCHEMA "${schema}" GRANT SELECT ON TABLES TO "${role}" sets default privileges only for tables subsequently created by the current role (the core db user executing t...
packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js:189 Integration tests suggestion The new sandbox-escape test verifies that INSERT is rejected after COMMIT + RESET ROLE, but only checks that the row count is 0. It does not assert that the HTTP response is a specific 4xx error (e...
packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js:191 Integration tests suggestion The sandbox-escape test only exercises the reporting connection (reporting schema). The raw connection grants access to the public schema which contains all production clinical data — it is t...
packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js:191 Integration tests suggestion The sandbox-escape test verifies the outcome (INSERT rejected, count stays 0) but doesn't verify the mechanism that makes the sandbox sound: that the connection is authenticated as the reporting ro...
packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js:196 BES Requirements suggestion The sandbox-escape test inserts a ReportDefinitionVersion with versionNumber: 1 without cleaning it up. If this test runs in the same context as others that also create version 1 for the same repor...
Local fix prompt (copy to your coding agent)

Fix these issues identified on the pull request. One commit per issue fixed.


packages/constants/src/reports.ts:93: The constant's JSDoc comment says these are 'NOLOGIN roles' used 'via SET ROLE', but the implementation in reporting.js creates them as LOGIN roles with passwords and connects as them directly. The config comments in default.json5 and the test file comment also say 'SET ROLE'. This is a direct contradiction that will mislead operators configuring the system — they'll expect NOLOGIN roles and no passwords, but the actual roles are LOGIN roles with derived passwords. The comment in reporting.js (lines 12–24) correctly describes the implementation; the constants comment and config comments need to be updated to match.


packages/database/src/services/reporting.js:48: String interpolation of role and password into SQL violates the project rule 'Parameterised queries only — never interpolate user input into SQL'. While password is currently HMAC hex (no SQL metacharacters) and role is from a hardcoded constant, the comment justifying it ('pure hex, so there is no escaping concern') sets a bad precedent — this justification doesn't match Tamanu's unconditional rule, and future changes to role naming could introduce characters that break the quoting. PostgreSQL DDL doesn't support $1 placeholders for passwords, but use sequelize.escape() for the password literal, or restructure as ALTER ROLE "${role}" WITH LOGIN PASSWORD ${sequelize.escape(password)} to make the intent explicit and rule-compliant.


packages/database/src/services/reporting.js:82: When pool is not set in a connection config entry (e.g. the test config "reporting": {}), destructuring yields pool = undefined, and the spread pool, in overrides explicitly sets pool: undefined — clobbering whatever pool setting was inherited from ...config.db. Sequelize/pg-pool will treat undefined as 'no pool config' and may fall back to defaults that differ from the core connection's pool settings. To avoid silently dropping the core pool config when the per-connection pool is omitted, only include pool in overrides when it is actually provided: ...(pool ? { pool } : {}).

@dannash100 dannash100 force-pushed the feat/TAM-6862/tamanu-manages-reporting-roles branch from de077a9 to d2645b1 Compare June 18, 2026 00:32
@dannash100 dannash100 changed the title feat(reporting): TAM-6862: Tamanu manages reporting/raw roles via SET ROLE feat(reporting): TAM-6862: Tamanu manages reporting/raw db roles Jun 18, 2026
@dannash100 dannash100 force-pushed the feat/TAM-6862/tamanu-manages-reporting-roles branch 2 times, most recently from 0350820 to b9c7168 Compare June 18, 2026 01:26
@dannash100 dannash100 requested a review from a team as a code owner June 18, 2026 01:26
@dannash100 dannash100 force-pushed the feat/TAM-6862/tamanu-manages-reporting-roles branch from b9c7168 to 3e408d5 Compare June 18, 2026 01:33
Comment thread packages/database/src/migrations/1780500000000-provisionReportingRoles.ts Outdated
Comment thread packages/database/src/migrations/1780500000000-provisionReportingRoles.ts Outdated
Comment thread packages/database/src/utils/reportingRolePassword.js Outdated
@review-hero

review-hero Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦸 Review Hero Summary
12 agents reviewed this PR | 1 critical | 4 suggestions | 0 nitpicks | Filtering: consensus 3 voters, 4 below threshold

Below consensus threshold (4 unique issues not confirmed by majority)
Location Agent Severity Comment
packages/central-server/__tests__/admin/reports/reportRoutes.test.js:429 Integration tests suggestion The new 'on a reporting connection' describe block only tests verifyQuery with REPORT_DB_CONNECTIONS.RAW (public schema, tamanu_raw role). There is no parallel test for REPORT_DB_CONNECTIONS.REPORT...
packages/database/src/migrations/1780500000000-provisionReportingRoles.ts:29 Security suggestion The ALTER ROLE ... PASSWORD statement is passed to query.sequelize.query() without { logging: false }. Sequelize will emit the full SQL string through its configured logger (application logs, debug...
packages/database/src/services/reporting.js:23 BES Requirements nitpick disableChangesAudit: true is a non-obvious override with no comment explaining why. The adjacent comment covers the host/port/username intent but not why audit must be explicitly disabled. Per `c...
packages/database/src/utils/reportingRolePassword.js:7 BES Requirements nitpick Per naming conventions (coding-rules.md), functions should use a verb/action prefix. reportingRolePassword is a noun phrase — rename to deriveReportingRolePassword or `getReportingRolePasswor...
Local fix prompt (copy to your coding agent)

Fix these issues identified on the pull request. One commit per issue fixed.


packages/database/src/migrations/1780500000000-provisionReportingRoles.ts:43: ALTER DEFAULT PRIVILEGES IN SCHEMA ... GRANT SELECT ON TABLES TO ... only sets default privileges for objects created by the current session's role (the Sequelize migration user). If any database tables are created by a different PostgreSQL user (e.g., a DBA running ad-hoc DDL, or a future CI step), those tables won't automatically be accessible to the reporting roles. Consider documenting this limitation explicitly, or ensure the GRANT SELECT ON ALL TABLES line is re-run at startup (e.g., in initReportStore) to catch any gaps. The current approach is safe for Sequelize-managed migrations but may silently exclude tables created outside the migration runner.


packages/central-server/__tests__/admin/reports/reportRoutes.test.js:426: The pre-existing test verifyQuery(query, [], ctx.store) was not updated when the function signature changed. verifyQuery now expects (query, { parameters }, { store, reportSchemaStores }, dbSchema), but ctx.store (a store object) is being passed where { store, reportSchemaStores } is expected. Destructuring gives store = ctx.store.store (undefined) and reportSchemaStores = undefined. The call then fails with TypeError: Cannot read properties of undefined (reading 'query') inside the try/catch, which gets re-thrown as 'Invalid query: Cannot read properties of undefined'. The test still passes (it only asserts rejects.toThrow()) but it is now testing a TypeErrror from undefined access rather than actually validating that an invalid SQL query is rejected. Fix: update the call to verifyQuery(query, { parameters: [] }, { store: ctx.store }, undefined) to match the new signature.


packages/central-server/__tests__/admin/reports/reportRoutes.test.js:429: The describe('on a reporting connection') block opens database connections via initReporting() in beforeAll but has no afterAll to close them. These connections will remain open for the duration of the Jest process, potentially interfering with teardown or other test suites. Add afterAll(async () => { await Promise.all(Object.values(reportSchemaStores).map(s => s?.sequelize?.close())); }) or equivalent cleanup.


packages/database/src/migrations/1780500000000-provisionReportingRoles.ts:17: The migration calls reportingRolePassword(role) at migration-run time, so the role password is derived from whatever config.db.password is at the moment the migration runs. If the core DB password is later rotated (as the code comment on reportingRolePassword.js notes), the reporting roles retain the stale derived password until the migration is manually re-run. This is a silent drift risk: the reporting connections will start failing with an authentication error rather than falling back gracefully. Consider verifying the derived password matches what's stored at initReporting() startup time, or documenting an operational runbook for password rotation.


packages/database/src/utils/reportingRolePassword.js:9: If config.db.password is null, undefined, or an empty string, the HMAC key silently becomes '', making both reporting role passwords fully predictable to anyone who reads this code (the input is just the hardcoded role name). The ?? '' fallback should be replaced with a guard that throws (or at minimum logs at error level) when the core DB password is absent, so misconfigured environments fail loudly rather than provisioning weak credentials. E.g.: const key = config.db.password; if (!key) throw new Error('Cannot derive reporting role password: config.db.password is not set');

The reporting and raw database connections no longer need their own login
credentials in config. Tamanu creates and manages the tamanu_reporting and
tamanu_raw roles itself: on startup it creates each role, grants it read-only
access to its schema (plus ALTER DEFAULT PRIVILEGES so tables materialised
later stay readable), and connects as it. Database credentials collapse to the
single core connection; the core db user needs CREATEROLE to provision the
roles.

The reporting connections authenticate AS the unprivileged role rather than
assuming it from the core user via SET ROLE. SET ROLE is reversible — the
authenticated user stays the privileged core user — so report SQL could
RESET ROLE, or end the surrounding transaction with COMMIT and continue in
autocommit, to write as core. Logging in as the role itself removes that
escape: the session has no write grants and is a member of no other role, so
report SQL has nothing to escalate to. The role password is derived (HMAC)
from the core db password already in config, so there is no separate secret to
manage and every replica derives the same value (no rotation race).

Removes the per-connection username/password config and the now-redundant
createMockReportingSchemaAndRoles test helper. Tests assert the sandbox holds:
a COMMIT + RESET ROLE write attempt through the report runner, and a trailing
write through the central verifyQuery path, are both rejected with no data
written.
@dannash100 dannash100 force-pushed the feat/TAM-6862/tamanu-manages-reporting-roles branch from 3e408d5 to 421f953 Compare June 18, 2026 02:34
Comment thread packages/database/src/services/reporting.js Outdated
@review-hero

review-hero Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦸 Review Hero Summary
9 agents reviewed this PR | 0 critical | 2 suggestions | 0 nitpicks | Filtering: consensus 3 voters, 9 below threshold

Below consensus threshold (9 unique issues not confirmed by majority)
Location Agent Severity Comment
packages/central-server/__tests__/admin/reports/reportRoutes.test.js:424 Bugs & Correctness critical The existing test verifyQuery(query, [], ctx.store) is now silently broken by the updated signature. verifyQuery now destructures { store, reportSchemaStores } from its third argument (line 3...
packages/database/src/services/reporting.js:17 BES Requirements nitpick Per project convention, functions should use verb/action names (deriveRolePassword or computeRolePassword), not noun phrases. reportingRolePassword reads as a value, not a function that compu...
packages/database/src/services/reporting.js:28 Bugs & Correctness suggestion CREATE ROLE "${role}" LOGIN creates the role without a password, then a separate round-trip ALTER ROLE ... PASSWORD sets it. Between those two statements the role exists with no password. On a ...
packages/database/src/services/reporting.js:39 Security suggestion ALTER ROLE ... PASSWORD will appear in full in PostgreSQL statement logs (pg_log, pg_stat_activity, audit extensions) whenever statement logging is enabled. The derived password is not th...
packages/database/src/services/reporting.js:47 BES Requirements suggestion GRANT SELECT ON ALL TABLES runs at setup time but grants on tables that exist at that moment in public. Combined with ALTER DEFAULT PRIVILEGES, future tables created by the same role are cove...
packages/database/src/services/reporting.js:63 Security suggestion The HMAC input is tamanu-report-role:${role} where role is REPORT_DB_CONNECTION_ROLES[connectionName] (e.g. 'tamanu_reporting'). The key material fed into HMAC is therefore `config.db.passw...
packages/database/src/services/reporting.js:63 Security suggestion reportingRolePassword() receives the role name string (e.g. 'tamanu_reporting') as its HMAC message, but the function parameter is named role — same as the variable in scope that holds the same v...
packages/database/src/services/reporting.js:70 BES Requirements nitpick disableChangesAudit: true is added without a comment. Per project rule, the WHY should be explained for non-obvious settings — add a brief note such as `// read-only connections have no writes to...
packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js:193 BES Requirements nitpick The test table's id column is varchar(255) (defined in the beforeAll setup), but the escape test inserts 99 as a bare integer literal rather than '99' as a string. PostgreSQL will cast it...
Local fix prompt (copy to your coding agent)

Fix these issues identified on the pull request. One commit per issue fixed.


packages/database/src/services/reporting.js:19: Using config.db.password ?? '' as the HMAC key means if the core DB password is undefined/null (e.g., trust-auth environments or misconfigured deployments), the key silently falls back to an empty string. All reporting role passwords would then be identical across any deployment with no DB password, and the HMAC no longer ties the derived credentials to this specific instance. Should throw or log.warn when config.db.password is falsy to surface this misconfiguration rather than silently degrading security.


packages/central-server/__tests__/admin/reports/reportRoutes.test.js:431: The reportSchemaStores opened in beforeAll are never closed. initReporting calls openDatabase for each connection, creating Sequelize connection pools. Without a matching afterAll that closes each store (e.g. await Promise.all(Object.values(reportSchemaStores).map(s => s.sequelize.close()))), the pools stay open after the suite finishes and Jest will warn about or hang on open handles.

…g roles

The reporting role password is derived (HMAC) from the core db password. If
that is empty the key silently falls back to an empty string, so the derived
passwords are no longer unique to the instance. Log a warning to surface this
(expected under trust auth, otherwise a misconfiguration).
Comment thread packages/database/src/services/reporting.js Outdated
Comment thread packages/database/src/services/reporting.js Outdated
@review-hero

review-hero Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦸 Review Hero Summary
12 agents reviewed this PR | 0 critical | 2 suggestions | 0 nitpicks | Filtering: consensus 3 voters, 12 below threshold, 1 suppressed

Below consensus threshold (12 unique issues not confirmed by majority)
Location Agent Severity Comment
packages/central-server/__tests__/admin/reports/reportRoutes.test.js:428 Bugs & Correctness suggestion The inner describe('on a reporting connection', ...) block opens connection pools via initReporting in beforeAll but has no afterAll to close them. The outer afterAll(() => ctx.close()) o...
packages/central-server/__tests__/admin/reports/reportRoutes.test.js:429 Integration tests suggestion The new 'on a reporting connection' block only tests verifyQuery with REPORT_DB_CONNECTIONS.RAW. The REPORTING connection uses a different schema ('reporting', via search_path) and the tamanu_repor...
packages/central-server/__tests__/utilities.js:26 BES Requirements nitpick Comment // initReporting provisions the reporting/raw roles, schema and grants itself. describes what the code does (which well-named identifiers already convey), not why. The intent was presumab...
packages/database/src/services/reporting.js:17 BES Requirements nitpick Per project conventions, function names should use a verb/action prefix (e.g. deriveRolePassword or buildRolePassword). reportingRolePassword reads like a noun/property rather than a function.
packages/database/src/services/reporting.js:17 BES Requirements suggestion The reportingRolePassword function reads config.db.password from closure, making its signature misleading: the caller sees reportingRolePassword(role) and can't tell that the result also depe...
packages/database/src/services/reporting.js:23 BES Requirements nitpick ensureReportingRole understates what the function does — it also creates the schema, sets the search_path, grants USAGE, grants SELECT on all current tables, and sets default privileges. A name l...
packages/database/src/services/reporting.js:39 BES Requirements nitpick ALTER ROLE "${role}" WITH LOGIN ... — the WITH LOGIN clause here is redundant: the role was already created with LOGIN on line 31. On an existing role (exception path) it's also harmless but ...
packages/database/src/services/reporting.js:48 Bugs & Correctness critical PostgreSQL's GRANT SELECT ON ALL TABLES IN SCHEMA and ALTER DEFAULT PRIVILEGES ... GRANT SELECT ON TABLES do NOT cover materialized views — they are a separate object type. The codebase has at ...
packages/database/src/services/reporting.js:51 BES Requirements suggestion ALTER DEFAULT PRIVILEGES IN SCHEMA ... GRANT SELECT ON TABLES TO only covers tables created by the current session user going forward. GRANT SELECT ON ALL TABLES on startup covers existing tabl...
packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js:35 Security nitpick Role names from REPORT_DB_CONNECTION_ROLES are interpolated into SQL without identifier quoting (TO ${REPORT_DB_CONNECTION_ROLES.reporting}), while production code in reporting.js consistently uses...
packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js:189 Integration tests suggestion The new 'cannot escape the sandbox by ending the transaction and switching roles' test exercises only the REPORTING connection (reportingDefinition). The RAW connection (rawDefinition, tamanu_raw r...
packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js:195 BES Requirements suggestion This new test creates a ReportDefinitionVersion with versionNumber: 1 for the shared reportingDefinition. Multiple other tests in the same file (lines 112, 129, 144, 161, 181) also create `ve...
Local fix prompt (copy to your coding agent)

Fix these issues identified on the pull request. One commit per issue fixed.


packages/database/src/services/reporting.js:19: When config.db.password is null/undefined/empty, the HMAC key is an empty string, so every Tamanu installation with trust auth will derive identical reporting role passwords (HMAC-SHA256('', 'tamanu-report-role:tamanu_reporting') is a fixed, source-code-derivable constant). Any attacker who reads the source and has network access to the PostgreSQL port can authenticate as the read-only reporting role without needing the application credentials. The code emits a warning but still proceeds and sets this predictable password on the LOGIN role. Consider either refusing to provision reporting roles when db.password is empty (throw instead of warn), or deriving the password from an additional installation-specific secret (e.g. a randomly generated value written to disk on first run) so that it is not globally inferrable from the source alone.


packages/database/src/services/reporting.js:63: The password derivation passes the role name (e.g. 'tamanu_reporting') as the HMAC message but uses that same value as the key lookup — reportingRolePassword(role) where role is already the resolved role string, not the connectionName. More importantly: if the core DB password is rotated, reporting role passwords silently change on next startup (ALTER ROLE PASSWORD is re-run). However if the new password is applied before a restart the existing connections authenticated with the old password will fail until restart. Document this operational dependency: rotating db.password requires a coordinated server restart to avoid reporting connection failures.

…ation caveat

Spell out the security implication in the empty-db.password warning (under
password auth the reporting role password derives from an empty key and is
inferable from source) and document that rotating db.password needs a
coordinated restart so live reporting connections don't fail.
Comment thread packages/database/src/services/reporting.js
Comment thread packages/database/src/services/reporting.js
Comment thread packages/database/src/services/reporting.js
expect(response.body.error.message).toEqual('permission denied for table reporting_test_table');
});

it('cannot escape the sandbox by ending the transaction and switching roles', async () => {

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Integration tests] suggestion

The sandbox-escape test (COMMIT; RESET ROLE; INSERT) only exercises the reporting connection (reportingDefinition). The raw connection accesses the public schema where all production tables (including patients) live, making it the higher-risk path. A parallel test using rawDefinition and a write targeting raw_test_table would confirm the unprivileged-role guarantee holds for that connection too. The existing rawDefinition fixture is already set up in beforeAll, so adding the case is straightforward.

Comment thread packages/database/src/services/reporting.js Outdated
Comment thread packages/database/src/services/reporting.js Outdated
@review-hero

review-hero Bot commented Jun 18, 2026

Copy link
Copy Markdown

🦸 Review Hero Summary
12 agents reviewed this PR | 1 critical | 6 suggestions | 0 nitpicks | Filtering: consensus 3 voters, 11 below threshold

Below consensus threshold (11 unique issues not confirmed by majority)
Location Agent Severity Comment
packages/central-server/__tests__/admin/reports/reportRoutes.test.js:426 Bugs & Correctness critical The existing 'rejects an invalid query' test calls verifyQuery(query, [], ctx.store) but the function signature was changed to `verifyQuery(query, { parameters }, { store, reportSchemaStores }, d...
packages/central-server/__tests__/admin/reports/reportRoutes.test.js:431 Bugs & Correctness suggestion The beforeAll opens Sequelize connection pools via initReporting but there is no matching afterAll to close them. Each call to openDatabase creates a pool that stays open for the life of th...
packages/central-server/__tests__/utilities.js:26 BES Requirements nitpick '// initReporting provisions the reporting/raw roles, schema and grants itself.' is a 'what' comment — it describes what the function does, which the name already communicates. Per coding-rules.md,...
packages/database/src/services/reporting.js:12 BES Requirements nitpick 7-line comment block violates the project's 'one short line max' convention (AGENTS.md). The non-obvious WHY (logging in as the role prevents RESET ROLE escalation) is worth a comment, but it shoul...
packages/database/src/services/reporting.js:19 Bugs & Correctness suggestion reportingRolePassword reads config.db.password at call time, but the comment in the module header says "Rotating db.password changes the derived password (re-applied on the next startup), so a ...
packages/database/src/services/reporting.js:50 BES Requirements suggestion GRANT SELECT ON ALL TABLES IN SCHEMA "${schema}" runs unconditionally on every server startup. For the 'raw' connection this targets the public schema, which on a production database can contain hu...
packages/database/src/services/reporting.js:65 Security suggestion The call reportingRolePassword(role) passes the role name (e.g. 'tamanu_reporting') as the HMAC message, but the HMAC key is already db.password. The same derived password is used for both th...
packages/database/src/services/reporting.js:73 BES Requirements nitpick pool ? { pool } : {} uses a truthiness check on an object, but the real intent is 'was pool configured at all?' Prefer pool !== undefined ? { pool } : {} to align with the project's preference ...
packages/database/src/services/reporting.js:94 Bugs & Correctness suggestion If initReportStore succeeds for the first connection but then throws on the second (e.g. the core user lacks CREATEROLE, or the second openDatabase fails), the first store's connection pool is ...
packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js:35 Integration tests suggestion The test tables are created after initReporting runs but are then explicitly granted (GRANT SELECT ON ... TO role), bypassing the ALTER DEFAULT PRIVILEGES path entirely. The comment in `repor...
packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js:193 BES Requirements nitpick The INSERT uses an integer literal 99 for the "id" column which is declared varchar(255). All other test rows use string literals ('1', '2'), and the subsequent assertion queries `WHERE i...
Local fix prompt (copy to your coding agent)

Fix these issues identified on the pull request. One commit per issue fixed.


packages/database/src/services/reporting.js:64: In initReportStore, role is looked up from connectionName only to derive the password, then ensureReportingRole receives connectionName and independently looks up the same role again (line 26). The role local variable here is redundant. Consider moving the reportingRolePassword call inside ensureReportingRole, where role is already computed, so the lookup happens once and password derivation stays co-located with role management.


packages/database/src/services/reporting.js:55: ALTER DEFAULT PRIVILEGES IN SCHEMA ... GRANT SELECT ON TABLES TO ... only covers tables subsequently created by the PostgreSQL role that executes this statement (the core DB user). If any reporting or materialised tables are ever created by a different role (e.g., a superuser runs a one-off migration), those tables won't automatically receive SELECT. This is an implicit coupling between 'who runs migrations' and 'which role's default privileges are set'. A comment here noting that this only applies to objects created by the current session user would prevent a future operator from being surprised when a table created under a different role is invisible to the reporting role.


packages/database/src/services/reporting.js:78: The for...of destructuring for (const [connectionName, { pool } = {}] of Object.entries(connections)) provides a default of {} only when the entry value is undefined. If the config ever contains an explicit null for a connection (e.g. "raw": null in a JSON5 override), JavaScript will throw TypeError: Cannot destructure property 'pool' of null. The guard in initReportStore for unknown connection names won't help because the crash happens before the function is called. Adding a null check for (const [connectionName, connectionConfig] of Object.entries(connections)) with const { pool } = connectionConfig ?? {} inside the loop body would be safer.


packages/central-server/__tests__/admin/reports/reportRoutes.test.js:425: The new verifyQuery tests for the RAW connection use direct function calls rather than HTTP. The POST / and POST /:reportId/versions routes call createReportDefinitionVersionverifyQuery with reportSchemaStores, and POST /import (line 227 of reportRoutes.js) calls verifyQuery directly with versionData.dbSchema. None of these HTTP paths are exercised in this test file with dbSchema: 'raw' to prove the newly provisioned role works end-to-end through the route layer. Since the provisioning model changed significantly (pre-configured credentials → auto-provisioned HMAC-derived passwords), an HTTP-level test like adminApp.post('/api/admin/reports', { ..., dbSchema: 'raw', query: 'select 1', queryOptions: { parameters: [] } }) would provide higher confidence than testing the utility in isolation. The closest pattern to follow is the existing POST / tests already in this file.


packages/facility-server/__tests__/apiv1/ReportSchemaRoles.test.js:189: The sandbox-escape test (COMMIT; RESET ROLE; INSERT) only exercises the reporting connection (reportingDefinition). The raw connection accesses the public schema where all production tables (including patients) live, making it the higher-risk path. A parallel test using rawDefinition and a write targeting raw_test_table would confirm the unprivileged-role guarantee holds for that connection too. The existing rawDefinition fixture is already set up in beforeAll, so adding the case is straightforward.


packages/database/src/services/reporting.js:41: The ALTER ROLE statement embeds the derived password as a plaintext SQL string. Sequelize logs queries at debug/info level by default, and PostgreSQL's log_statement or pg_stat_statements can also capture it. Since the reporting roles have SELECT on all tables — including patient PII — anyone with log access gains full read access to patient data. Fix: pass { logging: false } as the options argument to this specific sequelize.query() call to suppress logging of the password-bearing statement. The CREATE ROLE and GRANT calls above it are fine to log.


packages/database/src/services/reporting.js:83: When db.password is empty the HMAC key is '' and both role passwords are computable fixed values (HMAC('', 'tamanu-report-role:tamanu_reporting') etc.) that are identical across every deployment. The code only logs a warning and proceeds to provision and connect as these roles. In a healthcare deployment using password-based PostgreSQL auth this silently exposes all reporting/public schema tables — including patient data — to anyone who knows the source. For non-test environments with password auth in use, this should throw rather than warn. Consider: if (!config.db.password && process.env.NODE_ENV !== 'test') { throw new Error('db.password must be set when using reporting roles with password auth'); }

… connection

Drop the per-connection reportSchemas.connections.*.pool config — reporting
connections inherit the main db connection's pool. initReporting now iterates
the known connection roles instead of reading connections from config, so the
reportSchemas config shrinks to just `enabled` (and the now-unreachable
unknown-connection guard is removed).
…r raw-connection escape

The ALTER ROLE ... PASSWORD statement embeds the derived password as a literal,
which Sequelize would otherwise log — and the reporting roles can read patient
data, so a logged password is a read-access leak. Pass { logging: false } for
that one statement.

Also adds a raw-connection (public schema) variant of the sandbox-escape test,
and removes a couple of redundant test-setup comments.
@dannash100 dannash100 marked this pull request as draft June 21, 2026 22:58
…ection string

The core (tamanu) DB connection can now be given as one connection string via
the DATABASE_URL env var or a db.url config field, instead of separate
host/name/username/password fields. Sequelize pool settings can ride along as
query params (?max=10&min=2&idle=10000). resolveDbConfig() parses it and merges
over the structured config; when no URL is set the structured fields are used
unchanged, so existing deployments keep working. The reporting raw/reporting
connections reuse the resolved host/name but keep their own credentials.
The role passwords were an HMAC of config.db.password, which is empty on trust/
peer servers — so the "secret" was a fixed value derivable from source. Generate
a random per-server secret instead, stored in local_system_facts (not synced,
the same way the device key is), and derive the role passwords from that. Works
the same regardless of the core db auth method.

Reading the secret needs a migrated db, so facility opens its reporting
connections after migrate via a new initReportingStores() (called from the
start subcommands) rather than in init(). Central already inits after migrate.
@dannash100 dannash100 marked this pull request as ready for review June 21, 2026 23:31
…ine tooling and pool-headroom check

Apply resolveDbConfig() in the remaining spots that build a DB connection or
read the pool size directly from config.db: the dbt model generator, the
migration-baseline generator, and the startApi connection-pool headroom
warning. No-op when no URL is set, so structured configs are unaffected.
@dannash100 dannash100 requested review from passcod and removed request for a team June 22, 2026 03:05
…SE_URL env only

The connection string is supplied solely via the DATABASE_URL env var; removes
the db.url config key, its commented example, and the backwards-compatible
wording. Structured config remains the default when DATABASE_URL is unset.
@github-actions

github-actions Bot commented Jun 22, 2026

Copy link
Copy Markdown

The raw reporting role gets SELECT on all of public, which includes tables
holding cleartext credentials that report SQL has no business reading:
local_system_facts (device private key + reporting secret), one_time_logins,
portal_one_time_tokens, refresh_tokens, and the certificate signers tables.
Revoke SELECT on them from the public-scoped (raw) role after granting the
schema; to_regclass skips any not present on a given server.
@dannash100 dannash100 force-pushed the feat/TAM-6862/tamanu-manages-reporting-roles branch from 7793b35 to fec44ae Compare June 23, 2026 00:29
@github-actions

github-actions Bot commented Jun 23, 2026

Copy link
Copy Markdown

🍹 up on tamanu-on-k8s/bes/tamanu-on-k8s/feat-tam-6862-tamanu-manages-reporting-roles

Pulumi report
   Updating (feat-tam-6862-tamanu-manages-reporting-roles)

View Live: https://app.pulumi.com/bes/tamanu-on-k8s/feat-tam-6862-tamanu-manages-reporting-roles/updates/10

Downloading plugin random-4.19.0: starting
Downloading plugin random-4.19.0: done
Installing plugin random-4.19.0: starting
Installing plugin random-4.19.0: done

@ Updating....
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running 
@ Updating....
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running read pulumi:pulumi:StackReference bes/k8s-core/tamanu-internal-main
@ Updating....
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running read pulumi:pulumi:StackReference bes/k8s-core/tamanu-internal-main
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running read pulumi:pulumi:StackReference bes/core/tamanu-internal
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running read kubernetes:core/v1:Namespace tamanu-feat-tam-6862-tamanu-manages-reporting-roles
@ Updating....
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running read pulumi:pulumi:StackReference bes/core/tamanu-internal
@ Updating.....
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running read kubernetes:core/v1:Namespace tamanu-feat-tam-6862-tamanu-manages-reporting-roles
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Waiting for central-db...
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Waiting for facility-1-db...
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Waiting for facility-2-db...
~  kubernetes:apps/v1:Deployment patient-portal-web updating (0s) [diff: ~spec]
~  kubernetes:apps/v1:Deployment central-web updating (0s) [diff: ~spec]
~  kubernetes:apps/v1:Deployment facility-1-web updating (0s) [diff: ~spec]
~  kubernetes:apps/v1:Deployment facility-2-web updating (0s) [diff: ~spec]
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running read kubernetes:core/v1:ConfigMap actual-provisioning
+  kubernetes:batch/v1:Job ttl-wake-1782455504 creating (0s) 
@ Updating....
++ kubernetes:batch/v1:Job central-migrator creating replacement (0s) [diff: ~spec]
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running read kubernetes:core/v1:ConfigMap actual-provisioning
+  kubernetes:batch/v1:Job ttl-wake-1782455504 creating (0s) 
++ kubernetes:batch/v1:Job central-migrator creating replacement (0s) [diff: ~spec]; 
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Secret central-db-superuser not found or not ready: Error: HTTP-Code: 404
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Message: Unknown API Status Code!
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"central-db-superuser\\\" not found\",\"reason\":\"NotFound\",\"details\":{\"name\":\"central-db-superuser\",\"kind\":\"secrets\"},\"code\":404}
"
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Headers: {"audit-id":"e6b4dd47-e603-4d1a-ad2a-9b16a5f38bf4","cache-control":"no-cache, private","connection":"close","content-length":"214","content-type":"application/json","date":"Fri, 26 Jun 2026 02:31:48 GMT","x-kubernetes-pf-flowschema-uid":"3fb296fc-e46b-45d1-9306-057e37ddd229","x-kubernetes-pf-prioritylevel-uid":"feccf24d-a074-4fa8-aa6f-db82477fc2f5"}
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Secret facility-2-db-superuser not found or not ready: Error: HTTP-Code: 404
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Message: Unknown API Status Code!
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"facility-2-db-superuser\\\" not found\",\"reason\":\"NotFound\",\"details\":{\"name\":\"facility-2-db-superuser\",\"kind\":\"secrets\"},\"code\":404}
"
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Headers: {"audit-id":"bd098677-82e4-410f-908c-1e973cb77464","cache-control":"no-cache, private","connection":"close","content-length":"220","content-type":"application/json","date":"Fri, 26 Jun 2026 02:31:48 GMT","x-kubernetes-pf-flowschema-uid":"3fb296fc-e46b-45d1-9306-057e37ddd229","x-kubernetes-pf-prioritylevel-uid":"feccf24d-a074-4fa8-aa6f-db82477fc2f5"}
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Secret facility-1-db-superuser not found or not ready: Error: HTTP-Code: 404
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Message: Unknown API Status Code!
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"facility-1-db-superuser\\\" not found\",\"reason\":\"NotFound\",\"details\":{\"name\":\"facility-1-db-superuser\",\"kind\":\"secrets\"},\"code\":404}
"
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles running Headers: {"audit-id":"f2696dee-457d-480a-ba5b-8787bcaafc81","cache-control":"no-cache, private","connection":"close","content-length":"220","content-type":"application/json","date":"Fri, 26 Jun 2026 02:31:48 GMT","x-kubernetes-pf-flowschema-uid":"3fb296fc-e46b-45d1-9306-057e37ddd229","x-kubernetes-pf-prioritylevel-uid":"feccf24d-a074-4fa8-aa6f-db82477fc2f5"}
@ Updating....
++ kubernetes:batch/v1:Job facility-2-migrator creating replacement (0s) [diff: ~spec]
++ kubernetes:batch/v1:Job facility-1-migrator creating replacement (0s) [diff: ~spec]
++ kubernetes:batch/v1:Job facility-2-migrator creating replacement (0s) [diff: ~spec]; 
++ kubernetes:batch/v1:Job facility-1-migrator creating replacement (0s) [diff: ~spec]; 
++ kubernetes:batch/v1:Job facility-2-migrator creating replacement (0s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/facility-2-migrator-03ab738b" to start
++ kubernetes:batch/v1:Job central-migrator creating replacement (1s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/central-migrator-9e2008cf" to start
++ kubernetes:batch/v1:Job central-migrator creating replacement (1s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/central-migrator-9e2008cf" to succeed (Active: 1 | Succeeded: 0 | Failed: 0)
++ kubernetes:batch/v1:Job facility-2-migrator creating replacement (0s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/facility-2-migrator-03ab738b" to succeed (Active: 1 | Succeeded: 0 | Failed: 0)
+  kubernetes:batch/v1:Job ttl-wake-1782455504 creating (1s) Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/ttl-wake-1782455504-b9946401" to start
++ kubernetes:batch/v1:Job facility-1-migrator creating replacement (0s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/facility-1-migrator-7b6c065f" to start
+  kubernetes:batch/v1:Job ttl-wake-1782455504 creating (1s) Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/ttl-wake-1782455504-b9946401" to succeed (Active: 1 | Succeeded: 0 | Failed: 0)
++ kubernetes:batch/v1:Job facility-1-migrator creating replacement (0s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/facility-1-migrator-7b6c065f" to succeed (Active: 1 | Succeeded: 0 | Failed: 0)
@ Updating.....
~  kubernetes:apps/v1:Deployment central-web updating (3s) [diff: ~spec]; Waiting for app ReplicaSet to be available (0/1 Pods available)
~  kubernetes:apps/v1:Deployment facility-1-web updating (3s) [diff: ~spec]; Waiting for app ReplicaSet to be available (0/1 Pods available)
~  kubernetes:apps/v1:Deployment facility-2-web updating (3s) [diff: ~spec]; Waiting for app ReplicaSet to be available (0/1 Pods available)
~  kubernetes:apps/v1:Deployment patient-portal-web updating (3s) [diff: ~spec]; Waiting for app ReplicaSet to be available (0/1 Pods available)
@ Updating......
+  kubernetes:batch/v1:Job ttl-wake-1782455504 creating (6s) warning: [Pod tamanu-feat-tam-6862-tamanu-manages-reporting-roles/ttl-wake-1782455504-b9946401-5l2ft]: Container "wake-cnpg" completed with exit code 0
@ Updating.....
+  kubernetes:batch/v1:Job ttl-wake-1782455504 creating (8s) Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/ttl-wake-1782455504-b9946401" to succeed (Active: 0 | Succeeded: 0 | Failed: 0)
+  kubernetes:batch/v1:Job ttl-wake-1782455504 creating (8s) Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/ttl-wake-1782455504-b9946401" to succeed (Active: 0 | Succeeded: 1 | Failed: 0)
+  kubernetes:batch/v1:Job ttl-wake-1782455504 creating (8s) 
+  kubernetes:batch/v1:Job ttl-wake-1782455504 created (8s) 
@ Updating......
~  kubernetes:apps/v1:Deployment facility-1-web updating (11s) [diff: ~spec]; Waiting for app ReplicaSet to be available (1/2 Pods available)
~  kubernetes:apps/v1:Deployment central-web updating (11s) [diff: ~spec]; Waiting for app ReplicaSet to be available (1/2 Pods available)
~  kubernetes:apps/v1:Deployment facility-2-web updating (11s) [diff: ~spec]; Waiting for app ReplicaSet to be available (1/2 Pods available)
@ Updating.....
~  kubernetes:apps/v1:Deployment central-web updating (13s) [diff: ~spec]; warning: [Pod tamanu-feat-tam-6862-tamanu-manages-reporting-roles/central-web-609093c5-8448878df4-lfldk]: containers with unready status: [http]
~  kubernetes:apps/v1:Deployment facility-1-web updating (13s) [diff: ~spec]; warning: [Pod tamanu-feat-tam-6862-tamanu-manages-reporting-roles/facility-1-web-d1e885a0-6cdc64898d-ztmc6]: containers with unready status: [http]
~  kubernetes:apps/v1:Deployment facility-2-web updating (13s) [diff: ~spec]; warning: [Pod tamanu-feat-tam-6862-tamanu-manages-reporting-roles/facility-2-web-f4d85768-64f8d8c87f-fww8b]: containers with unready status: [http]
~  kubernetes:apps/v1:Deployment patient-portal-web updating (13s) [diff: ~spec]; warning: [Pod tamanu-feat-tam-6862-tamanu-manages-reporting-roles/patient-portal-web-f4115c75-7d58f5b4ff-68jzq]: containers with unready status: [http]
@ Updating......
~  kubernetes:apps/v1:Deployment patient-portal-web updating (16s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment patient-portal-web updating (16s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment patient-portal-web updated (16s) [diff: ~spec]; 
@ Updating....
++ kubernetes:batch/v1:Job facility-1-migrator creating replacement (16s) [diff: ~spec]; warning: [Pod tamanu-feat-tam-6862-tamanu-manages-reporting-roles/facility-1-migrator-7b6c065f-rsqtn]: Container "migrator" completed with exit code 0
@ Updating......
++ kubernetes:batch/v1:Job facility-1-migrator creating replacement (18s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/facility-1-migrator-7b6c065f" to succeed (Active: 0 | Succeeded: 0 | Failed: 0)
++ kubernetes:batch/v1:Job facility-1-migrator creating replacement (18s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/facility-1-migrator-7b6c065f" to succeed (Active: 0 | Succeeded: 1 | Failed: 0)
++ kubernetes:batch/v1:Job facility-1-migrator creating replacement (18s) [diff: ~spec]; 
++ kubernetes:batch/v1:Job facility-1-migrator created replacement (18s) [diff: ~spec]; 
+- kubernetes:batch/v1:Job facility-1-migrator replacing (0s) [diff: ~spec]; 
+- kubernetes:batch/v1:Job facility-1-migrator replaced (0.01s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-1-api updating (0s) [diff: ~spec]
~  kubernetes:apps/v1:Deployment facility-1-tasks updating (0s) [diff: ~spec]
~  kubernetes:apps/v1:Deployment facility-1-sync updating (0s) [diff: ~spec]
@ Updating....
~  kubernetes:apps/v1:Deployment facility-1-sync updating (0s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment facility-1-sync updating (0s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-1-sync updated (0.99s) [diff: ~spec]; 
@ Updating....
~  kubernetes:apps/v1:Deployment facility-1-api updating (2s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment facility-1-api updating (2s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-1-api updated (2s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-1-tasks updating (2s) [diff: ~spec]; warning: Replicas scaled to 0 for Deployment "facility-1-tasks-aa616941"
~  kubernetes:apps/v1:Deployment facility-1-tasks updating (2s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment facility-1-tasks updating (2s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-1-tasks updated (2s) [diff: ~spec]; 
@ Updating....
++ kubernetes:batch/v1:Job facility-2-migrator creating replacement (22s) [diff: ~spec]; warning: [Pod tamanu-feat-tam-6862-tamanu-manages-reporting-roles/facility-2-migrator-03ab738b-9cq4j]: Container "migrator" completed with exit code 0
@ Updating.....
++ kubernetes:batch/v1:Job facility-2-migrator creating replacement (24s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/facility-2-migrator-03ab738b" to succeed (Active: 0 | Succeeded: 0 | Failed: 0)
@ Updating....
++ kubernetes:batch/v1:Job facility-2-migrator creating replacement (24s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/facility-2-migrator-03ab738b" to succeed (Active: 0 | Succeeded: 1 | Failed: 0)
++ kubernetes:batch/v1:Job facility-2-migrator creating replacement (24s) [diff: ~spec]; 
++ kubernetes:batch/v1:Job facility-2-migrator created replacement (24s) [diff: ~spec]; 
+- kubernetes:batch/v1:Job facility-2-migrator replacing (0s) [diff: ~spec]; 
+- kubernetes:batch/v1:Job facility-2-migrator replaced (0.00s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-2-tasks updating (0s) [diff: ~spec]
~  kubernetes:apps/v1:Deployment facility-2-api updating (0s) [diff: ~spec]
~  kubernetes:apps/v1:Deployment facility-2-sync updating (0s) [diff: ~spec]
~  kubernetes:apps/v1:Deployment central-web updating (26s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment central-web updating (26s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment central-web updated (26s) [diff: ~spec]; 
@ Updating....
~  kubernetes:apps/v1:Deployment facility-2-web updating (27s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment facility-2-web updating (27s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-2-web updated (27s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-2-sync updating (1s) [diff: ~spec]; warning: Replicas scaled to 0 for Deployment "facility-2-sync"
~  kubernetes:apps/v1:Deployment facility-2-sync updating (1s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment facility-2-sync updating (1s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-2-tasks updating (1s) [diff: ~spec]; warning: Replicas scaled to 0 for Deployment "facility-2-tasks-bb54fe22"
~  kubernetes:apps/v1:Deployment facility-2-tasks updating (1s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment facility-2-tasks updating (1s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-2-sync updated (1s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-2-tasks updated (1s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-2-api updating (1s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment facility-2-api updating (1s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-2-api updated (1s) [diff: ~spec]; 
@ Updating....
++ kubernetes:batch/v1:Job central-migrator creating replacement (28s) [diff: ~spec]; warning: [Pod tamanu-feat-tam-6862-tamanu-manages-reporting-roles/central-migrator-9e2008cf-lpjjd]: Container "migrator" completed with exit code 0
@ Updating.....
++ kubernetes:batch/v1:Job central-migrator creating replacement (30s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/central-migrator-9e2008cf" to succeed (Active: 0 | Succeeded: 0 | Failed: 0)
++ kubernetes:batch/v1:Job central-migrator creating replacement (30s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/central-migrator-9e2008cf" to succeed (Active: 0 | Succeeded: 1 | Failed: 0)
++ kubernetes:batch/v1:Job central-migrator creating replacement (30s) [diff: ~spec]; 
++ kubernetes:batch/v1:Job central-migrator created replacement (30s) [diff: ~spec]; 
+- kubernetes:batch/v1:Job central-migrator replacing (0s) [diff: ~spec]; 
+- kubernetes:batch/v1:Job central-migrator replaced (0.00s) [diff: ~spec]; 
++ kubernetes:batch/v1:Job central-provisioner creating replacement (0s) [diff: ~spec]
@ Updating....
++ kubernetes:batch/v1:Job central-provisioner creating replacement (0s) [diff: ~spec]; 
++ kubernetes:batch/v1:Job central-provisioner creating replacement (0s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/central-provisioner-3f134bbe" to start
++ kubernetes:batch/v1:Job central-provisioner creating replacement (0s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/central-provisioner-3f134bbe" to succeed (Active: 1 | Succeeded: 0 | Failed: 0)
~  kubernetes:apps/v1:Deployment facility-1-web updating (31s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment facility-1-web updating (31s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment facility-1-web updated (31s) [diff: ~spec]; 
@ Updating...................
++ kubernetes:batch/v1:Job central-provisioner creating replacement (16s) [diff: ~spec]; warning: [Pod tamanu-feat-tam-6862-tamanu-manages-reporting-roles/central-provisioner-3f134bbe-tlv6x]: Container "provisioner" completed with exit code 0
@ Updating.....
++ kubernetes:batch/v1:Job central-provisioner creating replacement (18s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/central-provisioner-3f134bbe" to succeed (Active: 0 | Succeeded: 0 | Failed: 0)
++ kubernetes:batch/v1:Job central-provisioner creating replacement (18s) [diff: ~spec]; Waiting for Job "tamanu-feat-tam-6862-tamanu-manages-reporting-roles/central-provisioner-3f134bbe" to succeed (Active: 0 | Succeeded: 1 | Failed: 0)
++ kubernetes:batch/v1:Job central-provisioner creating replacement (18s) [diff: ~spec]; 
++ kubernetes:batch/v1:Job central-provisioner created replacement (18s) [diff: ~spec]; 
+- kubernetes:batch/v1:Job central-provisioner replacing (0s) [diff: ~spec]; 
+- kubernetes:batch/v1:Job central-provisioner replaced (0.00s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment central-fhir-resolver updating (0s) [diff: ~spec]
~  kubernetes:apps/v1:Deployment central-fhir-refresh updating (0s) [diff: ~spec]
~  kubernetes:apps/v1:Deployment central-tasks updating (0s) [diff: ~spec]
~  kubernetes:apps/v1:Deployment central-api updating (0s) [diff: ~spec]
@ Updating....
~  kubernetes:apps/v1:Deployment central-tasks updating (1s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment central-tasks updating (1s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment central-tasks updated (1s) [diff: ~spec]; 
@ Updating....
~  kubernetes:apps/v1:Deployment central-fhir-resolver updating (1s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment central-fhir-resolver updating (1s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment central-fhir-refresh updating (1s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment central-fhir-refresh updating (1s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment central-fhir-resolver updated (1s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment central-fhir-refresh updated (1s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment central-api updating (1s) [diff: ~spec]; Deployment initialization complete
~  kubernetes:apps/v1:Deployment central-api updating (1s) [diff: ~spec]; 
~  kubernetes:apps/v1:Deployment central-api updated (1s) [diff: ~spec]; 
-- kubernetes:batch/v1:Job central-provisioner deleting original (0s) [diff: ~spec]; 
@ Updating....
-- kubernetes:batch/v1:Job central-provisioner deleting original (0s) [diff: ~spec]; 
-- kubernetes:batch/v1:Job central-provisioner deleted original (0.51s) [diff: ~spec]; 
-  kubernetes:batch/v1:Job ttl-wake-1782455055 deleting (0s) 
-- kubernetes:batch/v1:Job facility-1-migrator deleting original (0s) [diff: ~spec]; 
-- kubernetes:batch/v1:Job facility-2-migrator deleting original (0s) [diff: ~spec]; 
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) [diff: ~spec]; 
@ Updating....
-  kubernetes:batch/v1:Job ttl-wake-1782455055 deleting (0s) 
-  kubernetes:batch/v1:Job ttl-wake-1782455055 deleted (0.81s) 
-- kubernetes:batch/v1:Job facility-2-migrator deleting original (1s) [diff: ~spec]; 
-- kubernetes:batch/v1:Job facility-2-migrator deleted original (1s) [diff: ~spec]; 
-- kubernetes:batch/v1:Job facility-1-migrator deleting original (1s) [diff: ~spec]; 
-- kubernetes:batch/v1:Job facility-1-migrator deleted original (1s) [diff: ~spec]; 
-- kubernetes:batch/v1:Job central-migrator deleting original (1s) [diff: ~spec]; 
-- kubernetes:batch/v1:Job central-migrator deleted original (1s) [diff: ~spec]; 
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles  15 messages
Diagnostics:
 pulumi:pulumi:Stack (tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles):
   Waiting for central-db...
   Waiting for facility-1-db...
   Waiting for facility-2-db...

   Secret central-db-superuser not found or not ready: Error: HTTP-Code: 404
   Message: Unknown API Status Code!
   Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"central-db-superuser\\\" not found\",\"reason\":\"NotFound\",\"details\":{\"name\":\"central-db-superuser\",\"kind\":\"secrets\"},\"code\":404}
"
   Headers: {"audit-id":"e6b4dd47-e603-4d1a-ad2a-9b16a5f38bf4","cache-control":"no-cache, private","connection":"close","content-length":"214","content-type":"application/json","date":"Fri, 26 Jun 2026 02:31:48 GMT","x-kubernetes-pf-flowschema-uid":"3fb296fc-e46b-45d1-9306-057e37ddd229","x-kubernetes-pf-prioritylevel-uid":"feccf24d-a074-4fa8-aa6f-db82477fc2f5"}
   Secret facility-2-db-superuser not found or not ready: Error: HTTP-Code: 404
   Message: Unknown API Status Code!
   Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"facility-2-db-superuser\\\" not found\",\"reason\":\"NotFound\",\"details\":{\"name\":\"facility-2-db-superuser\",\"kind\":\"secrets\"},\"code\":404}
"
   Headers: {"audit-id":"bd098677-82e4-410f-908c-1e973cb77464","cache-control":"no-cache, private","connection":"close","content-length":"220","content-type":"application/json","date":"Fri, 26 Jun 2026 02:31:48 GMT","x-kubernetes-pf-flowschema-uid":"3fb296fc-e46b-45d1-9306-057e37ddd229","x-kubernetes-pf-prioritylevel-uid":"feccf24d-a074-4fa8-aa6f-db82477fc2f5"}
   Secret facility-1-db-superuser not found or not ready: Error: HTTP-Code: 404
   Message: Unknown API Status Code!
   Body: "{\"kind\":\"Status\",\"apiVersion\":\"v1\",\"metadata\":{},\"status\":\"Failure\",\"message\":\"secrets \\\"facility-1-db-superuser\\\" not found\",\"reason\":\"NotFound\",\"details\":{\"name\":\"facility-1-db-superuser\",\"kind\":\"secrets\"},\"code\":404}
"
   Headers: {"audit-id":"f2696dee-457d-480a-ba5b-8787bcaafc81","cache-control":"no-cache, private","connection":"close","content-length":"220","content-type":"application/json","date":"Fri, 26 Jun 2026 02:31:48 GMT","x-kubernetes-pf-flowschema-uid":"3fb296fc-e46b-45d1-9306-057e37ddd229","x-kubernetes-pf-prioritylevel-uid":"feccf24d-a074-4fa8-aa6f-db82477fc2f5"}

Outputs:
   urls: {
       Central      : "https://central.feat-tam-6862-tamanu-manages-reporting-roles.cd.tamanu.app"
       Facility- 1  : "https://facility-1.feat-tam-6862-tamanu-manages-reporting-roles.cd.tamanu.app"
       Facility- 2  : "https://facility-2.feat-tam-6862-tamanu-manages-reporting-roles.cd.tamanu.app"
       PatientPortal: "https://portal.feat-tam-6862-tamanu-manages-reporting-roles.cd.tamanu.app"
   }

Resources:
   + 1 created
   ~ 14 updated
   - 1 deleted
   +-4 replaced
   20 changes. 74 unchanged

Duration: 1m1s

   

…_secrets

The raw reporting role is granted SELECT on all of public, so any secret stored
in local_system_facts (the device private key, the reporting-role secret) was
readable by report SQL. Move those into a new local_system_secrets table that
the raw role is never granted, and let it read local_system_facts normally again.

- new LocalSystemSecret model + table (mirrors local_system_facts; DO_NOT_SYNC)
- excluded from sync-tick + changelog triggers so values never reach logs.changes
- DDL + DML migrations (create table, then move the two rows out of facts)
- getDeviceKey + the reporting secret now live on LocalSystemSecret; callers
  (SendStatusToMetaServer, the initDeviceKey upgrade step) updated
- raw role revoke list now targets local_system_secrets instead of local_system_facts

Server-only: mobile has neither the device key in LSF nor reporting, so no
mobile migration is needed.
@dannash100 dannash100 force-pushed the feat/TAM-6862/tamanu-manages-reporting-roles branch from ebcf522 to c5af4a4 Compare June 23, 2026 02:22
…emSecret

Adds getSecret/setSecret (AES via the crypto.keyFile) to LocalSystemSecret, for
external credentials that shouldn't be plaintext even behind the table grant —
e.g. a sync_host password. Moved here from LocalSystemFact (where they were
unused). Encryption is opt-in per value: callers that store such a secret pull
in the key-file dependency, while the self-generated device key and reporting
secret stay on the plain get/set path so the always-on startup path doesn't
require crypto.keyFile.
@github-actions

Copy link
Copy Markdown

🍹 destroy on tamanu-on-k8s/bes/tamanu-on-k8s/feat-tam-6862-tamanu-manages-reporting-roles

Pulumi report
   Destroying (feat-tam-6862-tamanu-manages-reporting-roles)

View Live: https://app.pulumi.com/bes/tamanu-on-k8s/feat-tam-6862-tamanu-manages-reporting-roles/updates/6

@ Destroying.....
Downloading plugin random-4.19.0: starting
Downloading plugin random-4.19.0: done
Installing plugin random-4.19.0: starting
Installing plugin random-4.19.0: done

-  kubernetes:apps/v1:Deployment central-fhir-refresh deleting (0s) 
-  kubernetes:apps/v1:Deployment central-fhir-resolver deleting (0s) 
-  kubernetes:apps/v1:Deployment central-api deleting (0s) 
-  kubernetes:apps/v1:Deployment central-tasks deleting (0s) 
@ Destroying........
-  kubernetes:apps/v1:Deployment central-api deleting (4s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment central-tasks deleting (4s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment central-tasks deleting (4s) 
-  kubernetes:apps/v1:Deployment central-tasks deleted (4s) 
-  kubernetes:apps/v1:Deployment central-fhir-resolver deleting (4s) 
-  kubernetes:apps/v1:Deployment central-fhir-resolver deleted (4s) 
-  kubernetes:apps/v1:Deployment central-fhir-refresh deleting (4s) 
-  kubernetes:apps/v1:Deployment central-fhir-refresh deleted (4s) 
@ Destroying.................................
-  kubernetes:apps/v1:Deployment central-api deleting (34s) 
-  kubernetes:apps/v1:Deployment central-api deleted (34s) 
-  kubernetes:apps/v1:Deployment facility-2-tasks deleting (0s) 
-  kubernetes:apps/v1:Deployment facility-2-sync deleting (0s) 
-  kubernetes:apps/v1:Deployment facility-2-api deleting (0s) 
-- kubernetes:batch/v1:Job central-provisioner deleting original (0s) 
-  kubernetes:apps/v1:Deployment facility-1-api deleting (0s) 
-  kubernetes:batch/v1:Job central-provisioner deleting (0s) 
-  kubernetes:apps/v1:Deployment facility-1-sync deleting (0s) 
-- kubernetes:batch/v1:Job central-provisioner deleting original (0s) 
-  kubernetes:apps/v1:Deployment facility-1-tasks deleting (0s) 
-- kubernetes:batch/v1:Job central-provisioner deleting original (0s) 
@ Destroying....
-  kubernetes:apps/v1:Deployment facility-2-sync deleting (0s) Progress deadline exceeded
-  kubernetes:apps/v1:Deployment facility-2-sync deleting (0s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment facility-1-api deleting (1s) Progress deadline exceeded
-  kubernetes:apps/v1:Deployment facility-1-api deleting (1s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment facility-1-sync deleting (1s) Progress deadline exceeded
@ Destroying....
-  kubernetes:apps/v1:Deployment facility-2-tasks deleting (1s) Available: 0/1
-  kubernetes:apps/v1:Deployment facility-1-tasks deleting (1s) Available: 0/1
-  kubernetes:apps/v1:Deployment facility-1-tasks deleting (1s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment facility-1-sync deleting (1s) Resource scheduled for deletion
-  kubernetes:apps/v1:Deployment facility-2-tasks deleting (1s) Resource scheduled for deletion
-- kubernetes:batch/v1:Job central-provisioner deleting original (1s) 
-- kubernetes:batch/v1:Job central-provisioner deleted original (1s) 
-  kubernetes:apps/v1:Deployment facility-2-api deleting (1s) Progress deadline exceeded
-  kubernetes:apps/v1:Deployment facility-2-api deleting (1s) Resource scheduled for deletion
@ Destroying....
-- kubernetes:batch/v1:Job central-provisioner deleted original (1s) 
-- kubernetes:batch/v1:Job central-provisioner deleted original (2s) 
-- kubernetes:batch/v1:Job central-provisioner deleted original (2s) 
-- kubernetes:batch/v1:Job central-provisioner deleted original (2s) 
-- kubernetes:batch/v1:Job central-provisioner deleted original (2s) 
-  kubernetes:batch/v1:Job central-provisioner deleted (2s) 
-  kubernetes:apps/v1:Deployment facility-1-tasks deleting (3s) 
-  kubernetes:apps/v1:Deployment facility-1-tasks deleted (3s) 
@ Destroying....
-  kubernetes:apps/v1:Deployment facility-2-sync deleting (3s) 
-  kubernetes:apps/v1:Deployment facility-2-sync deleted (3s) 
-  kubernetes:apps/v1:Deployment facility-1-sync deleting (3s) 
-  kubernetes:apps/v1:Deployment facility-1-sync deleted (3s) 
-  kubernetes:apps/v1:Deployment facility-2-tasks deleting (3s) 
-  kubernetes:apps/v1:Deployment facility-2-tasks deleted (3s) 
@ Destroying...............................
-  kubernetes:apps/v1:Deployment facility-1-api deleting (31s) 
-  kubernetes:apps/v1:Deployment facility-1-api deleted (31s) 
@ Destroying....
-  kubernetes:apps/v1:Deployment facility-2-api deleting (32s) 
-  kubernetes:apps/v1:Deployment facility-2-api deleted (32s) 
-  kubernetes:batch/v1:Job facility-2-migrator deleting (0s) 
-- kubernetes:batch/v1:Job facility-1-migrator deleting original (0s) 
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) 
-  kubernetes:batch/v1:Job central-migrator deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-2-frontend deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute central-frontend deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-1-frontend deleting (0s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-1-db deleting (0s) 
-- kubernetes:batch/v1:Job facility-2-migrator deleting original (0s) 
-- kubernetes:batch/v1:Job facility-2-migrator deleting original (0s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-2-db deleting (0s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster central-db deleting (0s) 
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) 
-  kubernetes:batch/v1:Job facility-1-migrator deleting (0s) 
-- kubernetes:batch/v1:Job facility-1-migrator deleting original (0s) 
-- kubernetes:batch/v1:Job facility-1-migrator deleting original (0s) 
@ Destroying.....
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-1-frontend deleting (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-1-frontend deleted (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-2-frontend deleting (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-2-frontend deleted (1s) 
-- kubernetes:batch/v1:Job facility-2-migrator deleting original (1s) 
-- kubernetes:batch/v1:Job facility-2-migrator deleted original (1s) 
-- kubernetes:batch/v1:Job facility-2-migrator deleted original (1s) 
-- kubernetes:batch/v1:Job facility-2-migrator deleted original (1s) 
-- kubernetes:batch/v1:Job facility-1-migrator deleting original (1s) 
-- kubernetes:batch/v1:Job facility-1-migrator deleted original (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute central-frontend deleting (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute central-frontend deleted (1s) 
-- kubernetes:batch/v1:Job facility-2-migrator deleted original 
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) 
-- kubernetes:batch/v1:Job facility-1-migrator deleted original (1s) 
-  kubernetes:batch/v1:Job facility-1-migrator deleted (1s) 
-  kubernetes:batch/v1:Job facility-1-migrator deleted (1s) Job Completed. succeeded: 1/1
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) Job Completed. succeeded: 1/1
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) Job Completed. succeeded: 1/1
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) Job Completed. succeeded: 1/1
-- kubernetes:batch/v1:Job facility-2-migrator deleted original Job Completed. succeeded: 1/1
-  kubernetes:batch/v1:Job facility-1-migrator deleted (1s) Job Completed. succeeded: 1/1
-- kubernetes:batch/v1:Job facility-2-migrator deleted original Resource scheduled for deletion
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) Resource scheduled for deletion
-  kubernetes:batch/v1:Job facility-1-migrator deleted (1s) Resource scheduled for deletion
-  kubernetes:postgresql.cnpg.io/v1:Cluster central-db deleting (1s) Resource scheduled for deletion
-- kubernetes:batch/v1:Job facility-2-migrator deleted original Job Completed. succeeded: 1/1
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) Job Completed. succeeded: 1/1
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-2-db deleting (2s) Resource scheduled for deletion
-  kubernetes:batch/v1:Job facility-1-migrator deleted (1s) Resource scheduled for deletion
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) Resource scheduled for deletion
@ Destroying....
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-1-db deleting (2s) Resource scheduled for deletion
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) Resource scheduled for deletion
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) Resource scheduled for deletion
-- kubernetes:batch/v1:Job facility-2-migrator deleted original Resource scheduled for deletion
-- kubernetes:batch/v1:Job facility-2-migrator deleted original 
-- kubernetes:batch/v1:Job central-migrator deleting original (0s) 
-  kubernetes:batch/v1:Job facility-2-migrator deleted (0.98s) 
-- kubernetes:batch/v1:Job central-migrator deleted original (0.98s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-2-db deleting (2s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-2-db deleted (2s) 
-  kubernetes:batch/v1:Job facility-1-migrator deleted (1s) 
-- kubernetes:batch/v1:Job facility-1-migrator deleted original (2s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-1-db deleting (2s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster facility-1-db deleted (2s) 
-- kubernetes:batch/v1:Job central-migrator deleted original (0.98s) 
-- kubernetes:batch/v1:Job facility-1-migrator deleted original (2s) 
-- kubernetes:batch/v1:Job central-migrator deleted original (1s) 
-- kubernetes:batch/v1:Job facility-1-migrator deleted original (2s) 
-- kubernetes:batch/v1:Job central-migrator deleted original (1s) 
-  kubernetes:batch/v1:Job central-migrator deleted (1s) 
-  kubernetes:batch/v1:Job central-migrator deleted (1s) 
-  kubernetes:batch/v1:Job facility-2-migrator deleted (0.98s) 
-- kubernetes:batch/v1:Job central-migrator deleted original (1s) 
-- kubernetes:batch/v1:Job facility-2-migrator deleted original (1s) 
@ Destroying....
-  kubernetes:postgresql.cnpg.io/v1:Cluster central-db deleting (3s) 
-  kubernetes:postgresql.cnpg.io/v1:Cluster central-db deleted (3s) 
-  kubernetes:core/v1:Service central-web deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute patient-portal-frontend deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute patient-portal-api-legacy deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-1-api deleting (0s) 
-  kubernetes:core/v1:Secret facility-1-raw-db deleting (0s) 
-  kubernetes:gateway.envoyproxy.io/v1alpha1:ClientTrafficPolicy central-traffic-policy deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute patient-portal-api deleting (0s) 
-  kubernetes:core/v1:Service facility-2-web deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-2-api deleting (0s) 
-  kubernetes:gateway.envoyproxy.io/v1alpha1:ClientTrafficPolicy facility-2-traffic-policy deleting (0s) 
-  kubernetes:gateway.envoyproxy.io/v1alpha1:ClientTrafficPolicy patient-portal-traffic-policy deleting (0s) 
-  kubernetes:rbac.authorization.k8s.io/v1:RoleBinding tamanu-ttl deleting (0s) 
-  kubernetes:apps/v1:Deployment facility-2-web deleting (0s) 
-  kubernetes:batch/v1:Job ttl-wake-1782196046 deleting (0s) 
-  kubernetes:apps/v1:Deployment central-web deleting (0s) 
-  kubernetes:core/v1:Secret facility-2-raw-db deleting (0s) 
@ Destroying....
-  kubernetes:batch/v1:Job ttl-wake-1782196046 deleted (1s) 
-  kubernetes:apps/v1:Deployment facility-1-web deleting (0s) 
@ Destroying....
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute patient-portal-frontend deleting (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute patient-portal-frontend deleted (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute patient-portal-api deleting (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute patient-portal-api deleted (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute patient-portal-api-legacy deleting (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-2-api deleting (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute patient-portal-api-legacy deleted (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-2-api deleted (1s) 
-  kubernetes:gateway.envoyproxy.io/v1alpha1:ClientTrafficPolicy facility-2-traffic-policy deleting (1s) 
-  kubernetes:gateway.envoyproxy.io/v1alpha1:ClientTrafficPolicy facility-2-traffic-policy deleted (1s) 
-  kubernetes:gateway.envoyproxy.io/v1alpha1:ClientTrafficPolicy patient-portal-traffic-policy deleting (1s) 
-  kubernetes:gateway.envoyproxy.io/v1alpha1:ClientTrafficPolicy patient-portal-traffic-policy deleted (1s) 
-  kubernetes:rbac.authorization.k8s.io/v1:RoleBinding tamanu-ttl deleting (1s) 
-  kubernetes:rbac.authorization.k8s.io/v1:RoleBinding tamanu-ttl deleted (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-1-api deleting (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-1-api deleted (1s) 
-  kubernetes:apps/v1:Deployment facility-2-web deleting (1s) Deployment is available. Replicas: 2
-  kubernetes:apps/v1:Deployment facility-2-web deleting (1s) Resource scheduled for deletion
-  kubernetes:gateway.envoyproxy.io/v1alpha1:ClientTrafficPolicy central-traffic-policy deleting (1s) 
-  kubernetes:gateway.envoyproxy.io/v1alpha1:ClientTrafficPolicy central-traffic-policy deleted (1s) 
-  kubernetes:apps/v1:Deployment central-web deleting (1s) Deployment is available. Replicas: 2
-  kubernetes:apps/v1:Deployment central-web deleting (1s) Resource scheduled for deletion
-  kubernetes:core/v1:Secret central-reporting-db deleting (0s) 
-  kubernetes:core/v1:Secret facility-1-reporting-db deleting (0s) 
-  kubernetes:gateway.envoyproxy.io/v1alpha1:ClientTrafficPolicy facility-1-traffic-policy deleting (0s) 
-  kubernetes:batch/v1:Job ttl-wake-1782189877 deleting (0s) 
-  kubernetes:batch/v1:Job ttl-wake-1782189018 deleting (0s) 
-  kubernetes:batch/v1:Job ttl-wake-1782197360 deleting (0s) 
-  kubernetes:core/v1:Service facility-2-web deleting (1s) 
-  kubernetes:core/v1:Service central-web deleting (1s) 
-  kubernetes:core/v1:Service facility-2-web deleted (1s) 
-  kubernetes:core/v1:Service central-web deleted (1s) 
-  kubernetes:core/v1:Secret facility-1-raw-db deleting (1s) 
-  kubernetes:core/v1:Secret facility-2-raw-db deleting (1s) 
-  kubernetes:core/v1:Secret facility-2-raw-db deleted (1s) 
-  kubernetes:core/v1:Secret facility-1-raw-db deleted (1s) 
-  kubernetes:core/v1:Secret central-raw-db deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute central-api-legacy deleting (0s) 
-  kubernetes:core/v1:Service facility-1-web deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-1-api-legacy deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute central-api deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-2-api-legacy deleting (0s) 
-  kubernetes:batch/v1:CronJob ttl-hibernate deleting (0s) 
-  kubernetes:batch/v1:Job ttl-wake-1782197360 deleted (0.37s) 
-  kubernetes:apps/v1:Deployment facility-1-web deleting (0s) Deployment is available. Replicas: 2
-  kubernetes:batch/v1:Job ttl-wake-1782189877 deleted (0.42s) 
-  kubernetes:batch/v1:Job ttl-wake-1782189018 deleted (0.46s) 
-  kubernetes:apps/v1:Deployment facility-1-web deleting (0s) Resource scheduled for deletion
-  kubernetes:core/v1:Secret facility-2-reporting-db deleting (0s) 
@ Destroying....
-  kubernetes:gateway.envoyproxy.io/v1alpha1:ClientTrafficPolicy facility-1-traffic-policy deleting (0s) 
-  kubernetes:gateway.envoyproxy.io/v1alpha1:ClientTrafficPolicy facility-1-traffic-policy deleted (0.96s) 
-  kubernetes:apps/v1:Deployment central-web deleting (2s) 
-  kubernetes:apps/v1:Deployment central-web deleted (2s) 
-  kubernetes:apps/v1:Deployment facility-2-web deleting (2s) 
-  kubernetes:apps/v1:Deployment facility-2-web deleted (2s) 
-  kubernetes:core/v1:Secret facility-1-reporting-db deleting (1s) 
-  kubernetes:core/v1:Secret facility-1-reporting-db deleted (1s) 
-  kubernetes:apps/v1:Deployment facility-1-web deleting (1s) 
-  kubernetes:apps/v1:Deployment facility-1-web deleted (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute central-api-legacy deleting (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute central-api-legacy deleted (1s) 
-  kubernetes:core/v1:Secret central-raw-db deleting (1s) 
-  kubernetes:core/v1:Secret central-reporting-db deleting (1s) 
-  kubernetes:core/v1:Secret central-raw-db deleted (1s) 
-  kubernetes:core/v1:Secret central-reporting-db deleted (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-1-api-legacy deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-1-api-legacy deleted (0.96s) 
-  kubernetes:core/v1:Service facility-1-web deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-2-api-legacy deleting (0s) 
-  kubernetes:core/v1:Service facility-1-web deleted (1.00s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute facility-2-api-legacy deleted (1.00s) 
-  kubernetes:batch/v1:CronJob ttl-hibernate deleting (1s) 
-  kubernetes:batch/v1:CronJob ttl-hibernate deleted (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute central-api deleting (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:HTTPRoute central-api deleted (1s) 
-  kubernetes:core/v1:Secret facility-2-reporting-db deleting (0s) 
-  kubernetes:core/v1:Secret facility-2-reporting-db deleted (0.94s) 
@ Destroying....
-  bes:tamanu:WebFrontend central deleting (0s) 
-  kubernetes:core/v1:ConfigMap facility-1 deleting (0s) 
-  kubernetes:core/v1:Service facility-2-api deleting (0s) 
-  kubernetes:core/v1:Secret facility-2-db-url deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:Gateway facility-1 deleting (0s) 
-  kubernetes:core/v1:NamespacePatch tamanu-feat-tam-6862-tamanu-manages-reporting-roles-schedule deleting (0s) 
-  kubernetes:core/v1:ServiceAccount app-sa deleting (0s) 
-  random:index:RandomPassword facility-1-reporting-db deleting (0s) 
-  kubernetes:core/v1:Secret mailgun deleting (0s) 
-  kubernetes:core/v1:Secret central-db-url deleting (0s) 
-  random:index:RandomPassword facility-2-reporting-db deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:Gateway patient-portal deleting (0s) 
-  kubernetes:core/v1:Service patient-portal-web deleting (0s) 
-  kubernetes:core/v1:Secret facility-1-db-url deleting (0s) 
-  kubernetes:core/v1:Service facility-1-api deleting (0s) 
-  kubernetes:core/v1:Secret pullsecret-github deleting (0s) 
-  kubernetes:core/v1:Service central-db-tailscale deleting (0s) 
-  random:index:RandomPassword facility-1-reporting-db deleted (0.39s) 
-  random:index:RandomPassword facility-2-reporting-db deleted (0.39s) 
-  random:index:RandomPassword facility-2-raw-db deleting (0s) 
-  kubernetes:core/v1:ConfigMap provisioning deleting (0s) 
@ Destroying....
-  random:index:RandomPassword facility-2-raw-db deleted (0.34s) 
-  kubernetes:core/v1:NamespacePatch tamanu-feat-tam-6862-tamanu-manages-reporting-roles-schedule deleted (1s) 
-  kubernetes:core/v1:Service patient-portal-web deleting (1s) 
-  kubernetes:core/v1:Service patient-portal-web deleted (1s) 
-  kubernetes:core/v1:Service facility-1-api deleting (1s) 
-  kubernetes:core/v1:Service facility-1-api deleted (1s) 
-  kubernetes:core/v1:ConfigMap facility-2 deleting (0s) 
-  kubernetes:core/v1:Service central-api deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:Gateway patient-portal deleting (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:Gateway patient-portal deleted (1s) 
-  kubernetes:core/v1:ConfigMap facility-1 deleting (1s) 
-  kubernetes:core/v1:ConfigMap facility-1 deleted (1s) 
-  kubernetes:core/v1:Secret bugsnag deleting (0s) 
-  random:index:RandomPassword central-raw-db deleting (0s) 
-  kubernetes:apps/v1:Deployment patient-portal-web deleting (0s) 
-  kubernetes:core/v1:Secret facility-2-db-url deleting (1s) 
-  kubernetes:core/v1:Secret facility-2-db-url deleted (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:Gateway facility-1 deleting (1s) 
-  kubernetes:core/v1:Service facility-2-api deleting (1s) 
-  kubernetes:core/v1:Secret central-db-url deleting (1s) 
-  kubernetes:gateway.networking.k8s.io/v1:Gateway facility-1 deleted (1s) 
-  kubernetes:core/v1:Secret central-db-url deleted (1s) 
-  kubernetes:core/v1:Service facility-2-api deleted (1s) 
-  kubernetes:core/v1:Secret facility-1-db-url deleting (1s) 
-  kubernetes:core/v1:Secret facility-1-db-url deleted (1s) 
-  kubernetes:core/v1:Secret pullsecret-github deleting (1s) 
-  kubernetes:core/v1:Secret pullsecret-github deleted (1s) 
-  kubernetes:core/v1:Secret tamanu-crypto deleting (0s) 
-  kubernetes:core/v1:ServiceAccount app-sa deleting (1s) 
-  kubernetes:core/v1:ServiceAccount app-sa deleted (1s) 
-  kubernetes:core/v1:Service central-db-tailscale deleting (1s) Resource scheduled for deletion
-  kubernetes:core/v1:Secret mailgun deleting (1s) 
-  kubernetes:core/v1:Secret mailgun deleted (1s) 
-  kubernetes:core/v1:ConfigMap provisioning deleting (0s) 
-  kubernetes:core/v1:ConfigMap provisioning deleted (0.97s) 
-  bes:tamanu:WebFrontend facility-2 deleting (0s) 
-  kubernetes:core/v1:Secret tupaia deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:Gateway central deleting (0s) 
-  kubernetes:core/v1:ServiceAccount tamanu-ttl deleting (0s) 
-  bes:tamanu:WebFrontend facility-1 deleting (0s) 
-  kubernetes:rbac.authorization.k8s.io/v1:Role tamanu-ttl deleting (0s) 
-  kubernetes:core/v1:ConfigMap central deleting (0s) 
-  random:index:RandomPassword central-raw-db deleted (0.38s) 
-  kubernetes:core/v1:Service central-api deleting (0s) 
-  kubernetes:core/v1:Service central-api deleted (0.68s) 
-  kubernetes:core/v1:ConfigMap facility-2 deleting (0s) 
-  kubernetes:core/v1:ConfigMap facility-2 deleted (0.68s) 
-  kubernetes:core/v1:Service facility-2-sync deleting (0s) 
-  random:index:RandomPassword central-reporting-db deleting (0s) 
-  random:index:RandomPassword facility-1-raw-db deleting (0s) 
@ Destroying....
-  kubernetes:gateway.networking.k8s.io/v1:Gateway facility-2 deleting (0s) 
-  kubernetes:core/v1:Service facility-1-sync deleting (0s) 
-  kubernetes:apps/v1:Deployment patient-portal-web deleting (0s) Resource scheduled for deletion
-  random:index:RandomPassword facility-1-raw-db deleted (0.27s) 
-  random:index:RandomPassword central-reporting-db deleted (0.28s) 
-  kubernetes:core/v1:Secret bugsnag deleting (0s) 
-  kubernetes:core/v1:Secret bugsnag deleted (0.98s) 
-  kubernetes:core/v1:Secret tamanu-crypto deleting (0s) 
-  kubernetes:core/v1:Secret tamanu-crypto deleted (0.96s) 
-  kubernetes:core/v1:ConfigMap central deleting (0s) 
-  kubernetes:core/v1:ConfigMap central deleted (0.84s) 
-  kubernetes:core/v1:ServiceAccount tamanu-ttl deleting (0s) 
-  kubernetes:core/v1:ServiceAccount tamanu-ttl deleted (0.87s) 
-  kubernetes:core/v1:Secret tupaia deleting (0s) 
-  kubernetes:core/v1:Secret tupaia deleted (0.89s) 
-  kubernetes:core/v1:Service facility-2-sync deleting (0s) 
-  kubernetes:core/v1:Service facility-2-sync deleted (0.73s) 
-  kubernetes:rbac.authorization.k8s.io/v1:Role tamanu-ttl deleting (0s) 
-  kubernetes:rbac.authorization.k8s.io/v1:Role tamanu-ttl deleted (0.93s) 
-  kubernetes:gateway.networking.k8s.io/v1:Gateway central deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:Gateway central deleted (0.99s) 
-  kubernetes:core/v1:Service facility-1-sync deleting (0s) 
-  kubernetes:core/v1:Service facility-1-sync deleted (0.73s) 
-  kubernetes:gateway.networking.k8s.io/v1:Gateway facility-2 deleting (0s) 
-  kubernetes:gateway.networking.k8s.io/v1:Gateway facility-2 deleted (0.74s) 
@ Destroying....
-  kubernetes:apps/v1:Deployment patient-portal-web deleting (1s) 
-  kubernetes:apps/v1:Deployment patient-portal-web deleted (1s) 
@ Destroying....
-  kubernetes:core/v1:Service central-db-tailscale deleting (4s) 
-  kubernetes:core/v1:Service central-db-tailscale deleted (4s) 
-  bes:tamanu:CentralServer central deleting (0s) 
-  random:index:RandomBytes tamanu-crypto-key deleting (0s) 
-  bes:tamanu:FacilityServer 2 deleting (0s) 
-  random:index:RandomBytes tamanu-crypto-psk deleting (0s) 
-  bes:tamanu:FacilityServer 1 deleting (0s) 
-  random:index:RandomBytes tamanu-crypto-psk-iv deleting (0s) 
-  bes:tamanu:WebFrontend patient-portal deleting (0s) 
@ Destroying....
-  random:index:RandomBytes tamanu-crypto-key deleted (0.29s) 
-  random:index:RandomBytes tamanu-crypto-psk deleted (0.29s) 
-  random:index:RandomBytes tamanu-crypto-psk-iv deleted (0.29s) 
-  pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles deleting (0s) 
   pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles  
-  pulumi:pulumi:Stack tamanu-on-k8s-feat-tam-6862-tamanu-manages-reporting-roles deleted (0.14s) 
Resources:
   - 96 deleted

Duration: 1m21s

The resources in the stack have been deleted, but the history and configuration associated with the stack are still maintained. 
If you want to remove the stack completely, run `pulumi stack rm feat-tam-6862-tamanu-manages-reporting-roles`.
   

// behind the table grant. Encryption uses the server key file
// (config `crypto.keyFile`), so only callers that store such a secret pull in
// that dependency; the self-generated device key / reporting secret stay on
// the plain get/set path.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm, it might be more elegant to only have encrypted secrets in the table?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah I agree, ai pushed back on this because it would break deployments because of deviceKey if they dont have crypto keyFile, but if thats sortable thats all good.
I will QA this card and feat(database): TAM-6862: support a single DATABASE_URL connection string
today

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

deviceKey in particular is essentially dead

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Ok cool! Should i remove that logic even?

…tional

Every local_system_secrets value is now encrypted at rest via the server key
file (config crypto.keyFile). get/set/setIfAbsent encrypt and decrypt, and the
plaintext getSecret/setSecret accessors are removed so a secret can't be stored
in the clear by accident. A legacy plaintext value (e.g. one moved out of
local_system_facts before this change) self-heals: it is re-encrypted in place
the first time it is read.

Encryption now requires crypto.keyFile wherever the server boots, so:
- deploys already mount a key and set CRYPTO_KEY_FILE; add the env mapping so
  node-config resolves crypto.keyFile from it
- dev/test default to a committed key file (overridden in deploys)
…gration scripts

tsx's CJS require hook doesn't complete the extensionless `@tamanu/database/services/connectionConfig` export, so dbt-generate-model and generate-migration-baseline crashed. Use `await import()` like the adjacent services/database call already does.
…URL validation

- Re-throw the reporting-role password set without the original DB error, whose
  sql field carries the password inline and could leak via a generic error log.
- Reject a non-postgres DATABASE_URL up front with a clear message instead of a
  vague connection error later (prefix check keeps the unix-socket form working).
- Correct the stale "cleartext" comment: the reporting secret is stored encrypted.
@dannash100 dannash100 force-pushed the feat/TAM-6862/tamanu-manages-reporting-roles branch from 9c94dbf to 5553f3e Compare June 26, 2026 02:15
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants