Skip to content

feat(audit): append-only, hash-chained audit log (M3a, #107) - #137

Merged
amar-python merged 1 commit into
mainfrom
feat/107-m3a-append-only-audit
Oct 28, 2026
Merged

amar-python merged 1 commit into
mainfrom
feat/107-m3a-append-only-audit

Conversation

@amar-python

@amar-python amar-python commented Oct 28, 2026 •

Copy link
Copy Markdown
Owner

Refs #107. This is part M3a of ART Milestone 3. The parts and the product owner's decisions of 28 Oct are on #107.

What it does

  • Migration 0006, per tenant, upgrades audit_log in place (decision 1):
    • New columns: actor, tenant_schema, before_value, after_value, prev_hash and row_hash.
    • A BEFORE INSERT trigger takes an advisory lock and assigns the id under it, so id order is chain order. The column default is dropped so ids aren't consumed twice. It then links the row to the previous row's hash.
    • Rows written before the upgrade are chained in id order.
    • Triggers reject UPDATE, DELETE and TRUNCATE for every role, the owner included.
  • Actor: the signed-in username, or api_key for the shared key. tenant_scope sets it the same way it sets the tenant schema. Password resets now record the admin as the actor, which retires the stopgap of writing it into mode.
  • GET /api/audit/verify (tenant_admin, auditor) recomputes the chain. It returns {rows, intact, firstBadId}.
  • /api/audit/log entries now include actor.

What it doesn't do yet

  • Runtime database role (M3d, decision 2). The owner can still disable a trigger. The hash chain is what exposes a row changed or removed that way; test_a_changed_row_breaks_the_chain_at_that_row does exactly this.
  • Original files and source rows (M3b), and stored row errors and logs (M3c).
  • Before and after values: the columns exist, but nothing writes them yet. Cell edits arrive in M4.

Verification

Check Result
Local CI integration replica (PostgreSQL 16) 319 collected, 319 passed, 0 failed, 0 skipped
Evals p, i, s, e 26 of 26
make lint (flake8, bandit) Clean
make health 88 passed
New tests/test_audit_append_only.py 7 passed: backfill chained, UPDATE/DELETE/TRUNCATE rejected, actor/tenant/values recorded, changed row found by id, removed row detected, 4 concurrent writers × 15 rows → one intact chain of 60, downgrade

I broke the code in three places to check that the tests catch it. Each time, the expected test failed:

What I removed or weakened Test that failed
The advisory lock The concurrent test (firstBadId: 3)
The prev_hash link check The removed-row test
Setting the actor for API-key requests The new API test

Also changed: test_migrations and test_tenant_isolation now expect head 0006. The VCRM BR-35 row is updated, and its status stays ⚠️ Partial.


Generated by Claude Code

Upgrades each tenant's audit_log in place (product owner's decision,
28 Oct) instead of adding an audit_events table.

- Migration 0006 adds actor, tenant_schema, before_value, after_value,
  prev_hash and row_hash. A BEFORE INSERT trigger serialises writers
  with an advisory lock, assigns the id under it, and links each row
  to the previous one. Existing rows are chained during the upgrade.
- Triggers reject UPDATE, DELETE and TRUNCATE for every role, owner
  included. The hash chain exposes a row changed or removed with the
  trigger disabled.
- The actor is the signed-in username, or "api_key" for the shared
  key. Password resets now record the admin as the actor instead of
  in the mode column.
- GET /api/audit/verify recomputes the chain (tenant_admin, auditor).

CI replica 318/0/0, evals 26/26, make lint and make 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 b21737b 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