Restriction
Art. 18 restriction of processing — a queryable, audited flag, never enforcement.
effaced ships Recital 67’s “clearly indicated in the system”: an append-only
ledger of restriction events and a derived RestrictionLedger.status
your application consults before processing. Which processing must stop — and whether an
operation falls under an Art. 18(2) exception — is a determination only the
controller can make; nothing here intercepts queries or claims compliance.
RestrictionLedger
Section titled “RestrictionLedger”class RestrictionLedger: def __init__(restriction_records: Table, audit_sink: AuditSink) -> NoneAppend-only restriction bookkeeping (Art. 18).
Recital 67’s first method — the restriction “clearly indicated in the
system” — is exactly what this ledger ships: a queryable flag plus its
audited history. Nothing enforces it; status is the surface
your application consults before processing.
Restriction rows are written through the caller’s session and commit
with it; the mirrored audit event goes through the constructor’s sink,
which persists independently (ADR 0006). A failing sink raises out of
record before the caller can commit, so no restriction change
can persist unaudited — the converse (an audit event for a write the
caller then rolls back) is possible and is the deliberate, evidence-
preserving direction. Do not commit a session after record
raised.
RestrictionLedger.history
Section titled “RestrictionLedger.history”def history(session: Session, subject_id: SubjectIdentifier) -> tuple[RestrictionRecord, ...]Every restriction event for one subject, oldest first.
Equal recorded_at values order by record_id. Records come
back full and unredacted — reason and source included.
Together with the RESTRICTION_LIFTED audit event this is the
mechanical substrate for the Art. 18(3) duty to inform the subject
before a restriction is lifted; the informing itself stays the
controller’s process.
Args:
- session (
Session): An open database session. - subject_id (
SubjectIdentifier): Whose history to read (single-columnstror compositeCompositeSubjectId).
Returns:
tuple[RestrictionRecord, ...]— The full, unredacted event sequence.
RestrictionLedger.record
Section titled “RestrictionLedger.record”def record(session: Session, record: RestrictionRecord) -> NoneAppend one restriction event (placement or lift).
Mirrors a RESTRICTION_PLACED/RESTRICTION_LIFTED audit event
whose payload carries only the scope — {"purpose": ...} for a
purpose-scoped record, {"scope": "all"} for a global one. The
record’s reason and source never enter the payload: free
text is PII-bearing by nature.
Args:
- session (
Session): An open database session. - record (
RestrictionRecord): The event to append. Never updates existing records.
RestrictionLedger.status
Section titled “RestrictionLedger.status”def status(session: Session, subject_id: SubjectIdentifier, purpose: str | None = None) -> boolWhether the subject’s processing is currently restricted.
Derived, never stored. The answer considers two events: the latest
global record (purpose IS NULL) and, when purpose is given,
the latest record for that purpose — restricted if either
restricts. A purpose-level lift therefore cannot undo a global
restriction; lift globally instead. With purpose=None only the
global records are consulted, so a purpose-scoped restriction does
not answer the all-processing question. False when no record
exists.
“Latest” means greatest recorded_at (supply distinct,
caller-clock timestamps). Exact timestamp ties resolve to the
restricting record — when the order of a placement and a lift is
unknowable, effaced assumes the subject is restricted, the same
protective direction as consent’s withdrawn-wins tie-break.
Args:
- session (
Session): An open database session. - subject_id (
SubjectIdentifier): Whose restriction to check. - purpose (
str | None): The processing purpose, orNonefor the all-processing question.
Returns:
bool— Current restriction status.
RestrictionRecord
Section titled “RestrictionRecord”class RestrictionRecord(BaseModel): purpose: str | None = Field(default=None, min_length=1, max_length=255) reason: str | None = Field(default=None, max_length=255) recorded_at: datetime restricted: bool source: str | None = Field(default=None, max_length=255) subject_id: StoredSubjectIdOne restriction placement or lift (Art. 18), as it happened.
Records are immutable events, not mutable state: whether a subject is currently restricted is derived by reading the latest record per scope, never stored. There is no transition validation — lifting a restriction that was never placed simply appends. Events are evidence, not a state machine.
A record with purpose=None is global: it restricts (or lifts the
restriction on) all processing for the subject. A purpose-scoped
record touches only that purpose.
Fields:
- purpose (
str | None): The processing purpose restricted (e.g."ads");Nonemeans all processing. - reason (
str | None): Free-text grounds (e.g. the Art. 18(1) basis claimed). Kept in history, never mirrored into audit payloads. - recorded_at (
datetime): When the event happened (UTC). - restricted (
bool):Trueplaces a restriction,Falselifts one. - source (
str | None): Where the event came from ("dsar_portal","api"). - subject_id (
StoredSubjectId): Whose restriction this is — a single-columnstror a compositeCompositeSubjectId, stored as its canonical string (ADR 0025).