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
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
90 lines
4.4 KiB
Markdown
90 lines
4.4 KiB
Markdown
# 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.
|
||
|
||
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.
|