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.
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/PKG-INFO +2 -1
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/pyproject.toml +3 -3
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/requirements.txt +1 -0
- boto3_assist-0.4.0/src/boto3_assist/aws_lambda/event_info.py +414 -0
- boto3_assist-0.4.0/src/boto3_assist/cognito/cognito_authorizer.py +169 -0
- boto3_assist-0.4.0/src/boto3_assist/cognito/jwks_cache.py +21 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_model_base.py +3 -3
- boto3_assist-0.4.0/src/boto3_assist/http_status_codes.py +80 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/utilities/serialization_utility.py +1 -1
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/utilities/string_utility.py +1 -1
- boto3_assist-0.4.0/src/boto3_assist/version.py +1 -0
- boto3_assist-0.4.0/tests/lambda/event_info_test.py +119 -0
- boto3_assist-0.4.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.4.0}/.env.docker +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.env.docker.001 +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.env.docker.nosql.workbench +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.env.unittest +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.gitignore +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.vscode/launch.json +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.vscode/settings.json +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/.vscode/tasks.json +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/LICENSE-EXPLAINED.txt +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/LICENSE.txt +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/README.md +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/aws_regions_with_status.csv +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/aws_regions_with_status.json +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/devops/build.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/devops/readme.md +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/cloudwatch/log_report.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/models/order_item_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/models/order_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/models/product_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/models/user_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/models/user_post_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/order_example/main.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/order_example/products.json +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/order_item_service.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/order_service.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/product_service.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/table_service.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/user_post_service.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/user_service.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/user_service_client_example.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/services/user_service_resource_example.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/dynamodb/user_post_example/main.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/examples/ec2/regions_report.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/module-headers.txt +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/mypy.ini +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/requirements-dev.txt +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/run-checks.sh +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/boto3session.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/cloudwatch/cloudwatch_connection.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/cloudwatch/cloudwatch_log_connection.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/cloudwatch/cloudwatch_logs.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/cloudwatch/cloudwatch_query.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/connection.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/connection_tracker.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_connection.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_connection_tracker.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_helpers.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_importer.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_index.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_iservice.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_key.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_reindexer.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/dynamodb_reserved_words.txt +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/readme.md +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/dynamodb/troubleshooting.md +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/ec2/ec2_connection.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/environment_services/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/environment_services/environment_loader.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/environment_services/environment_variables.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/errors/custom_exceptions.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/s3/s3.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/s3/s3_connection.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/utilities/datetime_utility.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/utilities/file_operations.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/utilities/http_utility.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/src/boto3_assist/utilities/logging_utility.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/__top/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/dynamodb_model_base_test.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/dynamodb_model_projections_test.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/dynamodb_model_serializtion_test.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/dynamodb_moto_sorting_test.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/dynamodb_reindex_test.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/cms/base.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/cms/content_block.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/cms/page.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/cms/template.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/simple_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/user_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/dynamodb/models/user_required_fields_model.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/examples_test/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/examples_test/user_service_test.py +0 -0
- {boto3_assist-0.2.12/tests/s3 → boto3_assist-0.4.0/tests/lambda}/__init__.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/s3/files/test.txt +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/s3/s3_file_upload_test.py +0 -0
- {boto3_assist-0.2.12 → boto3_assist-0.4.0}/tests/utilities/__init__.py +0 -0
- {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.
|
|
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.
|
|
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
|
-
"
|
|
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
|
|
@@ -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
|
|
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
|
-
|
|
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
|
-
|
|
158
|
+
response = {}
|
|
159
159
|
|
|
160
160
|
item = response
|
|
161
161
|
|