ADR-0026: Audit: grantr's log, newest on top, filtered by type and maker¶
Status: Accepted Date: 2026-10-13 Deciders: Alex Nodeland
Context¶
lattice RFC-0002 gives admin an Audit area: grantr's audit log for a tenant, paged and filtered, for its admins. grantr reads the log:
- filtered by event type and by one subject (grantr ADR-0030);
- from its end back, with
lastandbefore_seq, as artifactr's log is read (grantr ADR-0032).
Reading it takes grantr:read_audit on the tenant, which owners and admins hold.
Decision¶
- Audit is
/t/$tenant/audit, on the rail where grantr allowsgrantr:read_audit.- The rail's one read of grantr's checks asks it beside Access's actions.
- The route is gated in
beforeLoadand reads inloader, as Access's parts are (ADR-0024). - Opened by its address where it isn't allowed, it says so in the tenant's frame. A failed read says so there, through admin's
ReadFailed, which Access shares, with a way to read it again, and, where the address has a filter, a way to clear it, so a filter grantr refuses isn't a dead end. A refusal says why, as the server gave it, not its body as it came.
- It opens on the newest changes.
client'saudit(tenant, filter)is an infinite query. Each page is the last 50 before the previous page's oldest entry, and a page short of 50 is the first.- Each page comes oldest first, so the table turns each around, and the newest is on top. "Show more" reads the page before.
- A log of exactly 50 shows "Show more" once: the page before is empty, and then it goes.
- "Show more" stays focusable while it reads. How many changes are shown is a status, so a reader hears the count when a page arrives and when "Show more" goes.
- A page before that can't be read says so, as an alert, and the log stays.
- A change to access on any screen reads the log again, as
changedAccessstales everything grantr answers in the tenant.
- Each entry reads as a sentence, with who made it and when, and each row, or card, is named by its sentence and its time:
- "added ci (service principal)";
- "set reviewers' members to alice (person)", a name's possessive as English writes it;
- "granted viewer on workspace notes to ops (group)".
said(event)covers each of grantr's 14 event types in an exhaustive switch, so a new type fails the typecheck.- The maker is a member, a rule, "the agent", or "no one" for the first operator's grant. People and principals are named with their kind, the tenant's first owner too.
- Times are absolute, in the reader's zone, with the relative time on hover, as ADR-0021 asks of the audit log:
ui'sMomentis a<time>titled fromIntl.RelativeTimeFormat. app's rules list uses it too. - The filter is in the address, so a filtered log can be linked to.
- It takes one type of change, or any. grantr takes several, which the UI doesn't offer yet.
- It takes who made it: a person or a service principal by id, a rule by its name and workspace, or the agent.
- The read uses only a filter grantr takes: a kind without what it needs, or a person's or a principal's id grantr can't hold (a
:, a~or a space), is dropped from the route's search, and the form shows what was read. A number or a yes in the address is read as text, and every value is trimmed; the id field's pattern says what grantr takes. - A maker is matched exactly, by kind and id, as grantr matches it.
- The scenario: alice adds a service principal, filters Audit to members added, and finds it at the top.
Options considered¶
| Option | For | Against |
|---|---|---|
Read from the end, with grantr's last (chosen) |
Opens on what changed last; one read | grantr needed it first (ADR-0032) |
| Read forward from the first entry | Nothing new in grantr | The newest on the last page |
| The filter in the address (chosen) | A filtered log can be linked to, and Back undoes a filter | The form and the address are kept in step |
| The filter in the screen's state | Simpler | Lost on reload, and not shareable |
Consequences¶
- Easier: the log can be read where it happened, as a sentence, filtered by what changed or who did it.
- Harder: people are named by their ids, which for a person is Supabase Auth's UUID, as on Members (ADR-0024).