From c824663946f37e681ad922107956f0b2b36cd2e3 Mon Sep 17 00:00:00 2001 From: Mira Weller Date: Wed, 5 Nov 2025 19:41:15 +0100 Subject: [PATCH] Implement schema validation and schema-based shredding --- src/pretix/base/logentrytype_registry.py | 52 +++++++++++++++++++++++- src/pretix/base/models/base.py | 7 ++++ 2 files changed, 58 insertions(+), 1 deletion(-) diff --git a/src/pretix/base/logentrytype_registry.py b/src/pretix/base/logentrytype_registry.py index 71576d3f35..882dd948e1 100644 --- a/src/pretix/base/logentrytype_registry.py +++ b/src/pretix/base/logentrytype_registry.py @@ -19,9 +19,12 @@ # You should have received a copy of the GNU Affero General Public License along with this program. If not, see # . # +import json from collections import defaultdict from typing import Optional +from functools import cached_property +import jsonschema from django.urls import reverse from django.utils.html import format_html from django.utils.translation import gettext_lazy as _ @@ -105,12 +108,38 @@ They are annotated with their ``action_type`` and the defining ``plugin``. log_entry_types = LogEntryTypeRegistry() +def prepare_schema(schema): + def handle_properties(t): + return {"shred_properties": [k for k, v in t["properties"].items() if v["shred"]]} + + def walk_tree(schema): + if type(schema) is dict: + new_keys = {} + for k, v in schema.items(): + if k == "properties": + new_keys = handle_properties(schema) + walk_tree(v) + if schema.get("type") == "object" and "additionalProperties" not in new_keys: + new_keys["additionalProperties"] = False + schema.update(new_keys) + elif type(schema) is list: + for v in schema: + walk_tree(v) + + walk_tree(schema) + return schema + + class LogEntryType: """ Base class for a type of LogEntry, identified by its action_type. """ + data_schema = None # {"type": "object", "properties": []} + def __init__(self, action_type=None, plain=None): + if self.data_schema: + print(self.__class__.__name__, "has schema", self._prepared_schema) if action_type: self.action_type = action_type if plain: @@ -147,12 +176,33 @@ class LogEntryType: object_link_wrapper = '{val}' + def validate_data(self, parsed_data): + if not self._prepared_schema: + return + jsonschema.validate(parsed_data, self._prepared_schema) + + @cached_property + def _prepared_schema(self): + if self.data_schema: + return prepare_schema(self.data_schema) + def shred_pii(self, logentry): """ To be used for shredding personally identified information contained in the data field of a LogEntry of this type. """ - raise NotImplementedError + if self._prepared_schema: + def shred_fun(validator, value, instance, schema): + for key in value: + instance[key] = "##########" + + v = jsonschema.validators.extend(jsonschema.validators.Draft202012Validator, + validators={"shred_properties": shred_fun}) + data = logentry.parsed_data + jsonschema.validate(data, self._prepared_schema, v) + logentry.data = json.dumps(data) + else: + raise NotImplementedError class NoOpShredderMixin: diff --git a/src/pretix/base/models/base.py b/src/pretix/base/models/base.py index d8154aa8f9..9b9dcc4f58 100644 --- a/src/pretix/base/models/base.py +++ b/src/pretix/base/models/base.py @@ -80,6 +80,7 @@ class LoggingMixin: from pretix.api.models import OAuthAccessToken, OAuthApplication from pretix.api.webhooks import notify_webhooks + from ..logentrytype_registry import log_entry_types from ..services.notifications import notify from .devices import Device from .event import Event @@ -124,7 +125,13 @@ class LoggingMixin: if (sensitivekey in k) and v: data[k] = "********" + type, meta = log_entry_types.get(action_type=action) + if not type: + raise TypeError("Undefined log entry type '%s'" % action) + logentry.data = json.dumps(data, cls=CustomJSONEncoder, sort_keys=True) + + type.validate_data(json.loads(logentry.data)) elif data: raise TypeError("You should only supply dictionaries as log data.") if save: