Consent Management

Published:

The GDPR requires to always ask for consent whenever processing personal data. The regulation itself categorizes them

Compressed Definition (Personal Data):

Anything that can be used to identify a person it is related to.

https://tietosuoja.fi/en/what-is-personal-data

Compliance Checklist

https://eur-lex.europa.eu/eli/reg/2016/679/oj

https://gdpr-info.eu/issues/consent/

Consent Architecture

The first we need to version the consents and use cases. We create a table name use_case.

CREATE TABLE consent_category (
    id SERIAL PRIMARY KEY,
    name VARCHAR(200) NOT NULL  -- such as necessary_cookies, tracking_cookies, privacy_policy
);

CREATE TABLE use_case (
    id SERIAL PRIMARY KEY,
    category INTEGER REFERENCES consent_category,
    version Integer UNIQUE NOT NULL,
    summary VARCHAR(500) NOT NULL,  -- So that programmers can read it easily.
    document_name VARCHAR(200) NOT NULL, -- Keep a separate document that user sees.
    created_at TIMESTAMP NOT NULL DEFAULT now(),
    updated_at TIMESTAMP NOT NULL DEFAULT now()
);

I think you are not allowed to change the consents, unless it is a typo. You should create a new version.

We also need a table consent to actually hold the consents

CREATE TABLE consent (
    id SERIAL PRIMARY KEY ,
    "user" INTEGER REFERENCES app_user,
    use_case INTEGER REFERENCES use_case,
    accepted BOOLEAN NOT NULL,
    created_at TIMESTAMP NOT NULL DEFAULT now()
);

The consent table points to use case and each time user accepts or cancels consent it should create a new row.

The idea is that we only read the latest created_at for a given use case and if it accepted is true then we can proceed. If you want to keep your database small, you should put as much as possible to single use_case.

The reason we keep history is for audit logs.

The Interface

def has_consent(user_id: int, category: str) -> bool:
    """
    Finds whether user has consent for a given action. Only the latest one matters.

    :param user_id for user to find consent
    :param category of consent

    :raise ConsentDoesNotExist

    :returns a boolean value whether consent is valid
    """
    raise NotImplementedError

Then before each sensitive action we do

if has_consent(user_id=user.id, category=ConsentCategory.TRACKING):
    # do your special stuff here

# for normal users it should execute differently

Another alternative is to define context

class ConsentSession:
    def __init__(self, user, category):
        # ...

    def __enter__(self):
        if self.has_consent():
            # ...
        else:
            self.__exit__()

    def __exit__(self, exc_type, exc_val, exc_tb):
        # ...


with ConsentSession(user, ConsentCategory.Tracking) as session:
    # ...

API Interface

For frontend processing and showing stuff you likely want to cache it and return all from single endpoint

GET /v1/caches/consents
{
  "necessaryCookies": true,
  "trackingCookies": false
}

Then implement a similar interface as in backend but read them from wherever those consents are stored such as localStorage.