boto3-assist 0.2.12__tar.gz → 0.3.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.
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/PKG-INFO +2 -1
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/pyproject.toml +3 -3
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/requirements.txt +1 -0
- boto3_assist-0.3.0/src/boto3_assist/aws_lambda/event_info.py +414 -0
- boto3_assist-0.3.0/src/boto3_assist/cognito/cognito_authorizer.py +169 -0
- boto3_assist-0.3.0/src/boto3_assist/cognito/jwks_cache.py +21 -0
- boto3_assist-0.3.0/src/boto3_assist/http_status_codes.py +80 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/utilities/serialization_utility.py +1 -1
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/utilities/string_utility.py +1 -1
- boto3_assist-0.3.0/src/boto3_assist/version.py +1 -0
- boto3_assist-0.3.0/tests/lambda/event_info_test.py +119 -0
- boto3_assist-0.3.0/tests/s3/__init__.py +0 -0
- boto3_assist-0.2.12/src/boto3_assist/version.py +0 -1
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/.env.docker +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/.env.docker.001 +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/.env.docker.nosql.workbench +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/.env.unittest +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/.gitignore +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/.vscode/launch.json +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/.vscode/settings.json +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/.vscode/tasks.json +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/LICENSE-EXPLAINED.txt +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/LICENSE.txt +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/README.md +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/aws_regions_with_status.csv +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/aws_regions_with_status.json +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/devops/build.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/devops/readme.md +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/cloudwatch/log_report.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/models/order_item_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/models/order_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/models/product_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/models/user_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/models/user_post_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/order_example/main.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/order_example/products.json +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/services/order_item_service.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/services/order_service.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/services/product_service.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/services/table_service.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/services/user_post_service.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/services/user_service.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/services/user_service_client_example.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/services/user_service_resource_example.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/user_post_example/main.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/ec2/regions_report.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/module-headers.txt +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/mypy.ini +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/requirements-dev.txt +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/run-checks.sh +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/boto3session.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/connection.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/connection_tracker.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_connection.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_connection_tracker.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_helpers.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_index.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_model_base.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/readme.md +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/ec2/ec2_connection.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/environment_services/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/environment_services/environment_loader.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/environment_services/environment_variables.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/errors/custom_exceptions.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/s3/s3.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/s3/s3_connection.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/utilities/datetime_utility.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/utilities/file_operations.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/utilities/http_utility.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/utilities/logging_utility.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/__top/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/dynamodb_model_base_test.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/dynamodb_model_projections_test.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/dynamodb_model_serializtion_test.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/dynamodb_moto_sorting_test.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/dynamodb_reindex_test.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/models/cms/base.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/models/cms/content_block.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/models/cms/page.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/models/cms/template.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/models/simple_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/models/user_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/models/user_required_fields_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/examples_test/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/examples_test/user_service_test.py +0 -0
- {boto3_assist-0.2.12/tests/s3 → boto3_assist-0.3.0/tests/lambda}/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/s3/files/test.txt +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/s3/s3_file_upload_test.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/utilities/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.3.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.
|
|
3
|
+
Version: 0.3.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.
|
|
10
|
+
version = "0.3.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
|
-
"
|
|
31
|
+
"python-dateutil",
|
|
32
|
+
"PyJWT",
|
|
33
33
|
"requests",
|
|
34
34
|
"types-python-dateutil",
|
|
35
35
|
]
|
|
@@ -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
|
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from enum import Enum
|
|
8
|
+
|
|
9
|
+
|
|
10
|
+
class HttpStatusCodes(Enum):
|
|
11
|
+
"""Http Status Codes"""
|
|
12
|
+
|
|
13
|
+
HTTP_400_BAD_REQUEST = 400
|
|
14
|
+
"""
|
|
15
|
+
The server cannot or will not process the request due to something that is perceived to be a client error
|
|
16
|
+
(e.g., malformed request syntax, invalid request message framing, or deceptive request routing)."""
|
|
17
|
+
HTTP_401_UNAUTHENTICATED = 401
|
|
18
|
+
"""
|
|
19
|
+
Although the HTTP standard specifies "unauthorized", semantically this response means "unauthenticated".
|
|
20
|
+
That is, the client must authenticate itself to get the requested response.
|
|
21
|
+
"""
|
|
22
|
+
HTTP_403_FORBIDDEN = 403
|
|
23
|
+
"""
|
|
24
|
+
The client does not have access rights to the content; that is, it is "unauthorized", so the server is refusing
|
|
25
|
+
to give the requested resource. Unlike 401 Unauthorized (which is technically UnAuthenticated);
|
|
26
|
+
here, the client's identity is known to the server.
|
|
27
|
+
"""
|
|
28
|
+
HTTP_404_NOT_FOUND = 404
|
|
29
|
+
"""
|
|
30
|
+
The server cannot find the requested resource. In the browser, this means the URL is not recognized.
|
|
31
|
+
In an API, this can also mean that the endpoint is valid but the resource itself does not exist.
|
|
32
|
+
Servers may also send this response instead of 403 Forbidden to hide the existence of a resource from
|
|
33
|
+
an unauthorized client. This response code is probably the most well known due to its frequent occurrence on the web.
|
|
34
|
+
"""
|
|
35
|
+
HTTP_405_METHOD_NOT_ALLOWED = 405
|
|
36
|
+
"""
|
|
37
|
+
The request method is known by the server but is not supported by the target resource.
|
|
38
|
+
For example, an API may not allow calling DELETE to remove a resource.
|
|
39
|
+
"""
|
|
40
|
+
HTTP_406_NOT_ACCEPTABLE = 406
|
|
41
|
+
"""
|
|
42
|
+
This response is sent when the web server, after performing server-driven content negotiation, doesn't find any
|
|
43
|
+
content that conforms to the criteria given by the user agent.
|
|
44
|
+
"""
|
|
45
|
+
HTTP_407_PROXY_AUTHENTICATION_REQUIRED = 407
|
|
46
|
+
"""
|
|
47
|
+
This is similar to 401 Unauthorized but authentication is needed to be done by a proxy.
|
|
48
|
+
"""
|
|
49
|
+
HTTP_408_REQUEST_TIMEOUT = 408
|
|
50
|
+
"""
|
|
51
|
+
This response is sent on an idle connection by some servers, even without any previous request by the client.
|
|
52
|
+
It means that the server would like to shut down this unused connection. This response is used much more since
|
|
53
|
+
some browsers, like Chrome, Firefox 27+, or IE9, use HTTP pre-connection mechanisms to speed up surfing.
|
|
54
|
+
Also note that some servers merely shut down the connection without sending this message.
|
|
55
|
+
"""
|
|
56
|
+
|
|
57
|
+
HTTP_415_UNSUPPORTED_MEDIA_TYPE = 415
|
|
58
|
+
"""
|
|
59
|
+
The media format of the requested data is not supported by the server, so the server is rejecting the request.
|
|
60
|
+
"""
|
|
61
|
+
|
|
62
|
+
HTTP_418_IM_A_TEAPOT = 418
|
|
63
|
+
"""
|
|
64
|
+
The server refuses the attempt to brew coffee with a teapot.
|
|
65
|
+
"""
|
|
66
|
+
|
|
67
|
+
HTTP_422_UNEXPECTED_OUTCOME = 422
|
|
68
|
+
"""
|
|
69
|
+
The request was well-formed but was unable to be followed due to semantic errors.
|
|
70
|
+
"""
|
|
71
|
+
|
|
72
|
+
HTTP_429_TOO_MANY_REQUESTS = 429
|
|
73
|
+
"""
|
|
74
|
+
The user has sent too many requests in a given amount of time ("rate limiting").
|
|
75
|
+
"""
|
|
76
|
+
|
|
77
|
+
HTTP_500_INTERNAL_SERVER_ERROR = 500
|
|
78
|
+
"""
|
|
79
|
+
The server has encountered a situation it does not know how to handle.
|
|
80
|
+
"""
|
{boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/utilities/serialization_utility.py
RENAMED
|
@@ -36,7 +36,7 @@ class JsonEncoder(json.JSONEncoder):
|
|
|
36
36
|
elif isinstance(o, Decimal):
|
|
37
37
|
return float(o)
|
|
38
38
|
|
|
39
|
-
logger.info(f"
|
|
39
|
+
logger.info(f"JsonEncoder failing back: ${type(o)}")
|
|
40
40
|
|
|
41
41
|
# Fallback to the base class implementation for other types
|
|
42
42
|
|
|
@@ -39,7 +39,7 @@ class JsonEncoder(json.JSONEncoder):
|
|
|
39
39
|
elif isinstance(o, Decimal):
|
|
40
40
|
return float(o)
|
|
41
41
|
|
|
42
|
-
logger.info(f"
|
|
42
|
+
logger.info(f"JsonEncoder failing back: ${type(o)}")
|
|
43
43
|
|
|
44
44
|
# Fallback to the base class implementation for other types
|
|
45
45
|
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
__version__ = '0.3.0'
|
|
@@ -0,0 +1,119 @@
|
|
|
1
|
+
import unittest
|
|
2
|
+
from unittest.mock import patch
|
|
3
|
+
|
|
4
|
+
from boto3_assist.aws_lambda.event_info import LambdaEventInfo
|
|
5
|
+
|
|
6
|
+
|
|
7
|
+
class TestLambdaEventInfo(unittest.TestCase):
|
|
8
|
+
"""
|
|
9
|
+
Unit tests for the LambdaEventInfo utility class.
|
|
10
|
+
Covers methods for extracting HTTP method types, resource paths,
|
|
11
|
+
authorizer claims, and overriding event information.
|
|
12
|
+
"""
|
|
13
|
+
|
|
14
|
+
def setUp(self):
|
|
15
|
+
"""
|
|
16
|
+
Set up the test environment with a sample Lambda event.
|
|
17
|
+
"""
|
|
18
|
+
self.event = {
|
|
19
|
+
"method_type": "POST",
|
|
20
|
+
"path": "/users/123",
|
|
21
|
+
"resourcePath": "/users/{user-id}",
|
|
22
|
+
"requestContext": {
|
|
23
|
+
"authorizer": {
|
|
24
|
+
"claims": {"token_use": "access", "email": "user@example.com"}
|
|
25
|
+
}
|
|
26
|
+
},
|
|
27
|
+
"headers": {"Authorization": "Bearer jwt.token.here"},
|
|
28
|
+
"pathParameters": {"user-id": "123"},
|
|
29
|
+
"Records": [{"messageId": "abc-123", "body": '{"key": "value"}'}],
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
def test_get_http_method_type(self):
|
|
33
|
+
"""
|
|
34
|
+
Test that the HTTP method type is correctly extracted from the event.
|
|
35
|
+
"""
|
|
36
|
+
result = LambdaEventInfo.ApiGatewayPayload.get_http_method_type(self.event)
|
|
37
|
+
self.assertEqual(result, "POST")
|
|
38
|
+
|
|
39
|
+
def test_get_resource_path(self):
|
|
40
|
+
"""
|
|
41
|
+
Test that the resource path is correctly extracted from the event.
|
|
42
|
+
"""
|
|
43
|
+
result = LambdaEventInfo.ApiGatewayPayload.get_resource_path(self.event)
|
|
44
|
+
self.assertEqual(result, "/users/123")
|
|
45
|
+
|
|
46
|
+
def test_get_resource_pattern(self):
|
|
47
|
+
"""
|
|
48
|
+
Test that the resource pattern with placeholders is correctly extracted.
|
|
49
|
+
"""
|
|
50
|
+
result = LambdaEventInfo.ApiGatewayPayload.get_resource_pattern(self.event)
|
|
51
|
+
self.assertEqual(result, "/users/{user-id}")
|
|
52
|
+
|
|
53
|
+
def test_get_authenticated_email(self):
|
|
54
|
+
"""
|
|
55
|
+
Test that the authenticated email is correctly extracted from the authorizer claims.
|
|
56
|
+
"""
|
|
57
|
+
result = (
|
|
58
|
+
LambdaEventInfo.ApiGatewayPayload.AuthorizerPayload.get_authenticated_email(
|
|
59
|
+
self.event
|
|
60
|
+
)
|
|
61
|
+
)
|
|
62
|
+
self.assertEqual(result, "user@example.com")
|
|
63
|
+
|
|
64
|
+
def test_get_token_use(self):
|
|
65
|
+
"""
|
|
66
|
+
Test that the token use is correctly extracted from the authorizer claims.
|
|
67
|
+
"""
|
|
68
|
+
result = LambdaEventInfo.ApiGatewayPayload.AuthorizerPayload.get_token_use(
|
|
69
|
+
self.event
|
|
70
|
+
)
|
|
71
|
+
self.assertEqual(result, "access")
|
|
72
|
+
|
|
73
|
+
@patch("boto3_assist.cognito.cognito_authorizer.CognitoCustomAuthorizer.parse_jwt")
|
|
74
|
+
def testget_value_from_token(self, mock_parse_jwt):
|
|
75
|
+
"""
|
|
76
|
+
Test that values are correctly extracted from the JWT token.
|
|
77
|
+
"""
|
|
78
|
+
mock_parse_jwt.return_value = {"email": "user@example.com"}
|
|
79
|
+
result = (
|
|
80
|
+
LambdaEventInfo.ApiGatewayPayload.AuthorizerPayload.get_value_from_token(
|
|
81
|
+
self.event, "email"
|
|
82
|
+
)
|
|
83
|
+
) # pylint: disable=w0212
|
|
84
|
+
self.assertEqual(result, "user@example.com")
|
|
85
|
+
|
|
86
|
+
def test_get_target_user_id(self):
|
|
87
|
+
"""
|
|
88
|
+
Test that the target user ID is correctly extracted from the path parameters.
|
|
89
|
+
"""
|
|
90
|
+
result = LambdaEventInfo.HttpPathParameters.get_target_user_id(self.event)
|
|
91
|
+
self.assertEqual(result, "123")
|
|
92
|
+
|
|
93
|
+
def test_get_message_id(self):
|
|
94
|
+
"""
|
|
95
|
+
Test that the message ID is correctly extracted from the event records.
|
|
96
|
+
"""
|
|
97
|
+
result = LambdaEventInfo.get_message_id(self.event)
|
|
98
|
+
self.assertEqual(result, "abc-123")
|
|
99
|
+
|
|
100
|
+
def test_get_body(self):
|
|
101
|
+
"""
|
|
102
|
+
Test that the body is correctly extracted and parsed from the event.
|
|
103
|
+
"""
|
|
104
|
+
result = LambdaEventInfo.get_body(self.event)
|
|
105
|
+
self.assertEqual(result, {"key": "value"})
|
|
106
|
+
|
|
107
|
+
def test_override_event_info(self):
|
|
108
|
+
"""
|
|
109
|
+
Test that event information can be correctly overridden.
|
|
110
|
+
"""
|
|
111
|
+
updated_event = LambdaEventInfo.override_event_info(
|
|
112
|
+
self.event, "new_key", "new_value"
|
|
113
|
+
)
|
|
114
|
+
self.assertIn("new_key", updated_event)
|
|
115
|
+
self.assertEqual(updated_event["new_key"], "new_value")
|
|
116
|
+
|
|
117
|
+
|
|
118
|
+
if __name__ == "__main__":
|
|
119
|
+
unittest.main()
|
|
File without changes
|
|
@@ -1 +0,0 @@
|
|
|
1
|
-
__version__ = '0.2.12'
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boto3_assist-0.2.12 → boto3_assist-0.3.0}/examples/dynamodb/services/user_service_client_example.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/cloudwatch/cloudwatch_connection.py
RENAMED
|
File without changes
|
|
File without changes
|
{boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_connection_tracker.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py
RENAMED
|
File without changes
|
{boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boto3_assist-0.2.12 → boto3_assist-0.3.0}/src/boto3_assist/environment_services/__init__.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/dynamodb_model_projections_test.py
RENAMED
|
File without changes
|
{boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/dynamodb_model_serializtion_test.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
{boto3_assist-0.2.12 → boto3_assist-0.3.0}/tests/dynamodb/models/user_required_fields_model.py
RENAMED
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|