boto3-assist 0.32.0__py3-none-any.whl
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/__init__.py +0 -0
- boto3_assist/aws_config.py +199 -0
- boto3_assist/aws_lambda/event_info.py +414 -0
- boto3_assist/aws_lambda/mock_context.py +5 -0
- boto3_assist/boto3session.py +87 -0
- boto3_assist/cloudwatch/cloudwatch_connection.py +84 -0
- boto3_assist/cloudwatch/cloudwatch_connection_tracker.py +17 -0
- boto3_assist/cloudwatch/cloudwatch_log_connection.py +62 -0
- boto3_assist/cloudwatch/cloudwatch_logs.py +39 -0
- boto3_assist/cloudwatch/cloudwatch_query.py +191 -0
- boto3_assist/cognito/cognito_authorizer.py +169 -0
- boto3_assist/cognito/cognito_connection.py +59 -0
- boto3_assist/cognito/cognito_utility.py +514 -0
- boto3_assist/cognito/jwks_cache.py +21 -0
- boto3_assist/cognito/user.py +27 -0
- boto3_assist/connection.py +146 -0
- boto3_assist/connection_tracker.py +120 -0
- boto3_assist/dynamodb/dynamodb.py +1206 -0
- boto3_assist/dynamodb/dynamodb_connection.py +113 -0
- boto3_assist/dynamodb/dynamodb_helpers.py +333 -0
- boto3_assist/dynamodb/dynamodb_importer.py +102 -0
- boto3_assist/dynamodb/dynamodb_index.py +507 -0
- boto3_assist/dynamodb/dynamodb_iservice.py +29 -0
- boto3_assist/dynamodb/dynamodb_key.py +130 -0
- boto3_assist/dynamodb/dynamodb_model_base.py +382 -0
- boto3_assist/dynamodb/dynamodb_model_base_interfaces.py +34 -0
- boto3_assist/dynamodb/dynamodb_re_indexer.py +165 -0
- boto3_assist/dynamodb/dynamodb_reindexer.py +165 -0
- boto3_assist/dynamodb/dynamodb_reserved_words.py +52 -0
- boto3_assist/dynamodb/dynamodb_reserved_words.txt +573 -0
- boto3_assist/dynamodb/readme.md +68 -0
- boto3_assist/dynamodb/troubleshooting.md +7 -0
- boto3_assist/ec2/ec2_connection.py +57 -0
- boto3_assist/environment_services/__init__.py +0 -0
- boto3_assist/environment_services/environment_loader.py +128 -0
- boto3_assist/environment_services/environment_variables.py +219 -0
- boto3_assist/erc/__init__.py +64 -0
- boto3_assist/erc/ecr_connection.py +57 -0
- boto3_assist/errors/custom_exceptions.py +46 -0
- boto3_assist/http_status_codes.py +80 -0
- boto3_assist/models/serializable_model.py +9 -0
- boto3_assist/role_assumption_mixin.py +38 -0
- boto3_assist/s3/s3.py +64 -0
- boto3_assist/s3/s3_bucket.py +67 -0
- boto3_assist/s3/s3_connection.py +76 -0
- boto3_assist/s3/s3_event_data.py +168 -0
- boto3_assist/s3/s3_object.py +695 -0
- boto3_assist/securityhub/securityhub.py +150 -0
- boto3_assist/securityhub/securityhub_connection.py +57 -0
- boto3_assist/session_setup_mixin.py +70 -0
- boto3_assist/ssm/connection.py +57 -0
- boto3_assist/ssm/parameter_store/parameter_store.py +116 -0
- boto3_assist/utilities/datetime_utility.py +349 -0
- boto3_assist/utilities/decimal_conversion_utility.py +140 -0
- boto3_assist/utilities/dictionary_utility.py +32 -0
- boto3_assist/utilities/file_operations.py +135 -0
- boto3_assist/utilities/http_utility.py +48 -0
- boto3_assist/utilities/logging_utility.py +0 -0
- boto3_assist/utilities/numbers_utility.py +329 -0
- boto3_assist/utilities/serialization_utility.py +664 -0
- boto3_assist/utilities/string_utility.py +337 -0
- boto3_assist/version.py +1 -0
- boto3_assist-0.32.0.dist-info/METADATA +76 -0
- boto3_assist-0.32.0.dist-info/RECORD +67 -0
- boto3_assist-0.32.0.dist-info/WHEEL +4 -0
- boto3_assist-0.32.0.dist-info/licenses/LICENSE-EXPLAINED.txt +11 -0
- boto3_assist-0.32.0.dist-info/licenses/LICENSE.txt +21 -0
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from typing import TYPE_CHECKING
|
|
9
|
+
|
|
10
|
+
from aws_lambda_powertools import Logger
|
|
11
|
+
from boto3_assist.connection import Connection
|
|
12
|
+
|
|
13
|
+
if TYPE_CHECKING:
|
|
14
|
+
from mypy_boto3_cognito_idp import CognitoIdentityProviderClient
|
|
15
|
+
else:
|
|
16
|
+
CognitoIdentityProviderClient = object
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
logger = Logger()
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class CognitoConnection(Connection):
|
|
23
|
+
"""Connection"""
|
|
24
|
+
|
|
25
|
+
def __init__(
|
|
26
|
+
self,
|
|
27
|
+
*,
|
|
28
|
+
aws_profile: Optional[str] = None,
|
|
29
|
+
aws_region: Optional[str] = None,
|
|
30
|
+
aws_access_key_id: Optional[str] = None,
|
|
31
|
+
aws_secret_access_key: Optional[str] = None,
|
|
32
|
+
) -> None:
|
|
33
|
+
super().__init__(
|
|
34
|
+
service_name="cognito-idp",
|
|
35
|
+
aws_profile=aws_profile,
|
|
36
|
+
aws_region=aws_region,
|
|
37
|
+
aws_access_key_id=aws_access_key_id,
|
|
38
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
39
|
+
)
|
|
40
|
+
|
|
41
|
+
self.__client: CognitoIdentityProviderClient | None = None
|
|
42
|
+
|
|
43
|
+
self.raise_on_error: bool = True
|
|
44
|
+
|
|
45
|
+
@property
|
|
46
|
+
def client(self) -> CognitoIdentityProviderClient:
|
|
47
|
+
"""Client Connection"""
|
|
48
|
+
if self.__client is None:
|
|
49
|
+
logger.info("Creating Client")
|
|
50
|
+
self.__client = self.session.client
|
|
51
|
+
|
|
52
|
+
if self.raise_on_error and self.__client is None:
|
|
53
|
+
raise RuntimeError("Client is not available")
|
|
54
|
+
return self.__client
|
|
55
|
+
|
|
56
|
+
@client.setter
|
|
57
|
+
def client(self, value: CognitoIdentityProviderClient):
|
|
58
|
+
logger.info("Setting Client")
|
|
59
|
+
self.__client = value
|
|
@@ -0,0 +1,514 @@
|
|
|
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 List, Dict, Any, Optional, Literal
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
from aws_lambda_powertools import Logger
|
|
12
|
+
|
|
13
|
+
from boto3_assist.cognito.user import CognitoUser
|
|
14
|
+
from boto3_assist.utilities.string_utility import StringUtility
|
|
15
|
+
from boto3_assist.utilities.dictionary_utility import DictionaryUtilitiy
|
|
16
|
+
from boto3_assist.cognito.cognito_connection import CognitoConnection
|
|
17
|
+
|
|
18
|
+
logger = Logger()
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CognitoCustomAttributes:
|
|
22
|
+
"""
|
|
23
|
+
Defines the custom Cognito attributes available in the application.
|
|
24
|
+
Use the defaults or override as needed.
|
|
25
|
+
|
|
26
|
+
Attributes:
|
|
27
|
+
USER_ID_KEY_NAME (str): The key for the custom user ID attribute.
|
|
28
|
+
TENANT_ID_KEY_NAME (str): The key for the custom tenant ID attribute.
|
|
29
|
+
USER_ROLES_KEY_NAME (str): The key for the custom roles attribute.
|
|
30
|
+
"""
|
|
31
|
+
|
|
32
|
+
def __init__(
|
|
33
|
+
self,
|
|
34
|
+
user_id_key: str = "custom:user-id",
|
|
35
|
+
tenant_id_key: str = "custom:tenant-id",
|
|
36
|
+
user_roles_key: str = "custom:roles",
|
|
37
|
+
):
|
|
38
|
+
self.user_id_custom_attribute = user_id_key
|
|
39
|
+
self.tenant_id_custom_attribute = tenant_id_key
|
|
40
|
+
self.user_roles_custom_attribute = user_roles_key
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CognitoUtility(CognitoConnection):
|
|
44
|
+
"""
|
|
45
|
+
A utility class for managing AWS Cognito operations, including user creation, modification, and authentication.
|
|
46
|
+
|
|
47
|
+
Inherits:
|
|
48
|
+
CognitoConnection: Base class providing a connection to AWS Cognito.
|
|
49
|
+
"""
|
|
50
|
+
|
|
51
|
+
def __init__(
|
|
52
|
+
self,
|
|
53
|
+
*,
|
|
54
|
+
aws_profile: Optional[str] = None,
|
|
55
|
+
aws_region: Optional[str] = None,
|
|
56
|
+
aws_access_key_id: Optional[str] = None,
|
|
57
|
+
aws_secret_access_key: Optional[str] = None,
|
|
58
|
+
custom_attributes: Optional[CognitoCustomAttributes] = None,
|
|
59
|
+
auto_lower_case_email_addresses: bool = True,
|
|
60
|
+
) -> None:
|
|
61
|
+
super().__init__(
|
|
62
|
+
aws_profile=aws_profile,
|
|
63
|
+
aws_region=aws_region,
|
|
64
|
+
aws_access_key_id=aws_access_key_id,
|
|
65
|
+
aws_secret_access_key=aws_secret_access_key,
|
|
66
|
+
)
|
|
67
|
+
|
|
68
|
+
self.custom_attributes = custom_attributes
|
|
69
|
+
self.auto_lower_case_email_addresses = auto_lower_case_email_addresses
|
|
70
|
+
|
|
71
|
+
def admin_create_user(
|
|
72
|
+
self,
|
|
73
|
+
user_pool_id: Optional[str] = None,
|
|
74
|
+
temp_password: Optional[str] = None,
|
|
75
|
+
*,
|
|
76
|
+
user: CognitoUser,
|
|
77
|
+
send_invitation: bool = False,
|
|
78
|
+
retry_count: int = 0,
|
|
79
|
+
) -> dict:
|
|
80
|
+
"""
|
|
81
|
+
Creates a new user in Cognito with custom attributes and optional invitation handling.
|
|
82
|
+
|
|
83
|
+
Args:
|
|
84
|
+
user_pool_id (Optional[str]): Cognito user pool ID.
|
|
85
|
+
temp_password (Optional[str]): Temporary password for the user.
|
|
86
|
+
user (CognitoUser): The user object containing details to create the user.
|
|
87
|
+
send_invitation (bool): Whether to send an invitation email to the user.
|
|
88
|
+
retry_count (int): Number of retries for password-related issues.
|
|
89
|
+
|
|
90
|
+
Returns:
|
|
91
|
+
dict: Response from the AWS Cognito admin create user API.
|
|
92
|
+
|
|
93
|
+
Raises:
|
|
94
|
+
ValueError: If user ID or tenant ID is missing.
|
|
95
|
+
Exception: If user creation fails for other reasons.
|
|
96
|
+
"""
|
|
97
|
+
user_supplied_password = temp_password is not None
|
|
98
|
+
|
|
99
|
+
if temp_password is None:
|
|
100
|
+
temp_password = StringUtility.generate_random_password(15)
|
|
101
|
+
|
|
102
|
+
if user.id is None:
|
|
103
|
+
raise ValueError("User id is required")
|
|
104
|
+
|
|
105
|
+
if user.tenant_id is None:
|
|
106
|
+
raise ValueError("Tenant id is required")
|
|
107
|
+
|
|
108
|
+
user_attributes = self.__set_user_attributes(user=user)
|
|
109
|
+
|
|
110
|
+
if not send_invitation:
|
|
111
|
+
user_attributes.append({"Name": "email_verified", "Value": "true"})
|
|
112
|
+
|
|
113
|
+
try:
|
|
114
|
+
kwargs = {
|
|
115
|
+
"UserPoolId": user_pool_id,
|
|
116
|
+
"Username": user.email,
|
|
117
|
+
"UserAttributes": user_attributes,
|
|
118
|
+
"DesiredDeliveryMediums": ["EMAIL"],
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
if not send_invitation:
|
|
122
|
+
kwargs["MessageAction"] = "SUPPRESS"
|
|
123
|
+
|
|
124
|
+
response = self.client.admin_create_user(**kwargs)
|
|
125
|
+
|
|
126
|
+
self.admin_set_user_password(
|
|
127
|
+
user_name=user.email,
|
|
128
|
+
password=temp_password,
|
|
129
|
+
user_pool_id=user_pool_id,
|
|
130
|
+
is_permanent=True,
|
|
131
|
+
)
|
|
132
|
+
|
|
133
|
+
return dict(response)
|
|
134
|
+
|
|
135
|
+
except self.client.exceptions.UsernameExistsException as e:
|
|
136
|
+
logger.error(f"Error: {e.response['Error']['Message']}")
|
|
137
|
+
raise
|
|
138
|
+
except self.client.exceptions.InvalidPasswordException:
|
|
139
|
+
if not user_supplied_password and retry_count < 5:
|
|
140
|
+
logger.debug(
|
|
141
|
+
{
|
|
142
|
+
"action": "admin_create_user",
|
|
143
|
+
"user_pool_id": user_pool_id,
|
|
144
|
+
"user_name": user.email,
|
|
145
|
+
"retry_count": retry_count,
|
|
146
|
+
}
|
|
147
|
+
)
|
|
148
|
+
retry_count += 1
|
|
149
|
+
return self.admin_create_user(
|
|
150
|
+
user_pool_id=user_pool_id,
|
|
151
|
+
temp_password=None,
|
|
152
|
+
send_invitation=send_invitation,
|
|
153
|
+
user=user,
|
|
154
|
+
retry_count=retry_count,
|
|
155
|
+
)
|
|
156
|
+
raise
|
|
157
|
+
except Exception as e:
|
|
158
|
+
logger.error(f"Error: {e}")
|
|
159
|
+
raise
|
|
160
|
+
|
|
161
|
+
def admin_disable_user(
|
|
162
|
+
self, user_name: str, user_pool_id: str, reset_password: bool = True
|
|
163
|
+
) -> dict:
|
|
164
|
+
"""Disable a user in cognito"""
|
|
165
|
+
response = self.client.admin_disable_user(
|
|
166
|
+
UserPoolId=user_pool_id, Username=user_name
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if reset_password:
|
|
170
|
+
self.admin_set_user_password(
|
|
171
|
+
user_name=user_name, user_pool_id=user_pool_id, password=None
|
|
172
|
+
)
|
|
173
|
+
|
|
174
|
+
return response
|
|
175
|
+
|
|
176
|
+
def admin_delete_user(self, user_name: str, user_pool_id: str) -> dict:
|
|
177
|
+
"""Delete the user account"""
|
|
178
|
+
|
|
179
|
+
# we need to disbale a user first
|
|
180
|
+
self.admin_disable_user(
|
|
181
|
+
user_name=user_name, user_pool_id=user_pool_id, reset_password=False
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
response = self.client.admin_delete_user(
|
|
185
|
+
UserPoolId=user_pool_id, Username=user_name
|
|
186
|
+
)
|
|
187
|
+
|
|
188
|
+
return dict(response)
|
|
189
|
+
|
|
190
|
+
def admin_enable_user(
|
|
191
|
+
self, user_name: str, user_pool_id: str, reset_password: bool = True
|
|
192
|
+
) -> dict:
|
|
193
|
+
"""Enable the user account"""
|
|
194
|
+
response = self.client.admin_enable_user(
|
|
195
|
+
UserPoolId=user_pool_id, Username=user_name
|
|
196
|
+
)
|
|
197
|
+
|
|
198
|
+
if reset_password:
|
|
199
|
+
# reset the password
|
|
200
|
+
self.admin_set_user_password(
|
|
201
|
+
user_name=user_name, user_pool_id=user_pool_id, password=None
|
|
202
|
+
)
|
|
203
|
+
return response
|
|
204
|
+
|
|
205
|
+
def admin_set_user_password(
|
|
206
|
+
self, user_name, password: str | None, user_pool_id, is_permanent=True
|
|
207
|
+
) -> dict:
|
|
208
|
+
"""Set a user password"""
|
|
209
|
+
|
|
210
|
+
if not password:
|
|
211
|
+
password = StringUtility.generate_random_password(15)
|
|
212
|
+
logger.debug(
|
|
213
|
+
{
|
|
214
|
+
"action": "admin_set_user_password",
|
|
215
|
+
"UserPoolId": user_pool_id,
|
|
216
|
+
"Username": user_name,
|
|
217
|
+
"Password": "****************",
|
|
218
|
+
"Permanent": is_permanent,
|
|
219
|
+
}
|
|
220
|
+
)
|
|
221
|
+
|
|
222
|
+
for i in range(5):
|
|
223
|
+
try:
|
|
224
|
+
response = self.client.admin_set_user_password(
|
|
225
|
+
UserPoolId=user_pool_id,
|
|
226
|
+
Username=user_name,
|
|
227
|
+
Password=password,
|
|
228
|
+
Permanent=is_permanent,
|
|
229
|
+
)
|
|
230
|
+
break
|
|
231
|
+
except Exception as e: # pylint: disable=w0718
|
|
232
|
+
time.sleep(5 * i + 1)
|
|
233
|
+
logger.error(f"Error: {e}")
|
|
234
|
+
if i >= 4:
|
|
235
|
+
raise e
|
|
236
|
+
|
|
237
|
+
return response
|
|
238
|
+
|
|
239
|
+
def update_user_account(self, *, user_pool_id: str, user: CognitoUser) -> dict:
|
|
240
|
+
"""
|
|
241
|
+
Update the cognito user account
|
|
242
|
+
"""
|
|
243
|
+
user_attributes = self.__set_user_attributes(user=user)
|
|
244
|
+
|
|
245
|
+
if user.cognito_user_name is None:
|
|
246
|
+
raise ValueError("User cognito user name is required")
|
|
247
|
+
|
|
248
|
+
response = self.client.admin_update_user_attributes(
|
|
249
|
+
UserPoolId=f"{user_pool_id}",
|
|
250
|
+
Username=f"{user.cognito_user_name}",
|
|
251
|
+
UserAttributes=user_attributes,
|
|
252
|
+
ClientMetadata={"string": "string"},
|
|
253
|
+
)
|
|
254
|
+
return response
|
|
255
|
+
|
|
256
|
+
def sign_up_cognito_user(self, email, password, client_id) -> dict | None:
|
|
257
|
+
"""
|
|
258
|
+
This is only allowed if the admin only flag is not being inforced.
|
|
259
|
+
Under most circumstatnces we won't have this enabled
|
|
260
|
+
"""
|
|
261
|
+
email = self.__format_email(email=email)
|
|
262
|
+
try:
|
|
263
|
+
# Create the user in Cognito
|
|
264
|
+
response = self.client.sign_up(
|
|
265
|
+
ClientId=client_id,
|
|
266
|
+
Username=email,
|
|
267
|
+
Password=password,
|
|
268
|
+
UserAttributes=[{"Name": "email", "Value": email}],
|
|
269
|
+
)
|
|
270
|
+
|
|
271
|
+
logger.debug(
|
|
272
|
+
f"User {email} created successfully. Confirmation code sent to {email}."
|
|
273
|
+
)
|
|
274
|
+
return dict(response)
|
|
275
|
+
|
|
276
|
+
except self.client.exceptions.UsernameExistsException as e:
|
|
277
|
+
logger.error(f"Error: {e.response['Error']['Message']}")
|
|
278
|
+
logger.error(
|
|
279
|
+
f"The username {email} already exists. Please choose a different username."
|
|
280
|
+
)
|
|
281
|
+
return None
|
|
282
|
+
|
|
283
|
+
except self.client.exceptions.InvalidPasswordException as e:
|
|
284
|
+
logger.error(f"Error: {e.response['Error']['Message']}")
|
|
285
|
+
logger.error(
|
|
286
|
+
"Password does not meet the requirements. Please choose a stronger password."
|
|
287
|
+
)
|
|
288
|
+
return None
|
|
289
|
+
|
|
290
|
+
except Exception as e: # pylint: disable=w0718
|
|
291
|
+
logger.error(f"Error: {e}")
|
|
292
|
+
return None
|
|
293
|
+
|
|
294
|
+
def authenticate_user_pass_auth(
|
|
295
|
+
self, username, password, client_id
|
|
296
|
+
) -> tuple[str, str, str]:
|
|
297
|
+
"""
|
|
298
|
+
Login with the username/passwrod combo + client_id
|
|
299
|
+
Returns:
|
|
300
|
+
Tuple: id_token, access_token, refresh_token
|
|
301
|
+
Use the id_token as the jwt
|
|
302
|
+
Use the access_token if you are directly accessing aws resources
|
|
303
|
+
Use the refresh_token if you are attempting to get a 'refreshed' jwt token
|
|
304
|
+
"""
|
|
305
|
+
# Initiate the authentication process and get the session
|
|
306
|
+
auth_response = self.client.initiate_auth(
|
|
307
|
+
ClientId=client_id,
|
|
308
|
+
AuthFlow="USER_PASSWORD_AUTH",
|
|
309
|
+
AuthParameters={"USERNAME": username, "PASSWORD": password},
|
|
310
|
+
)
|
|
311
|
+
|
|
312
|
+
if "ChallengeName" in auth_response:
|
|
313
|
+
raise RuntimeError("New password required before a token can be provided")
|
|
314
|
+
|
|
315
|
+
# Extract the session tokens
|
|
316
|
+
id_token = auth_response["AuthenticationResult"]["IdToken"]
|
|
317
|
+
access_token = auth_response["AuthenticationResult"]["AccessToken"]
|
|
318
|
+
refresh_token = auth_response["AuthenticationResult"]["RefreshToken"]
|
|
319
|
+
|
|
320
|
+
return id_token, access_token, refresh_token
|
|
321
|
+
|
|
322
|
+
def create_client_app_machine_to_machine(
|
|
323
|
+
self,
|
|
324
|
+
user_pool_id,
|
|
325
|
+
client_name,
|
|
326
|
+
id_token_time_out=60,
|
|
327
|
+
id_token_units: Literal["days", "hours", "minutes", "seconds"] = "minutes",
|
|
328
|
+
access_token_time_out=60,
|
|
329
|
+
access_token_units: Literal["days", "hours", "minutes", "seconds"] = "minutes",
|
|
330
|
+
refresh_token_time_out=60,
|
|
331
|
+
refresh_token_units: Literal["days", "hours", "minutes", "seconds"] = "minutes",
|
|
332
|
+
) -> dict:
|
|
333
|
+
# valid units: 'seconds'|'minutes'|'hours'|'days'
|
|
334
|
+
|
|
335
|
+
response = self.client.create_user_pool_client(
|
|
336
|
+
UserPoolId=f"{user_pool_id}",
|
|
337
|
+
ClientName=f"{client_name}",
|
|
338
|
+
GenerateSecret=True,
|
|
339
|
+
RefreshTokenValidity=refresh_token_time_out,
|
|
340
|
+
AccessTokenValidity=access_token_time_out,
|
|
341
|
+
IdTokenValidity=id_token_time_out,
|
|
342
|
+
TokenValidityUnits={
|
|
343
|
+
"AccessToken": access_token_units,
|
|
344
|
+
"IdToken": id_token_units,
|
|
345
|
+
"RefreshToken": refresh_token_units,
|
|
346
|
+
},
|
|
347
|
+
# ReadAttributes=[
|
|
348
|
+
# 'string',
|
|
349
|
+
# ],
|
|
350
|
+
# WriteAttributes=[
|
|
351
|
+
# 'string',
|
|
352
|
+
# ],
|
|
353
|
+
# ExplicitAuthFlows=[
|
|
354
|
+
# 'ADMIN_NO_SRP_AUTH'|'CUSTOM_AUTH_FLOW_ONLY'|'USER_PASSWORD_AUTH'|'ALLOW_ADMIN_USER_PASSWORD_AUTH'|'ALLOW_CUSTOM_AUTH'|'ALLOW_USER_PASSWORD_AUTH'|'ALLOW_USER_SRP_AUTH'|'ALLOW_REFRESH_TOKEN_AUTH',
|
|
355
|
+
# ],
|
|
356
|
+
# SupportedIdentityProviders=[
|
|
357
|
+
# 'string',
|
|
358
|
+
# ],
|
|
359
|
+
# CallbackURLs=[
|
|
360
|
+
# 'string',
|
|
361
|
+
# ],
|
|
362
|
+
# LogoutURLs=[
|
|
363
|
+
# 'string',
|
|
364
|
+
# ],
|
|
365
|
+
# DefaultRedirectURI='string',
|
|
366
|
+
AllowedOAuthFlows=["client_credentials"],
|
|
367
|
+
AllowedOAuthScopes=[
|
|
368
|
+
"string",
|
|
369
|
+
],
|
|
370
|
+
AllowedOAuthFlowsUserPoolClient=True,
|
|
371
|
+
# AnalyticsConfiguration={
|
|
372
|
+
# 'ApplicationId': 'string',
|
|
373
|
+
# 'ApplicationArn': 'string',
|
|
374
|
+
# 'RoleArn': 'string',
|
|
375
|
+
# 'ExternalId': 'string',
|
|
376
|
+
# 'UserDataShared': True|False
|
|
377
|
+
# },
|
|
378
|
+
# PreventUserExistenceErrors='LEGACY'|'ENABLED',
|
|
379
|
+
EnableTokenRevocation=True,
|
|
380
|
+
# EnablePropagateAdditionalUserContextData=True|False,
|
|
381
|
+
# AuthSessionValidity=123
|
|
382
|
+
)
|
|
383
|
+
|
|
384
|
+
return dict(response)
|
|
385
|
+
|
|
386
|
+
def search_cognito(self, email: str, user_pool_id: str) -> dict:
|
|
387
|
+
"""Search cognito for an existing user"""
|
|
388
|
+
|
|
389
|
+
email = self.__format_email(email=email) or ""
|
|
390
|
+
filter_string = f'email = "{email}"'
|
|
391
|
+
|
|
392
|
+
# Call the admin_list_users method with the filter
|
|
393
|
+
response = self.client.list_users(UserPoolId=user_pool_id, Filter=filter_string)
|
|
394
|
+
|
|
395
|
+
return dict(response)
|
|
396
|
+
|
|
397
|
+
def __set_user_attributes(self, *, user: CognitoUser) -> List[dict]:
|
|
398
|
+
"""
|
|
399
|
+
Constructs a list of user attributes for Cognito based on the provided user object.
|
|
400
|
+
|
|
401
|
+
Args:
|
|
402
|
+
user (CognitoUser): The user object containing attributes to set.
|
|
403
|
+
|
|
404
|
+
Returns:
|
|
405
|
+
List[dict]: A list of attribute dictionaries for Cognito.
|
|
406
|
+
"""
|
|
407
|
+
user_attributes: List[Dict[str, Any]] = [
|
|
408
|
+
{"Name": "email", "Value": str(user.email).lower()}
|
|
409
|
+
]
|
|
410
|
+
|
|
411
|
+
user_attributes.append({"Name": "email_verified", "Value": "true"})
|
|
412
|
+
|
|
413
|
+
if user.first_name is not None:
|
|
414
|
+
user_attributes.append({"Name": "given_name", "Value": user.first_name})
|
|
415
|
+
|
|
416
|
+
if user.last_name is not None:
|
|
417
|
+
user_attributes.append({"Name": "family_name", "Value": user.last_name})
|
|
418
|
+
|
|
419
|
+
if self.custom_attributes:
|
|
420
|
+
if user.id is not None:
|
|
421
|
+
user_attributes.append(
|
|
422
|
+
{
|
|
423
|
+
"Name": self.custom_attributes.user_id_custom_attribute,
|
|
424
|
+
"Value": user.id,
|
|
425
|
+
}
|
|
426
|
+
)
|
|
427
|
+
|
|
428
|
+
if user.roles is not None:
|
|
429
|
+
roles: str = (
|
|
430
|
+
",".join(user.roles) if isinstance(user.roles, list) else user.roles
|
|
431
|
+
)
|
|
432
|
+
user_attributes.append(
|
|
433
|
+
{
|
|
434
|
+
"Name": self.custom_attributes.user_roles_custom_attribute,
|
|
435
|
+
"Value": roles,
|
|
436
|
+
}
|
|
437
|
+
)
|
|
438
|
+
|
|
439
|
+
if user.tenant_id is not None:
|
|
440
|
+
user_attributes.append(
|
|
441
|
+
{
|
|
442
|
+
"Name": self.custom_attributes.tenant_id_custom_attribute,
|
|
443
|
+
"Value": user.tenant_id,
|
|
444
|
+
}
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
return user_attributes
|
|
448
|
+
|
|
449
|
+
def map(self, cognito_response: dict) -> CognitoUser:
|
|
450
|
+
"""Map the cognito response to a user object"""
|
|
451
|
+
user = CognitoUser()
|
|
452
|
+
# this is the internal Cognito ID that get's generated
|
|
453
|
+
user.cognito_user_name = self.get_cognito_attribute(
|
|
454
|
+
cognito_response, "Username"
|
|
455
|
+
)
|
|
456
|
+
user.email = self.get_cognito_attribute(cognito_response, "email", None)
|
|
457
|
+
user.first_name = self.get_cognito_attribute(
|
|
458
|
+
cognito_response, "given_name", None
|
|
459
|
+
)
|
|
460
|
+
user.last_name = self.get_cognito_attribute(
|
|
461
|
+
cognito_response, "family_name", None
|
|
462
|
+
)
|
|
463
|
+
if self.custom_attributes:
|
|
464
|
+
user.id = self.get_cognito_attribute(
|
|
465
|
+
cognito_response, self.custom_attributes.user_id_custom_attribute, None
|
|
466
|
+
)
|
|
467
|
+
user.tenant_id = self.get_cognito_attribute(
|
|
468
|
+
cognito_response,
|
|
469
|
+
self.custom_attributes.tenant_id_custom_attribute,
|
|
470
|
+
None,
|
|
471
|
+
)
|
|
472
|
+
|
|
473
|
+
roles: str | None | List[str] = self.get_cognito_attribute(
|
|
474
|
+
cognito_response,
|
|
475
|
+
self.custom_attributes.user_roles_custom_attribute,
|
|
476
|
+
None,
|
|
477
|
+
)
|
|
478
|
+
else:
|
|
479
|
+
user.id = self.get_cognito_attribute(cognito_response, "sub", None)
|
|
480
|
+
roles = self.get_cognito_attribute(cognito_response, "cognito:groups", None)
|
|
481
|
+
|
|
482
|
+
if roles is None:
|
|
483
|
+
roles = []
|
|
484
|
+
if isinstance(roles, str):
|
|
485
|
+
roles = roles.split(",")
|
|
486
|
+
user.roles = roles
|
|
487
|
+
return user
|
|
488
|
+
|
|
489
|
+
def get_cognito_attribute(
|
|
490
|
+
self, response: dict, name: str, default: Optional[str] = None
|
|
491
|
+
) -> Optional[str]:
|
|
492
|
+
if name in response:
|
|
493
|
+
return response.get(name, default)
|
|
494
|
+
|
|
495
|
+
attributes = response.get("Attributes", [])
|
|
496
|
+
attribute = DictionaryUtilitiy.find_dict_by_name(attributes, "Name", name)
|
|
497
|
+
if attribute and isinstance(attribute, list):
|
|
498
|
+
return str(attribute[0].get("Value", default))
|
|
499
|
+
return default
|
|
500
|
+
|
|
501
|
+
def __format_email(self, email: str | None) -> str | None:
|
|
502
|
+
"""
|
|
503
|
+
Formats an email address to be used in Cognito user pools. Converts to lowercase
|
|
504
|
+
if self.auto_lower_case_email_addresses is set to true (the default)
|
|
505
|
+
|
|
506
|
+
Args:
|
|
507
|
+
email (str | None): The email address to format.
|
|
508
|
+
|
|
509
|
+
Returns:
|
|
510
|
+
str | None: The formatted email address, or None if input is None.
|
|
511
|
+
"""
|
|
512
|
+
if self.auto_lower_case_email_addresses:
|
|
513
|
+
return None if email is None else str(email).lower()
|
|
514
|
+
return email
|
|
@@ -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,27 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Geek Cafe, LLC
|
|
3
|
+
Maintainers: Eric Wilson
|
|
4
|
+
MIT License. See Project Root for the license information.
|
|
5
|
+
"""
|
|
6
|
+
|
|
7
|
+
from typing import Optional
|
|
8
|
+
from boto3_assist.models.serializable_model import SerializableModel
|
|
9
|
+
|
|
10
|
+
|
|
11
|
+
class CognitoUser(SerializableModel):
|
|
12
|
+
"""A generic way to represent a cognito user"""
|
|
13
|
+
|
|
14
|
+
def __init__(
|
|
15
|
+
self,
|
|
16
|
+
id: Optional[str] = None, # pylint: disable=w0622
|
|
17
|
+
) -> None:
|
|
18
|
+
super().__init__()
|
|
19
|
+
self.id: Optional[str] = id
|
|
20
|
+
self.first_name: Optional[str] = None
|
|
21
|
+
self.last_name: Optional[str] = None
|
|
22
|
+
self.email: Optional[str] = None
|
|
23
|
+
self.tenant_id: Optional[str] = None
|
|
24
|
+
self.status: Optional[str] = None
|
|
25
|
+
self.company_name: Optional[str] = None
|
|
26
|
+
self.roles: list[str] = []
|
|
27
|
+
self.cognito_user_name: str | None = None
|