Skip to content

Commit d7b1881

Browse files
committed
feat(data-masking): support masking of Pydantic models, dataclasses, and standard classes (aws-powertools#3473)
1 parent b302139 commit d7b1881

File tree

2 files changed

+89
-1
lines changed

2 files changed

+89
-1
lines changed

Diff for: aws_lambda_powertools/utilities/data_masking/base.py

+15-1
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,18 @@
2626

2727
logger = logging.getLogger(__name__)
2828

29+
def prepare_data(data: Any) -> Any:
30+
if hasattr(data, "__dataclass_fields__"):
31+
import dataclasses
32+
return dataclasses.asdict(data)
33+
34+
if callable(getattr(data, "model_dump", None)):
35+
return data.model_dump()
36+
37+
if callable(getattr(data, "dict", None)):
38+
return data.dict()
39+
40+
return data
2941

3042
class DataMasking:
3143
"""
@@ -93,6 +105,7 @@ def encrypt(
93105
data_masker = DataMasking(provider=encryption_provider)
94106
encrypted = data_masker.encrypt({"secret": "value"})
95107
"""
108+
data = prepare_data(data)
96109
return self._apply_action(
97110
data=data,
98111
fields=None,
@@ -135,7 +148,7 @@ def decrypt(
135148
data_masker = DataMasking(provider=encryption_provider)
136149
encrypted = data_masker.decrypt(encrypted_data)
137150
"""
138-
151+
data = prepare_data(data)
139152
return self._apply_action(
140153
data=data,
141154
fields=None,
@@ -184,6 +197,7 @@ def erase(
184197
Any
185198
The data with sensitive information erased or masked.
186199
"""
200+
data = prepare_data(data)
187201
if masking_rules:
188202
return self._apply_masking_rules(data=data, masking_rules=masking_rules)
189203
else:
+74
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,74 @@
1+
import dataclasses
2+
import pytest
3+
from pydantic import BaseModel
4+
5+
from aws_lambda_powertools.utilities.data_masking.base import DataMasking
6+
from aws_lambda_powertools.utilities.data_masking.constants import DATA_MASKING_STRING
7+
8+
@pytest.fixture
9+
def data_masker() -> DataMasking:
10+
return DataMasking()
11+
12+
# ---------------------------
13+
# Test with a Pydantic model
14+
# ---------------------------
15+
class MyPydanticModel(BaseModel):
16+
name: str
17+
age: int
18+
19+
def test_erase_on_pydantic_model(data_masker):
20+
# GIVEN a Pydantic model instance
21+
model_instance = MyPydanticModel(name="powertools", age=5)
22+
23+
# WHEN calling erase with fields=["age"]
24+
result = data_masker.erase(model_instance, fields=["age"])
25+
26+
# THEN the result should be a dict with the "age" field masked
27+
assert isinstance(result, dict)
28+
assert result["age"] == DATA_MASKING_STRING
29+
assert result["name"] == "powertools"
30+
31+
32+
# ---------------------------
33+
# Test with a dataclass
34+
# ---------------------------
35+
@dataclasses.dataclass
36+
class MyDataClass:
37+
name: str
38+
age: int
39+
40+
def test_erase_on_dataclass(data_masker):
41+
# GIVEN a dataclass instance
42+
dc_instance = MyDataClass(name="powertools", age=5)
43+
44+
# WHEN calling erase with fields=["age"]
45+
result = data_masker.erase(dc_instance, fields=["age"])
46+
47+
# THEN the result should be a dict with the "age" field masked
48+
assert isinstance(result, dict)
49+
assert result["age"] == DATA_MASKING_STRING
50+
assert result["name"] == "powertools"
51+
52+
53+
# ---------------------------
54+
# Test with a custom class that implements dict()
55+
# ---------------------------
56+
class MyCustomClass:
57+
def __init__(self, name, age):
58+
self.name = name
59+
self.age = age
60+
61+
def dict(self):
62+
return {"name": self.name, "age": self.age}
63+
64+
def test_erase_on_custom_class(data_masker):
65+
# GIVEN an instance of a custom class with a dict() method
66+
custom_instance = MyCustomClass("powertools", 5)
67+
68+
# WHEN calling erase with fields=["age"]
69+
result = data_masker.erase(custom_instance, fields=["age"])
70+
71+
# THEN the result should be a dict with the "age" field masked
72+
assert isinstance(result, dict)
73+
assert result["age"] == DATA_MASKING_STRING
74+
assert result["name"] == "powertools"

0 commit comments

Comments
 (0)