boto3-assist 0.2.12__tar.gz → 0.4.0__tar.gz

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (108) hide show
  1. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/PKG-INFO +2 -1
  2. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/pyproject.toml +3 -3
  3. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/requirements.txt +1 -0
  4. boto3_assist-0.4.0/src/boto3_assist/aws_lambda/event_info.py +414 -0
  5. boto3_assist-0.4.0/src/boto3_assist/cognito/cognito_authorizer.py +169 -0
  6. boto3_assist-0.4.0/src/boto3_assist/cognito/jwks_cache.py +21 -0
  7. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_model_base.py +3 -3
  8. boto3_assist-0.4.0/src/boto3_assist/http_status_codes.py +80 -0
  9. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/utilities/serialization_utility.py +1 -1
  10. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/utilities/string_utility.py +1 -1
  11. boto3_assist-0.4.0/src/boto3_assist/version.py +1 -0
  12. boto3_assist-0.4.0/tests/lambda/event_info_test.py +119 -0
  13. boto3_assist-0.4.0/tests/s3/__init__.py +0 -0
  14. boto3_assist-0.2.12/src/boto3_assist/version.py +0 -1
  15. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.env.docker +0 -0
  16. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.env.docker.001 +0 -0
  17. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.env.docker.nosql.workbench +0 -0
  18. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.env.unittest +0 -0
  19. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.gitignore +0 -0
  20. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.vscode/launch.json +0 -0
  21. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.vscode/settings.json +0 -0
  22. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.vscode/tasks.json +0 -0
  23. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/LICENSE-EXPLAINED.txt +0 -0
  24. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/LICENSE.txt +0 -0
  25. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/README.md +0 -0
  26. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/aws_regions_with_status.csv +0 -0
  27. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/aws_regions_with_status.json +0 -0
  28. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/devops/build.py +0 -0
  29. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/devops/readme.md +0 -0
  30. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/__init__.py +0 -0
  31. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/cloudwatch/log_report.py +0 -0
  32. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/models/order_item_model.py +0 -0
  33. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/models/order_model.py +0 -0
  34. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/models/product_model.py +0 -0
  35. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/models/user_model.py +0 -0
  36. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/models/user_post_model.py +0 -0
  37. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/order_example/main.py +0 -0
  38. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/order_example/products.json +0 -0
  39. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/order_item_service.py +0 -0
  40. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/order_service.py +0 -0
  41. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/product_service.py +0 -0
  42. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/table_service.py +0 -0
  43. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/user_post_service.py +0 -0
  44. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/user_service.py +0 -0
  45. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/user_service_client_example.py +0 -0
  46. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/user_service_resource_example.py +0 -0
  47. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/user_post_example/main.py +0 -0
  48. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/ec2/regions_report.py +0 -0
  49. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/module-headers.txt +0 -0
  50. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/mypy.ini +0 -0
  51. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/requirements-dev.txt +0 -0
  52. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/run-checks.sh +0 -0
  53. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/__init__.py +0 -0
  54. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/boto3session.py +0 -0
  55. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +0 -0
  56. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
  57. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
  58. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
  59. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
  60. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/connection.py +0 -0
  61. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/connection_tracker.py +0 -0
  62. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb.py +0 -0
  63. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_connection.py +0 -0
  64. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_connection_tracker.py +0 -0
  65. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_helpers.py +0 -0
  66. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
  67. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_index.py +0 -0
  68. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
  69. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
  70. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
  71. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
  72. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
  73. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
  74. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/readme.md +0 -0
  75. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
  76. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/ec2/ec2_connection.py +0 -0
  77. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/environment_services/__init__.py +0 -0
  78. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/environment_services/environment_loader.py +0 -0
  79. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/environment_services/environment_variables.py +0 -0
  80. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/errors/custom_exceptions.py +0 -0
  81. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/s3/s3.py +0 -0
  82. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/s3/s3_connection.py +0 -0
  83. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/utilities/datetime_utility.py +0 -0
  84. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/utilities/file_operations.py +0 -0
  85. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/utilities/http_utility.py +0 -0
  86. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/utilities/logging_utility.py +0 -0
  87. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/__init__.py +0 -0
  88. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/__top/__init__.py +0 -0
  89. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/__init__.py +0 -0
  90. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/dynamodb_model_base_test.py +0 -0
  91. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/dynamodb_model_projections_test.py +0 -0
  92. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/dynamodb_model_serializtion_test.py +0 -0
  93. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/dynamodb_moto_sorting_test.py +0 -0
  94. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/dynamodb_reindex_test.py +0 -0
  95. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/cms/base.py +0 -0
  96. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/cms/content_block.py +0 -0
  97. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/cms/page.py +0 -0
  98. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/cms/template.py +0 -0
  99. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/simple_model.py +0 -0
  100. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/user_model.py +0 -0
  101. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/user_required_fields_model.py +0 -0
  102. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/examples_test/__init__.py +0 -0
  103. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/examples_test/user_service_test.py +0 -0
  104. {boto3_assist-0.2.12/tests/s3 → boto3_assist-0.4.0/tests/lambda}/__init__.py +0 -0
  105. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/s3/files/test.txt +0 -0
  106. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/s3/s3_file_upload_test.py +0 -0
  107. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/utilities/__init__.py +0 -0
  108. {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/utilities/serialization_utility_test.py +0 -0
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: boto3_assist
3
- Version: 0.2.12
3
+ Version: 0.4.0
4
4
  Summary: Additional boto3 wrappers to make your life a little easier
5
5
  Author-email: Eric Wilson <boto3-assist@geekcafe.com>
6
6
  License-File: LICENSE-EXPLAINED.txt
@@ -13,6 +13,7 @@ Requires-Dist: aws-lambda-powertools
13
13
  Requires-Dist: aws-xray-sdk
14
14
  Requires-Dist: boto3
15
15
  Requires-Dist: jsons
16
+ Requires-Dist: pyjwt
16
17
  Requires-Dist: python-dateutil
17
18
  Requires-Dist: python-dotenv
18
19
  Requires-Dist: pytz
@@ -7,7 +7,7 @@ packages = ["src/boto3_assist"]
7
7
 
8
8
  [project]
9
9
  name = "boto3_assist"
10
- version = "0.2.12"
10
+ version = "0.4.0"
11
11
 
12
12
  authors = [
13
13
  { name="Eric Wilson", email="boto3-assist@geekcafe.com" }
@@ -28,8 +28,8 @@ dependencies = [
28
28
  "boto3",
29
29
  "pytz",
30
30
  "python-dotenv",
31
- "python-dateutil",
32
- "pytz",
31
+ "python-dateutil",
32
+ "PyJWT",
33
33
  "requests",
34
34
  "types-python-dateutil",
35
35
  ]
@@ -7,3 +7,4 @@ python-dotenv
7
7
  pytz
8
8
  requests
9
9
  types-python-dateutil
10
+ PyJWT
@@ -0,0 +1,414 @@
1
+ import json
2
+ import re
3
+ from typing import Any, Dict, List, Optional, Union
4
+
5
+ from aws_lambda_powertools import Logger
6
+
7
+ from boto3_assist.cognito.cognito_authorizer import CognitoCustomAuthorizer
8
+ from boto3_assist.errors.custom_exceptions import Error
9
+ from boto3_assist.http_status_codes import HttpStatusCodes
10
+
11
+ logger = Logger()
12
+
13
+
14
+ class LambdaEventInfo:
15
+ """
16
+ Utility class for parsing and interacting with AWS Lambda event payloads.
17
+ Contains methods to extract data from API Gateway payloads, path parameters,
18
+ and headers.
19
+ """
20
+
21
+ class ApiGatewayPayload:
22
+ """
23
+ Handles API Gateway-specific event payloads.
24
+ Provides methods to extract HTTP method types, resource paths, and
25
+ authorizer claims.
26
+ """
27
+
28
+ @staticmethod
29
+ def get_http_method_type(event: Dict[str, Any]) -> Optional[str]:
30
+ """
31
+ Extracts the HTTP method type (e.g., GET, POST) from the event.
32
+
33
+ Args:
34
+ event: The Lambda event payload.
35
+
36
+ Returns:
37
+ The HTTP method type as a string, or None if not found.
38
+ """
39
+ return LambdaEventInfo._get_value(event, "method_type", str) # pylint: disable=w0212
40
+
41
+ @staticmethod
42
+ def get_resource_path(event: Dict[str, Any]) -> Optional[str]:
43
+ """
44
+ Extracts the resource path from the event.
45
+
46
+ Args:
47
+ event: The Lambda event payload.
48
+
49
+ Returns:
50
+ The resource path as a string, or None if not found.
51
+ """
52
+ return LambdaEventInfo._get_value_ex(event, "path", str) # pylint: disable=w0212
53
+
54
+ @staticmethod
55
+ def get_resource_pattern(event: Dict[str, Any]) -> Optional[str]:
56
+ """
57
+ Extracts the resource pattern from the event, replacing path variables
58
+ with placeholders (e.g., /users/{user-id}).
59
+
60
+ Args:
61
+ event: The Lambda event payload.
62
+
63
+ Returns:
64
+ The resource pattern as a string, or None if not found.
65
+ """
66
+ return LambdaEventInfo._get_value_ex(event, "resourcePath", str) # pylint: disable=w0212
67
+
68
+ class AuthorizerPayload:
69
+ """
70
+ Handles claims and tokens in API Gateway authorizer payloads.
71
+ """
72
+
73
+ @staticmethod
74
+ def get_authenticated_email(event: Dict[str, Any]) -> Optional[str]:
75
+ """
76
+ Extracts the authenticated email or client ID from the event based
77
+ on the token use.
78
+
79
+ Args:
80
+ event: The Lambda event payload.
81
+
82
+ Returns:
83
+ The email or client ID as a string, or None if not found.
84
+ """
85
+ token_use = (
86
+ LambdaEventInfo.ApiGatewayPayload.AuthorizerPayload.get_token_use(
87
+ event
88
+ )
89
+ ) # pylint: disable=w0212
90
+ key = "email" if token_use == "access" else "client_id"
91
+ return (
92
+ LambdaEventInfo.ApiGatewayPayload.AuthorizerPayload.get_claims_data(
93
+ event, key
94
+ )
95
+ ) # pylint: disable=w0212
96
+
97
+ @staticmethod
98
+ def get_token_use(event: Dict[str, Any]) -> Optional[str]:
99
+ """
100
+ Extracts the token use (e.g., "access" or "id") from the event.
101
+
102
+ Args:
103
+ event: The Lambda event payload.
104
+
105
+ Returns:
106
+ The token use as a string, or None if not found.
107
+ """
108
+ return LambdaEventInfo._get_value( # pylint: disable=w0212
109
+ event, "requestContext/authorizer/claims/token_use", str
110
+ ) # pylint: disable=w0212
111
+
112
+ @staticmethod
113
+ def get_claims_data(event: Dict[str, Any], key: str) -> Optional[str]:
114
+ """
115
+ Extracts a specific claim from the authorizer payload.
116
+
117
+ Args:
118
+ event: The Lambda event payload.
119
+ key: The claim key to extract.
120
+
121
+ Returns:
122
+ The claim value as a string, or None if not found.
123
+ """
124
+ try:
125
+ value = LambdaEventInfo._get_value( # pylint: disable=w0212
126
+ event, f"requestContext/authorizer/claims/{key}", str
127
+ ) # pylint: disable=w0212
128
+ if not value:
129
+ value = LambdaEventInfo.ApiGatewayPayload.AuthorizerPayload.get_value_from_token(
130
+ event, key
131
+ ) # pylint: disable=w0212
132
+
133
+ if value is None:
134
+ raise Error(
135
+ {
136
+ "status_code": HttpStatusCodes.HTTP_401_UNAUTHENTICATED.value,
137
+ "message": f"Failed to locate {key} info in JWT Token",
138
+ }
139
+ )
140
+ return value
141
+
142
+ except Exception as e:
143
+ raise Error(
144
+ {
145
+ "status_code": HttpStatusCodes.HTTP_401_UNAUTHENTICATED.value,
146
+ "message": f"Failed to locate {key} info in JWT Token",
147
+ "exception": str(e),
148
+ }
149
+ ) from e
150
+
151
+ @staticmethod
152
+ def get_value_from_token(event: Dict[str, Any], key: str) -> Optional[str]:
153
+ """
154
+ Extracts a value from the JWT token in the event.
155
+
156
+ Args:
157
+ event: The Lambda event payload.
158
+ key: The key to extract from the token.
159
+
160
+ Returns:
161
+ The extracted value as a string, or None if not found.
162
+ """
163
+ try:
164
+ jwt_token = LambdaEventInfo._get_value( # pylint: disable=w0212
165
+ event, "headers/Authorization", str
166
+ ) # pylint: disable=w0212
167
+ if jwt_token:
168
+ ccas = CognitoCustomAuthorizer()
169
+ decoded_token = ccas.parse_jwt(token=jwt_token)
170
+ return decoded_token.get(key)
171
+
172
+ raise Error(
173
+ {
174
+ "status_code": HttpStatusCodes.HTTP_404_NOT_FOUND.value,
175
+ "message": f"Failed to locate {key} info in JWT Token",
176
+ }
177
+ )
178
+ except Exception as e:
179
+ raise Error(
180
+ {
181
+ "status_code": HttpStatusCodes.HTTP_401_UNAUTHENTICATED.value,
182
+ "message": f"Failed to locate {key} info in JWT Token",
183
+ "exception": str(e),
184
+ }
185
+ ) from e
186
+
187
+ class HttpPathParameters:
188
+ """
189
+ Handles path parameters in API Gateway events.
190
+ """
191
+
192
+ @staticmethod
193
+ def get_target_user_id(
194
+ event: Dict[str, Any], key: str = "user-id"
195
+ ) -> Optional[str]:
196
+ """
197
+ Extracts the target user ID from the path parameters.
198
+
199
+ Args:
200
+ event: The Lambda event payload.
201
+ key: The key representing the user ID.
202
+
203
+ Returns:
204
+ The user ID as a string, or None if not found.
205
+ """
206
+ return LambdaEventInfo._get_value_from_path_parameters(event, key) # pylint: disable=w0212
207
+
208
+ @staticmethod
209
+ def get_target_tenant_id(
210
+ event: Dict[str, Any], key: str = "tenant-id"
211
+ ) -> Optional[str]:
212
+ """
213
+ Extracts the target tenant ID from the path parameters.
214
+
215
+ Args:
216
+ event: The Lambda event payload.
217
+ key: The key representing the tenant ID.
218
+
219
+ Returns:
220
+ The tenant ID as a string, or None if not found.
221
+ """
222
+ return LambdaEventInfo._get_value_from_path_parameters(event, key) # pylint: disable=w0212
223
+
224
+ @staticmethod
225
+ def get_message_id(event: Dict[str, Any], index: int = 0) -> Optional[str]:
226
+ """
227
+ Extracts the message ID from an event record.
228
+
229
+ Args:
230
+ event: The Lambda event payload.
231
+ index: The index of the record to extract the message ID from.
232
+
233
+ Returns:
234
+ The message ID as a string, or None if not found.
235
+ """
236
+ records: List[Dict[str, Any]] = event.get("Records", [])
237
+ if records and len(records) > index:
238
+ return records[index].get("messageId")
239
+ return None
240
+
241
+ @staticmethod
242
+ def _get_value_ex(
243
+ event: Dict[str, Any], key: str, expected_type: type
244
+ ) -> Optional[Any]:
245
+ """
246
+ Extracts a value from the event, checking additional paths if necessary.
247
+
248
+ Args:
249
+ event: The Lambda event payload.
250
+ key: The key to extract.
251
+ expected_type: The expected type of the value.
252
+
253
+ Returns:
254
+ The extracted value, or None if not found.
255
+ """
256
+ value = LambdaEventInfo._get_value(event, key, expected_type) # pylint: disable=w0212
257
+ if value is None:
258
+ value = LambdaEventInfo._get_value(
259
+ event, f"requestContext/{key}", expected_type
260
+ ) # pylint: disable=w0212
261
+ return value
262
+
263
+ @staticmethod
264
+ def _get_value(
265
+ event: Dict[str, Any], key: Union[str, List[str]], expected_type: type
266
+ ) -> Optional[Any]:
267
+ """
268
+ Extracts a value from the event based on the key.
269
+
270
+ Args:
271
+ event: The Lambda event payload.
272
+ key: The key to extract, which can be a string or a list of strings for nested keys.
273
+ expected_type: The expected type of the value.
274
+
275
+ Returns:
276
+ The extracted value, or None if not found.
277
+ """
278
+ logger.debug({"source": "_get_value", "event": event, "key": key})
279
+ if not event:
280
+ return None
281
+
282
+ if isinstance(key, str):
283
+ key = re.split(r"[./]", key)
284
+
285
+ value = event
286
+ for k in key:
287
+ if isinstance(value, dict):
288
+ value = value.get(k)
289
+ else:
290
+ return None
291
+
292
+ if isinstance(value, expected_type):
293
+ return value
294
+ return None
295
+
296
+ @staticmethod
297
+ def _get_value_from_path_parameters(
298
+ event: Dict[str, Any], key: str, default: Optional[Any] = None
299
+ ) -> Optional[str]:
300
+ """
301
+ Extracts a value from the path parameters in the event.
302
+
303
+ Args:
304
+ event: The Lambda event payload.
305
+ key: The key to extract from the path parameters.
306
+ default: The default value to return if the key is not found.
307
+
308
+ Returns:
309
+ The extracted value, or None if not found.
310
+ """
311
+ value = LambdaEventInfo._search_key(event, "pathParameters", key, default) # pylint: disable=w0212
312
+ if value is None:
313
+ path = LambdaEventInfo.ApiGatewayPayload.get_resource_path(event) # pylint: disable=w0212
314
+ pattern = LambdaEventInfo.ApiGatewayPayload.get_resource_pattern(event) # pylint: disable=w0212
315
+ if path and pattern:
316
+ value = LambdaEventInfo._extract_value_from_path(path, pattern, key) # pylint: disable=w0212
317
+ return value
318
+
319
+ @staticmethod
320
+ def _extract_value_from_path(
321
+ path: str, pattern: str, variable_name: str
322
+ ) -> Optional[str]:
323
+ """
324
+ Extracts a value from a path using a regex pattern.
325
+
326
+ Args:
327
+ path: The actual path from the event.
328
+ pattern: The pattern with placeholders (e.g., /users/{user-id}).
329
+ variable_name: The variable name to extract.
330
+
331
+ Returns:
332
+ The extracted value, or None if not found.
333
+ """
334
+ regex_pattern = re.sub(
335
+ r"\{([^}]+)\}",
336
+ lambda m: f"(?P<{m.group(1).replace('-', '_')}>[^/]+)",
337
+ pattern,
338
+ )
339
+ variable_name = variable_name.replace("-", "_")
340
+ match = re.match(regex_pattern, path)
341
+ if match:
342
+ return match.group(variable_name)
343
+ return None
344
+
345
+ @staticmethod
346
+ def _search_key(
347
+ event: Dict[str, Any], container: str, key: str, default: Optional[Any] = None
348
+ ) -> Optional[Any]:
349
+ """
350
+ Searches for a key within a specified container in the event.
351
+
352
+ Args:
353
+ event: The Lambda event payload.
354
+ container: The container key to search within (e.g., "headers").
355
+ key: The key to search for within the container.
356
+ default: The default value to return if the key is not found.
357
+
358
+ Returns:
359
+ The extracted value, or the default value if not found.
360
+ """
361
+ events = [event]
362
+
363
+ if "Records" in event:
364
+ record = event["Records"][0]
365
+ if "body" in record:
366
+ body = json.loads(record["body"])
367
+ events.append(body)
368
+ if "requestContext" in body:
369
+ events.append(body["requestContext"])
370
+
371
+ for e in events:
372
+ container_data = e.get(container)
373
+ if container_data and key in container_data:
374
+ return container_data[key]
375
+
376
+ return default
377
+
378
+ @staticmethod
379
+ def get_body(event: Dict[str, Any]) -> Optional[Dict[str, Any]]:
380
+ """
381
+ Extracts the body of the event payload.
382
+
383
+ Args:
384
+ event: The Lambda event payload.
385
+
386
+ Returns:
387
+ The body as a dictionary, or None if not found.
388
+ """
389
+ tmp = event.get("Records", [{}])[0].get("body", event)
390
+ if isinstance(tmp, str):
391
+ try:
392
+ return json.loads(tmp)
393
+ except json.JSONDecodeError as e:
394
+ raise ValueError("Invalid JSON body in the payload") from e
395
+ return tmp if isinstance(tmp, dict) else None
396
+
397
+ @staticmethod
398
+ def override_event_info(
399
+ event: Dict[str, Any], key: str, value: Any
400
+ ) -> Dict[str, Any]:
401
+ """
402
+ Overrides a value in the event payload.
403
+
404
+ Args:
405
+ event: The Lambda event payload.
406
+ key: The key to override.
407
+ value: The value to set.
408
+
409
+ Returns:
410
+ The updated event payload.
411
+ """
412
+ body = LambdaEventInfo.get_body(event) or {}
413
+ body[key] = value
414
+ return body
@@ -0,0 +1,169 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+ import time
8
+ from typing import Any, Dict, List
9
+
10
+ import jwt # PyJWT
11
+ from aws_lambda_powertools import Logger
12
+ from jwt import InvalidTokenError, PyJWKClient
13
+
14
+ from boto3_assist.boto3session import Boto3SessionManager
15
+ from boto3_assist.cognito.jwks_cache import JwksCache
16
+
17
+ logger = Logger()
18
+
19
+ jwks_cache = JwksCache()
20
+
21
+
22
+ class CognitoCustomAuthorizer:
23
+ """Cognito Custom Authorizer"""
24
+
25
+ def __init__(self):
26
+ self.__client_connections: Dict[str, Any] = {}
27
+
28
+ def __get_client_connection(
29
+ self, user_pool_id: str, refresh_client: bool = False
30
+ ) -> Any:
31
+ """Get the client connection to cognito"""
32
+ region = user_pool_id.split("_")[0]
33
+ client = self.__client_connections.get(region)
34
+ if refresh_client:
35
+ client = None
36
+ if not client:
37
+ session = Boto3SessionManager(service_name="cognito-idp", aws_region=region)
38
+ client = session.client
39
+ # boto3.client("cognito-idp", region_name=region)
40
+ self.__client_connections[region] = client
41
+
42
+ return client
43
+
44
+ def generate_policy(
45
+ self, user_pools: str | List[str], event: Dict[str, Any]
46
+ ) -> Dict[str, Any]:
47
+ """Generates the policy for the authorizer"""
48
+
49
+ token = event["authorizationToken"]
50
+ user_pools = self.__to_list(user_pools=user_pools)
51
+ for user_pool_id in user_pools:
52
+ try:
53
+ if not user_pool_id:
54
+ continue
55
+ # up_id = self.__to_id(user_pool_id=user_pool_id)
56
+ # Decode the token, assuming RS256 (used by Cognito)
57
+ # decoded_token = self.decode_jwt(token=token, user_pool_id=up_id)
58
+ issuer = self.build_issuer_url(user_pool_id)
59
+ claims = self.decode_jwt(token, issuer)
60
+ # Token is valid, return an IAM policy
61
+ return self.__generate_policy_doc(
62
+ principal_id=claims["sub"],
63
+ effect="Allow",
64
+ method_arn=event["methodArn"],
65
+ )
66
+
67
+ except InvalidTokenError as e:
68
+ # Token is not valid for this user pool, try the next one
69
+ logger.debug(str(e))
70
+ continue
71
+ except Exception as e: # pylint: disable=w0718
72
+ logger.error(str(e))
73
+
74
+ # if we get here we deny it
75
+ return self.__generate_policy_doc(
76
+ principal_id="user",
77
+ effect="Deny",
78
+ method_arn=event["methodArn"],
79
+ )
80
+
81
+ def __generate_policy_doc(self, *, principal_id, effect, method_arn):
82
+ """Generate the policy doc"""
83
+ auth_response: Dict[str, Any] = {"principalId": principal_id}
84
+
85
+ if effect and method_arn:
86
+ policy_document = {
87
+ "Version": "2012-10-17",
88
+ "Statement": [
89
+ {
90
+ "Action": "execute-api:Invoke",
91
+ "Effect": effect,
92
+ "Resource": method_arn,
93
+ }
94
+ ],
95
+ }
96
+ auth_response["policyDocument"] = policy_document
97
+
98
+ return auth_response
99
+
100
+ def build_issuer_url(self, user_pool_id: str) -> str:
101
+ """Build the issuer URL"""
102
+
103
+ # Extract region from user pool ID format, e.g., "us-east-1_ABC123"
104
+ region = user_pool_id.split("_")[0]
105
+ return f"https://cognito-idp.{region}.amazonaws.com/{user_pool_id}"
106
+
107
+ def __to_list(self, user_pools: str | List[str]) -> List[str]:
108
+ if isinstance(user_pools, str):
109
+ user_pools = str(user_pools).replace(";", ",").replace(" ", "")
110
+ user_pools = str(user_pools).split(",")
111
+ elif isinstance(user_pools, list):
112
+ pass
113
+ else:
114
+ logger.warning(
115
+ f"Missing/ Invalid user pool: {user_pools}, type: {type(user_pools)}"
116
+ )
117
+
118
+ return user_pools
119
+
120
+ def parse_jwt(self, token: str) -> dict:
121
+ """Parse the JWT"""
122
+ if "Bearer" in token:
123
+ token = token.replace("Bearer ", "")
124
+
125
+ decoded_jwt: dict = jwt.decode(token, options={"verify_signature": False})
126
+
127
+ return decoded_jwt
128
+
129
+ def decode_jwt(self, token: str, issuer) -> dict:
130
+ """Decode the JWT"""
131
+ # Get the public keys
132
+ # Get the JWKS client
133
+ jwks_client = self.get_jwks_client(issuer)
134
+ if "Bearer" in token:
135
+ token = token.replace("Bearer ", "")
136
+ # Fetch the signing key using the PyJWKClient
137
+ signing_key = jwks_client.get_signing_key_from_jwt(token)
138
+
139
+ # Decode and verify the token
140
+ claims = jwt.decode(
141
+ token,
142
+ signing_key.key,
143
+ algorithms=["RS256"],
144
+ # audience=user_pool_id,
145
+ issuer=issuer,
146
+ options={"verify_aud": False}, # Disable audience verification
147
+ )
148
+
149
+ # Optional claim checks
150
+ if claims["token_use"] != "id":
151
+ # we are currently only using ID tokens
152
+ raise RuntimeError("Not an id token")
153
+
154
+ return claims
155
+
156
+ def get_jwks_client(self, issuer) -> PyJWKClient:
157
+ """Get the JWT Client"""
158
+ if (
159
+ issuer in jwks_cache.cache
160
+ and (time.time() - jwks_cache.cache.get(issuer, {})["timestamp"]) < 3600
161
+ ):
162
+ # Return cached JWKS client if it’s less than an hour old
163
+ return jwks_cache.cache[issuer]["client"]
164
+ else:
165
+ # Create a new PyJWKClient and cache it
166
+ jwks_url = f"{issuer}/.well-known/jwks.json"
167
+ jwks_client = PyJWKClient(jwks_url)
168
+ jwks_cache.cache[issuer] = {"client": jwks_client, "timestamp": time.time()}
169
+ return jwks_client
@@ -0,0 +1,21 @@
1
+ """
2
+ Geek Cafe, LLC
3
+ Maintainers: Eric Wilson
4
+ MIT License. See Project Root for the license information.
5
+ """
6
+
7
+
8
+ class JwksCache:
9
+ """A JWT Caching object"""
10
+
11
+ def __init__(self):
12
+ self.__cache = {}
13
+
14
+ @property
15
+ def cache(self):
16
+ """The Cache"""
17
+ return self.__cache
18
+
19
+ @cache.setter
20
+ def cache(self, value):
21
+ self.__cache = value
@@ -127,7 +127,7 @@ class DynamoDBModelBase:
127
127
  def projection_expression_attribute_names(self, value: dict | None):
128
128
  self.__projection_expression_attribute_names = value
129
129
 
130
- def map(self: T, item: Dict[str, Any] | DynamoDBModelBase | None) -> T | None:
130
+ def map(self: T, item: Dict[str, Any] | DynamoDBModelBase | None) -> T:
131
131
  """
132
132
  Map the item to the instance. If the item is a DynamoDBModelBase,
133
133
  it will be converted to a dictionary first and then mapped.
@@ -144,7 +144,7 @@ class DynamoDBModelBase:
144
144
  that of the dictionary object or None
145
145
  """
146
146
  if item is None:
147
- return None
147
+ item = {}
148
148
 
149
149
  if isinstance(item, DynamoDBModelBase):
150
150
  item = item.to_resource_dictionary()
@@ -155,7 +155,7 @@ class DynamoDBModelBase:
155
155
  response: dict | None = item.get("Item")
156
156
 
157
157
  if response is None:
158
- return None
158
+ response = {}
159
159
 
160
160
  item = response
161
161