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,373 @@
1
+ """
2
+ Authentication and authorization decorators for Lambda handlers.
3
+
4
+ These decorators integrate with the authorization middleware to provide
5
+ fine-grained access control for hierarchical multi-tenant routes.
6
+
7
+ Design:
8
+ - Uses existing authorization middleware (tested, proven)
9
+ - Composable with other decorators
10
+ - Explicit and readable
11
+ - Follows industry patterns (Flask, FastAPI, Django)
12
+ """
13
+
14
+ import json
15
+ import functools
16
+ from typing import Callable, Any, Dict, Optional
17
+
18
+ from aws_lambda_powertools import Logger
19
+
20
+ from geek_cafe_saas_sdk.middleware.authorization import (
21
+ Operation,
22
+ Permission,
23
+ AuthorizationMiddleware,
24
+ extract_auth_context,
25
+ extract_resource_context,
26
+ AuthorizationResult
27
+ )
28
+ from geek_cafe_saas_sdk.utilities.response import error_response
29
+
30
+ logger = Logger()
31
+
32
+
33
+ def require_authorization(
34
+ operation: Optional[Operation] = None,
35
+ resource_type: Optional[str] = None,
36
+ extract_resource_fn: Optional[Callable] = None
37
+ ) -> Callable:
38
+ """
39
+ Require authorization for handler based on hierarchical route.
40
+
41
+ Checks:
42
+ - Can actor access target tenant?
43
+ - Can actor access target user's resources?
44
+ - Does actor have permission for operation?
45
+
46
+ Args:
47
+ operation: Operation to authorize (READ, WRITE, DELETE, CREATE).
48
+ If None, inferred from HTTP method.
49
+ resource_type: Type of resource (for logging/audit)
50
+ extract_resource_fn: Custom function to extract resource from event
51
+
52
+ Usage:
53
+ @require_authorization(operation=Operation.READ, resource_type="message")
54
+ def handler(event, context):
55
+ # Authorization already checked
56
+ message_id = event['pathParameters']['message_id']
57
+ return {'statusCode': 200}
58
+
59
+ # Auto-infer operation from HTTP method
60
+ @require_authorization(resource_type="message")
61
+ def handler(event, context):
62
+ # GET -> READ, POST -> CREATE, etc.
63
+ pass
64
+
65
+ Returns:
66
+ Decorated handler with authorization check
67
+ """
68
+ def decorator(handler: Callable) -> Callable:
69
+ @functools.wraps(handler)
70
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
71
+ try:
72
+ # Extract actor from JWT
73
+ actor = extract_auth_context(event)
74
+
75
+ # Extract resource from path
76
+ if extract_resource_fn:
77
+ resource = extract_resource_fn(event)
78
+ else:
79
+ resource = extract_resource_context(event, resource_type)
80
+
81
+ # Validate tenant_id is in path
82
+ if not resource.tenant_id:
83
+ logger.warning("Authorization failed: tenant_id not in path parameters")
84
+ return error_response(
85
+ "tenant_id is required in path parameters",
86
+ "AUTHORIZATION_ERROR",
87
+ 400
88
+ )
89
+
90
+ # Determine operation (explicit or inferred)
91
+ op = operation
92
+ if op is None:
93
+ # Infer from HTTP method
94
+ method = event.get('httpMethod', 'GET').upper()
95
+ if method == 'GET':
96
+ op = Operation.READ
97
+ elif method == 'POST':
98
+ op = Operation.CREATE
99
+ elif method in ['PUT', 'PATCH']:
100
+ op = Operation.WRITE
101
+ elif method == 'DELETE':
102
+ op = Operation.DELETE
103
+ else:
104
+ op = Operation.READ # Default
105
+
106
+ # Check authorization
107
+ result: AuthorizationResult = AuthorizationMiddleware.can_perform_operation(
108
+ actor, resource, op
109
+ )
110
+
111
+ if not result.allowed:
112
+ logger.warning(
113
+ f"Authorization denied: {result.reason}",
114
+ extra={
115
+ 'actor_user_id': actor.user_id,
116
+ 'actor_tenant_id': actor.tenant_id,
117
+ 'resource_tenant_id': resource.tenant_id,
118
+ 'resource_user_id': resource.user_id,
119
+ 'resource_id': resource.resource_id,
120
+ 'operation': op.value,
121
+ 'reason': result.reason
122
+ }
123
+ )
124
+ return error_response(
125
+ f"You do not have permission to access this resource. Reason: {result.reason}",
126
+ "AUTHORIZATION_DENIED",
127
+ 403
128
+ )
129
+
130
+ # Add authorization context to event
131
+ event['authorization_context'] = {
132
+ 'actor': actor,
133
+ 'resource': resource,
134
+ 'operation': op.value,
135
+ 'reason': result.reason,
136
+ 'context': result.context
137
+ }
138
+
139
+ # Log successful authorization for audit
140
+ logger.info(
141
+ f"Authorization granted: {result.reason}",
142
+ extra={
143
+ 'actor_user_id': actor.user_id,
144
+ 'actor_tenant_id': actor.tenant_id,
145
+ 'resource_tenant_id': resource.tenant_id,
146
+ 'resource_user_id': resource.user_id,
147
+ 'resource_id': resource.resource_id,
148
+ 'operation': op.value,
149
+ 'reason': result.reason
150
+ }
151
+ )
152
+
153
+ # Authorization passed - call handler
154
+ return handler(event, context, *args, **kwargs)
155
+
156
+ except KeyError as e:
157
+ logger.error(f"Authorization check failed: Missing JWT claim: {e}")
158
+ return error_response(
159
+ f"Missing required authentication claim: {str(e)}",
160
+ "AUTHENTICATION_ERROR",
161
+ 401
162
+ )
163
+ except Exception as e:
164
+ logger.exception(f"Authorization check failed: {e}")
165
+ return error_response(
166
+ "Authorization check failed",
167
+ "AUTHORIZATION_ERROR",
168
+ 500
169
+ )
170
+
171
+ return wrapper
172
+ return decorator
173
+
174
+
175
+ def require_admin(handler: Callable) -> Callable:
176
+ """
177
+ Require admin role (tenant admin or global admin).
178
+
179
+ This is a convenience decorator that checks for admin roles
180
+ without fine-grained resource-level authorization.
181
+
182
+ Usage:
183
+ @require_admin
184
+ def handler(event, context):
185
+ # User is guaranteed to be an admin
186
+ return {'statusCode': 200}
187
+
188
+ Returns:
189
+ Decorated handler requiring admin role
190
+ """
191
+ @functools.wraps(handler)
192
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
193
+ try:
194
+ actor = extract_auth_context(event)
195
+
196
+ # Check for admin permissions
197
+ is_admin = (
198
+ actor.has_permission(Permission.PLATFORM_ADMIN) or
199
+ actor.has_permission(Permission.TENANT_ADMIN) or
200
+ actor.has_role('admin') or
201
+ actor.has_role('tenant_admin')
202
+ )
203
+
204
+ if not is_admin:
205
+ logger.warning(
206
+ f"Admin access denied for user {actor.user_id}",
207
+ extra={'user_id': actor.user_id, 'tenant_id': actor.tenant_id}
208
+ )
209
+ return error_response(
210
+ "Admin role required",
211
+ "ADMIN_REQUIRED",
212
+ 403
213
+ )
214
+
215
+ # Add auth context to event
216
+ event['authorization_context'] = {
217
+ 'actor': actor,
218
+ 'reason': 'admin_role'
219
+ }
220
+
221
+ logger.info(
222
+ f"Admin access granted for user {actor.user_id}",
223
+ extra={'user_id': actor.user_id, 'tenant_id': actor.tenant_id}
224
+ )
225
+
226
+ return handler(event, context, *args, **kwargs)
227
+
228
+ except Exception as e:
229
+ logger.exception(f"Admin check failed: {e}")
230
+ return error_response(
231
+ "Authorization check failed",
232
+ "AUTHORIZATION_ERROR",
233
+ 500
234
+ )
235
+
236
+ return wrapper
237
+
238
+
239
+ def require_tenant_admin(handler: Callable) -> Callable:
240
+ """
241
+ Require tenant admin role (admin for their own tenant).
242
+
243
+ Usage:
244
+ @require_tenant_admin
245
+ def handler(event, context):
246
+ # User is tenant admin for their tenant
247
+ return {'statusCode': 200}
248
+
249
+ Returns:
250
+ Decorated handler requiring tenant admin role
251
+ """
252
+ @functools.wraps(handler)
253
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
254
+ try:
255
+ actor = extract_auth_context(event)
256
+
257
+ # Check for tenant admin permission
258
+ is_tenant_admin = (
259
+ actor.has_permission(Permission.TENANT_ADMIN) or
260
+ actor.has_role('tenant_admin')
261
+ )
262
+
263
+ if not is_tenant_admin:
264
+ logger.warning(
265
+ f"Tenant admin access denied for user {actor.user_id}",
266
+ extra={'user_id': actor.user_id, 'tenant_id': actor.tenant_id}
267
+ )
268
+ return error_response(
269
+ "Tenant admin role required",
270
+ "TENANT_ADMIN_REQUIRED",
271
+ 403
272
+ )
273
+
274
+ # Add auth context to event
275
+ event['authorization_context'] = {
276
+ 'actor': actor,
277
+ 'reason': 'tenant_admin_role'
278
+ }
279
+
280
+ return handler(event, context, *args, **kwargs)
281
+
282
+ except Exception as e:
283
+ logger.exception(f"Tenant admin check failed: {e}")
284
+ return error_response(
285
+ "Authorization check failed",
286
+ "AUTHORIZATION_ERROR",
287
+ 500
288
+ )
289
+
290
+ return wrapper
291
+
292
+
293
+ def require_platform_admin(handler: Callable) -> Callable:
294
+ """
295
+ Require platform admin role (platform-level admin).
296
+
297
+ Usage:
298
+ @require_platform_admin
299
+ def handler(event, context):
300
+ # User is platform admin
301
+ return {'statusCode': 200}
302
+
303
+ Returns:
304
+ Decorated handler requiring global admin role
305
+ """
306
+ @functools.wraps(handler)
307
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
308
+ try:
309
+ actor = extract_auth_context(event)
310
+
311
+ # Check for platform admin permission
312
+ is_platform_admin = (
313
+ actor.has_permission(Permission.PLATFORM_ADMIN) or
314
+ actor.has_role('platform_admin')
315
+ )
316
+
317
+ if not is_platform_admin:
318
+ logger.warning(
319
+ f"Platform admin access denied for user {actor.user_id}",
320
+ extra={'user_id': actor.user_id, 'tenant_id': actor.tenant_id}
321
+ )
322
+ return error_response(
323
+ "Platform admin role required",
324
+ "PLATFORM_ADMIN_REQUIRED",
325
+ 403
326
+ )
327
+
328
+ # Add auth context to event
329
+ event['authorization_context'] = {
330
+ 'actor': actor,
331
+ 'reason': 'platform_admin_role'
332
+ }
333
+
334
+ logger.info(
335
+ f"Platform admin access granted for user {actor.user_id}",
336
+ extra={'user_id': actor.user_id, 'tenant_id': actor.tenant_id}
337
+ )
338
+
339
+ return handler(event, context, *args, **kwargs)
340
+
341
+ except Exception as e:
342
+ logger.exception(f"Platform admin check failed: {e}")
343
+ return error_response(
344
+ "Authorization check failed",
345
+ "AUTHORIZATION_ERROR",
346
+ 500
347
+ )
348
+
349
+ return wrapper
350
+
351
+
352
+ def public(handler: Callable) -> Callable:
353
+ """
354
+ Mark handler as public (no authentication required).
355
+
356
+ This is primarily a marker decorator for documentation purposes.
357
+ The actual public access must be configured at the API Gateway level.
358
+
359
+ Usage:
360
+ @public
361
+ def handler(event, context):
362
+ # Public endpoint - no auth check
363
+ return {'statusCode': 200}
364
+
365
+ Returns:
366
+ Decorated handler (no actual modification)
367
+ """
368
+ @functools.wraps(handler)
369
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
370
+ logger.info("Public endpoint accessed (no authentication required)")
371
+ return handler(event, context, *args, **kwargs)
372
+
373
+ return wrapper