boto3-assist 0.3.0__py3-none-any.whl → 0.5.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/boto3session.py +3 -0
- boto3_assist/cognito/cognito_connection.py +59 -0
- boto3_assist/cognito/cognito_utility.py +514 -0
- boto3_assist/cognito/user.py +20 -0
- boto3_assist/connection.py +14 -8
- boto3_assist/connection_tracker.py +75 -35
- boto3_assist/dynamodb/dynamodb_connection.py +11 -61
- boto3_assist/dynamodb/dynamodb_model_base.py +3 -3
- boto3_assist/ec2/ec2_connection.py +12 -61
- boto3_assist/environment_services/environment_loader.py +1 -1
- boto3_assist/s3/s3_connection.py +10 -51
- boto3_assist/utilities/datetime_utility.py +26 -0
- boto3_assist/utilities/dictionaroy_utility.py +26 -0
- boto3_assist/utilities/numbers_utility.py +286 -0
- boto3_assist/utilities/string_utility.py +59 -0
- boto3_assist/version.py +1 -1
- {boto3_assist-0.3.0.dist-info → boto3_assist-0.5.0.dist-info}/METADATA +1 -1
- {boto3_assist-0.3.0.dist-info → boto3_assist-0.5.0.dist-info}/RECORD +21 -17
- boto3_assist/dynamodb/dynamodb_connection_tracker.py +0 -17
- {boto3_assist-0.3.0.dist-info → boto3_assist-0.5.0.dist-info}/WHEEL +0 -0
- {boto3_assist-0.3.0.dist-info → boto3_assist-0.5.0.dist-info}/licenses/LICENSE-EXPLAINED.txt +0 -0
- {boto3_assist-0.3.0.dist-info → boto3_assist-0.5.0.dist-info}/licenses/LICENSE.txt +0 -0
boto3_assist/boto3session.py
CHANGED
|
@@ -135,6 +135,7 @@ class Boto3SessionManager:
|
|
|
135
135
|
def client(self) -> Any:
|
|
136
136
|
"""Return the boto3 client connection."""
|
|
137
137
|
if not self.__client:
|
|
138
|
+
logger.debug(f"Creating {self.service_name} client")
|
|
138
139
|
self.__client = self.__session.client(
|
|
139
140
|
self.service_name,
|
|
140
141
|
config=self.config,
|
|
@@ -147,6 +148,7 @@ class Boto3SessionManager:
|
|
|
147
148
|
def resource(self) -> Any:
|
|
148
149
|
"""Return the boto3 resource connection."""
|
|
149
150
|
if not self.__resource:
|
|
151
|
+
logger.debug(f"Creating {self.service_name} resource")
|
|
150
152
|
self.__resource = self.__session.resource(
|
|
151
153
|
self.service_name,
|
|
152
154
|
config=self.config,
|
|
@@ -156,6 +158,7 @@ class Boto3SessionManager:
|
|
|
156
158
|
|
|
157
159
|
def __create_boto3_session(self) -> boto3.Session | None:
|
|
158
160
|
try:
|
|
161
|
+
logger.debug(f"Creating session for {self.service_name}")
|
|
159
162
|
session = boto3.Session(
|
|
160
163
|
profile_name=self.aws_profile,
|
|
161
164
|
region_name=self.aws_region,
|
|
@@ -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
|
|
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.dictionaroy_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 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 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 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="minutes",
|
|
328
|
+
access_token_time_out=60,
|
|
329
|
+
access_token_units="minutes",
|
|
330
|
+
refresh_token_time_out=60,
|
|
331
|
+
refresh_token_units="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": f"{access_token_units}",
|
|
344
|
+
"IdToken": f"{id_token_units}",
|
|
345
|
+
"RefreshToken": f"{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 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)
|
|
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 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,20 @@
|
|
|
1
|
+
from typing import Optional
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
class CognitoUser:
|
|
5
|
+
"""A generic way to represent a cognito user"""
|
|
6
|
+
|
|
7
|
+
def __init__(
|
|
8
|
+
self,
|
|
9
|
+
id: Optional[str] = None, # pylint: disable=w0622
|
|
10
|
+
) -> None:
|
|
11
|
+
super().__init__()
|
|
12
|
+
self.id: Optional[str] = id
|
|
13
|
+
self.first_name: Optional[str] = None
|
|
14
|
+
self.last_name: Optional[str] = None
|
|
15
|
+
self.email: Optional[str] = None
|
|
16
|
+
self.tenant_id: Optional[str] = None
|
|
17
|
+
self.status: Optional[str] = None
|
|
18
|
+
self.company_name: Optional[str] = None
|
|
19
|
+
self.roles: list[str] = []
|
|
20
|
+
self.cognito_user_name: str | None = None
|
boto3_assist/connection.py
CHANGED
|
@@ -11,17 +11,15 @@ from boto3_assist.boto3session import Boto3SessionManager
|
|
|
11
11
|
from boto3_assist.environment_services.environment_variables import (
|
|
12
12
|
EnvironmentVariables,
|
|
13
13
|
)
|
|
14
|
-
from boto3_assist.
|
|
15
|
-
CloudWatchConnectionTracker,
|
|
16
|
-
)
|
|
14
|
+
from boto3_assist.connection_tracker import ConnectionTracker
|
|
17
15
|
|
|
18
16
|
|
|
19
17
|
logger = Logger()
|
|
20
|
-
tracker:
|
|
18
|
+
tracker: ConnectionTracker = ConnectionTracker()
|
|
21
19
|
|
|
22
20
|
|
|
23
21
|
class Connection:
|
|
24
|
-
"""Boto 3 Connection"""
|
|
22
|
+
"""Base Boto 3 Connection"""
|
|
25
23
|
|
|
26
24
|
def __init__(
|
|
27
25
|
self,
|
|
@@ -31,7 +29,9 @@ class Connection:
|
|
|
31
29
|
aws_region: Optional[str] = None,
|
|
32
30
|
aws_access_key_id: Optional[str] = None,
|
|
33
31
|
aws_secret_access_key: Optional[str] = None,
|
|
32
|
+
aws_end_point_url: Optional[str] = None,
|
|
34
33
|
) -> None:
|
|
34
|
+
# TODO: determine if we want to pull from environment vars or not
|
|
35
35
|
self.aws_profile = aws_profile or EnvironmentVariables.AWS.profile()
|
|
36
36
|
self.aws_region = aws_region or EnvironmentVariables.AWS.region()
|
|
37
37
|
|
|
@@ -42,9 +42,16 @@ class Connection:
|
|
|
42
42
|
aws_secret_access_key
|
|
43
43
|
or EnvironmentVariables.AWS.DynamoDB.aws_secret_access_key()
|
|
44
44
|
)
|
|
45
|
+
self.end_point_url = aws_end_point_url
|
|
45
46
|
self.__session: Boto3SessionManager | None = None
|
|
46
47
|
|
|
47
48
|
self.__service_name: str | None = service_name
|
|
49
|
+
|
|
50
|
+
if self.__service_name is None:
|
|
51
|
+
raise RuntimeError(
|
|
52
|
+
"Service Name is not available. The service name is required."
|
|
53
|
+
)
|
|
54
|
+
|
|
48
55
|
self.raise_on_error: bool = True
|
|
49
56
|
|
|
50
57
|
def setup(self, setup_source: Optional[str] = None) -> None:
|
|
@@ -72,11 +79,10 @@ class Connection:
|
|
|
72
79
|
aws_region=self.aws_region,
|
|
73
80
|
aws_access_key_id=self.aws_access_key_id,
|
|
74
81
|
aws_secret_access_key=self.aws_secret_access_key,
|
|
82
|
+
aws_endpoint_url=self.end_point_url,
|
|
75
83
|
)
|
|
76
84
|
|
|
77
|
-
tracker.
|
|
78
|
-
|
|
79
|
-
self.raise_on_error = EnvironmentVariables.AWS.DynamoDB.raise_on_error_setting()
|
|
85
|
+
tracker.add(service_name=self.service_name)
|
|
80
86
|
|
|
81
87
|
@property
|
|
82
88
|
def service_name(self) -> str:
|