Skip to content

fix(publication): redact every version on erasure and keep tombstones out of shared caches - #679

Open
midego1 wants to merge 2 commits into
unopim:3.xfrom
midego1:fix/publication-redact-all-versions
Open

midego1 wants to merge 2 commits into
unopim:3.xfrom
midego1:fix/publication-redact-all-versions

Conversation

@midego1

@midego1 midego1 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor

Problem

Publisher::redactAll() redacts only the is_current versions and then flips the publication to Redacted. Every superseded version keeps its sealed payload readable in the database.

The method's own docblock frames it as GDPR Art. 17 erasure. As written, that erasure holds only for as long as nothing reads history: an admin export, an audit view, or any future per-version route would find earlier payloads intact under a publication that claims to be redacted.

Fix

Redact every version of the publication that has not been redacted yet, superseded ones included. Versions redacted individually earlier keep their own redacted_at / reason and are skipped, so redactAll() cannot throw on them. The empty-publication guard is unchanged.

The public tombstone is adjusted alongside, since both are about what a redaction guarantees:

  • Redacted answers 410 Gone. The state is irreversible, and 410 still resolves (a 404 would let a caller infer a passport never existed, which the enum docblock rightly avoids).
  • Withdrawn stays 200, because it can be reinstated.
  • Both tombstones are private, no-store and noindex, nofollow. The package has no cache-purge hook, so a shared cache must never hold a tombstone that a reinstatement would have to displace, and a tombstone must not be indexed regardless of the channel's indexable setting. Live pages keep the existing s-maxage behaviour.

The If-None-Match path already keys the ETag on status, so it is unaffected.

Tests

  • redacts superseded versions too, not only the current one
  • leaves a version that was already redacted individually untouched
  • answers 410 Gone, uncacheable and unindexable, for a redacted passport
  • keeps a withdrawn tombstone out of shared caches while still answering 200

Existing redaction, tombstone and 304 tests pass unchanged.

Related

Third in a series on making sealed passport states trustworthy over the product lifetime, after #677 (keep the document index of superseded versions) and #678 (content-addressed document copies).

… out of shared caches

`Publisher::redactAll()` redacted only the `is_current` versions before
flipping the publication to Redacted. Every superseded version kept its
sealed payload readable in the database, so a GDPR Art. 17 erasure held
only for as long as nothing ever read history. Now every not-yet-redacted
version of the publication is redacted; versions already redacted on
their own are left untouched.

The public tombstone changes with it. A redacted passport answers
`410 Gone` (the state is irreversible), while a withdrawn one stays `200`
because it can be reinstated. Both tombstones are now `private, no-store`
and `noindex`: the package has no cache purge hook, so a shared cache
must never hold a tombstone that a reinstatement would have to displace,
and the tombstone must not be indexed regardless of the channel's
`indexable` setting.

Tests: redactAll nulls superseded payloads and stamps the reason on
them; a version redacted individually earlier keeps its own reason; the
redacted route is 410 + no-store + noindex; the withdrawn route is 200 +
no-store + noindex.
@midego1

midego1 commented Sep 4, 2026

Copy link
Copy Markdown
Contributor Author

Context, motivation and suggested review order for this and the related PRs: #683

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