Coverage for /usr/local/lib/python3.10/site-packages/opal_common-0.0.0-py3.10.egg/opal_common/logging_utils/redaction.py: 27%
22 statements
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 11:54 +0000
« prev ^ index » next coverage.py v7.15.2, created at 2026-10-10 11:54 +0000
1from typing import ClassVar, Set
3from opal_common.http_utils import redact_url
6class RedactedReprMixin:
7 """Mixin for pydantic (v1) models that may carry credentials.
9 Overrides ``repr()`` / ``str()`` so that sensitive fields are masked instead
10 of rendering their real value. Two flavours of masking are supported:
12 - ``_redacted_repr_fields`` - the value is wholly replaced with
13 ``<redacted>`` (use for opaque secret carriers such as ``config``/``data``).
14 - ``_redacted_url_fields`` - the value is passed through ``redact_url`` so
15 embedded credentials are stripped while the host/path stay visible for
16 debugging (use for URL fields, which can embed ``user:token@`` or
17 ``?token=`` credentials but are otherwise useful to see).
19 Both apply on this class or *any* of its base classes.
21 This is the central defense against credential leaks in logs: OPAL logs via
22 loguru, and most leaks come from interpolating these models into log
23 messages (e.g. ``logger.info("... {entry}", entry=entry)``) or from loguru's
24 ``serialize=True`` sink, which dumps the record as JSON and falls back to
25 ``str()`` for non-JSON objects such as pydantic models. Masking once at the
26 model layer protects every such log site at once.
28 Each redaction set is the **union** of its declarations across the whole
29 MRO, so a subclass can only ever *add* fields to redact - it can never
30 accidentally drop protection inherited from a parent. Each subclass that
31 introduces a new secret-bearing field must still list it here.
33 Note: this only affects human / log rendering. Wire serialization uses
34 ``.dict()`` / ``.json()``, which are untouched, so transport is unaffected.
36 Caveat: this masks whole *fields* of the model. It does not protect a secret
37 that is logged by reaching *into* the model (e.g. ``logger.info("{h}",
38 h=config.headers)``); avoid logging credential-bearing attributes directly.
39 """
41 #: Field names whose values may carry secrets and must be masked in repr/str.
42 _redacted_repr_fields: ClassVar[Set[str]] = set()
43 #: URL field names whose embedded credentials must be stripped via redact_url.
44 _redacted_url_fields: ClassVar[Set[str]] = set()
46 @classmethod
47 def _union_over_mro(cls, attr: str) -> Set[str]:
48 # Union across the MRO so subclasses are additive (never lose a parent's
49 # protection, even if they redeclare the set).
50 result: Set[str] = set()
51 for klass in cls.__mro__:
52 result |= klass.__dict__.get(attr, set())
53 return result
55 def __repr_args__(self):
56 redacted = type(self)._union_over_mro("_redacted_repr_fields")
57 url_redacted = type(self)._union_over_mro("_redacted_url_fields")
58 args = []
59 for key, value in super().__repr_args__():
60 if key in redacted:
61 value = "<redacted>"
62 elif key in url_redacted and isinstance(value, str):
63 value = redact_url(value)
64 args.append((key, value))
65 return args