Skip to content

Commit 35fb001

Browse files
seshubawsroger-zhanggleandrodamascenaRoy Assisrubenfonseca
authoredSep 27, 2023
feat(data_masking): add new sensitive data masking utility (#2197)
Co-authored-by: Roger Zhang <[email protected]> Co-authored-by: Leandro Damascena <[email protected]> Co-authored-by: Roy Assis <[email protected]> Co-authored-by: Ruben Fonseca <[email protected]> Co-authored-by: Roger Zhang <[email protected]> Co-authored-by: aal80 <[email protected]> Co-authored-by: Seshu Brahma <[email protected]> Co-authored-by: Heitor Lessa <[email protected]>
1 parent e441c0b commit 35fb001

File tree

38 files changed

+2435
-160
lines changed

38 files changed

+2435
-160
lines changed
 

‎Makefile

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,13 +7,13 @@ target:
77
dev:
88
pip install --upgrade pip pre-commit poetry
99
@$(MAKE) dev-version-plugin
10-
poetry install --extras "all"
10+
poetry install --extras "all datamasking-aws-sdk"
1111
pre-commit install
1212

1313
dev-gitpod:
1414
pip install --upgrade pip poetry
1515
@$(MAKE) dev-version-plugin
16-
poetry install --extras "all"
16+
poetry install --extras "all datamasking-aws-sdk"
1717
pre-commit install
1818

1919
format:
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from aws_lambda_powertools.utilities.data_masking.base import DataMasking
2+
3+
__all__ = [
4+
"DataMasking",
5+
]
Lines changed: 170 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,170 @@
1+
import json
2+
from typing import Optional, Union
3+
4+
from aws_lambda_powertools.utilities.data_masking.provider import BaseProvider
5+
6+
7+
class DataMasking:
8+
"""
9+
A utility class for masking sensitive data within various data types.
10+
11+
This class provides methods for masking sensitive information, such as personal
12+
identifiers or confidential data, within different data types such as strings,
13+
dictionaries, lists, and more. It helps protect sensitive information while
14+
preserving the structure of the original data.
15+
16+
Usage:
17+
Instantiate an object of this class and use its methods to mask sensitive data
18+
based on the data type. Supported data types include strings, dictionaries,
19+
and more.
20+
21+
Example:
22+
```
23+
from aws_lambda_powertools.utilities.data_masking.base import DataMasking
24+
25+
def lambda_handler(event, context):
26+
masker = DataMasking()
27+
28+
data = {
29+
"project": "powertools",
30+
"sensitive": "xxxxxxxxxx"
31+
}
32+
33+
masked = masker.mask(data,fields=["sensitive"])
34+
35+
return masked
36+
37+
```
38+
"""
39+
40+
def __init__(self, provider: Optional[BaseProvider] = None):
41+
self.provider = provider or BaseProvider()
42+
43+
def encrypt(self, data, fields=None, **provider_options):
44+
return self._apply_action(data, fields, self.provider.encrypt, **provider_options)
45+
46+
def decrypt(self, data, fields=None, **provider_options):
47+
return self._apply_action(data, fields, self.provider.decrypt, **provider_options)
48+
49+
def mask(self, data, fields=None, **provider_options):
50+
return self._apply_action(data, fields, self.provider.mask, **provider_options)
51+
52+
def _apply_action(self, data, fields, action, **provider_options):
53+
"""
54+
Helper method to determine whether to apply a given action to the entire input data
55+
or to specific fields if the 'fields' argument is specified.
56+
57+
Parameters
58+
----------
59+
data : any
60+
The input data to process.
61+
fields : Optional[List[any]] = None
62+
A list of fields to apply the action to. If 'None', the action is applied to the entire 'data'.
63+
action : Callable
64+
The action to apply to the data. It should be a callable that performs an operation on the data
65+
and returns the modified value.
66+
67+
Returns
68+
-------
69+
any
70+
The modified data after applying the action.
71+
"""
72+
73+
if fields is not None:
74+
return self._apply_action_to_fields(data, fields, action, **provider_options)
75+
else:
76+
return action(data, **provider_options)
77+
78+
def _apply_action_to_fields(
79+
self,
80+
data: Union[dict, str],
81+
fields: list,
82+
action,
83+
**provider_options,
84+
) -> Union[dict, str]:
85+
"""
86+
This method takes the input data, which can be either a dictionary or a JSON string,
87+
and applies a mask, an encryption, or a decryption to the specified fields.
88+
89+
Parameters
90+
----------
91+
data : Union[dict, str])
92+
The input data to process. It can be either a dictionary or a JSON string.
93+
fields : List
94+
A list of fields to apply the action to. Each field can be specified as a string or
95+
a list of strings representing nested keys in the dictionary.
96+
action : Callable
97+
The action to apply to the fields. It should be a callable that takes the current
98+
value of the field as the first argument and any additional arguments that might be required
99+
for the action. It performs an operation on the current value using the provided arguments and
100+
returns the modified value.
101+
**provider_options:
102+
Additional keyword arguments to pass to the 'action' function.
103+
104+
Returns
105+
-------
106+
dict
107+
The modified dictionary after applying the action to the
108+
specified fields.
109+
110+
Raises
111+
-------
112+
ValueError
113+
If 'fields' parameter is None.
114+
TypeError
115+
If the 'data' parameter is not a traversable type
116+
117+
Example
118+
-------
119+
```python
120+
>>> data = {'a': {'b': {'c': 1}}, 'x': {'y': 2}}
121+
>>> fields = ['a.b.c', 'a.x.y']
122+
# The function will transform the value at 'a.b.c' (1) and 'a.x.y' (2)
123+
# and store the result as:
124+
new_dict = {'a': {'b': {'c': 'transformed_value'}}, 'x': {'y': 'transformed_value'}}
125+
```
126+
"""
127+
128+
if fields is None:
129+
raise ValueError("No fields specified.")
130+
131+
if isinstance(data, str):
132+
# Parse JSON string as dictionary
133+
my_dict_parsed = json.loads(data)
134+
elif isinstance(data, dict):
135+
# In case their data has keys that are not strings (i.e. ints), convert it all into a JSON string
136+
my_dict_parsed = json.dumps(data)
137+
# Turn back into dict so can parse it
138+
my_dict_parsed = json.loads(my_dict_parsed)
139+
else:
140+
raise TypeError(
141+
f"Unsupported data type for 'data' parameter. Expected a traversable type, but got {type(data)}.",
142+
)
143+
144+
# For example: ['a.b.c'] in ['a.b.c', 'a.x.y']
145+
for nested_key in fields:
146+
# Prevent overriding loop variable
147+
curr_nested_key = nested_key
148+
149+
# If the nested_key is not a string, convert it to a string representation
150+
if not isinstance(curr_nested_key, str):
151+
curr_nested_key = json.dumps(curr_nested_key)
152+
153+
# Split the nested key string into a list of nested keys
154+
# ['a.b.c'] -> ['a', 'b', 'c']
155+
keys = curr_nested_key.split(".")
156+
157+
# Initialize a current dictionary to the root dictionary
158+
curr_dict = my_dict_parsed
159+
160+
# Traverse the dictionary hierarchy by iterating through the list of nested keys
161+
for key in keys[:-1]:
162+
curr_dict = curr_dict[key]
163+
164+
# Retrieve the final value of the nested field
165+
valtochange = curr_dict[(keys[-1])]
166+
167+
# Apply the specified 'action' to the target value
168+
curr_dict[keys[-1]] = action(valtochange, **provider_options)
169+
170+
return my_dict_parsed
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
DATA_MASKING_STRING: str = "*****"
2+
CACHE_CAPACITY: int = 100
3+
MAX_CACHE_AGE_SECONDS: float = 300.0
4+
MAX_MESSAGES_ENCRYPTED: int = 200
5+
# NOTE: You can also set max messages/bytes per data key
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from aws_lambda_powertools.utilities.data_masking.provider.base import BaseProvider
2+
3+
__all__ = [
4+
"BaseProvider",
5+
]
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
import json
2+
from typing import Any
3+
4+
from aws_lambda_powertools.utilities.data_masking.constants import DATA_MASKING_STRING
5+
6+
7+
class BaseProvider:
8+
"""
9+
When you try to create an instance of a subclass that does not implement the encrypt method,
10+
you will get a NotImplementedError with a message that says the method is not implemented:
11+
"""
12+
13+
def __init__(self, json_serializer=None, json_deserializer=None) -> None:
14+
self.json_serializer = json_serializer or self.default_json_serializer
15+
self.json_deserializer = json_deserializer or self.default_json_deserializer
16+
17+
def default_json_serializer(self, data):
18+
return json.dumps(data).encode("utf-8")
19+
20+
def default_json_deserializer(self, data):
21+
return json.loads(data.decode("utf-8"))
22+
23+
def encrypt(self, data) -> str:
24+
raise NotImplementedError("Subclasses must implement encrypt()")
25+
26+
def decrypt(self, data) -> Any:
27+
raise NotImplementedError("Subclasses must implement decrypt()")
28+
29+
def mask(self, data) -> Any:
30+
if isinstance(data, (str, dict, bytes)):
31+
return DATA_MASKING_STRING
32+
elif isinstance(data, (list, tuple, set)):
33+
return type(data)([DATA_MASKING_STRING] * len(data))
34+
return DATA_MASKING_STRING
Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
from aws_lambda_powertools.utilities.data_masking.provider.kms.aws_encryption_sdk import AwsEncryptionSdkProvider
2+
3+
__all__ = [
4+
"AwsEncryptionSdkProvider",
5+
]
Lines changed: 177 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,177 @@
1+
from __future__ import annotations
2+
3+
import base64
4+
from typing import Any, Callable, Dict, List
5+
6+
import botocore
7+
from aws_encryption_sdk import (
8+
CachingCryptoMaterialsManager,
9+
EncryptionSDKClient,
10+
LocalCryptoMaterialsCache,
11+
StrictAwsKmsMasterKeyProvider,
12+
)
13+
14+
from aws_lambda_powertools.shared.user_agent import register_feature_to_botocore_session
15+
from aws_lambda_powertools.utilities.data_masking.constants import (
16+
CACHE_CAPACITY,
17+
MAX_CACHE_AGE_SECONDS,
18+
MAX_MESSAGES_ENCRYPTED,
19+
)
20+
from aws_lambda_powertools.utilities.data_masking.provider import BaseProvider
21+
22+
23+
class ContextMismatchError(Exception):
24+
def __init__(self, key):
25+
super().__init__(f"Encryption Context does not match expected value for key: {key}")
26+
self.key = key
27+
28+
29+
class AwsEncryptionSdkProvider(BaseProvider):
30+
"""
31+
The AwsEncryptionSdkProvider is used as a provider for the DataMasking class.
32+
33+
This provider allows you to perform data masking using the AWS Encryption SDK
34+
for encryption and decryption. It integrates with the DataMasking class to
35+
securely encrypt and decrypt sensitive data.
36+
37+
Usage Example:
38+
```
39+
from aws_lambda_powertools.utilities.data_masking import DataMasking
40+
from aws_lambda_powertools.utilities.data_masking.providers.kms.aws_encryption_sdk import (
41+
AwsEncryptionSdkProvider,
42+
)
43+
44+
45+
def lambda_handler(event, context):
46+
provider = AwsEncryptionSdkProvider(["arn:aws:kms:us-east-1:0123456789012:key/key-id"])
47+
masker = DataMasking(provider=provider)
48+
49+
data = {
50+
"project": "powertools",
51+
"sensitive": "xxxxxxxxxx"
52+
}
53+
54+
masked = masker.encrypt(data,fields=["sensitive"])
55+
56+
return masked
57+
58+
```
59+
"""
60+
61+
def __init__(
62+
self,
63+
keys: List[str],
64+
key_provider=None,
65+
local_cache_capacity: int = CACHE_CAPACITY,
66+
max_cache_age_seconds: float = MAX_CACHE_AGE_SECONDS,
67+
max_messages_encrypted: int = MAX_MESSAGES_ENCRYPTED,
68+
json_serializer: Callable | None = None,
69+
json_deserializer: Callable | None = None,
70+
):
71+
super().__init__(json_serializer=json_serializer, json_deserializer=json_deserializer)
72+
73+
self._key_provider = key_provider or KMSKeyProvider(
74+
keys=keys,
75+
local_cache_capacity=local_cache_capacity,
76+
max_cache_age_seconds=max_cache_age_seconds,
77+
max_messages_encrypted=max_messages_encrypted,
78+
json_serializer=self.json_serializer,
79+
json_deserializer=self.json_deserializer,
80+
)
81+
82+
def encrypt(self, data: bytes | str | Dict | int, **provider_options) -> str:
83+
return self._key_provider.encrypt(data=data, **provider_options)
84+
85+
def decrypt(self, data: str, **provider_options) -> Any:
86+
return self._key_provider.decrypt(data=data, **provider_options)
87+
88+
89+
class KMSKeyProvider:
90+
91+
"""
92+
The KMSKeyProvider is responsible for assembling an AWS Key Management Service (KMS)
93+
client, a caching mechanism, and a keyring for secure key management and data encryption.
94+
"""
95+
96+
def __init__(
97+
self,
98+
keys: List[str],
99+
json_serializer: Callable,
100+
json_deserializer: Callable,
101+
local_cache_capacity: int = CACHE_CAPACITY,
102+
max_cache_age_seconds: float = MAX_CACHE_AGE_SECONDS,
103+
max_messages_encrypted: int = MAX_MESSAGES_ENCRYPTED,
104+
):
105+
session = botocore.session.Session()
106+
register_feature_to_botocore_session(session, "data-masking")
107+
108+
self.json_serializer = json_serializer
109+
self.json_deserializer = json_deserializer
110+
self.client = EncryptionSDKClient()
111+
self.keys = keys
112+
self.cache = LocalCryptoMaterialsCache(local_cache_capacity)
113+
self.key_provider = StrictAwsKmsMasterKeyProvider(key_ids=self.keys, botocore_session=session)
114+
self.cache_cmm = CachingCryptoMaterialsManager(
115+
master_key_provider=self.key_provider,
116+
cache=self.cache,
117+
max_age=max_cache_age_seconds,
118+
max_messages_encrypted=max_messages_encrypted,
119+
)
120+
121+
def encrypt(self, data: bytes | str | Dict | float, **provider_options) -> str:
122+
"""
123+
Encrypt data using the AwsEncryptionSdkProvider.
124+
125+
Parameters
126+
-------
127+
data : Union[bytes, str]
128+
The data to be encrypted.
129+
provider_options
130+
Additional options for the aws_encryption_sdk.EncryptionSDKClient
131+
132+
Returns
133+
-------
134+
ciphertext : str
135+
The encrypted data, as a base64-encoded string.
136+
"""
137+
data_encoded = self.json_serializer(data)
138+
ciphertext, _ = self.client.encrypt(
139+
source=data_encoded,
140+
materials_manager=self.cache_cmm,
141+
**provider_options,
142+
)
143+
ciphertext = base64.b64encode(ciphertext).decode()
144+
return ciphertext
145+
146+
def decrypt(self, data: str, **provider_options) -> Any:
147+
"""
148+
Decrypt data using AwsEncryptionSdkProvider.
149+
150+
Parameters
151+
-------
152+
data : Union[bytes, str]
153+
The encrypted data, as a base64-encoded string
154+
provider_options
155+
Additional options for the aws_encryption_sdk.EncryptionSDKClient
156+
157+
Returns
158+
-------
159+
ciphertext : bytes
160+
The decrypted data in bytes
161+
"""
162+
ciphertext_decoded = base64.b64decode(data)
163+
164+
expected_context = provider_options.pop("encryption_context", {})
165+
166+
ciphertext, decryptor_header = self.client.decrypt(
167+
source=ciphertext_decoded,
168+
key_provider=self.key_provider,
169+
**provider_options,
170+
)
171+
172+
for key, value in expected_context.items():
173+
if decryptor_header.encryption_context.get(key) != value:
174+
raise ContextMismatchError(key)
175+
176+
ciphertext = self.json_deserializer(ciphertext)
177+
return ciphertext

‎mypy.ini

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,12 @@ disable_error_code = annotation-unchecked
1212
[mypy-jmespath]
1313
ignore_missing_imports=True
1414

15+
[mypy-aws_encryption_sdk]
16+
ignore_missing_imports=True
17+
18+
[mypy-sentry_sdk]
19+
ignore_missing_imports=True
20+
1521
[mypy-jmespath.exceptions]
1622
ignore_missing_imports=True
1723

‎poetry.lock

Lines changed: 262 additions & 113 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

‎pyproject.toml

Lines changed: 49 additions & 31 deletions
Original file line numberDiff line numberDiff line change
@@ -4,21 +4,31 @@ version = "2.25.1"
44
description = "Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity."
55
authors = ["Amazon Web Services"]
66
include = ["aws_lambda_powertools/py.typed", "THIRD-PARTY-LICENSES"]
7-
classifiers=[
8-
"Development Status :: 5 - Production/Stable",
9-
"Intended Audience :: Developers",
10-
"License :: OSI Approved :: MIT No Attribution License (MIT-0)",
11-
"Natural Language :: English",
12-
"Programming Language :: Python :: 3.7",
13-
"Programming Language :: Python :: 3.8",
14-
"Programming Language :: Python :: 3.9",
15-
"Programming Language :: Python :: 3.10",
16-
"Programming Language :: Python :: 3.11",
7+
classifiers = [
8+
"Development Status :: 5 - Production/Stable",
9+
"Intended Audience :: Developers",
10+
"License :: OSI Approved :: MIT No Attribution License (MIT-0)",
11+
"Natural Language :: English",
12+
"Programming Language :: Python :: 3.7",
13+
"Programming Language :: Python :: 3.8",
14+
"Programming Language :: Python :: 3.9",
15+
"Programming Language :: Python :: 3.10",
16+
"Programming Language :: Python :: 3.11",
1717
]
1818
repository = "https://github.com/aws-powertools/powertools-lambda-python"
1919
documentation = "https://docs.powertools.aws.dev/lambda/python/"
2020
readme = "README.md"
21-
keywords = ["aws_lambda_powertools", "aws", "tracing", "logging", "lambda", "powertools", "feature_flags", "idempotency", "middleware"]
21+
keywords = [
22+
"aws_lambda_powertools",
23+
"aws",
24+
"tracing",
25+
"logging",
26+
"lambda",
27+
"powertools",
28+
"feature_flags",
29+
"idempotency",
30+
"middleware",
31+
]
2232
# MIT-0 is not recognized as an existing license from poetry.
2333
# By using `MIT` as a license value, a `License :: OSI Approved :: MIT License` classifier is added to the classifiers list.
2434
license = "MIT"
@@ -35,6 +45,7 @@ pydantic = { version = "^1.8.2", optional = true }
3545
boto3 = { version = "^1.20.32", optional = true }
3646
typing-extensions = "^4.6.2"
3747
datadog-lambda = { version = "^4.77.0", optional = true }
48+
aws-encryption-sdk = { version = "^3.1.1", optional = true }
3849

3950
[tool.poetry.dev-dependencies]
4051
coverage = {extras = ["toml"], version = "^7.2"}
@@ -86,7 +97,8 @@ tracer = ["aws-xray-sdk"]
8697
all = ["pydantic", "aws-xray-sdk", "fastjsonschema"]
8798
# allow customers to run code locally without emulators (SAM CLI, etc.)
8899
aws-sdk = ["boto3"]
89-
datadog=["datadog-lambda"]
100+
datadog = ["datadog-lambda"]
101+
datamasking-aws-sdk = ["aws-encryption-sdk"]
90102

91103
[tool.poetry.group.dev.dependencies]
92104
cfn-lint = "0.80.3"
@@ -96,10 +108,16 @@ httpx = ">=0.23.3,<0.25.0"
96108
sentry-sdk = "^1.22.2"
97109
ruff = ">=0.0.272,<0.0.292"
98110
retry2 = "^0.9.5"
111+
pytest-socket = "^0.6.0"
99112

100113
[tool.coverage.run]
101114
source = ["aws_lambda_powertools"]
102-
omit = ["tests/*", "aws_lambda_powertools/exceptions/*", "aws_lambda_powertools/utilities/parser/types.py", "aws_lambda_powertools/utilities/jmespath_utils/envelopes.py"]
115+
omit = [
116+
"tests/*",
117+
"aws_lambda_powertools/exceptions/*",
118+
"aws_lambda_powertools/utilities/parser/types.py",
119+
"aws_lambda_powertools/utilities/jmespath_utils/envelopes.py",
120+
]
103121
branch = true
104122

105123
[tool.coverage.html]
@@ -109,26 +127,26 @@ title = "Powertools for AWS Lambda (Python) Test Coverage"
109127
[tool.coverage.report]
110128
fail_under = 90
111129
exclude_lines = [
112-
# Have to re-enable the standard pragma
113-
"pragma: no cover",
130+
# Have to re-enable the standard pragma
131+
"pragma: no cover",
114132

115-
# Don't complain about missing debug-only code:
116-
"def __repr__",
117-
"if self.debug",
133+
# Don't complain about missing debug-only code:
134+
"def __repr__",
135+
"if self.debug",
118136

119-
# Don't complain if tests don't hit defensive assertion code:
120-
"raise AssertionError",
121-
"raise NotImplementedError",
137+
# Don't complain if tests don't hit defensive assertion code:
138+
"raise AssertionError",
139+
"raise NotImplementedError",
122140

123-
# Don't complain if non-runnable code isn't run:
124-
"if 0:",
125-
"if __name__ == .__main__.:",
141+
# Don't complain if non-runnable code isn't run:
142+
"if 0:",
143+
"if __name__ == .__main__.:",
126144

127-
# Ignore runtime type checking
128-
"if TYPE_CHECKING:",
145+
# Ignore runtime type checking
146+
"if TYPE_CHECKING:",
129147

130-
# Ignore type function overload
131-
"@overload",
148+
# Ignore type function overload
149+
"@overload",
132150
]
133151

134152
[tool.isort]
@@ -161,16 +179,16 @@ minversion = "6.0"
161179
addopts = "-ra -vv"
162180
testpaths = "./tests"
163181
markers = [
164-
"perf: marks perf tests to be deselected (deselect with '-m \"not perf\"')",
182+
"perf: marks perf tests to be deselected (deselect with '-m \"not perf\"')",
165183
]
166184

167185
# MAINTENANCE: Remove these lines when drop support to Pydantic v1
168-
filterwarnings=[
186+
filterwarnings = [
169187
"ignore:.*The `parse_obj` method is deprecated*:DeprecationWarning",
170188
"ignore:.*The `parse_raw` method is deprecated*:DeprecationWarning",
171189
"ignore:.*load_str_bytes is deprecated*:DeprecationWarning",
172190
"ignore:.*The `dict` method is deprecated; use `model_dump` instead*:DeprecationWarning",
173-
"ignore:.*Pydantic V1 style `@validator` validators are deprecated*:DeprecationWarning"
191+
"ignore:.*Pydantic V1 style `@validator` validators are deprecated*:DeprecationWarning",
174192
]
175193

176194
[build-system]

‎tests/e2e/data_masking/__init__.py

Whitespace-only changes.

‎tests/e2e/data_masking/conftest.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
import pytest
2+
3+
from tests.e2e.data_masking.infrastructure import DataMaskingStack
4+
5+
6+
@pytest.fixture(autouse=True, scope="package")
7+
def infrastructure():
8+
"""Setup and teardown logic for E2E test infrastructure
9+
10+
Yields
11+
------
12+
Dict[str, str]
13+
CloudFormation Outputs from deployed infrastructure
14+
"""
15+
stack = DataMaskingStack()
16+
try:
17+
yield stack.deploy()
18+
finally:
19+
stack.delete()
Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,23 @@
1+
from aws_lambda_powertools import Logger
2+
from aws_lambda_powertools.utilities.data_masking import DataMasking
3+
from aws_lambda_powertools.utilities.data_masking.provider.kms.aws_encryption_sdk import AwsEncryptionSdkProvider
4+
5+
logger = Logger()
6+
7+
8+
@logger.inject_lambda_context
9+
def lambda_handler(event, context):
10+
# Generating logs for test_encryption_in_logs test
11+
message, append_keys = event.get("message", ""), event.get("append_keys", {})
12+
logger.append_keys(**append_keys)
13+
logger.info(message)
14+
15+
# Encrypting data for test_encryption_in_handler test
16+
kms_key = event.get("kms_key", "")
17+
data_masker = DataMasking(provider=AwsEncryptionSdkProvider(keys=[kms_key]))
18+
value = [1, 2, "string", 4.5]
19+
encrypted_data = data_masker.encrypt(value)
20+
response = {}
21+
response["encrypted_data"] = encrypted_data
22+
23+
return response
Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,20 @@
1+
import aws_cdk.aws_kms as kms
2+
from aws_cdk import CfnOutput, Duration
3+
from aws_cdk import aws_iam as iam
4+
5+
from tests.e2e.utils.infrastructure import BaseInfrastructure
6+
7+
8+
class DataMaskingStack(BaseInfrastructure):
9+
def create_resources(self):
10+
functions = self.create_lambda_functions(function_props={"timeout": Duration.seconds(10)})
11+
12+
key1 = kms.Key(self.stack, "MyKMSKey1", description="My KMS Key1")
13+
CfnOutput(self.stack, "KMSKey1Arn", value=key1.key_arn, description="ARN of the created KMS Key1")
14+
15+
key2 = kms.Key(self.stack, "MyKMSKey2", description="My KMS Key2")
16+
CfnOutput(self.stack, "KMSKey2Arn", value=key2.key_arn, description="ARN of the created KMS Key2")
17+
18+
functions["BasicHandler"].add_to_role_policy(
19+
iam.PolicyStatement(effect=iam.Effect.ALLOW, actions=["kms:*"], resources=[key1.key_arn, key2.key_arn]),
20+
)
Lines changed: 151 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,151 @@
1+
import json
2+
from uuid import uuid4
3+
4+
import pytest
5+
from aws_encryption_sdk.exceptions import DecryptKeyError
6+
7+
from aws_lambda_powertools.utilities.data_masking import DataMasking
8+
from aws_lambda_powertools.utilities.data_masking.provider.kms.aws_encryption_sdk import (
9+
AwsEncryptionSdkProvider,
10+
ContextMismatchError,
11+
)
12+
from tests.e2e.utils import data_fetcher
13+
14+
15+
@pytest.fixture
16+
def basic_handler_fn(infrastructure: dict) -> str:
17+
return infrastructure.get("BasicHandler", "")
18+
19+
20+
@pytest.fixture
21+
def basic_handler_fn_arn(infrastructure: dict) -> str:
22+
return infrastructure.get("BasicHandlerArn", "")
23+
24+
25+
@pytest.fixture
26+
def kms_key1_arn(infrastructure: dict) -> str:
27+
return infrastructure.get("KMSKey1Arn", "")
28+
29+
30+
@pytest.fixture
31+
def kms_key2_arn(infrastructure: dict) -> str:
32+
return infrastructure.get("KMSKey2Arn", "")
33+
34+
35+
@pytest.fixture
36+
def data_masker(kms_key1_arn) -> DataMasking:
37+
return DataMasking(provider=AwsEncryptionSdkProvider(keys=[kms_key1_arn]))
38+
39+
40+
@pytest.mark.xdist_group(name="data_masking")
41+
def test_encryption(data_masker):
42+
# GIVEN an instantiation of DataMasking with the AWS encryption provider
43+
44+
# AWS Encryption SDK encrypt method only takes in bytes or strings
45+
value = [1, 2, "string", 4.5]
46+
47+
# WHEN encrypting and then decrypting the encrypted data
48+
encrypted_data = data_masker.encrypt(value)
49+
decrypted_data = data_masker.decrypt(encrypted_data)
50+
51+
# THEN the result is the original input data
52+
assert decrypted_data == value
53+
54+
55+
@pytest.mark.xdist_group(name="data_masking")
56+
def test_encryption_context(data_masker):
57+
# GIVEN an instantiation of DataMasking with the AWS encryption provider
58+
59+
value = [1, 2, "string", 4.5]
60+
context = {"this": "is_secure"}
61+
62+
# WHEN encrypting and then decrypting the encrypted data with an encryption_context
63+
encrypted_data = data_masker.encrypt(value, encryption_context=context)
64+
decrypted_data = data_masker.decrypt(encrypted_data, encryption_context=context)
65+
66+
# THEN the result is the original input data
67+
assert decrypted_data == value
68+
69+
70+
@pytest.mark.xdist_group(name="data_masking")
71+
def test_encryption_context_mismatch(data_masker):
72+
# GIVEN an instantiation of DataMasking with the AWS encryption provider
73+
74+
value = [1, 2, "string", 4.5]
75+
76+
# WHEN encrypting with a encryption_context
77+
encrypted_data = data_masker.encrypt(value, encryption_context={"this": "is_secure"})
78+
79+
# THEN decrypting with a different encryption_context should raise a ContextMismatchError
80+
with pytest.raises(ContextMismatchError):
81+
data_masker.decrypt(encrypted_data, encryption_context={"not": "same_context"})
82+
83+
84+
@pytest.mark.xdist_group(name="data_masking")
85+
def test_encryption_no_context_fail(data_masker):
86+
# GIVEN an instantiation of DataMasking with the AWS encryption provider
87+
88+
value = [1, 2, "string", 4.5]
89+
90+
# WHEN encrypting with no encryption_context
91+
encrypted_data = data_masker.encrypt(value)
92+
93+
# THEN decrypting with an encryption_context should raise a ContextMismatchError
94+
with pytest.raises(ContextMismatchError):
95+
data_masker.decrypt(encrypted_data, encryption_context={"this": "is_secure"})
96+
97+
98+
@pytest.mark.xdist_group(name="data_masking")
99+
def test_encryption_decryption_key_mismatch(data_masker, kms_key2_arn):
100+
# GIVEN an instantiation of DataMasking with the AWS encryption provider with a certain key
101+
102+
# WHEN encrypting and then decrypting the encrypted data
103+
value = [1, 2, "string", 4.5]
104+
encrypted_data = data_masker.encrypt(value)
105+
106+
# THEN when decrypting with a different key it should fail
107+
data_masker_key2 = DataMasking(provider=AwsEncryptionSdkProvider(keys=[kms_key2_arn]))
108+
109+
with pytest.raises(DecryptKeyError):
110+
data_masker_key2.decrypt(encrypted_data)
111+
112+
113+
@pytest.mark.xdist_group(name="data_masking")
114+
def test_encryption_in_logs(data_masker, basic_handler_fn, basic_handler_fn_arn, kms_key1_arn):
115+
# GIVEN an instantiation of DataMasking with the AWS encryption provider
116+
117+
# WHEN encrypting a value and logging it
118+
value = [1, 2, "string", 4.5]
119+
encrypted_data = data_masker.encrypt(value)
120+
message = encrypted_data
121+
custom_key = "order_id"
122+
additional_keys = {custom_key: f"{uuid4()}"}
123+
payload = json.dumps({"message": message, "kms_key": kms_key1_arn, "append_keys": additional_keys})
124+
125+
_, execution_time = data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn, payload=payload)
126+
data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn, payload=payload)
127+
128+
logs = data_fetcher.get_logs(function_name=basic_handler_fn, start_time=execution_time, minimum_log_entries=2)
129+
130+
# THEN decrypting it from the logs should show the original value
131+
for log in logs.get_log(key=custom_key):
132+
encrypted_data = log.message
133+
decrypted_data = data_masker.decrypt(encrypted_data)
134+
assert decrypted_data == value
135+
136+
137+
@pytest.mark.xdist_group(name="data_masking")
138+
def test_encryption_in_handler(data_masker, basic_handler_fn_arn, kms_key1_arn):
139+
# GIVEN a lambda_handler with an instantiation the AWS encryption provider data masker
140+
141+
payload = {"kms_key": kms_key1_arn}
142+
143+
# WHEN the handler is invoked to encrypt data
144+
handler_result, _ = data_fetcher.get_lambda_response(lambda_arn=basic_handler_fn_arn, payload=json.dumps(payload))
145+
146+
response = json.loads(handler_result["Payload"].read())
147+
encrypted_data = response["encrypted_data"]
148+
decrypted_data = data_masker.decrypt(encrypted_data)
149+
150+
# THEN decrypting the encrypted data from the response should result in the original value
151+
assert decrypted_data == [1, 2, "string", 4.5]

‎tests/e2e/utils/lambda_layer/powertools_layer.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
1-
import logging
21
import subprocess
32
from pathlib import Path
3+
from typing import List
44

55
from aws_cdk.aws_lambda import Architecture
66
from checksumdir import dirhash
@@ -9,18 +9,20 @@
99
from tests.e2e.utils.constants import CDK_OUT_PATH, SOURCE_CODE_ROOT_PATH
1010
from tests.e2e.utils.lambda_layer.base import BaseLocalLambdaLayer
1111

12-
logger = logging.getLogger(__name__)
13-
1412

1513
class LocalLambdaPowertoolsLayer(BaseLocalLambdaLayer):
1614
IGNORE_EXTENSIONS = ["pyc"]
15+
ARCHITECTURE_PLATFORM_MAPPING = {
16+
Architecture.X86_64.name: ("manylinux_2_17_x86_64", "manylinux_2_28_x86_64"),
17+
Architecture.ARM_64.name: ("manylinux_2_17_aarch64", "manylinux_2_28_aarch64"),
18+
}
1719

1820
def __init__(self, output_dir: Path = CDK_OUT_PATH, architecture: Architecture = Architecture.X86_64):
1921
super().__init__(output_dir)
2022
self.package = f"{SOURCE_CODE_ROOT_PATH}[all]"
2123

22-
platform_name = self._resolve_platform(architecture)
23-
self.build_args = f"--platform {platform_name} --only-binary=:all: --upgrade"
24+
self.platform_args = self._resolve_platform(architecture)
25+
self.build_args = f"{self.platform_args} --only-binary=:all: --upgrade"
2426
self.build_command = f"python -m pip install {self.package} {self.build_args} --target {self.target_dir}"
2527
self.cleanup_command = (
2628
f"rm -rf {self.target_dir}/boto* {self.target_dir}/s3transfer* && "
@@ -62,16 +64,20 @@ def _has_source_changed(self) -> bool:
6264
return False
6365

6466
def _resolve_platform(self, architecture: Architecture) -> str:
65-
"""Returns the correct plaform name for the manylinux project (see PEP 599)
67+
"""Returns the correct pip platform tag argument for the manylinux project (see PEP 599)
6668
6769
Returns
6870
-------
69-
platform_name : str
70-
The platform tag
71+
str
72+
pip's platform argument, e.g., --platform manylinux_2_17_x86_64 --platform manylinux_2_28_x86_64
7173
"""
72-
if architecture.name == Architecture.X86_64.name:
73-
return "manylinux1_x86_64"
74-
elif architecture.name == Architecture.ARM_64.name:
75-
return "manylinux2014_aarch64"
76-
else:
77-
raise ValueError(f"unknown architecture {architecture.name}")
74+
platforms = self.ARCHITECTURE_PLATFORM_MAPPING.get(architecture.name)
75+
if not platforms:
76+
raise ValueError(
77+
f"unknown architecture {architecture.name}. Supported: {self.ARCHITECTURE_PLATFORM_MAPPING.keys()}",
78+
)
79+
80+
return self._build_platform_args(platforms)
81+
82+
def _build_platform_args(self, platforms: List[str]):
83+
return " ".join([f"--platform {platform}" for platform in platforms])

‎tests/functional/data_masking/__init__.py

Whitespace-only changes.
Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,6 @@
1+
from pytest_socket import disable_socket
2+
3+
4+
def pytest_runtest_setup():
5+
"""Disable Unix and TCP sockets for Data masking tests"""
6+
disable_socket()
Lines changed: 283 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,283 @@
1+
from __future__ import annotations
2+
3+
import base64
4+
import json
5+
from typing import Any, Callable, Dict, Union
6+
7+
import pytest
8+
9+
from aws_lambda_powertools.utilities.data_masking import DataMasking
10+
from aws_lambda_powertools.utilities.data_masking.constants import DATA_MASKING_STRING
11+
from aws_lambda_powertools.utilities.data_masking.provider import BaseProvider
12+
from aws_lambda_powertools.utilities.data_masking.provider.kms import (
13+
AwsEncryptionSdkProvider,
14+
)
15+
16+
17+
class FakeEncryptionKeyProvider(BaseProvider):
18+
def __init__(
19+
self,
20+
json_serializer: Callable[[Dict], str] | None = None,
21+
json_deserializer: Callable[[Union[Dict, str, bool, int, float]], str] | None = None,
22+
):
23+
super().__init__(json_serializer=json_serializer, json_deserializer=json_deserializer)
24+
25+
def encrypt(self, data: bytes | str, **kwargs) -> str:
26+
data = self.json_serializer(data)
27+
ciphertext = base64.b64encode(data).decode()
28+
return ciphertext
29+
30+
def decrypt(self, data: bytes, **kwargs) -> Any:
31+
ciphertext_decoded = base64.b64decode(data)
32+
ciphertext = self.json_deserializer(ciphertext_decoded)
33+
return ciphertext
34+
35+
36+
@pytest.fixture
37+
def data_masker(monkeypatch) -> DataMasking:
38+
"""DataMasking using AWS Encryption SDK Provider with a fake client"""
39+
fake_key_provider = FakeEncryptionKeyProvider()
40+
provider = AwsEncryptionSdkProvider(
41+
keys=["dummy"],
42+
key_provider=fake_key_provider,
43+
)
44+
return DataMasking(provider=provider)
45+
46+
47+
def test_mask_int(data_masker):
48+
# GIVEN an int data type
49+
50+
# WHEN mask is called with no fields argument
51+
masked_string = data_masker.mask(42)
52+
53+
# THEN the result is the data masked
54+
assert masked_string == DATA_MASKING_STRING
55+
56+
57+
def test_mask_float(data_masker):
58+
# GIVEN a float data type
59+
60+
# WHEN mask is called with no fields argument
61+
masked_string = data_masker.mask(4.2)
62+
63+
# THEN the result is the data masked
64+
assert masked_string == DATA_MASKING_STRING
65+
66+
67+
def test_mask_bool(data_masker):
68+
# GIVEN a bool data type
69+
70+
# WHEN mask is called with no fields argument
71+
masked_string = data_masker.mask(True)
72+
73+
# THEN the result is the data masked
74+
assert masked_string == DATA_MASKING_STRING
75+
76+
77+
def test_mask_none(data_masker):
78+
# GIVEN a None data type
79+
80+
# WHEN mask is called with no fields argument
81+
masked_string = data_masker.mask(None)
82+
83+
# THEN the result is the data masked
84+
assert masked_string == DATA_MASKING_STRING
85+
86+
87+
def test_mask_str(data_masker):
88+
# GIVEN a str data type
89+
90+
# WHEN mask is called with no fields argument
91+
masked_string = data_masker.mask("this is a string")
92+
93+
# THEN the result is the data masked
94+
assert masked_string == DATA_MASKING_STRING
95+
96+
97+
def test_mask_list(data_masker):
98+
# GIVEN a list data type
99+
100+
# WHEN mask is called with no fields argument
101+
masked_string = data_masker.mask([1, 2, "string", 3])
102+
103+
# THEN the result is the data masked, while maintaining type list
104+
assert masked_string == [DATA_MASKING_STRING, DATA_MASKING_STRING, DATA_MASKING_STRING, DATA_MASKING_STRING]
105+
106+
107+
def test_mask_dict(data_masker):
108+
# GIVEN a dict data type
109+
data = {
110+
"a": {
111+
"1": {"None": "hello", "four": "world"},
112+
"b": {"3": {"4": "goodbye", "e": "world"}},
113+
},
114+
}
115+
116+
# WHEN mask is called with no fields argument
117+
masked_string = data_masker.mask(data)
118+
119+
# THEN the result is the data masked
120+
assert masked_string == DATA_MASKING_STRING
121+
122+
123+
def test_mask_dict_with_fields(data_masker):
124+
# GIVEN a dict data type
125+
data = {
126+
"a": {
127+
"1": {"None": "hello", "four": "world"},
128+
"b": {"3": {"4": "goodbye", "e": "world"}},
129+
},
130+
}
131+
132+
# WHEN mask is called with a list of fields specified
133+
masked_string = data_masker.mask(data, fields=["a.1.None", "a.b.3.4"])
134+
135+
# THEN the result is only the specified fields are masked
136+
assert masked_string == {
137+
"a": {
138+
"1": {"None": DATA_MASKING_STRING, "four": "world"},
139+
"b": {"3": {"4": DATA_MASKING_STRING, "e": "world"}},
140+
},
141+
}
142+
143+
144+
def test_mask_json_dict_with_fields(data_masker):
145+
# GIVEN the data type is a json representation of a dictionary
146+
data = json.dumps(
147+
{
148+
"a": {
149+
"1": {"None": "hello", "four": "world"},
150+
"b": {"3": {"4": "goodbye", "e": "world"}},
151+
},
152+
},
153+
)
154+
155+
# WHEN mask is called with a list of fields specified
156+
masked_json_string = data_masker.mask(data, fields=["a.1.None", "a.b.3.4"])
157+
158+
# THEN the result is only the specified fields are masked
159+
assert masked_json_string == {
160+
"a": {
161+
"1": {"None": DATA_MASKING_STRING, "four": "world"},
162+
"b": {"3": {"4": DATA_MASKING_STRING, "e": "world"}},
163+
},
164+
}
165+
166+
167+
def test_encrypt_int(data_masker):
168+
# GIVEN an int data type
169+
170+
# WHEN encrypting and then decrypting the encrypted data
171+
encrypted_data = data_masker.encrypt(-1)
172+
decrypted_data = data_masker.decrypt(encrypted_data)
173+
174+
# THEN the result is the original input data
175+
assert decrypted_data == -1
176+
177+
178+
def test_encrypt_float(data_masker):
179+
# GIVEN an float data type
180+
181+
# WHEN encrypting and then decrypting the encrypted data
182+
encrypted_data = data_masker.encrypt(-1.11)
183+
decrypted_data = data_masker.decrypt(encrypted_data)
184+
185+
# THEN the result is the original input data
186+
assert decrypted_data == -1.11
187+
188+
189+
def test_encrypt_bool(data_masker):
190+
# GIVEN an bool data type
191+
192+
# WHEN encrypting and then decrypting the encrypted data
193+
encrypted_data = data_masker.encrypt(True)
194+
decrypted_data = data_masker.decrypt(encrypted_data)
195+
196+
# THEN the result is the original input data
197+
assert decrypted_data is True
198+
199+
200+
def test_encrypt_none(data_masker):
201+
# GIVEN an none data type
202+
203+
# WHEN encrypting and then decrypting the encrypted data
204+
encrypted_data = data_masker.encrypt(None)
205+
decrypted_data = data_masker.decrypt(encrypted_data)
206+
207+
# THEN the result is the original input data
208+
assert decrypted_data is None
209+
210+
211+
def test_encrypt_str(data_masker):
212+
# GIVEN an str data type
213+
214+
# WHEN encrypting and then decrypting the encrypted data
215+
encrypted_data = data_masker.encrypt("this is a string")
216+
decrypted_data = data_masker.decrypt(encrypted_data)
217+
218+
# THEN the result is the original input data
219+
assert decrypted_data == "this is a string"
220+
221+
222+
def test_encrypt_list(data_masker):
223+
# GIVEN an list data type
224+
225+
# WHEN encrypting and then decrypting the encrypted data
226+
encrypted_data = data_masker.encrypt([1, 2, "a string", 3.4])
227+
decrypted_data = data_masker.decrypt(encrypted_data)
228+
229+
# THEN the result is the original input data
230+
assert decrypted_data == [1, 2, "a string", 3.4]
231+
232+
233+
def test_encrypt_dict(data_masker):
234+
# GIVEN an dict data type
235+
data = {
236+
"a": {
237+
"1": {"None": "hello", "four": "world"},
238+
"b": {"3": {"4": "goodbye", "e": "world"}},
239+
},
240+
}
241+
242+
# WHEN encrypting and then decrypting the encrypted data
243+
encrypted_data = data_masker.encrypt(data)
244+
decrypted_data = data_masker.decrypt(encrypted_data)
245+
246+
# THEN the result is the original input data
247+
assert decrypted_data == data
248+
249+
250+
def test_encrypt_dict_with_fields(data_masker):
251+
# GIVEN the data type is a dictionary
252+
data = {
253+
"a": {
254+
"1": {"None": "hello", "four": "world"},
255+
"b": {"3": {"4": "goodbye", "e": "world"}},
256+
},
257+
}
258+
259+
# WHEN encrypting and then decrypting the encrypted data
260+
encrypted_data = data_masker.encrypt(data, fields=["a.1.None", "a.b.3.4"])
261+
decrypted_data = data_masker.decrypt(encrypted_data, fields=["a.1.None", "a.b.3.4"])
262+
263+
# THEN the result is only the specified fields are masked
264+
assert decrypted_data == data
265+
266+
267+
def test_encrypt_json_dict_with_fields(data_masker):
268+
# GIVEN the data type is a json representation of a dictionary
269+
data = json.dumps(
270+
{
271+
"a": {
272+
"1": {"None": "hello", "four": "world"},
273+
"b": {"3": {"4": "goodbye", "e": "world"}},
274+
},
275+
},
276+
)
277+
278+
# WHEN encrypting and then decrypting the encrypted data
279+
encrypted_data = data_masker.encrypt(data, fields=["a.1.None", "a.b.3.4"])
280+
decrypted_data = data_masker.decrypt(encrypted_data, fields=["a.1.None", "a.b.3.4"])
281+
282+
# THEN the result is only the specified fields are masked
283+
assert decrypted_data == json.loads(data)
Lines changed: 32 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
config:
2+
target: https://sebwc2y7gh.execute-api.us-west-2.amazonaws.com/Prod/function128
3+
phases:
4+
- duration: 60
5+
arrivalRate: 1
6+
rampTo: 5
7+
name: Warm up phase
8+
- duration: 60
9+
arrivalRate: 5
10+
rampTo: 10
11+
name: Ramp up load
12+
- duration: 30
13+
arrivalRate: 10
14+
rampTo: 30
15+
name: Spike phase
16+
# Load & configure a couple of useful plugins
17+
# https://docs.art/reference/extensions
18+
plugins:
19+
apdex: {}
20+
metrics-by-endpoint: {}
21+
apdex:
22+
threshold: 500
23+
scenarios:
24+
- flow:
25+
- loop:
26+
- get:
27+
url: "https://sebwc2y7gh.execute-api.us-west-2.amazonaws.com/Prod/function128"
28+
- get:
29+
url: "https://sebwc2y7gh.execute-api.us-west-2.amazonaws.com/Prod/function1024"
30+
- get:
31+
url: "https://sebwc2y7gh.execute-api.us-west-2.amazonaws.com/Prod/function1769"
32+
count: 100
Lines changed: 243 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,243 @@
1+
# Created by https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode
2+
3+
### Linux ###
4+
*~
5+
6+
# temporary files which can be created if a process still has a handle open of a deleted file
7+
.fuse_hidden*
8+
9+
# KDE directory preferences
10+
.directory
11+
12+
# Linux trash folder which might appear on any partition or disk
13+
.Trash-*
14+
15+
# .nfs files are created when an open file is removed but is still being accessed
16+
.nfs*
17+
18+
### OSX ###
19+
*.DS_Store
20+
.AppleDouble
21+
.LSOverride
22+
23+
# Icon must end with two \r
24+
Icon
25+
26+
# Thumbnails
27+
._*
28+
29+
# Files that might appear in the root of a volume
30+
.DocumentRevisions-V100
31+
.fseventsd
32+
.Spotlight-V100
33+
.TemporaryItems
34+
.Trashes
35+
.VolumeIcon.icns
36+
.com.apple.timemachine.donotpresent
37+
38+
# Directories potentially created on remote AFP share
39+
.AppleDB
40+
.AppleDesktop
41+
Network Trash Folder
42+
Temporary Items
43+
.apdisk
44+
45+
### PyCharm ###
46+
# Covers JetBrains IDEs: IntelliJ, RubyMine, PhpStorm, AppCode, PyCharm, CLion, Android Studio and Webstorm
47+
# Reference: https://intellij-support.jetbrains.com/hc/en-us/articles/206544839
48+
49+
# User-specific stuff:
50+
.idea/**/workspace.xml
51+
.idea/**/tasks.xml
52+
.idea/dictionaries
53+
54+
# Sensitive or high-churn files:
55+
.idea/**/dataSources/
56+
.idea/**/dataSources.ids
57+
.idea/**/dataSources.xml
58+
.idea/**/dataSources.local.xml
59+
.idea/**/sqlDataSources.xml
60+
.idea/**/dynamic.xml
61+
.idea/**/uiDesigner.xml
62+
63+
# Gradle:
64+
.idea/**/gradle.xml
65+
.idea/**/libraries
66+
67+
# CMake
68+
cmake-build-debug/
69+
70+
# Mongo Explorer plugin:
71+
.idea/**/mongoSettings.xml
72+
73+
## File-based project format:
74+
*.iws
75+
76+
## Plugin-specific files:
77+
78+
# IntelliJ
79+
/out/
80+
81+
# mpeltonen/sbt-idea plugin
82+
.idea_modules/
83+
84+
# JIRA plugin
85+
atlassian-ide-plugin.xml
86+
87+
# Cursive Clojure plugin
88+
.idea/replstate.xml
89+
90+
# Ruby plugin and RubyMine
91+
/.rakeTasks
92+
93+
# Crashlytics plugin (for Android Studio and IntelliJ)
94+
com_crashlytics_export_strings.xml
95+
crashlytics.properties
96+
crashlytics-build.properties
97+
fabric.properties
98+
99+
### PyCharm Patch ###
100+
# Comment Reason: https://github.com/joeblau/gitignore.io/issues/186#issuecomment-215987721
101+
102+
# *.iml
103+
# modules.xml
104+
# .idea/misc.xml
105+
# *.ipr
106+
107+
# Sonarlint plugin
108+
.idea/sonarlint
109+
110+
### Python ###
111+
# Byte-compiled / optimized / DLL files
112+
__pycache__/
113+
*.py[cod]
114+
*$py.class
115+
116+
# C extensions
117+
*.so
118+
119+
# Distribution / packaging
120+
.Python
121+
build/
122+
develop-eggs/
123+
dist/
124+
downloads/
125+
eggs/
126+
.eggs/
127+
lib/
128+
lib64/
129+
parts/
130+
sdist/
131+
var/
132+
wheels/
133+
*.egg-info/
134+
.installed.cfg
135+
*.egg
136+
137+
# PyInstaller
138+
# Usually these files are written by a python script from a template
139+
# before PyInstaller builds the exe, so as to inject date/other infos into it.
140+
*.manifest
141+
*.spec
142+
143+
# Installer logs
144+
pip-log.txt
145+
pip-delete-this-directory.txt
146+
147+
# Unit test / coverage reports
148+
htmlcov/
149+
.tox/
150+
.coverage
151+
.coverage.*
152+
.cache
153+
.pytest_cache/
154+
nosetests.xml
155+
coverage.xml
156+
*.cover
157+
.hypothesis/
158+
159+
# Translations
160+
*.mo
161+
*.pot
162+
163+
# Flask stuff:
164+
instance/
165+
.webassets-cache
166+
167+
# Scrapy stuff:
168+
.scrapy
169+
170+
# Sphinx documentation
171+
docs/_build/
172+
173+
# PyBuilder
174+
target/
175+
176+
# Jupyter Notebook
177+
.ipynb_checkpoints
178+
179+
# pyenv
180+
.python-version
181+
182+
# celery beat schedule file
183+
celerybeat-schedule.*
184+
185+
# SageMath parsed files
186+
*.sage.py
187+
188+
# Environments
189+
.env
190+
.venv
191+
env/
192+
venv/
193+
ENV/
194+
env.bak/
195+
venv.bak/
196+
197+
# Spyder project settings
198+
.spyderproject
199+
.spyproject
200+
201+
# Rope project settings
202+
.ropeproject
203+
204+
# mkdocs documentation
205+
/site
206+
207+
# mypy
208+
.mypy_cache/
209+
210+
### VisualStudioCode ###
211+
.vscode/*
212+
!.vscode/settings.json
213+
!.vscode/tasks.json
214+
!.vscode/launch.json
215+
!.vscode/extensions.json
216+
.history
217+
218+
### Windows ###
219+
# Windows thumbnail cache files
220+
Thumbs.db
221+
ehthumbs.db
222+
ehthumbs_vista.db
223+
224+
# Folder config file
225+
Desktop.ini
226+
227+
# Recycle Bin used on file shares
228+
$RECYCLE.BIN/
229+
230+
# Windows Installer files
231+
*.cab
232+
*.msi
233+
*.msm
234+
*.msp
235+
236+
# Windows shortcuts
237+
*.lnk
238+
239+
# Build folder
240+
241+
*/build/*
242+
243+
# End of https://www.gitignore.io/api/osx,linux,python,windows,pycharm,visualstudiocode
Lines changed: 163 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,163 @@
1+
# pt-load-test-stack
2+
3+
Congratulations, you have just created a Serverless "Hello World" application using the AWS Serverless Application Model (AWS SAM) for the `python3.10` runtime, and options to bootstrap it with [**Powertools for AWS Lambda (Python)**](https://awslabs.github.io/aws-lambda-powertools-python/latest/) (Powertools for AWS Lambda (Python)) utilities for Logging, Tracing and Metrics.
4+
5+
Powertools for AWS Lambda (Python) is a developer toolkit to implement Serverless best practices and increase developer velocity.
6+
7+
## Powertools for AWS Lambda (Python) features
8+
9+
Powertools for AWS Lambda (Python) provides three core utilities:
10+
11+
* **[Tracing](https://awslabs.github.io/aws-lambda-powertools-python/latest/core/tracer/)** - Decorators and utilities to trace Lambda function handlers, and both synchronous and asynchronous functions
12+
* **[Logging](https://awslabs.github.io/aws-lambda-powertools-python/latest/core/logger/)** - Structured logging made easier, and decorator to enrich structured logging with key Lambda context details
13+
* **[Metrics](https://awslabs.github.io/aws-lambda-powertools-python/latest/core/metrics/)** - Custom Metrics created asynchronously via CloudWatch Embedded Metric Format (EMF)
14+
15+
Find the complete project's [documentation here](https://awslabs.github.io/aws-lambda-powertools-python).
16+
17+
### Installing Powertools for AWS Lambda (Python)n
18+
19+
With [pip](https://pip.pypa.io/en/latest/index.html) installed, run:
20+
21+
```bash
22+
pip install aws-lambda-powertools
23+
```
24+
25+
### Powertools for AWS Lambda (Python) Examples
26+
27+
* [Tutorial](https://awslabs.github.io/aws-lambda-powertools-python/latest/tutorial)
28+
* [Serverless Shopping cart](https://github.com/aws-samples/aws-serverless-shopping-cart)
29+
* [Serverless Airline](https://github.com/aws-samples/aws-serverless-airline-booking)
30+
* [Serverless E-commerce platform](https://github.com/aws-samples/aws-serverless-ecommerce-platform)
31+
* [Serverless GraphQL Nanny Booking Api](https://github.com/trey-rosius/babysitter_api)
32+
33+
## Working with this project
34+
35+
This project contains source code and supporting files for a serverless application that you can deploy with the SAM CLI. It includes the following files and folders.
36+
37+
* hello_world - Code for the application's Lambda function.
38+
* events - Invocation events that you can use to invoke the function.
39+
* tests - Unit tests for the application code.
40+
* template.yaml - A template that defines the application's AWS resources.
41+
42+
The application uses several AWS resources, including Lambda functions and an API Gateway API. These resources are defined in the `template.yaml` file in this project. You can update the template to add AWS resources through the same deployment process that updates your application code.
43+
44+
If you prefer to use an integrated development environment (IDE) to build and test your application, you can use the AWS Toolkit.
45+
The AWS Toolkit is an open source plug-in for popular IDEs that uses the SAM CLI to build and deploy serverless applications on AWS. The AWS Toolkit also adds a simplified step-through debugging experience for Lambda function code. See the following links to get started.
46+
47+
* [CLion](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
48+
* [GoLand](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
49+
* [IntelliJ](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
50+
* [WebStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
51+
* [Rider](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
52+
* [PhpStorm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
53+
* [PyCharm](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
54+
* [RubyMine](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
55+
* [DataGrip](https://docs.aws.amazon.com/toolkit-for-jetbrains/latest/userguide/welcome.html)
56+
* [VS Code](https://docs.aws.amazon.com/toolkit-for-vscode/latest/userguide/welcome.html)
57+
* [Visual Studio](https://docs.aws.amazon.com/toolkit-for-visual-studio/latest/user-guide/welcome.html)
58+
59+
### Deploy the sample application
60+
61+
The Serverless Application Model Command Line Interface (SAM CLI) is an extension of the AWS CLI that adds functionality for building and testing Lambda applications. It uses Docker to run your functions in an Amazon Linux environment that matches Lambda. It can also emulate your application's build environment and API.
62+
63+
To use the SAM CLI, you need the following tools.
64+
65+
* SAM CLI - [Install the SAM CLI](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-install.html)
66+
* [Python 3 installed](https://www.python.org/downloads/)
67+
* Docker - [Install Docker community edition](https://hub.docker.com/search/?type=edition&offering=community)
68+
69+
To build and deploy your application for the first time, run the following in your shell:
70+
71+
```bash
72+
sam build --use-container
73+
sam deploy --guided
74+
```
75+
76+
The first command will build the source of your application. The second command will package and deploy your application to AWS, with a series of prompts:
77+
78+
* **Stack Name**: The name of the stack to deploy to CloudFormation. This should be unique to your account and region, and a good starting point would be something matching your project name.
79+
* **AWS Region**: The AWS region you want to deploy your app to.
80+
* **Confirm changes before deploy**: If set to yes, any change sets will be shown to you before execution for manual review. If set to no, the AWS SAM CLI will automatically deploy application changes.
81+
* **Allow SAM CLI IAM role creation**: Many AWS SAM templates, including this example, create AWS IAM roles required for the AWS Lambda function(s) included to access AWS services. By default, these are scoped down to minimum required permissions. To deploy an AWS CloudFormation stack which creates or modifies IAM roles, the `CAPABILITY_IAM` value for `capabilities` must be provided. If permission isn't provided through this prompt, to deploy this example you must explicitly pass `--capabilities CAPABILITY_IAM` to the `sam deploy` command.
82+
* **Save arguments to samconfig.toml**: If set to yes, your choices will be saved to a configuration file inside the project, so that in the future you can just re-run `sam deploy` without parameters to deploy changes to your application.
83+
84+
You can find your API Gateway Endpoint URL in the output values displayed after deployment.
85+
86+
### Use the SAM CLI to build and test locally
87+
88+
Build your application with the `sam build --use-container` command.
89+
90+
```bash
91+
pt-load-test-stack$ sam build --use-container
92+
```
93+
94+
The SAM CLI installs dependencies defined in `hello_world/requirements.txt`, creates a deployment package, and saves it in the `.aws-sam/build` folder.
95+
96+
Test a single function by invoking it directly with a test event. An event is a JSON document that represents the input that the function receives from the event source. Test events are included in the `events` folder in this project.
97+
98+
Run functions locally and invoke them with the `sam local invoke` command.
99+
100+
```bash
101+
pt-load-test-stack$ sam local invoke HelloWorldFunction --event events/event.json
102+
```
103+
104+
The SAM CLI can also emulate your application's API. Use the `sam local start-api` to run the API locally on port 3000.
105+
106+
```bash
107+
pt-load-test-stack$ sam local start-api
108+
pt-load-test-stack$ curl http://localhost:3000/
109+
```
110+
111+
The SAM CLI reads the application template to determine the API's routes and the functions that they invoke. The `Events` property on each function's definition includes the route and method for each path.
112+
113+
```yaml
114+
Events:
115+
HelloWorld:
116+
Type: Api
117+
Properties:
118+
Path: /hello
119+
Method: get
120+
```
121+
122+
### Add a resource to your application
123+
124+
The application template uses AWS Serverless Application Model (AWS SAM) to define application resources. AWS SAM is an extension of AWS CloudFormation with a simpler syntax for configuring common serverless application resources such as functions, triggers, and APIs. For resources not included in [the SAM specification](https://github.com/awslabs/serverless-application-model/blob/master/versions/2016-10-31.md), you can use standard [AWS CloudFormation](https://docs.aws.amazon.com/AWSCloudFormation/latest/UserGuide/aws-template-resource-type-ref.html) resource types.
125+
126+
### Fetch, tail, and filter Lambda function logs
127+
128+
To simplify troubleshooting, SAM CLI has a command called `sam logs`. `sam logs` lets you fetch logs generated by your deployed Lambda function from the command line. In addition to printing the logs on the terminal, this command has several nifty features to help you quickly find the bug.
129+
130+
`NOTE`: This command works for all AWS Lambda functions; not just the ones you deploy using SAM.
131+
132+
```bash
133+
pt-load-test-stack$ sam logs -n HelloWorldFunction --stack-name pt-load-test-stack --tail
134+
```
135+
136+
You can find more information and examples about filtering Lambda function logs in the [SAM CLI Documentation](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-logging.html).
137+
138+
### Tests
139+
140+
Tests are defined in the `tests` folder in this project. Use PIP to install the test dependencies and run tests.
141+
142+
```bash
143+
pt-load-test-stack$ pip install -r tests/requirements.txt --user
144+
# unit test
145+
pt-load-test-stack$ python -m pytest tests/unit -v
146+
# integration test, requiring deploying the stack first.
147+
# Create the env variable AWS_SAM_STACK_NAME with the name of the stack we are testing
148+
pt-load-test-stack$ AWS_SAM_STACK_NAME="pt-load-test-stack" python -m pytest tests/integration -v
149+
```
150+
151+
### Cleanup
152+
153+
To delete the sample application that you created, use the AWS CLI. Assuming you used your project name for the stack name, you can run the following:
154+
155+
```bash
156+
sam delete --stack-name "pt-load-test-stack"
157+
```
158+
159+
## Resources
160+
161+
See the [AWS SAM developer guide](https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/what-is-sam.html) for an introduction to SAM specification, the SAM CLI, and serverless application concepts.
162+
163+
Next, you can use AWS Serverless Application Repository to deploy ready to use Apps that go beyond hello world samples and learn how authors developed their applications: [AWS Serverless Application Repository main page](https://aws.amazon.com/serverless/serverlessrepo/)

‎tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/__init__.py

Whitespace-only changes.
Lines changed: 111 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,111 @@
1+
{
2+
"body":"",
3+
"headers":{
4+
"Accept":"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9",
5+
"Accept-Encoding":"gzip, deflate, br",
6+
"Accept-Language":"pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7",
7+
"Cache-Control":"max-age=0",
8+
"Connection":"keep-alive",
9+
"Host":"127.0.0.1:3000",
10+
"Sec-Ch-Ua":"\"Google Chrome\";v=\"105\", \"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"105\"",
11+
"Sec-Ch-Ua-Mobile":"?0",
12+
"Sec-Ch-Ua-Platform":"\"Linux\"",
13+
"Sec-Fetch-Dest":"document",
14+
"Sec-Fetch-Mode":"navigate",
15+
"Sec-Fetch-Site":"none",
16+
"Sec-Fetch-User":"?1",
17+
"Upgrade-Insecure-Requests":"1",
18+
"User-Agent":"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36",
19+
"X-Forwarded-Port":"3000",
20+
"X-Forwarded-Proto":"http"
21+
},
22+
"httpMethod":"GET",
23+
"isBase64Encoded": false,
24+
"multiValueHeaders":{
25+
"Accept":[
26+
"text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.9"
27+
],
28+
"Accept-Encoding":[
29+
"gzip, deflate, br"
30+
],
31+
"Accept-Language":[
32+
"pt-BR,pt;q=0.9,en-US;q=0.8,en;q=0.7"
33+
],
34+
"Cache-Control":[
35+
"max-age=0"
36+
],
37+
"Connection":[
38+
"keep-alive"
39+
],
40+
"Host":[
41+
"127.0.0.1:3000"
42+
],
43+
"Sec-Ch-Ua":[
44+
"\"Google Chrome\";v=\"105\", \"Not)A;Brand\";v=\"8\", \"Chromium\";v=\"105\""
45+
],
46+
"Sec-Ch-Ua-Mobile":[
47+
"?0"
48+
],
49+
"Sec-Ch-Ua-Platform":[
50+
"\"Linux\""
51+
],
52+
"Sec-Fetch-Dest":[
53+
"document"
54+
],
55+
"Sec-Fetch-Mode":[
56+
"navigate"
57+
],
58+
"Sec-Fetch-Site":[
59+
"none"
60+
],
61+
"Sec-Fetch-User":[
62+
"?1"
63+
],
64+
"Upgrade-Insecure-Requests":[
65+
"1"
66+
],
67+
"User-Agent":[
68+
"Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/105.0.0.0 Safari/537.36"
69+
],
70+
"X-Forwarded-Port":[
71+
"3000"
72+
],
73+
"X-Forwarded-Proto":[
74+
"http"
75+
]
76+
},
77+
"multiValueQueryStringParameters":"",
78+
"path":"/hello",
79+
"pathParameters":"",
80+
"queryStringParameters":"",
81+
"requestContext":{
82+
"accountId":"123456789012",
83+
"apiId":"1234567890",
84+
"domainName":"127.0.0.1:3000",
85+
"extendedRequestId":"",
86+
"httpMethod":"GET",
87+
"identity":{
88+
"accountId":"",
89+
"apiKey":"",
90+
"caller":"",
91+
"cognitoAuthenticationProvider":"",
92+
"cognitoAuthenticationType":"",
93+
"cognitoIdentityPoolId":"",
94+
"sourceIp":"127.0.0.1",
95+
"user":"",
96+
"userAgent":"Custom User Agent String",
97+
"userArn":""
98+
},
99+
"path":"/hello",
100+
"protocol":"HTTP/1.1",
101+
"requestId":"a3590457-cac2-4f10-8fc9-e47114bf7c62",
102+
"requestTime":"02/Feb/2023:11:45:26 +0000",
103+
"requestTimeEpoch":1675338326,
104+
"resourceId":"123456",
105+
"resourcePath":"/hello",
106+
"stage":"Prod"
107+
},
108+
"resource":"/hello",
109+
"stageVariables":"",
110+
"version":"1.0"
111+
}

‎tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_1024/__init__.py

Whitespace-only changes.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
3+
from aws_lambda_powertools import Logger, Tracer
4+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
5+
from aws_lambda_powertools.logging import correlation_paths
6+
from aws_lambda_powertools.utilities.data_masking import DataMasking
7+
from aws_lambda_powertools.utilities.data_masking.provider.kms.aws_encryption_sdk import AwsEncryptionSdkProvider
8+
from aws_lambda_powertools.utilities.typing import LambdaContext
9+
10+
KMS_KEY_ARN = os.environ["KMS_KEY_ARN"]
11+
12+
json_blob = {
13+
"id": 1,
14+
"name": "John Doe",
15+
"age": 30,
16+
"email": "johndoe@example.com",
17+
"address": {"street": "123 Main St", "city": "Anytown", "state": "CA", "zip": "12345"},
18+
"phone_numbers": ["+1-555-555-1234", "+1-555-555-5678"],
19+
"interests": ["Hiking", "Traveling", "Photography", "Reading"],
20+
"job_history": {
21+
"company": {
22+
"company_name": "Acme Inc.",
23+
"company_address": "5678 Interview Dr.",
24+
},
25+
"position": "Software Engineer",
26+
"start_date": "2015-01-01",
27+
"end_date": "2017-12-31",
28+
},
29+
"about_me": """
30+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tincidunt velit quis
31+
sapien mollis, at egestas massa tincidunt. Suspendisse ultrices arcu a dolor dapibus,
32+
ut pretium turpis volutpat. Vestibulum at sapien quis sapien dignissim volutpat ut a enim.
33+
Praesent fringilla sem eu dui convallis luctus. Donec ullamcorper, sapien ut convallis congue,
34+
risus mauris pretium tortor, nec dignissim arcu urna a nisl. Vivamus non fermentum ex. Proin
35+
interdum nisi id sagittis egestas. Nam sit amet nisi nec quam pharetra sagittis. Aliquam erat
36+
volutpat. Donec nec luctus sem, nec ornare lorem. Vivamus vitae orci quis enim faucibus placerat.
37+
Nulla facilisi. Proin in turpis orci. Donec imperdiet velit ac tellus gravida, eget laoreet tellus
38+
malesuada. Praesent venenatis tellus ac urna blandit, at varius felis posuere. Integer a commodo nunc.
39+
""",
40+
}
41+
42+
app = APIGatewayRestResolver()
43+
tracer = Tracer()
44+
logger = Logger()
45+
46+
47+
@app.get("/function1024")
48+
@tracer.capture_method
49+
def function1024():
50+
logger.info("Hello world function1024 - HTTP 200")
51+
data_masker = DataMasking(provider=AwsEncryptionSdkProvider(keys=[KMS_KEY_ARN]))
52+
encrypted = data_masker.encrypt(json_blob, fields=["address.street", "job_history.company.company_name"])
53+
decrypted = data_masker.decrypt(encrypted, fields=["address.street", "job_history.company.company_name"])
54+
return {"Decrypted_json_blob_function_1024": decrypted}
55+
56+
57+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
58+
@tracer.capture_lambda_handler
59+
def lambda_handler(event: dict, context: LambdaContext) -> dict:
60+
return app.resolve(event, context)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
requests
2+
aws-lambda-powertools[tracer]
3+
aws-encryption-sdk

‎tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_128/__init__.py

Whitespace-only changes.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
3+
from aws_lambda_powertools import Logger, Tracer
4+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
5+
from aws_lambda_powertools.logging import correlation_paths
6+
from aws_lambda_powertools.utilities.data_masking import DataMasking
7+
from aws_lambda_powertools.utilities.data_masking.provider.kms.aws_encryption_sdk import AwsEncryptionSdkProvider
8+
from aws_lambda_powertools.utilities.typing import LambdaContext
9+
10+
KMS_KEY_ARN = os.environ["KMS_KEY_ARN"]
11+
12+
json_blob = {
13+
"id": 1,
14+
"name": "John Doe",
15+
"age": 30,
16+
"email": "johndoe@example.com",
17+
"address": {"street": "123 Main St", "city": "Anytown", "state": "CA", "zip": "12345"},
18+
"phone_numbers": ["+1-555-555-1234", "+1-555-555-5678"],
19+
"interests": ["Hiking", "Traveling", "Photography", "Reading"],
20+
"job_history": {
21+
"company": {
22+
"company_name": "Acme Inc.",
23+
"company_address": "5678 Interview Dr.",
24+
},
25+
"position": "Software Engineer",
26+
"start_date": "2015-01-01",
27+
"end_date": "2017-12-31",
28+
},
29+
"about_me": """
30+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tincidunt velit quis
31+
sapien mollis, at egestas massa tincidunt. Suspendisse ultrices arcu a dolor dapibus,
32+
ut pretium turpis volutpat. Vestibulum at sapien quis sapien dignissim volutpat ut a enim.
33+
Praesent fringilla sem eu dui convallis luctus. Donec ullamcorper, sapien ut convallis congue,
34+
risus mauris pretium tortor, nec dignissim arcu urna a nisl. Vivamus non fermentum ex. Proin
35+
interdum nisi id sagittis egestas. Nam sit amet nisi nec quam pharetra sagittis. Aliquam erat
36+
volutpat. Donec nec luctus sem, nec ornare lorem. Vivamus vitae orci quis enim faucibus placerat.
37+
Nulla facilisi. Proin in turpis orci. Donec imperdiet velit ac tellus gravida, eget laoreet tellus
38+
malesuada. Praesent venenatis tellus ac urna blandit, at varius felis posuere. Integer a commodo nunc.
39+
""",
40+
}
41+
42+
app = APIGatewayRestResolver()
43+
tracer = Tracer()
44+
logger = Logger()
45+
46+
47+
@app.get("/function128")
48+
@tracer.capture_method
49+
def function128():
50+
logger.info("Hello world function128 - HTTP 200")
51+
data_masker = DataMasking(provider=AwsEncryptionSdkProvider(keys=[KMS_KEY_ARN]))
52+
encrypted = data_masker.encrypt(json_blob, fields=["address.street", "job_history.company.company_name"])
53+
decrypted = data_masker.decrypt(encrypted, fields=["address.street", "job_history.company.company_name"])
54+
return {"Decrypted_json_blob_function_128": decrypted}
55+
56+
57+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
58+
@tracer.capture_lambda_handler
59+
def lambda_handler(event: dict, context: LambdaContext) -> dict:
60+
return app.resolve(event, context)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
requests
2+
aws-lambda-powertools[tracer]
3+
aws-encryption-sdk

‎tests/performance/data_masking/load_test_data_masking/pt-load-test-stack/function_1769/__init__.py

Whitespace-only changes.
Lines changed: 60 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
import os
2+
3+
from aws_lambda_powertools import Logger, Tracer
4+
from aws_lambda_powertools.event_handler import APIGatewayRestResolver
5+
from aws_lambda_powertools.logging import correlation_paths
6+
from aws_lambda_powertools.utilities.data_masking import DataMasking
7+
from aws_lambda_powertools.utilities.data_masking.provider.kms.aws_encryption_sdk import AwsEncryptionSdkProvider
8+
from aws_lambda_powertools.utilities.typing import LambdaContext
9+
10+
KMS_KEY_ARN = os.environ["KMS_KEY_ARN"]
11+
12+
json_blob = {
13+
"id": 1,
14+
"name": "John Doe",
15+
"age": 30,
16+
"email": "johndoe@example.com",
17+
"address": {"street": "123 Main St", "city": "Anytown", "state": "CA", "zip": "12345"},
18+
"phone_numbers": ["+1-555-555-1234", "+1-555-555-5678"],
19+
"interests": ["Hiking", "Traveling", "Photography", "Reading"],
20+
"job_history": {
21+
"company": {
22+
"company_name": "Acme Inc.",
23+
"company_address": "5678 Interview Dr.",
24+
},
25+
"position": "Software Engineer",
26+
"start_date": "2015-01-01",
27+
"end_date": "2017-12-31",
28+
},
29+
"about_me": """
30+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tincidunt velit quis
31+
sapien mollis, at egestas massa tincidunt. Suspendisse ultrices arcu a dolor dapibus,
32+
ut pretium turpis volutpat. Vestibulum at sapien quis sapien dignissim volutpat ut a enim.
33+
Praesent fringilla sem eu dui convallis luctus. Donec ullamcorper, sapien ut convallis congue,
34+
risus mauris pretium tortor, nec dignissim arcu urna a nisl. Vivamus non fermentum ex. Proin
35+
interdum nisi id sagittis egestas. Nam sit amet nisi nec quam pharetra sagittis. Aliquam erat
36+
volutpat. Donec nec luctus sem, nec ornare lorem. Vivamus vitae orci quis enim faucibus placerat.
37+
Nulla facilisi. Proin in turpis orci. Donec imperdiet velit ac tellus gravida, eget laoreet tellus
38+
malesuada. Praesent venenatis tellus ac urna blandit, at varius felis posuere. Integer a commodo nunc.
39+
""",
40+
}
41+
42+
app = APIGatewayRestResolver()
43+
tracer = Tracer()
44+
logger = Logger()
45+
46+
47+
@app.get("/function1769")
48+
@tracer.capture_method
49+
def function1769():
50+
logger.info("Hello world function1769 - HTTP 200")
51+
data_masker = DataMasking(provider=AwsEncryptionSdkProvider(keys=[KMS_KEY_ARN]))
52+
encrypted = data_masker.encrypt(json_blob, fields=["address.street", "job_history.company.company_name"])
53+
decrypted = data_masker.decrypt(encrypted, fields=["address.street", "job_history.company.company_name"])
54+
return {"Decrypted_json_blob_function_1769": decrypted}
55+
56+
57+
@logger.inject_lambda_context(correlation_id_path=correlation_paths.API_GATEWAY_REST)
58+
@tracer.capture_lambda_handler
59+
def lambda_handler(event: dict, context: LambdaContext) -> dict:
60+
return app.resolve(event, context)
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
requests
2+
aws-lambda-powertools[tracer]
3+
aws-encryption-sdk
Lines changed: 34 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,34 @@
1+
# More information about the configuration file can be found here:
2+
# https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/serverless-sam-cli-config.html
3+
version = 0.1
4+
5+
[default]
6+
[default.global.parameters]
7+
stack_name = "pt-load-test-stack"
8+
9+
[default.build.parameters]
10+
cached = true
11+
parallel = true
12+
13+
[default.validate.parameters]
14+
lint = true
15+
16+
[default.deploy.parameters]
17+
capabilities = "CAPABILITY_IAM"
18+
confirm_changeset = true
19+
resolve_s3 = true
20+
s3_prefix = "pt-load-test-stack"
21+
region = "us-west-2"
22+
image_repositories = []
23+
24+
[default.package.parameters]
25+
resolve_s3 = true
26+
27+
[default.sync.parameters]
28+
watch = true
29+
30+
[default.local_start_api.parameters]
31+
warm_containers = "EAGER"
32+
33+
[default.local_start_lambda.parameters]
34+
warm_containers = "EAGER"
Lines changed: 147 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,147 @@
1+
AWSTemplateFormatVersion: '2010-09-09'
2+
Transform: AWS::Serverless-2016-10-31
3+
Description: >
4+
pt-load-test-stack
5+
6+
Powertools for AWS Lambda (Python) example
7+
8+
Globals: # https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-specification-template-anatomy-globals.html
9+
Function:
10+
Timeout: 5
11+
Runtime: python3.10
12+
13+
Tracing: Active
14+
Api:
15+
TracingEnabled: true
16+
Resources:
17+
MyKMSKey:
18+
Type: AWS::KMS::Key
19+
Properties:
20+
Enabled: true
21+
KeyPolicy:
22+
Version: 2012-10-17
23+
Statement:
24+
- Effect: Allow
25+
Action: kms:*
26+
Resource: "*"
27+
Principal:
28+
AWS: !Join [ "", [ "arn:aws:iam::", !Ref "AWS::AccountId", ":root" ] ]
29+
Function128:
30+
Type: AWS::Serverless::Function # More info about Function Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html
31+
Properties:
32+
Handler: app.lambda_handler
33+
CodeUri: function_128
34+
Description: function 128 MB
35+
MemorySize: 128
36+
Architectures:
37+
- x86_64
38+
Policies:
39+
Statement:
40+
- Effect: Allow
41+
Action: kms:*
42+
Resource: !GetAtt MyKMSKey.Arn
43+
Tracing: Active
44+
Events:
45+
HelloPath:
46+
Type: Api # More info about API Event Source: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-api.html
47+
Properties:
48+
Path: /function128
49+
Method: GET
50+
# Powertools for AWS Lambda (Python) env vars: https://awslabs.github.io/aws-lambda-powertools-python/#environment-variables
51+
Environment:
52+
Variables:
53+
POWERTOOLS_SERVICE_NAME: PowertoolsHelloWorld
54+
POWERTOOLS_METRICS_NAMESPACE: Powertools
55+
LOG_LEVEL: INFO
56+
KMS_KEY_ARN: !GetAtt MyKMSKey.Arn
57+
Tags:
58+
LambdaPowertools: python
59+
Function1024:
60+
Type: AWS::Serverless::Function # More info about Function Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html
61+
Properties:
62+
Handler: app.lambda_handler
63+
CodeUri: function_1024
64+
Description: function 1024 MB
65+
MemorySize: 1024
66+
Architectures:
67+
- x86_64
68+
Policies:
69+
Statement:
70+
- Effect: Allow
71+
Action: kms:*
72+
Resource: !GetAtt MyKMSKey.Arn
73+
Tracing: Active
74+
Events:
75+
HelloPath:
76+
Type: Api # More info about API Event Source: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-api.html
77+
Properties:
78+
Path: /function1024
79+
Method: GET
80+
# Powertools for AWS Lambda (Python) env vars: https://awslabs.github.io/aws-lambda-powertools-python/#environment-variables
81+
Environment:
82+
Variables:
83+
POWERTOOLS_SERVICE_NAME: PowertoolsHelloWorld
84+
POWERTOOLS_METRICS_NAMESPACE: Powertools
85+
LOG_LEVEL: INFO
86+
KMS_KEY_ARN: !GetAtt MyKMSKey.Arn
87+
Tags:
88+
LambdaPowertools: python
89+
Function1769:
90+
Type: AWS::Serverless::Function # More info about Function Resource: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-resource-function.html
91+
Properties:
92+
Handler: app.lambda_handler
93+
CodeUri: function_1769
94+
Description: function 1769 MB
95+
MemorySize: 1769
96+
Architectures:
97+
- x86_64
98+
Policies:
99+
Statement:
100+
- Effect: Allow
101+
Action: kms:*
102+
Resource: !GetAtt MyKMSKey.Arn
103+
Tracing: Active
104+
Events:
105+
HelloPath:
106+
Type: Api # More info about API Event Source: https://docs.aws.amazon.com/serverless-application-model/latest/developerguide/sam-property-function-api.html
107+
Properties:
108+
Path: /function1769
109+
Method: GET
110+
# Powertools for AWS Lambda (Python) env vars: https://awslabs.github.io/aws-lambda-powertools-python/#environment-variables
111+
Environment:
112+
Variables:
113+
POWERTOOLS_SERVICE_NAME: PowertoolsHelloWorld
114+
POWERTOOLS_METRICS_NAMESPACE: Powertools
115+
LOG_LEVEL: INFO
116+
KMS_KEY_ARN: !GetAtt MyKMSKey.Arn
117+
Tags:
118+
LambdaPowertools: python
119+
120+
Outputs:
121+
KMSKeyArn:
122+
Description: ARN of the KMS Key
123+
Value: !GetAtt MyKMSKey.Arn
124+
125+
128FunctionApi:
126+
Description: API Gateway endpoint URL for Prod environment for Function 128 MB
127+
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/function128"
128+
129+
1024FunctionApi:
130+
Description: API Gateway endpoint URL for Prod environment for Function 1024 MB
131+
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/function1024"
132+
133+
1769FunctionApi:
134+
Description: API Gateway endpoint URL for Prod environment for Function 1769 MB
135+
Value: !Sub "https://${ServerlessRestApi}.execute-api.${AWS::Region}.amazonaws.com/Prod/function1769"
136+
137+
Function128:
138+
Description: Lambda Function 128 MB ARN
139+
Value: !GetAtt Function128.Arn
140+
141+
Function1024:
142+
Description: Lambda Function 1024 MB ARN
143+
Value: !GetAtt Function1024.Arn
144+
145+
Function1769:
146+
Description: Lambda Function 1769 MB ARN
147+
Value: !GetAtt Function1769.Arn
Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
import importlib
2+
from types import ModuleType
3+
4+
import pytest
5+
6+
from aws_lambda_powertools.utilities.data_masking.base import DataMasking
7+
8+
DATA_MASKING_PACKAGE = "aws_lambda_powertools.utilities.data_masking"
9+
DATA_MASKING_INIT_SLA: float = 0.002
10+
DATA_MASKING_NESTED_ENCRYPT_SLA: float = 0.001
11+
12+
json_blob = {
13+
"id": 1,
14+
"name": "John Doe",
15+
"age": 30,
16+
"email": "johndoe@example.com",
17+
"address": {"street": "123 Main St", "city": "Anytown", "state": "CA", "zip": "12345"},
18+
"phone_numbers": ["+1-555-555-1234", "+1-555-555-5678"],
19+
"interests": ["Hiking", "Traveling", "Photography", "Reading"],
20+
"job_history": {
21+
"company": {
22+
"company_name": "Acme Inc.",
23+
"company_address": "5678 Interview Dr.",
24+
},
25+
"position": "Software Engineer",
26+
"start_date": "2015-01-01",
27+
"end_date": "2017-12-31",
28+
},
29+
"about_me": """
30+
Lorem ipsum dolor sit amet, consectetur adipiscing elit. Nulla tincidunt velit quis
31+
sapien mollis, at egestas massa tincidunt. Suspendisse ultrices arcu a dolor dapibus,
32+
ut pretium turpis volutpat. Vestibulum at sapien quis sapien dignissim volutpat ut a enim.
33+
Praesent fringilla sem eu dui convallis luctus. Donec ullamcorper, sapien ut convallis congue,
34+
risus mauris pretium tortor, nec dignissim arcu urna a nisl. Vivamus non fermentum ex. Proin
35+
interdum nisi id sagittis egestas. Nam sit amet nisi nec quam pharetra sagittis. Aliquam erat
36+
volutpat. Donec nec luctus sem, nec ornare lorem. Vivamus vitae orci quis enim faucibus placerat.
37+
Nulla facilisi. Proin in turpis orci. Donec imperdiet velit ac tellus gravida, eget laoreet tellus
38+
malesuada. Praesent venenatis tellus ac urna blandit, at varius felis posuere. Integer a commodo nunc.
39+
""",
40+
}
41+
json_blob_fields = ["address.street", "job_history.company.company_name"]
42+
43+
44+
def import_data_masking_utility() -> ModuleType:
45+
"""Dynamically imports and return DataMasking module"""
46+
return importlib.import_module(DATA_MASKING_PACKAGE)
47+
48+
49+
@pytest.mark.perf
50+
@pytest.mark.benchmark(group="core", disable_gc=True, warmup=False)
51+
def test_data_masking_init(benchmark):
52+
benchmark.pedantic(import_data_masking_utility)
53+
stat = benchmark.stats.stats.max
54+
if stat > DATA_MASKING_INIT_SLA:
55+
pytest.fail(f"High level imports should be below {DATA_MASKING_INIT_SLA}s: {stat}")
56+
57+
58+
def mask_json_blob():
59+
data_masker = DataMasking()
60+
data_masker.mask(json_blob, json_blob_fields)
61+
62+
63+
@pytest.mark.perf
64+
@pytest.mark.benchmark(group="core", disable_gc=True, warmup=False)
65+
def test_data_masking_encrypt_with_json_blob(benchmark):
66+
benchmark.pedantic(mask_json_blob)
67+
stat = benchmark.stats.stats.max
68+
if stat > DATA_MASKING_NESTED_ENCRYPT_SLA:
69+
pytest.fail(f"High level imports should be below {DATA_MASKING_NESTED_ENCRYPT_SLA}s: {stat}")
Lines changed: 205 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,205 @@
1+
import json
2+
3+
import pytest
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+
9+
@pytest.fixture
10+
def data_masker() -> DataMasking:
11+
return DataMasking()
12+
13+
14+
def test_mask_int(data_masker):
15+
# GIVEN an int data type
16+
17+
# WHEN mask is called with no fields argument
18+
masked_string = data_masker.mask(42)
19+
20+
# THEN the result is the data masked
21+
assert masked_string == DATA_MASKING_STRING
22+
23+
24+
def test_mask_float(data_masker):
25+
# GIVEN a float data type
26+
27+
# WHEN mask is called with no fields argument
28+
masked_string = data_masker.mask(4.2)
29+
30+
# THEN the result is the data masked
31+
assert masked_string == DATA_MASKING_STRING
32+
33+
34+
def test_mask_bool(data_masker):
35+
# GIVEN a bool data type
36+
37+
# WHEN mask is called with no fields argument
38+
masked_string = data_masker.mask(True)
39+
40+
# THEN the result is the data masked
41+
assert masked_string == DATA_MASKING_STRING
42+
43+
44+
def test_mask_none(data_masker):
45+
# GIVEN a None data type
46+
47+
# WHEN mask is called with no fields argument
48+
masked_string = data_masker.mask(None)
49+
50+
# THEN the result is the data masked
51+
assert masked_string == DATA_MASKING_STRING
52+
53+
54+
def test_mask_str(data_masker):
55+
# GIVEN a str data type
56+
57+
# WHEN mask is called with no fields argument
58+
masked_string = data_masker.mask("this is a string")
59+
60+
# THEN the result is the data masked
61+
assert masked_string == DATA_MASKING_STRING
62+
63+
64+
def test_mask_list(data_masker):
65+
# GIVEN a list data type
66+
67+
# WHEN mask is called with no fields argument
68+
masked_string = data_masker.mask([1, 2, "string", 3])
69+
70+
# THEN the result is the data masked, while maintaining type list
71+
assert masked_string == [DATA_MASKING_STRING, DATA_MASKING_STRING, DATA_MASKING_STRING, DATA_MASKING_STRING]
72+
73+
74+
def test_mask_dict(data_masker):
75+
# GIVEN a dict data type
76+
data = {
77+
"a": {
78+
"1": {"None": "hello", "four": "world"},
79+
"b": {"3": {"4": "goodbye", "e": "world"}},
80+
},
81+
}
82+
83+
# WHEN mask is called with no fields argument
84+
masked_string = data_masker.mask(data)
85+
86+
# THEN the result is the data masked
87+
assert masked_string == DATA_MASKING_STRING
88+
89+
90+
def test_mask_dict_with_fields(data_masker):
91+
# GIVEN a dict data type
92+
data = {
93+
"a": {
94+
"1": {"None": "hello", "four": "world"},
95+
"b": {"3": {"4": "goodbye", "e": "world"}},
96+
},
97+
}
98+
99+
# WHEN mask is called with a list of fields specified
100+
masked_string = data_masker.mask(data, fields=["a.1.None", "a.b.3.4"])
101+
102+
# THEN the result is only the specified fields are masked
103+
assert masked_string == {
104+
"a": {
105+
"1": {"None": DATA_MASKING_STRING, "four": "world"},
106+
"b": {"3": {"4": DATA_MASKING_STRING, "e": "world"}},
107+
},
108+
}
109+
110+
111+
def test_mask_json_dict_with_fields(data_masker):
112+
# GIVEN the data type is a json representation of a dictionary
113+
data = json.dumps(
114+
{
115+
"a": {
116+
"1": {"None": "hello", "four": "world"},
117+
"b": {"3": {"4": "goodbye", "e": "world"}},
118+
},
119+
},
120+
)
121+
122+
# WHEN mask is called with a list of fields specified
123+
masked_json_string = data_masker.mask(data, fields=["a.1.None", "a.b.3.4"])
124+
125+
# THEN the result is only the specified fields are masked
126+
assert masked_json_string == {
127+
"a": {
128+
"1": {"None": DATA_MASKING_STRING, "four": "world"},
129+
"b": {"3": {"4": DATA_MASKING_STRING, "e": "world"}},
130+
},
131+
}
132+
133+
134+
def test_encrypt_not_implemented(data_masker):
135+
# GIVEN DataMasking is not initialized with a Provider
136+
137+
# WHEN attempting to call the encrypt method on the data
138+
with pytest.raises(NotImplementedError):
139+
# THEN the result is a NotImplementedError
140+
data_masker.encrypt("hello world")
141+
142+
143+
def test_decrypt_not_implemented(data_masker):
144+
# GIVEN DataMasking is not initialized with a Provider
145+
146+
# WHEN attempting to call the decrypt method on the data
147+
with pytest.raises(NotImplementedError):
148+
# THEN the result is a NotImplementedError
149+
data_masker.decrypt("hello world")
150+
151+
152+
def test_parsing_unsupported_data_type(data_masker):
153+
# GIVEN an initialization of the DataMasking class
154+
155+
# WHEN attempting to pass in a list of fields with input data that is not a dict
156+
with pytest.raises(TypeError):
157+
# THEN the result is a TypeError
158+
data_masker.mask(42, ["this.field"])
159+
160+
161+
def test_parsing_nonexistent_fields(data_masker):
162+
# GIVEN a dict data type
163+
data = {
164+
"3": {
165+
"1": {"None": "hello", "four": "world"},
166+
"4": {"33": {"5": "goodbye", "e": "world"}},
167+
},
168+
}
169+
170+
# WHEN attempting to pass in fields that do not exist in the input data
171+
with pytest.raises(KeyError):
172+
# THEN the result is a KeyError
173+
data_masker.mask(data, ["3.1.True"])
174+
175+
176+
def test_parsing_nonstring_fields(data_masker):
177+
# GIVEN a dict data type
178+
data = {
179+
"3": {
180+
"1": {"None": "hello", "four": "world"},
181+
"4": {"33": {"5": "goodbye", "e": "world"}},
182+
},
183+
}
184+
185+
# WHEN attempting to pass in a list of fields that are not strings
186+
masked = data_masker.mask(data, fields=[3.4])
187+
188+
# THEN the result is the value of the nested field should be masked as normal
189+
assert masked == {"3": {"1": {"None": "hello", "four": "world"}, "4": DATA_MASKING_STRING}}
190+
191+
192+
def test_parsing_nonstring_keys_and_fields(data_masker):
193+
# GIVEN a dict data type with integer keys
194+
data = {
195+
3: {
196+
"1": {"None": "hello", "four": "world"},
197+
4: {"33": {"5": "goodbye", "e": "world"}},
198+
},
199+
}
200+
201+
# WHEN masked with a list of fields that are integer keys
202+
masked = data_masker.mask(data, fields=[3.4])
203+
204+
# THEN the result is the value of the nested field should be masked
205+
assert masked == {"3": {"1": {"None": "hello", "four": "world"}, "4": DATA_MASKING_STRING}}

0 commit comments

Comments
 (0)
Please sign in to comment.