Skip to content

Consent

This content is for 0.1. Switch to the latest version for up-to-date documentation.

Art. 7 consent — versioned, timestamped, queryable records.

class ConsentLedger:
def __init__(consent_records: Table, audit_sink: AuditSink) -> None

Append-only consent bookkeeping (Art. 7).

Withdrawal is as easy as granting — both are one call appending one immutable record. Every call is mirrored into the audit trail.

Consent 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 consent change can persist unaudited — the converse (an audit event for a consent 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: str) -> tuple[ConsentRecord, ...]

Every consent event for one subject, oldest first.

Args:

  • session (Session): An open database session.
  • subject_id (str): Whose history to read.

Returns:

  • ConsentRecord — The full, unredacted event sequence — this is the Art. 5(2)
  • ... — accountability answer.
def record(session: Session, record: ConsentRecord) -> None

Append one consent event (grant or withdrawal).

Mirrors a CONSENT_GRANTED/CONSENT_WITHDRAWN audit event whose payload carries only the purpose and policy version — never the record’s source or any other potential PII.

Args:

  • session (Session): An open database session.
  • record (ConsentRecord): The event to append. Never updates existing records.
def status(session: Session, subject_id: str, purpose: str) -> bool

Whether the subject currently consents to a purpose.

Derived from the latest record for (subject, purpose); False when no record exists. “Latest” means greatest recorded_at (supply distinct, caller-clock timestamps). Exact timestamp ties resolve to the withdrawing record — when the order of a grant and a withdrawal is unknowable, effaced assumes consent was withdrawn.

Args:

  • session (Session): An open database session.
  • subject_id (str): Whose consent to check.
  • purpose (str): The processing purpose.

Returns:

  • bool — Current consent status.
class ConsentRecord(BaseModel):
granted: bool
policy_version: str = Field(min_length=1, max_length=255)
purpose: str = Field(min_length=1, max_length=255)
recorded_at: datetime
source: str | None = Field(default=None, max_length=255)
subject_id: str = Field(min_length=1, max_length=255)

One consent grant or withdrawal, as it happened.

Records are immutable events, not mutable state: current consent status is derived by reading the latest record per (subject, purpose). This is what makes Art. 5(2) accountability answerable — what consent was given, when, and against which policy version.

Fields:

  • granted (bool): True for a grant, False for a withdrawal.
  • policy_version (str): Version of the policy text the subject saw.
  • purpose (str): The processing purpose consented to (e.g. "newsletter").
  • recorded_at (datetime): When the event happened (UTC).
  • source (str | None): Where the event came from ("signup_form", "api").
  • subject_id (str): Whose consent this is.