Skip to content

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.

class RestrictionLedger:
def __init__(restriction_records: Table, audit_sink: AuditSink) -> None

Append-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.

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-column str or composite CompositeSubjectId).

Returns:

  • tuple[RestrictionRecord, ...] — The full, unredacted event sequence.
def record(session: Session, record: RestrictionRecord) -> None

Append 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.
def status(session: Session, subject_id: SubjectIdentifier, purpose: str | None = None) -> bool

Whether 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, or None for the all-processing question.

Returns:

  • bool — Current restriction status.
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: StoredSubjectId

One 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"); None means 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): True places a restriction, False lifts one.
  • source (str | None): Where the event came from ("dsar_portal", "api").
  • subject_id (StoredSubjectId): Whose restriction this is — a single-column str or a composite CompositeSubjectId, stored as its canonical string (ADR 0025).