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,218 @@
1
+ """
2
+ Authorized Secure Handler with hierarchical routing support.
3
+
4
+ Extends SecureLambdaHandler to add fine-grained authorization using the
5
+ authorization middleware for hierarchical multi-tenant routes.
6
+ """
7
+
8
+ from typing import Dict, Any, Optional, Callable
9
+ from aws_lambda_powertools import Logger
10
+
11
+ from .secure_handler import SecureLambdaHandler
12
+ from geek_cafe_saas_sdk.middleware.authorization import (
13
+ Operation,
14
+ AuthorizationMiddleware,
15
+ extract_auth_context,
16
+ extract_resource_context,
17
+ AuthorizationResult
18
+ )
19
+
20
+ logger = Logger()
21
+
22
+
23
+ class AuthorizedSecureLambdaHandler(SecureLambdaHandler):
24
+ """
25
+ Secure handler with built-in authorization for hierarchical routes.
26
+
27
+ Use this when:
28
+ - Using hierarchical routes: /tenants/{tid}/users/{uid}/resources/{rid}
29
+ - Need fine-grained authorization (global admin, tenant admin, user)
30
+ - Want automatic tenant isolation and permission checks
31
+
32
+ The handler:
33
+ 1. Validates JWT via API Gateway (inherited from SecureLambdaHandler)
34
+ 2. Extracts actor from JWT (who is making request)
35
+ 3. Extracts resource from path (what they want to access)
36
+ 4. Checks authorization (can actor access resource?)
37
+ 5. Calls business logic only if authorized
38
+
39
+ Example:
40
+ handler = create_handler(
41
+ service_class=MessageService,
42
+ require_authorization=True, # Enable authorization
43
+ operation=Operation.READ,
44
+ resource_type="message"
45
+ )
46
+
47
+ def lambda_handler(event, context):
48
+ return handler.execute(event, context, business_logic)
49
+
50
+ def business_logic(event, service, user_context):
51
+ # Authorization already checked - just implement logic
52
+ message_id = event['pathParameters']['message_id']
53
+ return service.get_by_id(message_id)
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ operation: Optional[Operation] = None,
59
+ resource_type: Optional[str] = None,
60
+ extract_resource_fn: Optional[Callable[[Dict[str, Any]], Any]] = None,
61
+ skip_authorization: bool = False,
62
+ **kwargs
63
+ ):
64
+ """
65
+ Initialize the authorized secure handler.
66
+
67
+ Args:
68
+ operation: Operation to authorize (READ, WRITE, DELETE, CREATE).
69
+ If None, inferred from HTTP method.
70
+ resource_type: Type of resource (e.g., "message", "contact_thread")
71
+ extract_resource_fn: Custom function to extract resource context from event
72
+ skip_authorization: Skip authorization check (for backward compatibility)
73
+ **kwargs: Arguments passed to SecureLambdaHandler
74
+ """
75
+ super().__init__(**kwargs)
76
+ self.operation = operation
77
+ self.resource_type = resource_type
78
+ self.extract_resource_fn = extract_resource_fn
79
+ self.skip_authorization = skip_authorization
80
+
81
+ def _infer_operation(self, event: Dict[str, Any]) -> Operation:
82
+ """Infer operation from HTTP method."""
83
+ method = event.get('httpMethod', 'GET').upper()
84
+
85
+ if method == 'GET':
86
+ return Operation.READ
87
+ elif method == 'POST':
88
+ return Operation.CREATE
89
+ elif method in ['PUT', 'PATCH']:
90
+ return Operation.WRITE
91
+ elif method == 'DELETE':
92
+ return Operation.DELETE
93
+ else:
94
+ return Operation.READ # Default
95
+
96
+ def _check_authorization(
97
+ self,
98
+ event: Dict[str, Any]
99
+ ) -> Optional[Dict[str, Any]]:
100
+ """
101
+ Check if user is authorized to access the resource.
102
+
103
+ Returns:
104
+ Error response dict if unauthorized, None if authorized
105
+ """
106
+ if self.skip_authorization:
107
+ return None
108
+
109
+ try:
110
+ # Extract actor from JWT
111
+ actor = extract_auth_context(event)
112
+
113
+ # Extract resource from path
114
+ if self.extract_resource_fn:
115
+ resource = self.extract_resource_fn(event)
116
+ else:
117
+ resource = extract_resource_context(event, self.resource_type)
118
+
119
+ # Validate tenant_id is in path
120
+ if not resource.tenant_id:
121
+ logger.warning("Authorization check failed: tenant_id not in path parameters")
122
+ from geek_cafe_saas_sdk.utilities.response import error_response
123
+ return error_response(
124
+ "tenant_id is required in path parameters",
125
+ "AUTHORIZATION_ERROR",
126
+ 400
127
+ )
128
+
129
+ # Determine operation
130
+ operation = self.operation if self.operation else self._infer_operation(event)
131
+
132
+ # Check authorization
133
+ result: AuthorizationResult = AuthorizationMiddleware.can_perform_operation(
134
+ actor, resource, operation
135
+ )
136
+
137
+ if not result.allowed:
138
+ logger.warning(
139
+ f"Authorization denied: {result.reason}",
140
+ extra={
141
+ 'actor_user_id': actor.user_id,
142
+ 'actor_tenant_id': actor.tenant_id,
143
+ 'resource_tenant_id': resource.tenant_id,
144
+ 'resource_user_id': resource.user_id,
145
+ 'resource_id': resource.resource_id,
146
+ 'operation': operation.value,
147
+ 'reason': result.reason
148
+ }
149
+ )
150
+ from geek_cafe_saas_sdk.utilities.response import error_response
151
+ return error_response(
152
+ "You do not have permission to access this resource",
153
+ "AUTHORIZATION_DENIED",
154
+ 403,
155
+ additional_data={'reason': result.reason}
156
+ )
157
+
158
+ # Authorization passed - add context to event for business logic
159
+ event['authorization_context'] = {
160
+ 'actor': actor,
161
+ 'resource': resource,
162
+ 'operation': operation.value,
163
+ 'reason': result.reason,
164
+ 'context': result.context
165
+ }
166
+
167
+ # Log successful authorization for audit
168
+ logger.info(
169
+ f"Authorization granted: {result.reason}",
170
+ extra={
171
+ 'actor_user_id': actor.user_id,
172
+ 'actor_tenant_id': actor.tenant_id,
173
+ 'resource_tenant_id': resource.tenant_id,
174
+ 'resource_user_id': resource.user_id,
175
+ 'resource_id': resource.resource_id,
176
+ 'operation': operation.value,
177
+ 'reason': result.reason
178
+ }
179
+ )
180
+
181
+ return None # Authorized
182
+
183
+ except KeyError as e:
184
+ logger.error(f"Authorization check failed: Missing JWT claim: {e}")
185
+ from geek_cafe_saas_sdk.utilities.response import error_response
186
+ return error_response(
187
+ f"Missing required authentication claim: {str(e)}",
188
+ "AUTHENTICATION_ERROR",
189
+ 401
190
+ )
191
+ except Exception as e:
192
+ logger.exception(f"Authorization check failed with unexpected error: {e}")
193
+ from geek_cafe_saas_sdk.utilities.response import error_response
194
+ return error_response(
195
+ "Authorization check failed",
196
+ "AUTHORIZATION_ERROR",
197
+ 500
198
+ )
199
+
200
+ def execute(
201
+ self,
202
+ event: Dict[str, Any],
203
+ context: Any,
204
+ business_logic: Callable[[Dict[str, Any], Any, Dict[str, Any]], Any],
205
+ injected_service: Optional[Any] = None
206
+ ) -> Dict[str, Any]:
207
+ """
208
+ Execute handler with authorization check.
209
+
210
+ Overrides parent execute() to add authorization before calling business logic.
211
+ """
212
+ # Check authorization first
213
+ auth_error = self._check_authorization(event)
214
+ if auth_error:
215
+ return auth_error
216
+
217
+ # Authorization passed - call parent execute (handles everything else)
218
+ return super().execute(event, context, business_logic, injected_service)
@@ -0,0 +1,185 @@
1
+ """
2
+ Base Lambda handler with common functionality.
3
+
4
+ Provides a foundation for creating Lambda handlers with standardized
5
+ request/response handling, error management, and service injection.
6
+ """
7
+
8
+ import json
9
+ from typing import Dict, Any, Callable, Optional, Type, TypeVar
10
+ from aws_lambda_powertools import Logger
11
+
12
+ from geek_cafe_saas_sdk.utilities.response import (
13
+ error_response,
14
+ service_result_to_response,
15
+ )
16
+ from geek_cafe_saas_sdk.utilities.lambda_event_utility import LambdaEventUtility
17
+ from geek_cafe_saas_sdk.middleware.auth import extract_user_context
18
+ from .service_pool import ServicePool
19
+
20
+ logger = Logger()
21
+
22
+ T = TypeVar('T') # Service type
23
+
24
+
25
+ class BaseLambdaHandler:
26
+ """
27
+ Base class for Lambda handlers with common functionality.
28
+
29
+ Handles:
30
+ - Request body parsing and case conversion
31
+ - Service initialization and pooling
32
+ - User context extraction
33
+ - Response formatting
34
+ - Event unwrapping (SQS, SNS, etc.)
35
+
36
+ """
37
+
38
+ def __init__(
39
+ self,
40
+ service_class: Optional[Type[T]] = None,
41
+ service_kwargs: Optional[Dict[str, Any]] = None,
42
+ require_body: bool = False,
43
+ convert_case: bool = True,
44
+ unwrap_message: bool = True,
45
+ apply_cors: bool = True,
46
+ apply_error_handling: bool = True,
47
+ require_auth: bool = True,
48
+ ):
49
+ self.service_class = service_class
50
+ self.service_kwargs = service_kwargs or {}
51
+ self.require_body = require_body
52
+ self.convert_case = convert_case
53
+ self.unwrap_message = unwrap_message
54
+ self.apply_cors = apply_cors
55
+ self.apply_error_handling = apply_error_handling
56
+ self.require_auth = require_auth
57
+
58
+ # Initialize service pool if a class is provided
59
+ self._service_pool = ServicePool(service_class, **self.service_kwargs) if service_class else None
60
+
61
+ def _get_service(self, injected_service: Optional[T]) -> Optional[T]:
62
+ """
63
+ Get service instance (injected or from pool).
64
+ """
65
+ if injected_service:
66
+ return injected_service
67
+
68
+ if self._service_pool:
69
+ return self._service_pool.get()
70
+
71
+ # Fallback for direct instantiation if pooling is not used (rare)
72
+ if self.service_class:
73
+ return self.service_class(**self.service_kwargs)
74
+
75
+ return None
76
+
77
+ def execute(
78
+ self,
79
+ event: Dict[str, Any],
80
+ context: Any,
81
+ business_logic: Callable[[Dict[str, Any], Any, Dict[str, Any]], Any],
82
+ injected_service: Optional[T] = None
83
+ ) -> Dict[str, Any]:
84
+ """
85
+ Execute the Lambda handler with the given business logic.
86
+
87
+ Args:
88
+ event: Lambda event dictionary
89
+ context: Lambda context object
90
+ business_logic: Callable that implements the business logic
91
+ injected_service: Optional service instance for testing
92
+
93
+ Returns:
94
+ Lambda response dictionary
95
+ """
96
+ try:
97
+ # Unwrap message if needed (SQS, SNS, etc.)
98
+ if self.unwrap_message and "message" in event:
99
+ event = event["message"]
100
+
101
+ # Validate requestContext presence (Rule #4)
102
+ if "requestContext" not in event:
103
+ return error_response(
104
+ "requestContext missing from event. Ensure API Gateway is properly configured.",
105
+ "CONFIGURATION_ERROR",
106
+ 500
107
+ )
108
+
109
+ # Validate authentication if required
110
+ if self.require_auth:
111
+ authorizer = event.get("requestContext", {}).get("authorizer")
112
+ if not authorizer or not authorizer.get("claims", {}).get("custom:user_id"):
113
+ return error_response(
114
+ "Authentication required but not provided",
115
+ "AUTHENTICATION_REQUIRED",
116
+ 401
117
+ )
118
+
119
+ # Check if body is required
120
+ if self.require_body and not event.get("body"):
121
+ return error_response(
122
+ "Request body is required",
123
+ "VALIDATION_ERROR",
124
+ 400
125
+ )
126
+
127
+ # Parse and validate body
128
+ if event.get("body"):
129
+ try:
130
+ body = LambdaEventUtility.get_body_from_event(event, raise_on_error=self.require_body)
131
+ if body and self.convert_case:
132
+ body = LambdaEventUtility.to_snake_case_for_backend(body)
133
+ if body:
134
+ event["parsed_body"] = body
135
+ except (ValueError, KeyError) as e:
136
+ # If error handling is disabled, let the exception propagate for testing
137
+ if not self.apply_error_handling:
138
+ raise
139
+ return error_response(
140
+ str(e),
141
+ "VALIDATION_ERROR",
142
+ 400
143
+ )
144
+
145
+ # Extract user context from authorizer claims
146
+ user_context = extract_user_context(event)
147
+
148
+ # Get service instance
149
+ service = self._get_service(injected_service)
150
+
151
+ # Execute business logic
152
+ result = business_logic(event, service, user_context)
153
+
154
+ # Determine appropriate HTTP status code based on HTTP method
155
+ http_method = event.get('httpMethod', '').upper()
156
+ if http_method == 'POST':
157
+ success_status = 201 # Created
158
+ elif http_method == 'DELETE':
159
+ success_status = 204 # No Content
160
+ else:
161
+ success_status = 200 # OK (GET, PUT, PATCH, etc.)
162
+
163
+ # Format response - handle both ServiceResult and plain dict
164
+ if hasattr(result, 'success'):
165
+ # It's a ServiceResult object
166
+ response = service_result_to_response(result, success_status=success_status)
167
+ else:
168
+ # It's a plain dict - wrap it in a success response
169
+ from geek_cafe_saas_sdk.utilities.response import success_response
170
+ response = success_response(result, success_status)
171
+
172
+ # Note: CORS headers are already added by success_response/service_result_to_response
173
+ # The apply_cors flag is for decorator usage, not runtime response modification
174
+ return response
175
+
176
+ except Exception as e:
177
+ logger.exception(f"Handler execution error: {e}")
178
+ if self.apply_error_handling:
179
+ # Convert exception to error response (error_response is imported at top)
180
+ return error_response(
181
+ str(e),
182
+ "INTERNAL_ERROR",
183
+ 500
184
+ )
185
+ raise
@@ -0,0 +1,256 @@
1
+ """
2
+ Factory for creating Lambda handlers based on configuration.
3
+
4
+ Centralizes handler selection logic and provides a single point
5
+ for configuring authentication strategy across all Lambda functions.
6
+ """
7
+
8
+ import os
9
+ from typing import Optional, Type, TypeVar, Any
10
+ from aws_lambda_powertools import Logger
11
+
12
+ from .base_handler import BaseLambdaHandler
13
+ from .api_key_handler import ApiKeyLambdaHandler
14
+ from .public_handler import PublicLambdaHandler
15
+ from .secure_handler import SecureLambdaHandler
16
+ from .authorized_secure_handler import AuthorizedSecureLambdaHandler
17
+
18
+ logger = Logger()
19
+
20
+ T = TypeVar('T') # Service type
21
+
22
+
23
+ class HandlerFactory:
24
+ """
25
+ Factory for creating Lambda handlers with appropriate authentication.
26
+
27
+ Configuration via environment variables:
28
+
29
+ - AUTH_TYPE (default: "secure"):
30
+ - "secure": Uses API Gateway authorizer (Cognito/Lambda/IAM)
31
+ - "api_key": Validates x-api-key header against API_KEY env var
32
+ - "public": No authentication required
33
+ - "none": Alias for "public"
34
+
35
+ - AUTH_STRICT (default: "true"):
36
+ - "true": Strict validation, fail if auth is missing
37
+ - "false": Permissive mode for local dev/testing
38
+
39
+ Usage:
40
+ # Simple usage - defaults to secure handler
41
+ handler = HandlerFactory.create(
42
+ service_class=VoteService,
43
+ require_body=True
44
+ )
45
+
46
+ # Explicit type
47
+ handler = HandlerFactory.create(
48
+ service_class=VoteService,
49
+ auth_type="api_key", # Override environment
50
+ require_body=True
51
+ )
52
+
53
+ # Public endpoint
54
+ handler = HandlerFactory.create_public(
55
+ service_class=ConfigService
56
+ )
57
+
58
+ # In lambda function
59
+ def lambda_handler(event, context):
60
+ return handler.execute(event, context, business_logic)
61
+ """
62
+
63
+ # Auth type constants
64
+ AUTH_TYPE_SECURE = "secure"
65
+ AUTH_TYPE_API_KEY = "api_key"
66
+ AUTH_TYPE_PUBLIC = "public"
67
+ AUTH_TYPE_NONE = "none" # Alias for public
68
+
69
+ # Environment variable names
70
+ ENV_AUTH_TYPE = "AUTH_TYPE"
71
+ ENV_AUTH_STRICT = "AUTH_STRICT"
72
+
73
+ @classmethod
74
+ def create(
75
+ cls,
76
+ service_class: Optional[Type[T]] = None,
77
+ auth_type: Optional[str] = None,
78
+ strict: Optional[bool] = None,
79
+ require_authorization: bool = False,
80
+ operation: Optional[Any] = None,
81
+ resource_type: Optional[str] = None,
82
+ **handler_kwargs
83
+ ) -> BaseLambdaHandler:
84
+ """
85
+ Create a handler with appropriate authentication and optional authorization.
86
+
87
+ Args:
88
+ service_class: Service class to instantiate
89
+ auth_type: Override AUTH_TYPE env var ("secure", "api_key", "public")
90
+ strict: Override AUTH_STRICT env var (True/False)
91
+ require_authorization: Enable fine-grained authorization for hierarchical routes
92
+ operation: Operation to authorize (Operation.READ, etc.). If None, inferred from HTTP method
93
+ resource_type: Type of resource for authorization (e.g., "message", "contact_thread")
94
+ **handler_kwargs: Additional arguments for handler (require_body, etc.)
95
+
96
+ Returns:
97
+ Configured handler instance
98
+
99
+ Examples:
100
+ # Simple secure handler (existing behavior)
101
+ handler = HandlerFactory.create(service_class=MessageService)
102
+
103
+ # With authorization for hierarchical routes
104
+ handler = HandlerFactory.create(
105
+ service_class=MessageService,
106
+ require_authorization=True,
107
+ operation=Operation.READ,
108
+ resource_type="message"
109
+ )
110
+ """
111
+ # Get auth type from args or environment
112
+ if auth_type is None:
113
+ auth_type = os.getenv(cls.ENV_AUTH_TYPE, cls.AUTH_TYPE_SECURE).lower()
114
+ else:
115
+ auth_type = auth_type.lower()
116
+
117
+ # Get strict mode
118
+ if strict is None:
119
+ strict_str = os.getenv(cls.ENV_AUTH_STRICT, "true").lower()
120
+ strict = strict_str in ("true", "1", "yes")
121
+
122
+ # Log configuration
123
+ logger.info(
124
+ f"Creating handler with auth_type={auth_type}, strict={strict}, "
125
+ f"service={service_class.__name__ if service_class else 'None'}"
126
+ )
127
+
128
+ # Prepare service kwargs, including the system inbox ID if applicable
129
+ service_kwargs = {}
130
+ if service_class and hasattr(service_class, '__init__') and 'system_inbox_id' in service_class.__init__.__code__.co_varnames:
131
+ service_kwargs['system_inbox_id'] = os.getenv('SYSTEM_INBOX_ID', 'support-inbox')
132
+
133
+ # Create appropriate handler
134
+ if auth_type == cls.AUTH_TYPE_API_KEY:
135
+ return ApiKeyLambdaHandler(
136
+ service_class=service_class,
137
+ service_kwargs=service_kwargs,
138
+ **handler_kwargs
139
+ )
140
+ elif auth_type in (cls.AUTH_TYPE_PUBLIC, cls.AUTH_TYPE_NONE):
141
+ return PublicLambdaHandler(
142
+ service_class=service_class,
143
+ service_kwargs=service_kwargs,
144
+ **handler_kwargs
145
+ )
146
+ elif auth_type == cls.AUTH_TYPE_SECURE:
147
+ # Use AuthorizedSecureLambdaHandler if authorization is required
148
+ if require_authorization:
149
+ return AuthorizedSecureLambdaHandler(
150
+ service_class=service_class,
151
+ service_kwargs=service_kwargs,
152
+ require_authorizer_claims=strict,
153
+ operation=operation,
154
+ resource_type=resource_type,
155
+ **handler_kwargs
156
+ )
157
+ else:
158
+ return SecureLambdaHandler(
159
+ service_class=service_class,
160
+ service_kwargs=service_kwargs,
161
+ require_authorizer_claims=strict,
162
+ **handler_kwargs
163
+ )
164
+ else:
165
+ logger.warning(
166
+ f"Unknown auth_type '{auth_type}', defaulting to secure handler"
167
+ )
168
+ # Use AuthorizedSecureLambdaHandler if authorization is required
169
+ if require_authorization:
170
+ return AuthorizedSecureLambdaHandler(
171
+ service_class=service_class,
172
+ service_kwargs=service_kwargs,
173
+ require_authorizer_claims=strict,
174
+ operation=operation,
175
+ resource_type=resource_type,
176
+ **handler_kwargs
177
+ )
178
+ else:
179
+ return SecureLambdaHandler(
180
+ service_class=service_class,
181
+ service_kwargs=service_kwargs,
182
+ require_authorizer_claims=strict,
183
+ **handler_kwargs
184
+ )
185
+
186
+ @classmethod
187
+ def create_secure(
188
+ cls,
189
+ service_class: Optional[Type[T]] = None,
190
+ **handler_kwargs
191
+ ) -> SecureLambdaHandler:
192
+ """
193
+ Create a secure handler (API Gateway authorizer).
194
+
195
+ Convenience method that explicitly creates a secure handler
196
+ regardless of environment configuration.
197
+ """
198
+ return SecureLambdaHandler(
199
+ service_class=service_class,
200
+ **handler_kwargs
201
+ )
202
+
203
+ @classmethod
204
+ def create_api_key(
205
+ cls,
206
+ service_class: Optional[Type[T]] = None,
207
+ **handler_kwargs
208
+ ) -> ApiKeyLambdaHandler:
209
+ """
210
+ Create an API key handler.
211
+
212
+ Convenience method that explicitly creates an API key handler
213
+ regardless of environment configuration.
214
+ """
215
+ return ApiKeyLambdaHandler(
216
+ service_class=service_class,
217
+ **handler_kwargs
218
+ )
219
+
220
+ @classmethod
221
+ def create_public(
222
+ cls,
223
+ service_class: Optional[Type[T]] = None,
224
+ **handler_kwargs
225
+ ) -> PublicLambdaHandler:
226
+ """
227
+ Create a public handler (no auth).
228
+
229
+ Convenience method that explicitly creates a public handler
230
+ regardless of environment configuration.
231
+ """
232
+ return PublicLambdaHandler(
233
+ service_class=service_class,
234
+ **handler_kwargs
235
+ )
236
+
237
+
238
+ # Convenience function for quick handler creation
239
+ def create_handler(
240
+ service_class: Optional[Type[T]] = None,
241
+ **kwargs
242
+ ) -> BaseLambdaHandler:
243
+ """
244
+ Convenience function for creating handlers.
245
+
246
+ Equivalent to HandlerFactory.create()
247
+
248
+ Example:
249
+ from geek_cafe_saas_sdk.lambda_handlers import create_handler
250
+
251
+ handler = create_handler(
252
+ service_class=VoteService,
253
+ require_body=True
254
+ )
255
+ """
256
+ return HandlerFactory.create(service_class=service_class, **kwargs)