Skip to content

✨ Every administrative action logged, including a tenant-level log (EMS-AUDIT) - #3902

Merged
ereslibre merged 7 commits into
ovcsfrom
feat/meta-13781-tenant-audit-log/ovcs
Oct 14, 2026
Merged

ereslibre merged 7 commits into
ovcsfrom
feat/meta-13781-tenant-audit-log/ovcs

Conversation

@ereslibre

@ereslibre ereslibre commented Oct 13, 2026 •

Copy link
Copy Markdown
Contributor

Parent issue: https://github.com/sequentech/meta/issues/13781

Design package EMS-AUDIT. Every administrative action is logged, including the ones that happen outside an election event. Each tenant gets a signed log of its own, shown in Settings > Logs, and every change carries its old and new values, in the tenant log and in each event's log.

Replaces #3900's tenant log (VOTE-AUDIT). #3900 merged into ovcs first; ovcs is merged in here and this tenant log replaces its one. Gone: #3900's boards in the base log database, listTenantLog, POST /tenant-log and its Settings > Logs. Kept, and written into this tenant log: StatementBody::TenantActivity at Borsh tag 43 with statement types 62–63, so stored entries still decode (Audit is now tag 44, types 64–68); Harvest's request entries (TenantAdminAction: accepted, denied or failed); MESSAGE_SENT without the message's text; entries stamped with the time their producer saw them.

What changes

Area Change
electoral-log Audit statement body with five kinds (AdminChange, KeycloakAdminEvent, LogsExported, LogWriterStarted, LogWriterStopped). A log database per tenant (<base>_t<tenant>). A deleted event's database is retired (renamed, closed to connections), not dropped. event_type and log_type columns so lists filter by them
Database audit_log_outbox, and the audit_change() trigger on the 26 tables an admin role can write through Hasura. It reads the Hasura session, diffs JSON key by key, redacts secrets and writes in the same transaction. A coverage test fails when such a table has no trigger
Windmill Outbox worker: signs each entry with the admin's key and the log's key and posts it exactly once, in order. Keycloak events of the tenant realm and admin events reach the tenant log; an event that cannot be placed becomes an ERROR entry instead of failing its batch. Exports by scope and range as CSV, SQL or PDF, each recorded as LogsExported with the file's SHA-256. One JSON line per posted entry for monitoring. One-off task for existing tenants (log, key, realm listener, the two roles)
Harvest listElectoralLog without an election event reads the tenant log (needs tenant-logs-read, 403 otherwise). export_tenant_logs (needs tenant-logs-export). The event export checks logs-export instead of report-write. Audit entries in the routes for staff accounts, roles, permissions, phone blacklist, realm attributes, password policy, voter authentication and the Google service account key
Keycloak custom-event-listener handles the tenant realm (sign-ins, failures, lockouts, credential changes; no token refreshes) and admin events, with secrets redacted. An unknown account is recorded without the name typed
Admin Portal Settings > Logs, for tenant-logs-read. Entry popup with who, when, what, old and new values, sign-in or Keycloak details and signatures, in both logs. Export scopes and SQL. "Timezone for tenant logs" in Languages. An auditor with only the log's permissions sees only Logs
Docs Manager reference for Settings > Logs, auditor tutorial for verifying an SQL export, developer page for the architecture

Not in this pull request

  • EMS-SBEI-QUORUM's signing entries still use their own signing_log_outbox.
  • Many event-level Harvest routes write no AdminChange yet (areas, elections, dates, ceremonies, tally, publications, scheduled events, reports). Changes made to those tables through Hasura are covered by the trigger.
  • Certificate-authority routes are unchanged.
  • No command erases or reopens a retired event database (OPS-RETENTION), and one account's export does not read retired events.
  • The tenant log has no checkpoints or audit task yet.
  • Beyond's client-setup chart is not changed here; existing realms are set up by the one-off task.

Upgrade notes

  • Run electoral-log-admin upgrade-event-databases before the new Windmill and Harvest start: appends write the new event_type and log_type columns.
  • Roles that exported event logs with report-write need logs-export.

Tests

  • electoral-log: the whole suite, including the database-backed tests (tenant databases, retiring, filters, backfill).
  • windmill: unit tests for entries, redaction, export and the Keycloak consumer; postgres_audit_log_outbox (6 tests: signing, exactly-once posting, ordering after an outage, rerouting, deletion, and an SQL export loaded into PostgreSQL with every signature verified); postgres_audit_change (18 tests of the trigger).
  • harvest: 544 pass.
  • hasura: migrations apply, roll back and reapply to an identical schema; 115 metadata tests pass.
  • custom-event-listener: 38 Java tests; spotless clean.
  • admin-portal: 2174 jest tests; the story and journey suites of the changed screens passed before the last portal fix (the role used to read the schema), which is covered by jest only.
  • Run end to end on a local stack (Keycloak, Hasura, Harvest, Windmill, PostgreSQL), which found and fixed seven defects.

Failing on ovcs itself, not caused by this change: migrate_realm_permissions::the_migration_adds_the_signing_and_messaging_permissions_with_their_labels (portal labels and migration labels differ since #3885), journeys events.spec.ts:215 and the /trustee route smoke, and Hasura metadata uses an undeclared enum RestorePrivateKeyOutcome. PDF export does not render in the dev image (its Chromium crashes with the flags sequent-core passes); CSV and SQL do.

🤖 Generated with Claude Code

ereslibre and others added 2 commits October 13, 2026 21:50
…MS-AUDIT)

Each tenant gets a signed log of its own for what happens outside election
events: Admin Portal sign-ins and failures, staff accounts and roles, tenant
settings, templates and the lifecycle of election events. Changes carry
their old and new values, in the tenant log and in each event's log.

- electoral-log: Audit statement kinds, a database per tenant, retiring a
  deleted event's database, event type and log type filters
- hasura: audit_log_outbox and the audit_change() trigger on every table an
  admin role can write, with its coverage test
- windmill: outbox worker, Keycloak events of the tenant realm and admin
  events, exports with scope, range and SQL, monitoring lines
- harvest: tenant log list and export routes, audit entries in the routes
  that change state
- keycloak: custom-event-listener for the tenant realm and admin events
- admin-portal: Settings > Logs, entry popup, export scopes

Parent issue: sequentech/meta#13781

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
- An auditor who holds only the tenant log's permissions can open Settings >
  Logs: the portal reads the schema and the tenant row as tenant-logs-read
- The steps of a sign-in are recorded as LOGIN_STEP, so one sign-in is one LOGIN
- Saving the Google service account key writes its entry, without the key
- One-time codes are redacted in Keycloak admin events
- The tenant log's permissions get their own migration, with the log
- The whole-log CSV and PDF export records LogsExported with the file's hash
- A deleted event's entry names the event; the writer's start is retried
  until the database is ready

Parent issue: sequentech/meta#13781

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@coderabbitai

coderabbitai Bot commented Oct 13, 2026 •

Copy link
Copy Markdown

Important

Review skipped

Auto reviews are disabled on base/target branches other than the default branch.

🗂️ Base branches to auto review: 2
  • release/10.0
  • feat/meta-12767/.*

Please check the settings in the CodeRabbit UI or the .coderabbit.yaml file in this repository. To trigger a single review, invoke the @coderabbitai review command.

⚙️ Run configuration
  • Configuration used: Repository: sequentech/step/.coderabbit.yaml
  • Review profile: CHILL
  • Plan: Advanced
  • Run ID: 10a20986-34fd-488d-a133-bd899b0af6d0

You can disable this status message by setting the reviews.review_status to false in the CodeRabbit configuration file.

Use the checkbox below for a quick retry:

  • 🔍 Trigger review
  • Autofix · Keep fixing CodeRabbit findings and required CI, and resolving merge conflicts

Comment @coderabbitai help to get the list of available commands.

ereslibre and others added 2 commits October 13, 2026 23:08
Parent issue: sequentech/meta#13781

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…s, an undeclared action enum, the log crate's coverage

- scripts/coverage/run.py: a profile without scope_exceptions is measured
  instead of failing with a KeyError
- hasura: declare RestorePrivateKeyOutcome, which AuthorizeTallyCustodyOutput
  uses, so the metadata is consistent
- electoral-log: a server that is down names its databases and fails every
  other operation

Parent issue: sequentech/meta#13781

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@github-actions

github-actions Bot commented Oct 13, 2026 •

Copy link
Copy Markdown
PR Preview Action v1.8.1
Preview removed because the pull request was closed.
2026-10-14 15:50 UTC

ereslibre and others added 3 commits October 13, 2026 23:56
Parent issue: sequentech/meta#13781

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
#3900 merged a tenant log of its own into ovcs. EMS-AUDIT's replaces it:

- One tenant log: the per-tenant database, outbox and trigger, listed by
  listElectoralLog with no election event. #3900's boards in the base
  database, listTenantLog, POST /tenant-log and its Settings > Logs go.
- StatementBody::TenantActivity keeps Borsh tag 43 and its statement types
  62-63; Audit moves to tag 44 and its types to 64-68.
- Kept from #3900, written into the same log: Harvest's request entries
  (TenantAdminAction, with accepted, denied or failed), MESSAGE_SENT
  without the message's text, and entries stamped with the time their
  producer saw them. An event of an unknown tenant is dead-lettered
  instead of creating a log.
- Keycloak listener: one realm parser (LogRealm); a message sent to a
  tenant account leaves Keycloak without its text.
- #3900's repairs of ovcs's checks are taken as they are.
- sequent-core WASM package regenerated.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
…eycloak account is its own

The backend journeys, which had stopped before any journey on ovcs,
found two defects:

- The audit entry of an imported event read `election_event.name`,
  a column dropped when names moved to the presentation, so every
  import failed. It now reads the name in the event's default language.
- Step signs in to Keycloak's admin API as the master realm's user named
  by KEYCLOAK_ADMIN_CLIENT_ID (a password grant through admin-cli), not
  as a service account. Its realm updates during an import were logged
  in the new event's log as an operator's changes. That user, in the
  master realm, is now Step's own account.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
@ereslibre
ereslibre merged commit ea4c3a4 into ovcs Oct 14, 2026
171 of 174 checks passed
@ereslibre
ereslibre deleted the feat/meta-13781-tenant-audit-log/ovcs branch October 14, 2026 15:45
ereslibre added a commit that referenced this pull request Oct 17, 2026
…dmin keys after commit, new tenants' realms (part 2) (#3929)

Parent issue: sequentech/meta#13781

Design package
[EMS-AUDIT](https://project-1.sequent.sh/#package/EMS-AUDIT), second
part. #3902 gave each tenant a signed tenant log; this pull request
closes the gaps it listed or left open, apart from the route-by-route
entries (see "Split with #3928" below).

## What changes

| Area | Change |
| --- | --- |
| Imports | Importing an election event or a tenant configuration
records the file's SHA-256, computed by the task from the file it read,
and compares it with the checksum the admin declared (`undeclared`,
`matches` or `mismatch`). The entry also counts what the file held:
elections, contests, candidates, areas and areas per contest, or
configuration sections. A failed commit of a tenant configuration import
no longer records the import. |
| Entry popup | A new **The imported file** section shows the checksum
verdict, the SHA-256, the counts and the document, instead of raw extra
details. The deleted election event section names **The log's system
key**. Translated into the portal's 8 languages. |
| Deleted events in exports | One account's export with "every election
event" now also reads the logs of deleted events from their retired
databases, merged by time; their rows name `retired event <ID>`. Each
retired database is checked against the catalog, opened read-only under
the event database lock, and closed to connections again, including when
the read fails. The deletion entry keeps the retired log's system public
key, since the event's keys are deleted with it. |
| Admin public keys | `AdminPublicKey` is posted to the tenant log only
once the transaction that saved the key commits. A rollback leaves
nothing behind (before, it could leave a public key in the log whose key
was then lost). The delivery id is per account and key, so the key is
posted once. The tenant log migration backfills keys created before keys
went to the tenant log, and running it again does nothing. The outbox
can't carry this statement (it only posts `Audit` bodies), so a small
post-commit task does it instead. |
| New tenants | Creating a tenant prepares its Keycloak realm for the
tenant log whatever the realm template: `custom-event-listener`, admin
events with details, and the `tenant-logs-read` / `tenant-logs-export`
roles granted to the `admin` group. |
| Monitoring | Every audit outbox worker pass writes
`{"target":"audit_log_outbox","pending_entries":N,"oldest_pending_seconds":S}`,
zeros included, so security monitoring can alert on entries waiting more
than 5 minutes. This replaces the `waiting`/`oldest_seconds` names. It
is documented in the developer page. |
| Permissions | `/export-tenant-logs` needs `tenant-logs-read` as well
as `tenant-logs-export` (403 otherwise). The portal shows the tenant
log's Export only to people who have both. |
| Fix | Tenant log exports could not be downloaded: `fetch-document`
built a private document's S3 key from the event the caller named. The
tenant log asks with an empty event, so the link pointed at
`tenant-…/event-/document-…` (NoSuchKey). The key now comes from the
document's own event. |
| Docs | Settings > Logs reference: the popup's new sections, deleted
events in account exports, the export permissions. Developer page: the
backlog line and its alert threshold. Auditor tutorial: retired logs,
which have no system key in the manifest. |

## Split with #3928

#3928 (VOTE-AUDIT, meta#14259) closes the "routes write no AdminChange"
gap from #3902 for every Harvest route, including messaging accounts,
certificate authorities, countries and deleting a tenant. It does this
by letting the `audit_change()` trigger see Step's own writes. To avoid
logging each change twice, this pull request adds no per-route entries.
The two pull requests touch different files.

## Acceptance (meta#13781)

Ticked where #3902 (merged) and this pull request together meet the
criterion. Items 2, 3 and 5 also need #3928.

- [x] Signing in to the Admin Portal, failing (wrong password, wrong
authenticator code, unknown account), being locked out, setting up or
removing an authenticator, resetting or changing a password and signing
out each write one KeycloakUserEvent entry to the tenant log, with the
client, IP address and authentication used. An unknown username is
recorded as unknown. Token refreshes and code exchanges are not logged.
*(#3902; this PR adds the listener to new tenants' realms)*
- [ ] Every tenant-level change writes one AdminChange entry to the
tenant log with the person, the object, the action and each changed
field's old and new value, and no secret values. This covers the
Settings tabs, the tenant record, election types, trustees, templates,
staff accounts, roles and permissions, countries, backup and restore,
and creating, importing, archiving, unarchiving and deleting election
events. *(Imports now record the file's checksum and counts here.
Messaging accounts, countries with old values and deleting a tenant are
in #3928.)*
- [ ] Event-level create, update, delete, import, configure, approve and
schedule operations write AdminChange entries with old and new values to
their event's log (baseline). *(Changes made through Hasura are covered
since #3902; the Harvest routes are in #3928.)*
- [x] A change made through Hasura to any table an admin role can write
is logged in the same transaction. The coverage test fails when such a
table has no trigger. *(#3902)*
- [ ] A change whose transaction rolls back leaves no entry. A committed
change always gets exactly one entry, even when immudb or the worker is
down for a while: its entries wait and are posted in order afterwards.
*(The outbox does this, and this PR moves AdminPublicKey to after
commit. Certificate-authority routes still post their own entry after
commit, on a best-effort basis; with #3928 they also get an AdminChange
through the outbox.)*
- [x] A change to the tenant realm made in Keycloak's console is logged
as KeycloakAdminEvent, with the operator and the new value. Changes Step
makes through its service account appear as SYSTEM entries next to the
admin's AdminChange. *(#3902; new tenants' realms have admin events with
details since this PR)*
- [x] Windmill puts an event without an election event in the tenant
log. An event it can't place becomes an ERROR entry and the rest of its
batch is still posted. An unreadable message goes to the dead-letter
queue, and later messages are processed. *(#3902)*
- [x] Deleting an election event writes its tenant log entry, with the
event log's last entry and state hash, before anything is removed. The
event's log database is unloaded, not deleted. *(#3902; the entry also
keeps the retired log's system key since this PR. Checked on the local
stack.)*
- [x] Settings shows the Logs tab only with tenant-logs-read, and its
other tabs only with tenant-write: an Auditor sees only Logs, a Security
Officer every tab, a Configuration Manager every tab but Logs, and an
SBEI no Settings. Without tenant-logs-read, listElectoralLog and the
exports refuse (403), including direct API calls. *(#3902; this PR adds
the tenant-logs-read check to the tenant export)*
- [x] Settings > Logs has the same header, date range, filters, columns,
details popup and export menu as Election Event > Logs, and differs only
in its subtitle, timezone, statement kinds and export scopes. *(#3902)*
- [x] Times show in the tenant log's timezone (PhST for COMELEC) and in
my time. Filters by username, statement kind, event type, log type and
created range (inclusive, to the minute, with PhST boundary cases
verified) return exactly the matching entries in the tenant log and in
event logs. *(#3902)*
- [x] Clicking an entry in either list opens a popup (the shared Dialog,
not a side drawer) with who, when (the log's zone, my time and UTC),
what, the old and new values, the sign-in details or the Keycloak
change, the log and the signing keys; Close and Escape dismiss it.
*(#3902; this PR adds the imported file and the deleted log's system
key)*
- [x] Exports reconcile exactly with the list for the same scope and
range. They cover the tenant log (whole, or one account with or without
its election event entries) and event logs (All Posts, one Post, one
country, one account), over a date range, as CSV, SQL or PDF. Each
export writes LogsExported with the file's SHA-256. The event export
checks logs-export and the tenant export checks tenant-logs-export.
*(#3902. This PR adds deleted events' logs to one account's export and
fixes the tenant export's download. CSV was checked on the local stack;
PDF was not, because it does not render in the dev image.)*
- [x] The SQL export loads into PostgreSQL, and every entry's signatures
verify with the exported public keys. *(#3902; test
`postgres_audit_log_outbox`)*
- [x] Every posted entry appears as one JSON line in Windmill's output,
without old or new values or secrets. The outbox's waiting time is
available to VOTE-SECOPS's alerts. *(#3902; this PR writes the waiting
time on every worker pass)*
- [x] The one-off task gives every existing tenant its log and key, and
AdminPublicKey entries are written to the tenant log. *(#3902; this PR
posts keys after commit and backfills keys made earlier)*
- [x] The second organization's tenant log works by configuration only
(its timezone, accounts and labels); no code refers to COMELEC. *(#3902;
nothing in this PR names a client)*
- [ ] Apply every linked source requirement, preserving its exact
targets, alternatives and lifecycle stage. This package estimate is
shared and must not be summed per row. *(A rule for the whole package,
not something one pull request can tick)*

## Not in this pull request

- EMS-SBEI-QUORUM's signing entries still use their own
`signing_log_outbox`.
- Tenant log checkpoints and an audit task for the tenant log
(VOTE-AUDIT).
- Erasing or reopening a retired event database (OPS-RETENTION).
- Beyond's client-setup chart still configures only `jboss-logging` for
the tenant realm. Existing realms are set up by the one-off task, and
new tenants by this pull request.

## Upgrade notes

- Monitoring that parsed the outbox line's `waiting`/`oldest_seconds`
must read `pending_entries`/`oldest_pending_seconds`.
- Roles that exported the tenant log with only `tenant-logs-export` need
`tenant-logs-read` too.

## Tests

- electoral-log: 124 tests with the database tests
(`--include-ignored`), including reading a retired database and closing
it again.
- windmill: 1705 lib tests pass.
`decrypted_archives_use_private_owned_files_and_leave_no_plaintext`
needs an `openssl` binary on the host, and passes with one.
`postgres_audit_log_outbox` (8, including the post-commit key
announcement, the backfill and retired events in exports) and
`postgres_audit_change` (18) pass.
- harvest: 561 pass, including the new 403 for an export without read
permission.
- admin-portal: 2233 jest tests, `LogEntryDialog` stories (17, with a
new import story), story inventory, `typecheck:stories`, and the
journeys `settings/logs`, `settings/access` and `events/operations`
(29).
- End to end on a local stack built from this branch (Keycloak, Hasura,
Harvest, Windmill, PostgreSQL, MinIO, production portals), using the
backend E2E bootstrap. All 11 UI journeys pass. The stack also checked,
by hand: the import entry's checksum and counts; retired databases read
and closed again; the account export's download and its retired rows; a
new tenant's realm settings and roles; the backlog line. That run found
the export download bug fixed here.

Not measured locally: the per-package coverage gates.

## Demo

Video with voiceover, to upload to meta#13781: import, then an event
edit, deletion, the account export with the retired log, a new tenant's
realm, and the backlog line.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
ereslibre added a commit that referenced this pull request Oct 17, 2026
…DIT, SW-F-0364) (#3928)

Parent issue: sequentech/meta#14259

Requirement
[SW-F-0364](https://project-1.sequent.sh/#requirement/SW-F-0364): log
all system activity from start to finish. #3900 and #3902 log what a
person changes through Hasura, accounts and roles, Keycloak events and
the tenant-level actions. #3902's "Not in this pull request" left the
rest open: most event-level routes (areas, elections, dates, voting
status, ceremonies, tallies, publications, scheduled events, reports,
applications, the approval matrix, monitoring, signing settings) write
with Harvest's or Windmill's own SQL, which `audit_change()` skipped, so
they wrote no `AdminChange`. This pull request closes that gap.

> **Scope agreed with EMS-AUDIT (meta#13781):** this pull request
carries the route coverage. The tenant deletion and country-limit
entries come from that session's work; its own pull request keeps the
rest of EMS-AUDIT.

## What changes

| Area | Change |
| --- | --- |
| Database | Migration `1791800000000_audit_changes_step_makes`:
`audit_change()` also logs the writes of a transaction whose admin Step
names in the setting `sequent.audit_actor` (`{user_id, username, role,
origin, tenant_id}`), with old and new values; a Hasura session still
takes precedence. The trigger on the tables only Step writes for an
admin: `keys_ceremony`, `messaging_account`, `messaging_event_config`,
`monitoring_config`, `signing_checks`, `staff_certificate`,
`tally_results_publication`, `tally_sheet_import`,
`tally_sheet_import_item`. `webhook_key` is a secret (SQL and Rust
rules) |
| Windmill | `audit_log::act_for(&tx, &actor, origin, tenant_id)`.
Results publication (publish, revoke, website policy), monitoring
settings (`save_as`, `reset_to_preset_as`, `set_mode_as`) and a signing
approval (as the signer whose signature runs the action) name their
admin. `insert_tenant` takes the creating admin |
| Harvest | `audit::act_for(&tx, &claims)` right after the transaction
opens in every state-changing admin route: areas, elections, election
dates and schedule recompute, keys and tally ceremonies, trustee
custody, ballot publication, voting status, tally sheets and their
imports, signing certificates, issuers, checks and rules, messaging
accounts and settings, applications, scheduled events, lifecycle
policies, uploads and previews, certificate authority import and delete
|
| Harvest, own entries | Where the trigger cannot see the change: area
and candidate imports (one entry with the document and declared
checksum), export requests (event, tally results, ballot publication,
applications, tasks, trustees, certificate authorities, reports), the
approval matrix (old and new version), a report's password set (named,
never recorded), a document's password revealed, an electoral-log
console query, a voter information letter (it resets the voter's
password), the country limits before and after |
| Windmill, own entries | Deleting a tenant: an `AdminChange` staged in
the deleting transaction, in the log of the tenant whose admin deleted
it (the route passes the admin as an optional task input) |
| Guard | `harvest/tests/support/audited_routes.rs` fails when a route
file commits a transaction without naming its admin or staging an entry.
Exemptions, with their reasons, in
`tests/fixtures/unaudited-committing-routes.txt` (voting portal routes,
reads that commit, callbacks) |
| Docs | Developer page "Tenant log and audit entries": sections 5, 6.1,
6.2 and 13 |

Also fixed on the way:

- `/encrypt-report` logged the report's password (`info!("body …")` and
the tracing span).
- `/miru/upload-signature` recorded its input, password included, in its
tracing span.
- `/import-candidates` wrote its task row before checking the
permission; it now checks first (its `route_permissions` case is
updated).
- `/delete-election-event` passes the admin's role like every other
route.

## Not in this pull request

- What a Windmill task writes on its own after the route answered
(ballot styles of a publication, a tally's execution, documents of
reports and letters, reconciliation patches) has no admin; the route's
entry records the decision.
- Keycloak changes when an application is confirmed or rejected are
recorded only by Keycloak's admin events.
- `/fetch-document` downloads write no entry.

## Tests

- windmill: the whole suite, 3088 pass, including
`postgres_audit_change` (24 tests; 6 new: an admin named by Harvest or
Windmill, the setting ending with the transaction, Hasura's precedence,
the new tables, a keys ceremony, `webhook_key`).
- harvest: 570 pass, including the route guard, `act_for` on the
migrated test database, import and export entries, and the route
permission inventory.
- hasura: migrations apply, roll back and reapply to an identical
schema; 115 metadata tests pass.
- `reuse lint` passes.

## Demo

A narrated video of the feature on a local stack is attached to
meta#14259.

🤖 Generated with [Claude Code](https://claude.com/claude-code)

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
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.

1 participant