Skip to content

Add the append-only audit event store - #2443

Merged
pournasserian merged 1 commit into
v2from
2442-audit-event-store
Oct 18, 2026
Merged

pournasserian merged 1 commit into
v2from
2442-audit-event-store

Conversation

@pournasserian

Copy link
Copy Markdown
Member

Closes #2442

What

The first slice of F-AUD (#2424): the store every later epic writes through.

  • Core: AuditEvent with what AUD-01 lists (id, timestamp, initiator as UserInitiator or AgentInitiator, agent credential id, model, prompt hash, sources, change set id, previous version id) plus the operation name and an optional subject; the record validates itself. NewAuditEvent is the caller-supplied part. IAuditLog.AppendAsync stamps the current initiator and time itself, so a service cannot record someone else as the actor, and throws outside a unit of work or when no initiator is known. IUnitOfWork.BeginAsync() returns a scope whose CommitAsync() saves and commits; disposing without committing rolls back. Inner scopes join the outer transaction, and an abandoned inner scope fails the outer commit.
  • Infrastructure: UnitOfWork over the DbContext transaction, so Identity's own saves inside a scope roll back with it, and the change tracker is cleared after a rollback. AuditLog adds a row inside the current transaction. The AuditEvents table: flat columns, sources as a primitive collection (text[] on PostgreSQL, JSON text on SQLite), indexes on timestamp, change set, agent credential and subject, and a check constraint that exactly the agent rows carry a credential id. No code path updates or deletes a row.
  • Migrations AuditEvents for both providers, generated with the documented commands. The PostgreSQL one also runs REVOKE UPDATE, DELETE, TRUNCATE ON "AuditEvents" FROM PUBLIC, CURRENT_USER.
  • CONTRIBUTING.md: a section on units of work and the audit log.

Why

AUD-N1 and ADR-0005: provenance is written in the same transaction as the change, and the log is append-only. The seam lands now so F-SCH and F-CNT write through it from their first commit.

The revoke and the deploying role

By default PUBLIC holds no table privileges on PostgreSQL, so revoking from PUBLIC alone changes nothing for the role that matters: the host applies migrations at startup (OPS-04), so the application role is the table's owner, and an owner holds every privilege until it revokes its own. The migration therefore also revokes from CURRENT_USER. Two caveats, both in CONTRIBUTING.md:

  • A superuser bypasses privilege checks, so the deploying role must not be one for the revoke to bind. CI's postgres role is one, which is why the test below creates its own ordinary role.
  • An operator who later grants ALL on the table to another role hands the privileges back. Grant SELECT, INSERT only.

The retention purge (a later slice of #2424) needs a privileged path around this; that slice decides which. SQLite has no roles; there the code is the guarantee.

Tests

Unit: the event rejects an empty id, a null initiator, a blank operation and a blank source; exposes the credential id for an agent and null for a user; keeps every AUD-01 field.

Integration on SQLite: an event appended inside a unit of work is read back with the current user, timestamp and every field; an agent event records its credential; a scope disposed without commit leaves neither the event nor the user Identity had already saved; appending outside a unit of work or with no initiator throws; inner scopes flush into the outer transaction and only the outer commit makes them visible; an abandoned inner scope fails the outer commit and the next unit of work starts clean; the newest migration's Down reverts and reapplies.

Integration on PostgreSQL: the migrations apply, and a database migrated by a throwaway non-superuser role lets that role append through IAuditLog but rejects UPDATE, DELETE and TRUNCATE with 42501 insufficient_privilege. The test creates and drops its own role and database, so it needs a connection that may do both; the documented local setup and CI have one. No PostgreSQL was available where this was written, so CI is its first run.

Checklist

  • Linked to an issue a maintainer has confirmed
  • Targets v2
  • Tests or docs updated where behaviour changed
  • No tool-generated co-author trailers or footers

Core gets AuditEvent, NewAuditEvent, IAuditLog and the IUnitOfWork seam;
Infrastructure implements the unit of work over the DbContext transaction,
the audit log and the AuditEvents table with migrations for both providers.
On PostgreSQL the migration revokes UPDATE, DELETE and TRUNCATE from PUBLIC
and from the deploying role.
@pournasserian pournasserian added this to the M1 Governance core milestone Oct 17, 2026
@pournasserian pournasserian added type:feature New capability v2 FluentCMS v2 (v2 branch) area:audit Provenance and audit (AUD) labels Oct 17, 2026
@pournasserian
pournasserian merged commit ef9966b into v2 Oct 18, 2026
2 checks passed
@pournasserian
pournasserian deleted the 2442-audit-event-store branch October 18, 2026 12:20
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

area:audit Provenance and audit (AUD) type:feature New capability v2 FluentCMS v2 (v2 branch)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Append-only audit event store: event model, unit of work and migrations for both providers

1 participant