geek-cafe-saas-sdk 0.6.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.
Potentially problematic release.
This version of geek-cafe-saas-sdk might be problematic. Click here for more details.
- geek_cafe_saas_sdk/__init__.py +9 -0
- geek_cafe_saas_sdk/core/__init__.py +11 -0
- geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
- geek_cafe_saas_sdk/core/error_codes.py +132 -0
- geek_cafe_saas_sdk/core/service_errors.py +19 -0
- geek_cafe_saas_sdk/core/service_result.py +121 -0
- geek_cafe_saas_sdk/decorators/__init__.py +64 -0
- geek_cafe_saas_sdk/decorators/auth.py +373 -0
- geek_cafe_saas_sdk/decorators/core.py +358 -0
- geek_cafe_saas_sdk/domains/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
- geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
- geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
- geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
- geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
- geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
- geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
- geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
- geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
- geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
- geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
- geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
- geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
- geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
- geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
- geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
- geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
- geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
- geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
- geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
- geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
- geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
- geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
- geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
- geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
- geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
- geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
- geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
- geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
- geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
- geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
- geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
- geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
- geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
- geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
- geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
- geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
- geek_cafe_saas_sdk/middleware/__init__.py +36 -0
- geek_cafe_saas_sdk/middleware/auth.py +85 -0
- geek_cafe_saas_sdk/middleware/authorization.py +523 -0
- geek_cafe_saas_sdk/middleware/cors.py +63 -0
- geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
- geek_cafe_saas_sdk/middleware/validation.py +80 -0
- geek_cafe_saas_sdk/models/__init__.py +20 -0
- geek_cafe_saas_sdk/models/base_model.py +233 -0
- geek_cafe_saas_sdk/services/__init__.py +18 -0
- geek_cafe_saas_sdk/services/database_service.py +441 -0
- geek_cafe_saas_sdk/utilities/__init__.py +88 -0
- geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
- geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
- geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
- geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
- geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
- geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
- geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
- geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
- geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
- geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
- geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
- geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
- geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
- geek_cafe_saas_sdk/utilities/response.py +209 -0
- geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
|
@@ -0,0 +1,568 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Cognito Utility
|
|
3
|
+
"""
|
|
4
|
+
|
|
5
|
+
import os
|
|
6
|
+
import time
|
|
7
|
+
from typing import List, Dict, Any, Optional
|
|
8
|
+
import boto3
|
|
9
|
+
|
|
10
|
+
from geek_cafe_saas_sdk.utilities.logging_utility import LoggingUtility, LogLevels
|
|
11
|
+
from geek_cafe_saas_sdk.utilities.environment_variables import (
|
|
12
|
+
EnvironmentVariables,
|
|
13
|
+
)
|
|
14
|
+
from geek_cafe_saas_sdk.domains.auth.models import User
|
|
15
|
+
from geek_cafe_saas_sdk.utilities.string_functions import StringFunctions
|
|
16
|
+
from geek_cafe_saas_sdk.utilities.dictionary_utility import DictionaryUtility
|
|
17
|
+
|
|
18
|
+
AWS_PROFILE = os.getenv("AWS_PROFILE")
|
|
19
|
+
AWS_REGION = os.getenv("AWS_REGION")
|
|
20
|
+
# Create a Cognito client
|
|
21
|
+
PROVISIONED_SESSION = None
|
|
22
|
+
PROVISIONED_CLIENT = None
|
|
23
|
+
try:
|
|
24
|
+
PROVISIONED_SESSION = boto3.Session(
|
|
25
|
+
profile_name=AWS_PROFILE, region_name=AWS_REGION
|
|
26
|
+
)
|
|
27
|
+
PROVISIONED_CLIENT = PROVISIONED_SESSION.client("cognito-idp")
|
|
28
|
+
except: # noqa: E722, pylint: disable=w0702
|
|
29
|
+
pass
|
|
30
|
+
|
|
31
|
+
logger = LoggingUtility.get_logger(__name__, LogLevels.INFO)
|
|
32
|
+
|
|
33
|
+
|
|
34
|
+
class CognitoCustomAttributes:
|
|
35
|
+
"""Defines the Cognito Custom Attributes we have available"""
|
|
36
|
+
|
|
37
|
+
USER_ID_KEY_NAME: str = "custom:user_id"
|
|
38
|
+
TENANT_ID_KEY_NAME: str = "custom:tenant_id"
|
|
39
|
+
USER_ROLES_KEY_NAME: str = "custom:roles"
|
|
40
|
+
USER_PERMISSIONS_KEY_NAME: str = "custom:permissions"
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
class CognitoUtility:
|
|
44
|
+
"""AWS Cognito Utility"""
|
|
45
|
+
|
|
46
|
+
def __init__(
|
|
47
|
+
self, aws_profile: Optional[str] = None, aws_region: Optional[str] = None
|
|
48
|
+
) -> None:
|
|
49
|
+
aws_profile = aws_profile or os.getenv("AWS_PROFILE")
|
|
50
|
+
aws_region = aws_region or os.getenv("AWS_REGION")
|
|
51
|
+
if aws_profile is not None:
|
|
52
|
+
self.session = boto3.Session(
|
|
53
|
+
profile_name=aws_profile, region_name=aws_region
|
|
54
|
+
)
|
|
55
|
+
# use one with the profile provided
|
|
56
|
+
self.client = self.session.client("cognito-idp")
|
|
57
|
+
else:
|
|
58
|
+
# use the one already provisioned
|
|
59
|
+
self.client = PROVISIONED_CLIENT
|
|
60
|
+
|
|
61
|
+
self.use_custom_attributes: bool = True
|
|
62
|
+
|
|
63
|
+
def admin_create_user(
|
|
64
|
+
self,
|
|
65
|
+
user_pool_id: Optional[str] = None,
|
|
66
|
+
temp_password: Optional[str] = None,
|
|
67
|
+
*,
|
|
68
|
+
user: User,
|
|
69
|
+
send_invitation: bool = False,
|
|
70
|
+
retry_count: int = 0,
|
|
71
|
+
) -> dict:
|
|
72
|
+
"""
|
|
73
|
+
Creates a user for the geek cafe saas system. The user is created
|
|
74
|
+
in the environment for the Cognito User Pool and added to DynamoDB
|
|
75
|
+
for tracking in the SaaS system.
|
|
76
|
+
|
|
77
|
+
Users will have a sub/id which is the Cognito Id however we'll use an
|
|
78
|
+
internal id (user_id), which will be useful if we need failover
|
|
79
|
+
Cognito User Pools in the future.
|
|
80
|
+
|
|
81
|
+
"""
|
|
82
|
+
user_supplied_password = temp_password is not None
|
|
83
|
+
|
|
84
|
+
if temp_password is None:
|
|
85
|
+
temp_password = StringFunctions.generate_random_password(15)
|
|
86
|
+
|
|
87
|
+
if user.id is None:
|
|
88
|
+
raise ValueError("User id is required")
|
|
89
|
+
|
|
90
|
+
if user.tenant_id is None:
|
|
91
|
+
raise ValueError("Tenant id is required")
|
|
92
|
+
|
|
93
|
+
user_attributes = self.__set_user_attributes(user=user)
|
|
94
|
+
|
|
95
|
+
# email_verified
|
|
96
|
+
# this sets their email address as if it's had been verified.
|
|
97
|
+
# we may want to remove this in the future.
|
|
98
|
+
# if this is not set then they are in a locked state and must use a temp password
|
|
99
|
+
if not send_invitation:
|
|
100
|
+
user_attributes.append({"Name": "email_verified", "Value": "true"})
|
|
101
|
+
|
|
102
|
+
try:
|
|
103
|
+
kwargs = {
|
|
104
|
+
"UserPoolId": user_pool_id,
|
|
105
|
+
"Username": user.email,
|
|
106
|
+
"UserAttributes": user_attributes,
|
|
107
|
+
# "TemporaryPassword": temp_password,
|
|
108
|
+
# ForceAliasCreation=True|False,
|
|
109
|
+
"DesiredDeliveryMediums": [
|
|
110
|
+
"EMAIL",
|
|
111
|
+
],
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
if not send_invitation:
|
|
115
|
+
# add to the args
|
|
116
|
+
kwargs["MessageAction"] = "SUPPRESS"
|
|
117
|
+
|
|
118
|
+
# create the user in cognito
|
|
119
|
+
response = self.client.admin_create_user(**kwargs)
|
|
120
|
+
|
|
121
|
+
# something changed and we need to reset the password
|
|
122
|
+
# otherwise they get into a force password change and they're locked out
|
|
123
|
+
# (they need the temp password which we sending them at the moment)
|
|
124
|
+
self.admin_set_user_password(
|
|
125
|
+
user_name=user.email,
|
|
126
|
+
password=temp_password,
|
|
127
|
+
user_pool_id=user_pool_id,
|
|
128
|
+
is_permanent=True,
|
|
129
|
+
)
|
|
130
|
+
|
|
131
|
+
return response
|
|
132
|
+
|
|
133
|
+
except self.client.exceptions.UsernameExistsException as e:
|
|
134
|
+
logger.error(f"Error: {e.response['Error']['Message']}")
|
|
135
|
+
logger.error(
|
|
136
|
+
f"The username {user.email} already exists. Please choose a different username."
|
|
137
|
+
)
|
|
138
|
+
raise e
|
|
139
|
+
|
|
140
|
+
except self.client.exceptions.InvalidPasswordException as e:
|
|
141
|
+
logger.error(f"Error: {e.response['Error']['Message']}")
|
|
142
|
+
logger.error(
|
|
143
|
+
"Password does not meet the requirements. Please choose a stronger password."
|
|
144
|
+
)
|
|
145
|
+
if not user_supplied_password and retry_count < 5:
|
|
146
|
+
logger.debug(
|
|
147
|
+
{
|
|
148
|
+
"action": "admin_create_user",
|
|
149
|
+
"user_pool_id": user_pool_id,
|
|
150
|
+
"user_name": user.email,
|
|
151
|
+
"user_supplied_password": user_supplied_password,
|
|
152
|
+
"retry_count": retry_count,
|
|
153
|
+
"message": (
|
|
154
|
+
"User did not supply the password. We created one automatically, "
|
|
155
|
+
"but it did not meet the requirements. Trying again."
|
|
156
|
+
),
|
|
157
|
+
"error": f"Error: {e.response['Error']['Message']}",
|
|
158
|
+
}
|
|
159
|
+
)
|
|
160
|
+
retry_count += 1
|
|
161
|
+
return self.admin_create_user(
|
|
162
|
+
user_pool_id=user_pool_id,
|
|
163
|
+
temp_password=None,
|
|
164
|
+
send_invitation=send_invitation,
|
|
165
|
+
user=user,
|
|
166
|
+
retry_count=retry_count,
|
|
167
|
+
)
|
|
168
|
+
else:
|
|
169
|
+
logger.debug(
|
|
170
|
+
{
|
|
171
|
+
"action": "admin_create_user",
|
|
172
|
+
"user_pool_id": user_pool_id,
|
|
173
|
+
"user_name": user.email,
|
|
174
|
+
"user_supplied_password": user_supplied_password,
|
|
175
|
+
"retry_count": retry_count,
|
|
176
|
+
}
|
|
177
|
+
)
|
|
178
|
+
raise e
|
|
179
|
+
except self.client.exceptions.InvalidParameterException as e:
|
|
180
|
+
logger.error(f"Error: {e.response['Error']['Message']}")
|
|
181
|
+
logger.error(
|
|
182
|
+
"An invalid parameter was added. This is mostlikely an attempt to add a custome attribute that isn't registered."
|
|
183
|
+
)
|
|
184
|
+
raise e
|
|
185
|
+
except Exception as e:
|
|
186
|
+
logger.error(f"Error: {e}")
|
|
187
|
+
raise e
|
|
188
|
+
|
|
189
|
+
def admin_disable_user(
|
|
190
|
+
self, user_name: str, user_pool_id: str, reset_password: bool = True
|
|
191
|
+
) -> dict:
|
|
192
|
+
"""Disable a user in cognito"""
|
|
193
|
+
response = self.client.admin_disable_user(
|
|
194
|
+
UserPoolId=user_pool_id, Username=user_name
|
|
195
|
+
)
|
|
196
|
+
|
|
197
|
+
if reset_password:
|
|
198
|
+
self.admin_set_user_password(
|
|
199
|
+
user_name=user_name, user_pool_id=user_pool_id, password=None
|
|
200
|
+
)
|
|
201
|
+
|
|
202
|
+
return response
|
|
203
|
+
|
|
204
|
+
def admin_delete_user(self, user_name: str, user_pool_id: str) -> dict:
|
|
205
|
+
"""Delete the user account"""
|
|
206
|
+
|
|
207
|
+
# we need to disbale a user first
|
|
208
|
+
self.admin_disable_user(
|
|
209
|
+
user_name=user_name, user_pool_id=user_pool_id, reset_password=False
|
|
210
|
+
)
|
|
211
|
+
|
|
212
|
+
response = self.client.admin_delete_user(
|
|
213
|
+
UserPoolId=user_pool_id, Username=user_name
|
|
214
|
+
)
|
|
215
|
+
|
|
216
|
+
return response
|
|
217
|
+
|
|
218
|
+
def admin_enable_user(
|
|
219
|
+
self, user_name: str, user_pool_id: str, reset_password: bool = True
|
|
220
|
+
) -> dict:
|
|
221
|
+
"""Enable the user account"""
|
|
222
|
+
response = self.client.admin_enable_user(
|
|
223
|
+
UserPoolId=user_pool_id, Username=user_name
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
if reset_password:
|
|
227
|
+
# reset the password
|
|
228
|
+
self.admin_set_user_password(
|
|
229
|
+
user_name=user_name, user_pool_id=user_pool_id, password=None
|
|
230
|
+
)
|
|
231
|
+
return response
|
|
232
|
+
|
|
233
|
+
def admin_set_user_password(
|
|
234
|
+
self, user_name, password: str | None, user_pool_id, is_permanent=True
|
|
235
|
+
) -> dict:
|
|
236
|
+
"""Set a user password"""
|
|
237
|
+
|
|
238
|
+
if not password:
|
|
239
|
+
password = StringFunctions.generate_random_password(15)
|
|
240
|
+
logger.debug(
|
|
241
|
+
{
|
|
242
|
+
"action": "admin_set_user_password",
|
|
243
|
+
"UserPoolId": user_pool_id,
|
|
244
|
+
"Username": user_name,
|
|
245
|
+
"Password": "****************",
|
|
246
|
+
"Permanent": is_permanent,
|
|
247
|
+
}
|
|
248
|
+
)
|
|
249
|
+
|
|
250
|
+
for i in range(5):
|
|
251
|
+
try:
|
|
252
|
+
response = self.client.admin_set_user_password(
|
|
253
|
+
UserPoolId=user_pool_id,
|
|
254
|
+
Username=user_name,
|
|
255
|
+
Password=password,
|
|
256
|
+
Permanent=is_permanent,
|
|
257
|
+
)
|
|
258
|
+
break
|
|
259
|
+
except Exception as e: # pylint: disable=w0718
|
|
260
|
+
time.sleep(5 * i + 1)
|
|
261
|
+
logger.error(f"Error: {e}")
|
|
262
|
+
if i >= 4:
|
|
263
|
+
raise e
|
|
264
|
+
|
|
265
|
+
return response
|
|
266
|
+
|
|
267
|
+
def update_user_account(self, *, user_pool_id: str, user: User) -> dict:
|
|
268
|
+
"""
|
|
269
|
+
Update the cognito user account
|
|
270
|
+
"""
|
|
271
|
+
user_attributes = self.__set_user_attributes(user=user)
|
|
272
|
+
|
|
273
|
+
if user.cognito_user_name is None:
|
|
274
|
+
raise ValueError("User cognito user name is required")
|
|
275
|
+
|
|
276
|
+
response = self.client.admin_update_user_attributes(
|
|
277
|
+
UserPoolId=f"{user_pool_id}",
|
|
278
|
+
Username=f"{user.cognito_user_name}",
|
|
279
|
+
UserAttributes=user_attributes,
|
|
280
|
+
ClientMetadata={"string": "string"},
|
|
281
|
+
)
|
|
282
|
+
return response
|
|
283
|
+
|
|
284
|
+
def sign_up_cognito_user(self, email, password, client_id) -> dict | None:
|
|
285
|
+
"""
|
|
286
|
+
This is only allowed if the admin only flag is not being enforced.
|
|
287
|
+
Under most circumstances we won't have this enabled
|
|
288
|
+
"""
|
|
289
|
+
email = self.__format_email(email=email)
|
|
290
|
+
try:
|
|
291
|
+
# Create the user in Cognito
|
|
292
|
+
response = self.client.sign_up(
|
|
293
|
+
ClientId=client_id,
|
|
294
|
+
Username=email,
|
|
295
|
+
Password=password,
|
|
296
|
+
UserAttributes=[{"Name": "email", "Value": email}],
|
|
297
|
+
)
|
|
298
|
+
|
|
299
|
+
logger.debug(
|
|
300
|
+
f"User {email} created successfully. Confirmation code sent to {email}."
|
|
301
|
+
)
|
|
302
|
+
return response
|
|
303
|
+
|
|
304
|
+
except self.client.exceptions.UsernameExistsException as e:
|
|
305
|
+
logger.error(f"Error: {e.response['Error']['Message']}")
|
|
306
|
+
logger.error(
|
|
307
|
+
f"The username {email} already exists. Please choose a different username."
|
|
308
|
+
)
|
|
309
|
+
return None
|
|
310
|
+
|
|
311
|
+
except self.client.exceptions.InvalidPasswordException as e:
|
|
312
|
+
logger.error(f"Error: {e.response['Error']['Message']}")
|
|
313
|
+
logger.error(
|
|
314
|
+
"Password does not meet the requirements. Please choose a stronger password."
|
|
315
|
+
)
|
|
316
|
+
return None
|
|
317
|
+
|
|
318
|
+
except Exception as e: # pylint: disable=w0718
|
|
319
|
+
logger.error(f"Error: {e}")
|
|
320
|
+
return None
|
|
321
|
+
|
|
322
|
+
def authenticate_user_pass_auth(
|
|
323
|
+
self, username, password, client_id
|
|
324
|
+
) -> tuple[str, str, str]:
|
|
325
|
+
"""
|
|
326
|
+
Login with the username/passwrod combo + client_id
|
|
327
|
+
Returns:
|
|
328
|
+
Tuple: id_token, access_token, refresh_token
|
|
329
|
+
Use the id_token as the jwt
|
|
330
|
+
Use the access_token if you are directly accessing aws resources
|
|
331
|
+
Use the refresh_token if you are attempting to get a 'refreshed' jwt token
|
|
332
|
+
"""
|
|
333
|
+
# Initiate the authentication process and get the session
|
|
334
|
+
auth_response = self.client.initiate_auth(
|
|
335
|
+
ClientId=client_id,
|
|
336
|
+
AuthFlow="USER_PASSWORD_AUTH",
|
|
337
|
+
AuthParameters={"USERNAME": username, "PASSWORD": password},
|
|
338
|
+
)
|
|
339
|
+
|
|
340
|
+
if "ChallengeName" in auth_response:
|
|
341
|
+
raise RuntimeError("New password required before a token can be provided")
|
|
342
|
+
|
|
343
|
+
# Extract the session tokens
|
|
344
|
+
id_token = auth_response["AuthenticationResult"]["IdToken"]
|
|
345
|
+
access_token = auth_response["AuthenticationResult"]["AccessToken"]
|
|
346
|
+
refresh_token = auth_response["AuthenticationResult"]["RefreshToken"]
|
|
347
|
+
|
|
348
|
+
return id_token, access_token, refresh_token
|
|
349
|
+
|
|
350
|
+
def create_resource_server(
|
|
351
|
+
self,
|
|
352
|
+
user_pool_id,
|
|
353
|
+
resource_server_name=None,
|
|
354
|
+
resource_server_identifier=None,
|
|
355
|
+
scopes=None,
|
|
356
|
+
) -> dict:
|
|
357
|
+
if not resource_server_name:
|
|
358
|
+
resource_server_name = "nca-resources"
|
|
359
|
+
|
|
360
|
+
if not resource_server_identifier:
|
|
361
|
+
tenant_id = EnvironmentVariables.get_tenant_id()
|
|
362
|
+
resource_server_identifier = tenant_id
|
|
363
|
+
if scopes is None or len(scopes) == 0:
|
|
364
|
+
scopes = []
|
|
365
|
+
scopes.append(
|
|
366
|
+
{
|
|
367
|
+
"ScopeName": "analysis:execution",
|
|
368
|
+
"ScopeDescription": "ability to execute an analysis",
|
|
369
|
+
},
|
|
370
|
+
{
|
|
371
|
+
"ScopeName": "analysis:download",
|
|
372
|
+
"ScopeDescription": "ability to download an analysis",
|
|
373
|
+
},
|
|
374
|
+
)
|
|
375
|
+
|
|
376
|
+
response = self.client.create_resource_server(
|
|
377
|
+
UserPoolId=user_pool_id,
|
|
378
|
+
Identifier=resource_server_identifier,
|
|
379
|
+
Name=f"{resource_server_name}",
|
|
380
|
+
Scopes=scopes,
|
|
381
|
+
)
|
|
382
|
+
|
|
383
|
+
return response
|
|
384
|
+
|
|
385
|
+
def create_client_app_machine_to_machine(
|
|
386
|
+
self,
|
|
387
|
+
user_pool_id,
|
|
388
|
+
client_name,
|
|
389
|
+
id_token_time_out=60,
|
|
390
|
+
id_token_units="minutes",
|
|
391
|
+
access_token_time_out=60,
|
|
392
|
+
access_token_units="minutes",
|
|
393
|
+
refresh_token_time_out=60,
|
|
394
|
+
refresh_token_units="minutes",
|
|
395
|
+
) -> dict:
|
|
396
|
+
# valid units: 'seconds'|'minutes'|'hours'|'days'
|
|
397
|
+
|
|
398
|
+
response = self.client.create_user_pool_client(
|
|
399
|
+
UserPoolId=f"{user_pool_id}",
|
|
400
|
+
ClientName=f"{client_name}",
|
|
401
|
+
GenerateSecret=True,
|
|
402
|
+
RefreshTokenValidity=refresh_token_time_out,
|
|
403
|
+
AccessTokenValidity=access_token_time_out,
|
|
404
|
+
IdTokenValidity=id_token_time_out,
|
|
405
|
+
TokenValidityUnits={
|
|
406
|
+
"AccessToken": f"{access_token_units}",
|
|
407
|
+
"IdToken": f"{id_token_units}",
|
|
408
|
+
"RefreshToken": f"{refresh_token_units}",
|
|
409
|
+
},
|
|
410
|
+
# ReadAttributes=[
|
|
411
|
+
# 'string',
|
|
412
|
+
# ],
|
|
413
|
+
# WriteAttributes=[
|
|
414
|
+
# 'string',
|
|
415
|
+
# ],
|
|
416
|
+
# ExplicitAuthFlows=[
|
|
417
|
+
# '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',
|
|
418
|
+
# ],
|
|
419
|
+
# SupportedIdentityProviders=[
|
|
420
|
+
# 'string',
|
|
421
|
+
# ],
|
|
422
|
+
# CallbackURLs=[
|
|
423
|
+
# 'string',
|
|
424
|
+
# ],
|
|
425
|
+
# LogoutURLs=[
|
|
426
|
+
# 'string',
|
|
427
|
+
# ],
|
|
428
|
+
# DefaultRedirectURI='string',
|
|
429
|
+
AllowedOAuthFlows=["client_credentials"],
|
|
430
|
+
AllowedOAuthScopes=[
|
|
431
|
+
"string",
|
|
432
|
+
],
|
|
433
|
+
AllowedOAuthFlowsUserPoolClient=True,
|
|
434
|
+
# AnalyticsConfiguration={
|
|
435
|
+
# 'ApplicationId': 'string',
|
|
436
|
+
# 'ApplicationArn': 'string',
|
|
437
|
+
# 'RoleArn': 'string',
|
|
438
|
+
# 'ExternalId': 'string',
|
|
439
|
+
# 'UserDataShared': True|False
|
|
440
|
+
# },
|
|
441
|
+
# PreventUserExistenceErrors='LEGACY'|'ENABLED',
|
|
442
|
+
EnableTokenRevocation=True,
|
|
443
|
+
# EnablePropagateAdditionalUserContextData=True|False,
|
|
444
|
+
# AuthSessionValidity=123
|
|
445
|
+
)
|
|
446
|
+
|
|
447
|
+
return response
|
|
448
|
+
|
|
449
|
+
def search_cognito(self, email_address: str, user_pool_id: str) -> dict:
|
|
450
|
+
"""Search cognito for an existing user"""
|
|
451
|
+
|
|
452
|
+
email_address = self.__format_email(email=email_address) or ""
|
|
453
|
+
filter_string = f'email = "{email_address}"'
|
|
454
|
+
|
|
455
|
+
# Call the admin_list_users method with the filter
|
|
456
|
+
response = self.client.list_users(UserPoolId=user_pool_id, Filter=filter_string)
|
|
457
|
+
|
|
458
|
+
return response
|
|
459
|
+
|
|
460
|
+
def __set_user_attributes(self, *, user: User) -> List[dict]:
|
|
461
|
+
"""Set the user attributes"""
|
|
462
|
+
|
|
463
|
+
user_attributes: List[Dict[str, Any]] = [
|
|
464
|
+
{"Name": "email", "Value": str(user.email).lower()}
|
|
465
|
+
]
|
|
466
|
+
|
|
467
|
+
user_attributes.append({"Name": "email_verified", "Value": "true"})
|
|
468
|
+
|
|
469
|
+
if user.first_name is not None:
|
|
470
|
+
user_attributes.append({"Name": "given_name", "Value": user.first_name})
|
|
471
|
+
|
|
472
|
+
if user.last_name is not None:
|
|
473
|
+
user_attributes.append({"Name": "family_name", "Value": user.last_name})
|
|
474
|
+
|
|
475
|
+
if self.use_custom_attributes:
|
|
476
|
+
# we have the ability to turn this off for backward compatibility
|
|
477
|
+
# once early access is over we can always allow this.
|
|
478
|
+
# if we try to add them and they aren't registered we will get an error
|
|
479
|
+
# one workaround is to manually add them to the user pool
|
|
480
|
+
if user.id is not None:
|
|
481
|
+
user_attributes.append(
|
|
482
|
+
{
|
|
483
|
+
"Name": CognitoCustomAttributes.USER_ID_KEY_NAME,
|
|
484
|
+
"Value": user.id,
|
|
485
|
+
}
|
|
486
|
+
)
|
|
487
|
+
|
|
488
|
+
if user.roles is not None:
|
|
489
|
+
roles: str = ""
|
|
490
|
+
if isinstance(user.roles, list):
|
|
491
|
+
roles = ",".join(user.roles)
|
|
492
|
+
elif isinstance(user.roles, str):
|
|
493
|
+
roles = user.roles
|
|
494
|
+
user_attributes.append(
|
|
495
|
+
{
|
|
496
|
+
"Name": CognitoCustomAttributes.USER_ROLES_KEY_NAME,
|
|
497
|
+
"Value": roles,
|
|
498
|
+
}
|
|
499
|
+
)
|
|
500
|
+
|
|
501
|
+
if user.tenant_id is not None:
|
|
502
|
+
user_attributes.append(
|
|
503
|
+
{
|
|
504
|
+
"Name": CognitoCustomAttributes.TENANT_ID_KEY_NAME,
|
|
505
|
+
"Value": user.tenant_id,
|
|
506
|
+
}
|
|
507
|
+
)
|
|
508
|
+
|
|
509
|
+
return user_attributes
|
|
510
|
+
|
|
511
|
+
def map(self, cognito_response: dict) -> User:
|
|
512
|
+
"""Map the cognito response to a user object"""
|
|
513
|
+
user = User()
|
|
514
|
+
user.cognito_user_name = self.get_cognito_attribute(
|
|
515
|
+
cognito_response, "Username"
|
|
516
|
+
)
|
|
517
|
+
user.email = self.get_cognito_attribute(cognito_response, "email", None)
|
|
518
|
+
user.first_name = self.get_cognito_attribute(
|
|
519
|
+
cognito_response, "given_name", None
|
|
520
|
+
)
|
|
521
|
+
user.last_name = self.get_cognito_attribute(
|
|
522
|
+
cognito_response, "family_name", None
|
|
523
|
+
)
|
|
524
|
+
user.id = self.get_cognito_attribute(
|
|
525
|
+
cognito_response, CognitoCustomAttributes.USER_ID_KEY_NAME, None
|
|
526
|
+
)
|
|
527
|
+
user.tenant_id = self.get_cognito_attribute(
|
|
528
|
+
cognito_response, CognitoCustomAttributes.TENANT_ID_KEY_NAME, None
|
|
529
|
+
)
|
|
530
|
+
|
|
531
|
+
roles: str | None | List[str] = self.get_cognito_attribute(
|
|
532
|
+
cognito_response, CognitoCustomAttributes.USER_ROLES_KEY_NAME, None
|
|
533
|
+
)
|
|
534
|
+
if roles is None:
|
|
535
|
+
roles = []
|
|
536
|
+
if isinstance(roles, str):
|
|
537
|
+
roles = roles.split(",")
|
|
538
|
+
user.roles = roles
|
|
539
|
+
return user
|
|
540
|
+
|
|
541
|
+
def get_cognito_attribute(
|
|
542
|
+
self, response: dict, name: str, default: Optional[str] = None
|
|
543
|
+
) -> Optional[str]:
|
|
544
|
+
if name in response:
|
|
545
|
+
return response.get(name, default)
|
|
546
|
+
|
|
547
|
+
attributes = response.get("Attributes", [])
|
|
548
|
+
attribute = DictionaryUtility.find_dict_by_name(attributes, "Name", name)
|
|
549
|
+
if attribute and isinstance(attribute, list):
|
|
550
|
+
return str(attribute[0].get("Value", default))
|
|
551
|
+
return default
|
|
552
|
+
|
|
553
|
+
def __format_email(self, email: str | None) -> str | None:
|
|
554
|
+
"""
|
|
555
|
+
Format the email to be used in the cognito user pool.
|
|
556
|
+
We have some installations that were set up case-sensitive, until we can
|
|
557
|
+
migrate them over to a case-insensitive system, we make sure we only
|
|
558
|
+
deal with lower case usernames.
|
|
559
|
+
"""
|
|
560
|
+
|
|
561
|
+
if email is None:
|
|
562
|
+
return None
|
|
563
|
+
|
|
564
|
+
return str(email).lower()
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
if __name__ == "__main__":
|
|
568
|
+
pass
|