dorfteich/docs/architecture/adr/0023-read-access-audit-trail.md
Claude Fable 5 05a979bac3
Some checks failed
CI / Lint, typecheck, test (pull_request) Successful in 6m25s
CI / Build container images (pull_request) Successful in 2m58s
CI / Auth e2e pack (pull_request) Successful in 8m35s
CI / Import/export fidelity gate (pull_request) Successful in 1m7s
CD / Deploy to Test (push) Blocked by required conditions
CD / Smoke tests against Test (push) Blocked by required conditions
CD / Promote to Int (push) Blocked by required conditions
CI / Auth e2e pack (push) Blocked by required conditions
CI / Import/export fidelity gate (push) Blocked by required conditions
CI / Build container images (push) Blocked by required conditions
CI / Lint, typecheck, test (push) Has been cancelled
CD / Build and push images (push) Has been cancelled
#222: read-access trail for classified pages
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
2026-07-31 12:12:55 +02:00

90 lines
4.4 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 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 (1820 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 (810 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.
1. **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.
2. **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.
3. **Its own table**, with time partitioning and its own retention period —
independent of `audit_log`, because volume, purpose and legal basis all
differ.
4. **Failure is not silent.** `AuditService` swallows 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.
5. **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.
6. **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_events` write 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 to `AuditService`, 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 MCP `read_page` path), `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 `pageId` match) 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.