Skip to content

feat(db): serve requests as a runtime role that can't change the audit or upload records (M3d, #107) - #140

Merged
amar-python merged 1 commit into
mainfrom
feat/107-m3d-runtime-role
Oct 28, 2026
Merged

amar-python merged 1 commit into
mainfrom
feat/107-m3d-runtime-role

Conversation

@amar-python

Copy link
Copy Markdown
Owner

Refs #107. Part M3d, the last part of ART Milestone 3 (decision 2 on #107: the runtime role, with installer and setup changes).

What it does

  • Two roles. The API's request pool connects as APP_DB_USER. The owner (PGUSER) is used only at startup, for Alembic, tenant provisioning and grants (db.owner_connect()).
  • Grants are applied after every migration run, for the default uploads schema, control, the T&E schema and every tenant schema:
    • read and write on ordinary tables;
    • SELECT and INSERT only on audit_log, uploads, upload_blobs, source_rows and alembic_version;
    • CREATE on the uploads schemas, so the role owns, and can drop, the csv_* tables it creates. Tables the owner created before M3d are handed to it.
  • scripts/create_app_role.py creates the role, or gives an existing one a new random password. It grants the role to the owner and applies the grants. It writes APP_DB_USER and APP_DB_PASSWORD to ~/.te-migration/app-db.env with mode 600, and never prints the password.
  • start-api.ps1 loads that file. Without it, the API warns APP_DB_USER not set and keeps today's behaviour (requests run as the owner).
  • install.sh creates te_prod_runtime, idempotently, and adds APP_DB_* to /opt/te-migration/env.
  • Docs: QUICKSTART has a one-off setup step; offline-install describes the two roles. VCRM BR-24 and BR-35 are updated, and both stay ⚠️ (M4 and M7 remain).

Known limitation

The API process still holds the owner's password, because it runs migrations at startup. Moving migrations to a separate step would remove that. That's a deployment change, so it's not in this PR.

Verification

Check Result
Local CI integration replica (PostgreSQL 16) 329 collected, 329 passed, 0 failed, 0 skipped
Evals 26 of 26
make lint Clean
make health 88 passed
New tests/test_runtime_role.py (3 tests) Runs the real create_app_role.py. The password isn't in its output and the env file is mode 600. As the runtime role, the API uploads, overwrites, deletes and verifies the chain, and the pool's current_user is the runtime role. As that role, DELETE and TRUNCATE on the 4 tables, UPDATE on audit_log and uploads, and ALTER TABLE … DISABLE TRIGGER are all refused by privilege, not by the trigger. INSERT into audit_log still works
Simulated install (local PostgreSQL) install.sh's role commands run twice, which shows they can be re-run. The API started with the non-superuser owner te_prod_user and the runtime role te_prod_runtime. Upload, overwrite, delete and /api/audit/verify (intact) all worked, and every pooled connection was te_prod_runtime. create_tenant_admin.py as that owner made a tenant, and a signed-in upload there worked
start-api.ps1 Parses with 0 errors (pwsh). The env-file loader keeps a password containing = intact

I broke the code twice to check that the tests catch it. Both times the expected test failed:

What I removed or weakened Test that failed
The REVOKE The privilege test: UPDATE on uploads was then stopped only by the trigger
The pool connecting as the runtime role (made to connect as the owner) The API test

Not run: install.sh end to end on an offline Ubuntu VM. That's the #60 dry run, which will exercise it.

After merging, on the product owner's PC (once)

Stop the API, then run python scripts\create_app_role.py with the step-3 PG* values set. Then start the API. Details are in QUICKSTART.


Generated by Claude Code

…t or upload records (M3d, #107)

The API's request pool now connects as APP_DB_USER when it's set. The
owner (PG_USER) is used only at startup, for migrations and grants.
After each migration run, the runtime role gets read and write on
ordinary tables, but only SELECT and INSERT on audit_log, uploads,
upload_blobs, source_rows and alembic_version. It owns the per-upload
csv_* tables it creates; ones the owner created before are handed over.

- scripts/create_app_role.py creates the role with a random password,
  applies the grants, and writes the settings to
  ~/.te-migration/app-db.env (mode 600). It never prints the password.
- start-api.ps1 loads that file and warns if the role isn't set up.
- install.sh creates te_prod_runtime and writes APP_DB_* to its env file.

Local CI replica 329/0/0, evals 26/26, lint and health clean.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_019YPoXzBSj89ZHgw7nfTmbB
@amar-python
amar-python merged commit 18ff57e into main Oct 28, 2026
3 checks passed
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