Instrument every full-content read channel for pages with classification = vs_nfd (ADR 0023, variant A): SPA state fetch and read rendering, public JSON content, no-JS shell, expanded embeds, public API GET (incl. the MCP read_page path and write echoes), attachment download under the #212 effective classification, all export shapes (markdown, pond ZIP, account data export, queued docx/odt/pdf at enqueue), and collab-token issuance as the api-side proxy for the WS join. Events land in the new read_events table (no FKs — evidence survives page purges and hard user deletions) with actor, session key (session:/token:/job:/anon), page, pond, channel and the classification at read time. Recording failures are NOT swallowed: a failed write aborts the read (hard failure, the deliberate contrast to AuditService — decision recorded in ADR 0023 and security.md §Logging, together with the recorded residuals: content fragments and feeds). One e2e test per channel proves both the event and its absence for unclassified pages, plus the hard-failure semantics. Refs #222. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01AUtYMxwTCMHG9mVHnwbFg8
4.4 KiB
ADR 0023: Read-access audit trail limited to classified content
- Status: proposed
- Date: 2026-07-29
Context
The existing audit trail (audit_log, issue #86) is deliberately scoped to
"who changed access or configuration", and its own service comment states
that content activity stays log-only. There is no record of reads.
For "operable in an approved environment", read logging is not a mandatory product feature — evidence collection can be a platform function. In practice platform logging cannot answer the question that matters: a proxy log knows URLs, not classifications, so it cannot say which classified page was read. Leistungsbeschreibungen tend to list this as a must.
Two variants were considered. Variant B logs all reads (18–20 AT) and brings volume, latency and retention problems, plus the requirement that no event may be lost. Variant A logs reads of classified pages only (8–10 AT).
A live editing session is the volume hazard: Yjs sync means continuous traffic per open document.
Decision
Variant A: read events are recorded only for pages with
classification = VS_NFD. Requires ADR 0022.
- All read channels are instrumented, or the feature is worthless: SPA page fetch, public API GET, attachment download, export, no-JS shell, collab WS join.
- A dedup window (session + page + channel within N minutes = one event) keeps Yjs sync from flooding the trail. The recorded event states that it represents a window, so the evidence is not overread.
- Its own table, with time partitioning and its own retention period —
independent of
audit_log, because volume, purpose and legal basis all differ. - Failure is not silent.
AuditServiceswallows write failures by design; for classified reads a lost event is a gap in evidence, so the behaviour is either hard failure or an explicitly documented degradation. Which one is decided in #222 and stated in the security documentation. - Switchable, with a written purpose limitation. Off means nothing is written anywhere; a startup log line states the trail is off so a gap is never ambiguous.
- Variant B is rejected, and the rejection is recorded rather than left open: unbounded volume, the no-loss requirement, and a purpose limitation that is much harder to defend.
Decisions taken in #222
- Failure mode: hard failure. A failed
read_eventswrite aborts the read with the ordinary 500. The alternative (documented degradation) was rejected: the reader retrying is cheap, a gap in evidence is not. This is the deliberate contrast toAuditService, which swallows failures. - Collab WS join: the api emits at token issuance. The collab server keeps zero permission/classification context; tokens live 60 s, so a live session re-requests one per minute — per-minute granularity for free, which the dedup window (#223) collapses. The trail therefore proves "held a live connection during this window", not individual sync frames.
- Scope: full-content channels. Content fragments (search snippets,
task-overview rows, backlink titles) and the Atom feeds (off in the
reference configuration) are deliberately not instrumented — recorded in
security.md§Logging as a residual. - Session key vocabulary:
session:<id>(cookie),token:<id>(PAT — also the MCPread_pagepath),job:<id>(background builds such as the account data export),anon(anonymous reader on a public grant).
Consequences
- The scope limit is the feature's strongest argument in the works-council discussion at the customer: only classified content is observed.
- Reads of unclassified content are not evidenced. Deliberate, and it goes into the residual-risk list.
- The collab WS join is the awkward channel: authorization there is
token-only (signature plus
pageIdmatch) and the collab server has no permission context. Either the event carries what the token asserts, or the api emits it at token issuance. #222 decides and documents; the choice affects what the trail can prove about live sessions. - Retention and partition maintenance are operational obligations that must ship with the feature, not after it.
- Classification at read time is stored with the event: a later reclassification must not rewrite history.
Implementing issues
#222 (instrumentation), #223 (dedup window), #224 (table, retention, partitioning), #225 (switch + purpose limitation). Depends on #204/#205.