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.

Files changed (194) hide show
  1. geek_cafe_saas_sdk/__init__.py +9 -0
  2. geek_cafe_saas_sdk/core/__init__.py +11 -0
  3. geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
  4. geek_cafe_saas_sdk/core/error_codes.py +132 -0
  5. geek_cafe_saas_sdk/core/service_errors.py +19 -0
  6. geek_cafe_saas_sdk/core/service_result.py +121 -0
  7. geek_cafe_saas_sdk/decorators/__init__.py +64 -0
  8. geek_cafe_saas_sdk/decorators/auth.py +373 -0
  9. geek_cafe_saas_sdk/decorators/core.py +358 -0
  10. geek_cafe_saas_sdk/domains/__init__.py +0 -0
  11. geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
  12. geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
  13. geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
  14. geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
  15. geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
  16. geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
  17. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
  18. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
  19. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
  20. geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
  21. geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
  22. geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
  23. geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
  24. geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
  25. geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
  26. geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
  27. geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
  28. geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
  29. geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
  30. geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
  31. geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
  32. geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
  33. geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
  34. geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
  35. geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
  36. geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
  37. geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
  38. geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
  39. geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
  40. geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
  41. geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
  42. geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
  43. geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
  44. geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
  45. geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
  46. geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
  47. geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
  48. geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
  49. geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
  50. geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
  51. geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
  52. geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
  53. geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
  54. geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
  55. geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
  56. geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
  57. geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
  58. geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
  59. geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
  60. geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
  61. geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
  62. geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
  63. geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
  64. geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
  65. geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
  66. geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
  67. geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
  68. geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
  69. geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
  70. geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
  71. geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
  72. geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
  73. geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
  74. geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
  75. geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
  76. geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
  77. geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
  78. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
  79. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
  80. geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
  81. geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
  82. geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
  83. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
  84. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
  85. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
  86. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
  87. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
  88. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
  89. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
  90. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
  91. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
  92. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
  93. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
  94. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
  95. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
  96. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
  97. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
  98. geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
  99. geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
  100. geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
  101. geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
  102. geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
  103. geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
  104. geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
  105. geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
  106. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
  107. geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
  108. geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
  109. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
  110. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
  111. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
  112. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
  113. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
  114. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
  115. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
  116. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
  117. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
  118. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
  119. geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
  120. geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
  121. geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
  122. geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
  123. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
  124. geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
  125. geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
  126. geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
  127. geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
  128. geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
  129. geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
  130. geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
  131. geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
  132. geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
  133. geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
  134. geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
  135. geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
  136. geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
  137. geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
  138. geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
  139. geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
  140. geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
  141. geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
  142. geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
  143. geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
  144. geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
  145. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
  146. geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
  147. geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
  148. geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
  149. geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
  150. geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
  151. geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
  152. geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
  153. geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
  154. geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
  155. geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
  156. geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
  157. geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
  158. geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
  159. geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
  160. geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
  161. geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
  162. geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
  163. geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
  164. geek_cafe_saas_sdk/middleware/__init__.py +36 -0
  165. geek_cafe_saas_sdk/middleware/auth.py +85 -0
  166. geek_cafe_saas_sdk/middleware/authorization.py +523 -0
  167. geek_cafe_saas_sdk/middleware/cors.py +63 -0
  168. geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
  169. geek_cafe_saas_sdk/middleware/validation.py +80 -0
  170. geek_cafe_saas_sdk/models/__init__.py +20 -0
  171. geek_cafe_saas_sdk/models/base_model.py +233 -0
  172. geek_cafe_saas_sdk/services/__init__.py +18 -0
  173. geek_cafe_saas_sdk/services/database_service.py +441 -0
  174. geek_cafe_saas_sdk/utilities/__init__.py +88 -0
  175. geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
  176. geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
  177. geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
  178. geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
  179. geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
  180. geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
  181. geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
  182. geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
  183. geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
  184. geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
  185. geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
  186. geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
  187. geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
  188. geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
  189. geek_cafe_saas_sdk/utilities/response.py +209 -0
  190. geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
  191. geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
  192. geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
  193. geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
  194. 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