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,523 @@
1
+ """
2
+ Authorization middleware for hierarchical routing with tenant sharing support.
3
+
4
+ This module provides centralized authorization logic that supports:
5
+ - Multi-tenant access control
6
+ - Tenant-to-tenant resource sharing
7
+ - Role-based permissions (global admin, tenant admin, user)
8
+ - Operation-level access control (read, write, delete)
9
+ """
10
+
11
+ import json
12
+ import functools
13
+ from enum import Enum
14
+ from typing import Dict, Any, List, Optional, Tuple, Callable
15
+ from dataclasses import dataclass, field
16
+
17
+
18
+ class Permission(Enum):
19
+ """System-wide permissions."""
20
+
21
+ # Platform permissions
22
+ PLATFORM_ADMIN = "platform_admin"
23
+ PLATFORM_READ = "platform_read"
24
+
25
+ # Tenant permissions
26
+ TENANT_ADMIN = "tenant_admin"
27
+ TENANT_READ = "tenant_read"
28
+ TENANT_WRITE = "tenant_write"
29
+
30
+ # User permissions
31
+ USER_READ_OWN = "user_read_own"
32
+ USER_WRITE_OWN = "user_write_own"
33
+ USER_READ_OTHERS = "user_read_others"
34
+ USER_WRITE_OTHERS = "user_write_others"
35
+
36
+ # Shared resource permissions
37
+ USER_READ_SHARED = "user_read_shared"
38
+ USER_WRITE_SHARED = "user_write_shared"
39
+
40
+
41
+ class Operation(Enum):
42
+ """Resource operations."""
43
+ READ = "read"
44
+ WRITE = "write"
45
+ DELETE = "delete"
46
+ CREATE = "create"
47
+
48
+
49
+ @dataclass
50
+ class AuthContext:
51
+ """
52
+ Actor information from JWT.
53
+
54
+ Represents WHO is making the request and their permissions.
55
+ """
56
+ user_id: str
57
+ tenant_id: str
58
+ roles: List[str] = field(default_factory=list)
59
+ permissions: List[str] = field(default_factory=list)
60
+ shared_tenants: List[str] = field(default_factory=list)
61
+ email: Optional[str] = None
62
+ name: Optional[str] = None
63
+
64
+ def has_permission(self, permission: Permission) -> bool:
65
+ """Check if actor has a specific permission."""
66
+ return permission.value in self.permissions
67
+
68
+ def has_role(self, role: str) -> bool:
69
+ """Check if actor has a specific role."""
70
+ return role in self.roles
71
+
72
+ def can_access_tenant(self, tenant_id: str) -> bool:
73
+ """
74
+ Check if actor can access a specific tenant.
75
+
76
+ Returns True if:
77
+ - It's the actor's own tenant
78
+ - The tenant is in the actor's shared_tenants list
79
+ - The actor has global admin permission
80
+ """
81
+ # Own tenant
82
+ if self.tenant_id == tenant_id:
83
+ return True
84
+
85
+ # Shared tenant
86
+ if tenant_id in self.shared_tenants:
87
+ return True
88
+
89
+ # Global admin can access any tenant
90
+ if self.has_permission(Permission.PLATFORM_ADMIN):
91
+ return True
92
+
93
+ return False
94
+
95
+ def is_own_tenant(self, tenant_id: str) -> bool:
96
+ """Check if tenant_id is actor's own tenant."""
97
+ return self.tenant_id == tenant_id
98
+
99
+ def is_shared_tenant(self, tenant_id: str) -> bool:
100
+ """Check if tenant_id is a shared tenant."""
101
+ return tenant_id in self.shared_tenants
102
+
103
+
104
+ @dataclass
105
+ class ResourceContext:
106
+ """
107
+ Target resource information from path parameters.
108
+
109
+ Represents WHAT resource is being accessed.
110
+ """
111
+ tenant_id: str
112
+ user_id: Optional[str] = None
113
+ resource_id: Optional[str] = None
114
+ resource_type: Optional[str] = None
115
+
116
+ def to_dict(self) -> Dict[str, Any]:
117
+ """Convert to dictionary."""
118
+ return {
119
+ 'tenant_id': self.tenant_id,
120
+ 'user_id': self.user_id,
121
+ 'resource_id': self.resource_id,
122
+ 'resource_type': self.resource_type
123
+ }
124
+
125
+
126
+ @dataclass
127
+ class AuthorizationResult:
128
+ """Result of an authorization check."""
129
+ allowed: bool
130
+ reason: str
131
+ context: Optional[Dict[str, Any]] = None
132
+
133
+ @staticmethod
134
+ def allow(reason: str, context: Optional[Dict[str, Any]] = None) -> 'AuthorizationResult':
135
+ """Create an allow result."""
136
+ return AuthorizationResult(allowed=True, reason=reason, context=context)
137
+
138
+ @staticmethod
139
+ def deny(reason: str, context: Optional[Dict[str, Any]] = None) -> 'AuthorizationResult':
140
+ """Create a deny result."""
141
+ return AuthorizationResult(allowed=False, reason=reason, context=context)
142
+
143
+
144
+ class AuthorizationMiddleware:
145
+ """
146
+ Centralized authorization logic with support for:
147
+ - Multi-tenant access control
148
+ - Tenant sharing
149
+ - Role-based permissions
150
+ - Operation-level access control
151
+ """
152
+
153
+ @staticmethod
154
+ def can_access_tenant(actor: AuthContext, target_tenant_id: str) -> AuthorizationResult:
155
+ """
156
+ Check if actor can access target tenant.
157
+
158
+ Args:
159
+ actor: The actor making the request
160
+ target_tenant_id: The tenant being accessed
161
+
162
+ Returns:
163
+ AuthorizationResult with allowed status and reason
164
+ """
165
+ # Platform admins can access any tenant
166
+ if actor.has_permission(Permission.PLATFORM_ADMIN):
167
+ return AuthorizationResult.allow("platform_admin")
168
+
169
+ # Own tenant access
170
+ if actor.is_own_tenant(target_tenant_id):
171
+ return AuthorizationResult.allow("own_tenant")
172
+
173
+ # Shared tenant access
174
+ if actor.is_shared_tenant(target_tenant_id):
175
+ return AuthorizationResult.allow("shared_tenant")
176
+
177
+ return AuthorizationResult.deny("tenant_access_denied")
178
+
179
+ @staticmethod
180
+ def can_access_user(
181
+ actor: AuthContext,
182
+ target_tenant_id: str,
183
+ target_user_id: str
184
+ ) -> AuthorizationResult:
185
+ """
186
+ Check if actor can access target user's data.
187
+
188
+ Args:
189
+ actor: The actor making the request
190
+ target_tenant_id: The tenant the user belongs to
191
+ target_user_id: The user whose data is being accessed
192
+
193
+ Returns:
194
+ AuthorizationResult with allowed status and reason
195
+ """
196
+ # Platform admins can access any user
197
+ if actor.has_permission(Permission.PLATFORM_ADMIN):
198
+ return AuthorizationResult.allow("platform_admin")
199
+
200
+ # Must be able to access the tenant first
201
+ tenant_result = AuthorizationMiddleware.can_access_tenant(actor, target_tenant_id)
202
+ if not tenant_result.allowed:
203
+ return tenant_result
204
+
205
+ # Own tenant - check additional permissions
206
+ if actor.is_own_tenant(target_tenant_id):
207
+ # Tenant admins can access any user in their tenant
208
+ if actor.has_permission(Permission.TENANT_ADMIN):
209
+ return AuthorizationResult.allow("tenant_admin")
210
+
211
+ # User accessing their own data
212
+ if actor.user_id == target_user_id:
213
+ return AuthorizationResult.allow("own_user")
214
+
215
+ # User accessing another user's data (requires special permission)
216
+ if actor.has_permission(Permission.USER_READ_OTHERS):
217
+ return AuthorizationResult.allow("user_read_others")
218
+
219
+ return AuthorizationResult.deny("user_access_denied")
220
+
221
+ # Shared tenant - can access if they have shared read permission
222
+ if actor.is_shared_tenant(target_tenant_id):
223
+ if actor.has_permission(Permission.USER_READ_SHARED):
224
+ return AuthorizationResult.allow("shared_tenant_user_access")
225
+ return AuthorizationResult.deny("shared_tenant_permission_required")
226
+
227
+ return AuthorizationResult.deny("user_access_denied")
228
+
229
+ @staticmethod
230
+ def can_perform_operation(
231
+ actor: AuthContext,
232
+ resource: ResourceContext,
233
+ operation: Operation
234
+ ) -> AuthorizationResult:
235
+ """
236
+ Check if actor can perform operation on resource.
237
+
238
+ Args:
239
+ actor: The actor making the request
240
+ resource: The target resource
241
+ operation: The operation to perform
242
+
243
+ Returns:
244
+ AuthorizationResult with allowed status and reason
245
+ """
246
+ # Platform admins can do anything
247
+ if actor.has_permission(Permission.PLATFORM_ADMIN):
248
+ return AuthorizationResult.allow("platform_admin", {
249
+ 'operation': operation.value,
250
+ 'resource': resource.to_dict()
251
+ })
252
+
253
+ # Check tenant-level access
254
+ tenant_result = AuthorizationMiddleware.can_access_tenant(actor, resource.tenant_id)
255
+ if not tenant_result.allowed:
256
+ return tenant_result
257
+
258
+ # Check user-level access if user_id is specified
259
+ if resource.user_id:
260
+ user_result = AuthorizationMiddleware.can_access_user(
261
+ actor, resource.tenant_id, resource.user_id
262
+ )
263
+ if not user_result.allowed:
264
+ return user_result
265
+
266
+ # Determine if this is own tenant or shared tenant
267
+ is_own_tenant = actor.is_own_tenant(resource.tenant_id)
268
+ is_shared_tenant = actor.is_shared_tenant(resource.tenant_id)
269
+
270
+ # Own tenant access rules
271
+ if is_own_tenant:
272
+ # Tenant admin can do anything in their tenant
273
+ if actor.has_permission(Permission.TENANT_ADMIN):
274
+ return AuthorizationResult.allow("tenant_admin", {
275
+ 'operation': operation.value
276
+ })
277
+
278
+ # User accessing their own resource
279
+ if resource.user_id and resource.user_id == actor.user_id:
280
+ if operation == Operation.READ:
281
+ if actor.has_permission(Permission.USER_READ_OWN):
282
+ return AuthorizationResult.allow("user_read_own")
283
+ elif operation in [Operation.WRITE, Operation.DELETE, Operation.CREATE]:
284
+ if actor.has_permission(Permission.USER_WRITE_OWN):
285
+ return AuthorizationResult.allow("user_write_own")
286
+
287
+ # User accessing another user's resource
288
+ if resource.user_id and resource.user_id != actor.user_id:
289
+ if operation == Operation.READ:
290
+ if actor.has_permission(Permission.USER_READ_OTHERS):
291
+ return AuthorizationResult.allow("user_read_others")
292
+ elif operation in [Operation.WRITE, Operation.DELETE]:
293
+ if actor.has_permission(Permission.USER_WRITE_OTHERS):
294
+ return AuthorizationResult.allow("user_write_others")
295
+
296
+ # Tenant-level resource (no user_id)
297
+ if not resource.user_id:
298
+ if operation == Operation.READ:
299
+ if actor.has_permission(Permission.TENANT_READ):
300
+ return AuthorizationResult.allow("tenant_read")
301
+ elif operation in [Operation.WRITE, Operation.DELETE, Operation.CREATE]:
302
+ if actor.has_permission(Permission.TENANT_WRITE):
303
+ return AuthorizationResult.allow("tenant_write")
304
+
305
+ # Shared tenant access rules
306
+ if is_shared_tenant:
307
+ # Read access to shared resources
308
+ if operation == Operation.READ:
309
+ if actor.has_permission(Permission.USER_READ_SHARED):
310
+ return AuthorizationResult.allow("shared_read")
311
+ return AuthorizationResult.deny("shared_read_permission_required")
312
+
313
+ # Write access to shared resources (typically not allowed)
314
+ if operation in [Operation.WRITE, Operation.DELETE, Operation.CREATE]:
315
+ if actor.has_permission(Permission.USER_WRITE_SHARED):
316
+ return AuthorizationResult.allow("shared_write")
317
+ return AuthorizationResult.deny("shared_resources_readonly")
318
+
319
+ # Default deny
320
+ return AuthorizationResult.deny("no_permission", {
321
+ 'required_operation': operation.value,
322
+ 'resource': resource.to_dict()
323
+ })
324
+
325
+
326
+ def extract_auth_context(event: Dict[str, Any]) -> AuthContext:
327
+ """
328
+ Extract AuthContext from API Gateway event.
329
+
330
+ Args:
331
+ event: API Gateway Lambda event
332
+
333
+ Returns:
334
+ AuthContext with actor information from JWT
335
+ """
336
+ from .auth import extract_user_context
337
+
338
+ user_context = extract_user_context(event)
339
+
340
+ return AuthContext(
341
+ user_id=user_context.get('user_id', ''),
342
+ tenant_id=user_context.get('tenant_id', ''),
343
+ roles=user_context.get('roles', []),
344
+ permissions=user_context.get('permissions', []),
345
+ shared_tenants=user_context.get('shared_tenants', []),
346
+ email=user_context.get('email'),
347
+ name=user_context.get('name')
348
+ )
349
+
350
+
351
+ def extract_resource_context(
352
+ event: Dict[str, Any],
353
+ resource_type: Optional[str] = None
354
+ ) -> ResourceContext:
355
+ """
356
+ Extract ResourceContext from API Gateway path parameters.
357
+
358
+ Args:
359
+ event: API Gateway Lambda event
360
+ resource_type: Optional resource type override
361
+
362
+ Returns:
363
+ ResourceContext with target resource information
364
+ """
365
+ path_params = event.get('pathParameters', {})
366
+
367
+ # Extract resource_id from various possible parameter names
368
+ resource_id = (
369
+ path_params.get('id') or
370
+ path_params.get('message_id') or
371
+ path_params.get('thread_id') or
372
+ path_params.get('channel_id') or
373
+ path_params.get('resource_id')
374
+ )
375
+
376
+ return ResourceContext(
377
+ tenant_id=path_params.get('tenant_id', ''),
378
+ user_id=path_params.get('user_id'),
379
+ resource_id=resource_id,
380
+ resource_type=resource_type
381
+ )
382
+
383
+
384
+ def infer_operation(event: Dict[str, Any]) -> Operation:
385
+ """
386
+ Infer operation from HTTP method.
387
+
388
+ Args:
389
+ event: API Gateway Lambda event
390
+
391
+ Returns:
392
+ Operation enum value
393
+ """
394
+ method = event.get('httpMethod', 'GET').upper()
395
+
396
+ if method == 'GET':
397
+ return Operation.READ
398
+ elif method == 'POST':
399
+ return Operation.CREATE
400
+ elif method in ['PUT', 'PATCH']:
401
+ return Operation.WRITE
402
+ elif method == 'DELETE':
403
+ return Operation.DELETE
404
+ else:
405
+ return Operation.READ # Default to read
406
+
407
+
408
+ def require_authorization(
409
+ operation: Optional[Operation] = None,
410
+ resource_type: Optional[str] = None,
411
+ extract_resource: Optional[Callable[[Dict[str, Any]], ResourceContext]] = None
412
+ ) -> Callable:
413
+ """
414
+ Decorator for automatic authorization checks on Lambda handlers.
415
+
416
+ This decorator:
417
+ 1. Extracts actor context from JWT
418
+ 2. Extracts resource context from path parameters
419
+ 3. Checks if actor can perform operation on resource
420
+ 4. Returns 403 if unauthorized
421
+ 5. Adds authorization context to event for handler use
422
+
423
+ Args:
424
+ operation: Operation to check (if None, inferred from HTTP method)
425
+ resource_type: Type of resource being accessed
426
+ extract_resource: Custom function to extract ResourceContext from event
427
+
428
+ Usage:
429
+ @require_authorization(operation=Operation.READ, resource_type="message")
430
+ def handler(event, context):
431
+ # Authorization already checked
432
+ # Access auth info via event['authorization_context']
433
+ pass
434
+ """
435
+ def decorator(handler_func: Callable) -> Callable:
436
+ @functools.wraps(handler_func)
437
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
438
+ try:
439
+ # Extract actor from JWT
440
+ actor = extract_auth_context(event)
441
+
442
+ # Extract resource from path
443
+ if extract_resource:
444
+ resource = extract_resource(event)
445
+ else:
446
+ resource = extract_resource_context(event, resource_type)
447
+
448
+ # Validate tenant_id is provided in path
449
+ if not resource.tenant_id:
450
+ return {
451
+ 'statusCode': 400,
452
+ 'headers': {
453
+ 'Content-Type': 'application/json',
454
+ 'Access-Control-Allow-Origin': '*'
455
+ },
456
+ 'body': json.dumps({
457
+ 'error': 'Bad Request',
458
+ 'message': 'tenant_id is required in path parameters'
459
+ })
460
+ }
461
+
462
+ # Determine operation
463
+ op = operation if operation else infer_operation(event)
464
+
465
+ # Check authorization
466
+ result = AuthorizationMiddleware.can_perform_operation(actor, resource, op)
467
+
468
+ if not result.allowed:
469
+ return {
470
+ 'statusCode': 403,
471
+ 'headers': {
472
+ 'Content-Type': 'application/json',
473
+ 'Access-Control-Allow-Origin': '*'
474
+ },
475
+ 'body': json.dumps({
476
+ 'error': 'Forbidden',
477
+ 'message': 'You do not have permission to access this resource',
478
+ 'reason': result.reason
479
+ })
480
+ }
481
+
482
+ # Add authorization context to event for handler use
483
+ event['authorization_context'] = {
484
+ 'actor': actor,
485
+ 'resource': resource,
486
+ 'operation': op.value,
487
+ 'reason': result.reason,
488
+ 'context': result.context
489
+ }
490
+
491
+ # Authorization passed - call handler
492
+ return handler_func(event, context, *args, **kwargs)
493
+
494
+ except KeyError as e:
495
+ # Missing required JWT claim
496
+ return {
497
+ 'statusCode': 401,
498
+ 'headers': {
499
+ 'Content-Type': 'application/json',
500
+ 'Access-Control-Allow-Origin': '*'
501
+ },
502
+ 'body': json.dumps({
503
+ 'error': 'Unauthorized',
504
+ 'message': f'Missing required authentication claim: {str(e)}'
505
+ })
506
+ }
507
+ except Exception as e:
508
+ # Unexpected error during authorization
509
+ return {
510
+ 'statusCode': 500,
511
+ 'headers': {
512
+ 'Content-Type': 'application/json',
513
+ 'Access-Control-Allow-Origin': '*'
514
+ },
515
+ 'body': json.dumps({
516
+ 'error': 'Internal Server Error',
517
+ 'message': 'Authorization check failed',
518
+ 'detail': str(e)
519
+ })
520
+ }
521
+
522
+ return wrapper
523
+ return decorator
@@ -0,0 +1,63 @@
1
+ """
2
+ CORS middleware for Lambda handlers.
3
+ """
4
+ import functools
5
+ from typing import Dict, Any, Callable
6
+
7
+
8
+ def add_cors_headers(handler: Callable) -> Callable:
9
+ """
10
+ Decorator that adds CORS headers to Lambda response.
11
+ """
12
+ @functools.wraps(handler)
13
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
14
+ response = handler(event, context, *args, **kwargs)
15
+
16
+ # Ensure headers exist
17
+ if 'headers' not in response:
18
+ response['headers'] = {}
19
+
20
+ # Add CORS headers
21
+ cors_headers = {
22
+ 'Access-Control-Allow-Origin': '*',
23
+ 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
24
+ 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
25
+ 'Content-Type': 'application/json'
26
+ }
27
+
28
+ response['headers'].update(cors_headers)
29
+ return response
30
+
31
+ return wrapper
32
+
33
+
34
+ def handle_preflight(handler: Callable) -> Callable:
35
+ """
36
+ Decorator that handles OPTIONS preflight requests.
37
+ """
38
+ @functools.wraps(handler)
39
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
40
+ # Handle OPTIONS request
41
+ if event.get('httpMethod') == 'OPTIONS':
42
+ return {
43
+ 'statusCode': 200,
44
+ 'headers': {
45
+ 'Access-Control-Allow-Origin': '*',
46
+ 'Access-Control-Allow-Headers': 'Content-Type,X-Amz-Date,Authorization,X-Api-Key,X-Amz-Security-Token',
47
+ 'Access-Control-Allow-Methods': 'GET,POST,PUT,DELETE,OPTIONS',
48
+ 'Content-Type': 'application/json'
49
+ },
50
+ 'body': ''
51
+ }
52
+
53
+ return handler(event, context, *args, **kwargs)
54
+
55
+ return wrapper
56
+
57
+
58
+ # Convenience decorator that combines both CORS functionalities
59
+ def handle_cors(handler: Callable) -> Callable:
60
+ """
61
+ Decorator that handles both preflight requests and adds CORS headers.
62
+ """
63
+ return add_cors_headers(handle_preflight(handler))
@@ -0,0 +1,114 @@
1
+ """
2
+ Error handling middleware for Lambda handlers.
3
+ """
4
+ import json
5
+ import logging
6
+ import traceback
7
+ import functools
8
+ from typing import Dict, Any, Callable
9
+ from ..core.service_errors import ValidationError, NotFoundError, AccessDeniedError
10
+ logger = logging.getLogger(__name__)
11
+
12
+
13
+ def handle_errors(handler: Callable) -> Callable:
14
+ """
15
+ Decorator that converts service errors to appropriate HTTP responses.
16
+ """
17
+ @functools.wraps(handler)
18
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
19
+ try:
20
+ return handler(event, context, *args, **kwargs)
21
+ except ValidationError as e:
22
+ logger.warning(f"Validation error: {str(e)}")
23
+ return {
24
+ 'statusCode': 400,
25
+ 'headers': {'Content-Type': 'application/json'},
26
+ 'body': json.dumps({
27
+ 'error': str(e),
28
+ 'error_code': 'VALIDATION_ERROR'
29
+ })
30
+ }
31
+ except AccessDeniedError as e:
32
+ logger.warning(f"Access denied: {str(e)}")
33
+ return {
34
+ 'statusCode': 403,
35
+ 'headers': {'Content-Type': 'application/json'},
36
+ 'body': json.dumps({
37
+ 'error': str(e),
38
+ 'error_code': 'ACCESS_DENIED'
39
+ })
40
+ }
41
+ except NotFoundError as e:
42
+ logger.info(f"Resource not found: {str(e)}")
43
+ return {
44
+ 'statusCode': 404,
45
+ 'headers': {'Content-Type': 'application/json'},
46
+ 'body': json.dumps({
47
+ 'error': str(e),
48
+ 'error_code': 'NOT_FOUND'
49
+ })
50
+ }
51
+ except Exception as e:
52
+ logger.error(f"Unexpected error in {handler.__name__}: {str(e)}")
53
+ logger.error(traceback.format_exc())
54
+ return {
55
+ 'statusCode': 500,
56
+ 'headers': {'Content-Type': 'application/json'},
57
+ 'body': json.dumps({
58
+ 'error': 'Internal server error',
59
+ 'error_code': 'INTERNAL_ERROR'
60
+ })
61
+ }
62
+
63
+ return wrapper
64
+
65
+
66
+ def validate_request_body(required_fields: list = None, optional_fields: list = None):
67
+ """
68
+ Decorator that validates request body fields.
69
+
70
+ Args:
71
+ required_fields: List of required field names
72
+ optional_fields: List of optional field names (for documentation)
73
+ """
74
+ def decorator(handler: Callable) -> Callable:
75
+ @functools.wraps(handler)
76
+ def wrapper(event: Dict[str, Any], context: Any, *args, **kwargs) -> Dict[str, Any]:
77
+ # Parse request body
78
+ body_str = event.get('body', '{}')
79
+ try:
80
+ body = json.loads(body_str) if isinstance(body_str, str) else body_str
81
+ if body is None:
82
+ raise json.JSONDecodeError("Body is None", "", 0)
83
+ except json.JSONDecodeError:
84
+ return {
85
+ 'statusCode': 400,
86
+ 'headers': {'Content-Type': 'application/json'},
87
+ 'body': json.dumps({
88
+ 'error': 'Invalid JSON in request body',
89
+ 'error_code': 'INVALID_JSON'
90
+ })
91
+ }
92
+
93
+ # Check required fields
94
+ if required_fields:
95
+ missing_fields = [field for field in required_fields if field not in body]
96
+ if missing_fields:
97
+ return {
98
+ 'statusCode': 400,
99
+ 'headers': {'Content-Type': 'application/json'},
100
+ 'body': json.dumps({
101
+ 'error': f'Missing required fields: {", ".join(missing_fields)}',
102
+ 'error_code': 'MISSING_FIELDS'
103
+ })
104
+ }
105
+
106
+ # Add parsed body to event for handler
107
+ event['parsed_body'] = body
108
+ return handler(event, context, *args, **kwargs)
109
+
110
+ return wrapper
111
+ return decorator
112
+
113
+
114
+ # Custom exception classes are imported from core module