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

1from typing import ClassVar, Set 

2 

3from opal_common.http_utils import redact_url 

4 

5 

6class RedactedReprMixin: 

7 """Mixin for pydantic (v1) models that may carry credentials. 

8 

9 Overrides ``repr()`` / ``str()`` so that sensitive fields are masked instead 

10 of rendering their real value. Two flavours of masking are supported: 

11 

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). 

18 

19 Both apply on this class or *any* of its base classes. 

20 

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. 

27 

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. 

32 

33 Note: this only affects human / log rendering. Wire serialization uses 

34 ``.dict()`` / ``.json()``, which are untouched, so transport is unaffected. 

35 

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 """ 

40 

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() 

45 

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 

54 

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