audit_trail.serialization¶
JSON encoding of audited values and keyed pseudonymization.
Values written to data go through :func:encode_value, which turns them
into JSON-native data deterministically. :func:pseudonymize and
:func:hash_value replace a value with an HMAC-SHA256 token whose prefix
carries the key version, so rows written before a key rotation stay
distinguishable and, while the old key is kept, correlatable via
:func:verify.
MIN_KEY_LENGTH
module-attribute
¶
Minimum HMAC key length in bytes (the SHA-256 output size).
Pseudonymized
module-attribute
¶
Marks a payload schema field whose value is stored pseudonymized.
Use as login: Pseudonymized[str] on a pydantic model. Validation and the
JSON schema are unchanged; the writer pseudonymizes the fields listed by
pseudonymized_fields. Built from typing only, so importing it does not
need pydantic.
UnserializableValueError ¶
Bases: TypeError
A value has no JSON encoding for the audit log.
Raised instead of falling back to str(), so the flush aborts rather
than writing a partial entry. The message names the type, never the value,
because the value may be personal data.
KeyRing ¶
Versioned HMAC keys for pseudonymization and the hash field policy.
New tokens always use the highest version; :func:verify picks the key by
the version in a token's prefix. Rotating means adding a higher version and
keeping old keys for as long as old rows must stay correlatable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
keys
|
bytes | Mapping[int, bytes]
|
One key as |
required |
Attributes:
| Name | Type | Description |
|---|---|---|
current_version |
The highest version, used for new tokens. |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
ValueError
|
No keys, a version below 1, or a key shorter than
|
Source code in audit_trail/serialization.py
get ¶
Return the key for version, or None when it is not kept.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
version
|
int
|
Key version. |
required |
Returns:
| Type | Description |
|---|---|
bytes | None
|
The key, or |
encode_value ¶
Encode a value into JSON-native data for the audit log.
Built-in rules:
None,bool,int,strand finitefloatunchanged; NaN and infinities raise.Enum-> its value, encoded recursively.datetime,date,time-> ISO 8601. An aware value keeps its own offset (it is not converted to UTC). A naive value is written without an offset, as stored: no zone is invented for it.Decimal->str(value)without normalization, soDecimal("1.0")andDecimal("1.00")encode (and hash) differently.UUID-> canonical lowercase hyphenated string.bytes,bytearray,memoryview->{"sha256": <hex digest>, "len": <byte count>}.dictwithstrkeys andlist/tuple-> encoded recursively. Non-strkeys raise, since JSON would silently turn1into"1".
Anything else, including set/frozenset (no stable order), goes to
json_encoder().default() and the result is encoded again; a result of
the same type as the input raises instead of recursing. The built-in
rules always win: the host encoder only sees types they do not cover.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
object
|
The value to encode. |
required |
json_encoder
|
type[JSONEncoder] | None
|
Host encoder class for additional types. |
None
|
Returns:
| Type | Description |
|---|---|
JSONValue
|
JSON-native data. |
Raises:
| Type | Description |
|---|---|
UnserializableValueError
|
Neither a built-in rule nor the host encoder handles the value (or a nested value), or a float is not finite. |
Source code in audit_trail/serialization.py
pseudonymize ¶
pseudonymize(
value: object,
*,
purpose: str,
keys: KeyRing,
json_encoder: type[JSONEncoder] | None = None,
) -> str
Replace a value with a keyed pseudonym, e.g. audit.login_attempt.v2:<hex>.
The token is prefix + hex(HMAC-SHA256(key, prefix + canonical)) with
the highest key version. canonical is the UTF-8 encoding of
json.dumps(encode_value(value), sort_keys=True, separators=(",", ":"),
ensure_ascii=False). So a UUID and its canonical string, or
Decimal("1.0") and "1.0", give the same token, while 1,
1.0 and "1" all differ. Strings are not normalized (no case
folding). The same value under two purposes gives unrelated digests.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
object
|
The value to pseudonymize. Must not be |
required |
purpose
|
str
|
Snake_case name separating unrelated uses
(for example |
required |
keys
|
KeyRing
|
Key ring to sign with. |
required |
json_encoder
|
type[JSONEncoder] | None
|
Host encoder for types |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The pseudonym token. |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
ValueError
|
|
UnserializableValueError
|
|
Source code in audit_trail/serialization.py
hash_value ¶
hash_value(
value: object,
*,
keys: KeyRing,
json_encoder: type[JSONEncoder] | None = None,
) -> str
Hash a column value for the hash field policy, e.g. hv2:<hex>.
Same construction and canonical encoding as pseudonymize, with the
prefix hv{n}:. None is not hashed: the caller keeps null as null,
so null and a value stay distinguishable.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
value
|
object
|
The value to hash. Must not be |
required |
keys
|
KeyRing
|
Key ring to sign with. |
required |
json_encoder
|
type[JSONEncoder] | None
|
Host encoder for types |
None
|
Returns:
| Type | Description |
|---|---|
str
|
The hash token. |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
UnserializableValueError
|
|
Source code in audit_trail/serialization.py
verify ¶
verify(
stored: str,
value: object,
*,
keys: KeyRing,
json_encoder: type[JSONEncoder] | None = None,
) -> bool
Check whether a stored token was made from value.
Works for tokens from both pseudonymize and hash_value. The key is
chosen by the version in the token's prefix, so tokens written before a
rotation still verify while their key is kept in keys.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
stored
|
str
|
A token read from the log. |
required |
value
|
object
|
The candidate value. |
required |
keys
|
KeyRing
|
Key ring holding the versions to check against. |
required |
json_encoder
|
type[JSONEncoder] | None
|
Host encoder for types |
None
|
Returns:
| Type | Description |
|---|---|
bool
|
|
bool
|
malformed, or its key version is not in |
Raises:
| Type | Description |
|---|---|
TypeError
|
|
UnserializableValueError
|
|
Source code in audit_trail/serialization.py
pseudonymized_fields ¶
Return the names of top-level fields annotated with Pseudonymized.
The marker is found on the field itself (Pseudonymized[str | None])
and inside a union (Pseudonymized[str] | None,
Optional[Pseudonymized[str]]). Fields of nested models and items of
containers are not included.
Parameters:
| Name | Type | Description | Default |
|---|---|---|---|
schema
|
type[BaseModel]
|
A pydantic model class. |
required |
Returns:
| Type | Description |
|---|---|
frozenset[str]
|
Names of the marked fields. |