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,376 @@
1
+ """
2
+ Copyright 2024-2025 Geek Cafe, LLC
3
+ MIT License. See Project Root for the license information.
4
+
5
+ Authorization Service for permission checking with DB lookups and caching.
6
+ """
7
+
8
+ from typing import List, Set, Optional, Dict, Any
9
+ import time
10
+ from dataclasses import dataclass
11
+
12
+ from .permission_registry import permission_registry
13
+ from geek_cafe_saas_sdk.domains.auth.models import User, ResourcePermission
14
+
15
+
16
+ @dataclass
17
+ class AuthorizationContext:
18
+ """
19
+ Authorization context for a user.
20
+ Cached per-request to avoid repeated DB lookups.
21
+ """
22
+ user_id: str
23
+ tenant_id: str
24
+ roles: List[str]
25
+ permissions: Set[str] # Resolved from roles
26
+ resource_grants: Dict[str, Set[str]] # resource_key -> set of permissions
27
+ plan_tier: str = "free"
28
+
29
+ def has_permission(self, permission: str) -> bool:
30
+ """Check if user has a specific permission from roles."""
31
+ # Check for wildcard permission (platform admin)
32
+ if "*:*" in self.permissions or "platform:admin" in self.permissions:
33
+ return True
34
+
35
+ # Check exact permission
36
+ if permission in self.permissions:
37
+ return True
38
+
39
+ # Check wildcard category (e.g., "events:*")
40
+ category = permission.split(":")[0] if ":" in permission else ""
41
+ if category and f"{category}:*" in self.permissions:
42
+ return True
43
+
44
+ return False
45
+
46
+ def has_resource_permission(self, resource_type: str, resource_id: str, permission: str) -> bool:
47
+ """Check if user has permission on a specific resource."""
48
+ resource_key = f"{resource_type}:{resource_id}"
49
+ resource_perms = self.resource_grants.get(resource_key, set())
50
+
51
+ # Check for wildcard or specific permission
52
+ return "*" in resource_perms or permission in resource_perms
53
+
54
+
55
+ class AuthorizationService:
56
+ """
57
+ Authorization service for permission checking.
58
+
59
+ Provides:
60
+ - Role-based permission checks (RBAC)
61
+ - Resource-level permission checks (ABAC)
62
+ - Per-request caching to minimize DB lookups
63
+ - Real-time permission resolution from DB
64
+ """
65
+
66
+ def __init__(self, db=None, user_service=None, resource_permission_service=None, tenant_service=None):
67
+ """
68
+ Initialize authorization service.
69
+
70
+ Args:
71
+ db: DynamoDB connection (optional, will create if not provided)
72
+ user_service: UserService instance (optional, will create if not provided)
73
+ resource_permission_service: Service for ResourcePermission (optional)
74
+ tenant_service: TenantService instance (optional, will create if not provided)
75
+ """
76
+ self.db = db
77
+ self.user_service = user_service
78
+ self.resource_permission_service = resource_permission_service
79
+ self.tenant_service = tenant_service
80
+
81
+ # Per-request cache
82
+ self._request_cache: Dict[str, AuthorizationContext] = {}
83
+
84
+ def clear_cache(self):
85
+ """Clear the request cache. Call this at the end of each request."""
86
+ self._request_cache.clear()
87
+
88
+ def get_user_context(self, user_id: str, tenant_id: str) -> AuthorizationContext:
89
+ """
90
+ Get or build authorization context for a user.
91
+
92
+ Args:
93
+ user_id: User ID
94
+ tenant_id: Tenant ID
95
+
96
+ Returns:
97
+ AuthorizationContext with resolved permissions
98
+ """
99
+ cache_key = f"{user_id}:{tenant_id}"
100
+
101
+ # Check cache first
102
+ if cache_key in self._request_cache:
103
+ return self._request_cache[cache_key]
104
+
105
+ # Load from DB
106
+ context = self._load_user_context(user_id, tenant_id)
107
+
108
+ # Cache for this request
109
+ self._request_cache[cache_key] = context
110
+
111
+ return context
112
+
113
+ def _load_user_context(self, user_id: str, tenant_id: str) -> AuthorizationContext:
114
+ """
115
+ Load user context from database.
116
+
117
+ Args:
118
+ user_id: User ID
119
+ tenant_id: Tenant ID
120
+
121
+ Returns:
122
+ AuthorizationContext
123
+ """
124
+ # Import here to avoid circular dependencies
125
+ if self.user_service is None:
126
+ from geek_cafe_saas_sdk.domains.auth.services import UserService
127
+ self.user_service = UserService(dynamodb=self.db)
128
+
129
+ # Get user from DB
130
+ user_result = self.user_service.get_by_id(resource_id=user_id, tenant_id=tenant_id, user_id=user_id)
131
+
132
+ if not user_result.success or not user_result.data:
133
+ # User not found, return minimal context
134
+ return AuthorizationContext(
135
+ user_id=user_id,
136
+ tenant_id=tenant_id,
137
+ roles=[],
138
+ permissions=set(),
139
+ resource_grants={}
140
+ )
141
+
142
+ user: User = user_result.data
143
+
144
+ # Resolve permissions from roles
145
+ permissions = set(permission_registry.get_permissions_for_roles(user.roles))
146
+
147
+ # Load resource-level grants
148
+ resource_grants = self._load_resource_grants(user_id, tenant_id)
149
+
150
+ # Get plan tier from tenant
151
+ plan_tier = self._load_plan_tier(tenant_id)
152
+
153
+ return AuthorizationContext(
154
+ user_id=user_id,
155
+ tenant_id=tenant_id,
156
+ roles=user.roles,
157
+ permissions=permissions,
158
+ resource_grants=resource_grants,
159
+ plan_tier=plan_tier
160
+ )
161
+
162
+ def _load_resource_grants(self, user_id: str, tenant_id: str) -> Dict[str, Set[str]]:
163
+ """
164
+ Load resource-level permission grants for user.
165
+
166
+ Args:
167
+ user_id: User ID
168
+ tenant_id: Tenant ID
169
+
170
+ Returns:
171
+ Dict mapping resource_key to set of permissions
172
+ """
173
+ # Import here to avoid circular dependencies
174
+ if self.resource_permission_service is None:
175
+ from geek_cafe_saas_sdk.domains.auth.services import ResourcePermissionService
176
+ self.resource_permission_service = ResourcePermissionService(dynamodb=self.db)
177
+
178
+ # Load all grants for this user
179
+ grants_result = self.resource_permission_service.list_user_grants(user_id, tenant_id, limit=100)
180
+
181
+ if not grants_result.success or not grants_result.data:
182
+ return {}
183
+
184
+ # Build dict mapping resource_key -> set of permissions
185
+ resource_grants: Dict[str, Set[str]] = {}
186
+
187
+ for grant in grants_result.data:
188
+ resource_key = f"{grant.resource_type}:{grant.resource_id}"
189
+ resource_grants[resource_key] = set(grant.permissions)
190
+
191
+ return resource_grants
192
+
193
+ def _load_plan_tier(self, tenant_id: str) -> str:
194
+ """
195
+ Load plan tier from tenant.
196
+
197
+ Args:
198
+ tenant_id: Tenant ID
199
+
200
+ Returns:
201
+ Plan tier string (free, basic, pro, enterprise)
202
+ """
203
+ # Import here to avoid circular dependencies
204
+ if self.tenant_service is None:
205
+ from geek_cafe_saas_sdk.domains.tenancy.services import TenantService
206
+ self.tenant_service = TenantService(dynamodb=self.db)
207
+
208
+ # Get tenant from DB
209
+ tenant_result = self.tenant_service.get_by_id(resource_id=tenant_id, tenant_id=tenant_id, user_id="system")
210
+
211
+ if not tenant_result.success or not tenant_result.data:
212
+ return "free" # Default if tenant not found
213
+
214
+ return tenant_result.data.plan_tier
215
+
216
+ def can_user_perform(
217
+ self,
218
+ user_id: str,
219
+ tenant_id: str,
220
+ permission: str,
221
+ resource_type: Optional[str] = None,
222
+ resource_id: Optional[str] = None
223
+ ) -> bool:
224
+ """
225
+ Check if user can perform an action.
226
+
227
+ Args:
228
+ user_id: User ID
229
+ tenant_id: Tenant ID
230
+ permission: Permission code (e.g., "events:write")
231
+ resource_type: Optional resource type for ABAC check
232
+ resource_id: Optional resource ID for ABAC check
233
+
234
+ Returns:
235
+ True if user has permission, False otherwise
236
+ """
237
+ context = self.get_user_context(user_id, tenant_id)
238
+
239
+ # First check role-based permissions (RBAC)
240
+ if context.has_permission(permission):
241
+ return True
242
+
243
+ # If resource specified, check resource-level permissions (ABAC)
244
+ if resource_type and resource_id:
245
+ # Extract action from permission (e.g., "write" from "events:write")
246
+ action = permission.split(":")[-1] if ":" in permission else permission
247
+
248
+ if context.has_resource_permission(resource_type, resource_id, action):
249
+ return True
250
+
251
+ return False
252
+
253
+ def get_user_permissions(self, user_id: str, tenant_id: str) -> List[str]:
254
+ """
255
+ Get all permissions for a user.
256
+
257
+ Args:
258
+ user_id: User ID
259
+ tenant_id: Tenant ID
260
+
261
+ Returns:
262
+ List of permission codes
263
+ """
264
+ context = self.get_user_context(user_id, tenant_id)
265
+ return list(context.permissions)
266
+
267
+ def get_user_roles(self, user_id: str, tenant_id: str) -> List[str]:
268
+ """
269
+ Get all roles for a user.
270
+
271
+ Args:
272
+ user_id: User ID
273
+ tenant_id: Tenant ID
274
+
275
+ Returns:
276
+ List of role codes
277
+ """
278
+ context = self.get_user_context(user_id, tenant_id)
279
+ return context.roles
280
+
281
+ def grant_resource_permission(
282
+ self,
283
+ user_id: str,
284
+ tenant_id: str,
285
+ resource_type: str,
286
+ resource_id: str,
287
+ permissions: List[str],
288
+ granted_by: str,
289
+ reason: Optional[str] = None,
290
+ expires_at: Optional[int] = None
291
+ ) -> ResourcePermission:
292
+ """
293
+ Grant resource-level permissions to a user.
294
+
295
+ Args:
296
+ user_id: User to grant to
297
+ tenant_id: Tenant context
298
+ resource_type: Type of resource
299
+ resource_id: Resource ID
300
+ permissions: List of permissions to grant
301
+ granted_by: User ID who is granting
302
+ reason: Optional reason
303
+ expires_at: Optional expiration timestamp
304
+
305
+ Returns:
306
+ Created ResourcePermission object
307
+ """
308
+ grant = ResourcePermission()
309
+ grant.user_id = user_id
310
+ grant.tenant_id = tenant_id
311
+ grant.resource_type = resource_type
312
+ grant.resource_id = resource_id
313
+ grant.permissions = permissions
314
+ grant.granted_by = granted_by
315
+ grant.granted_at = int(time.time())
316
+ grant.expires_at = expires_at
317
+ grant.reason = reason
318
+
319
+ # Import here to avoid circular dependencies
320
+ if self.resource_permission_service is None:
321
+ from geek_cafe_saas_sdk.domains.auth.services import ResourcePermissionService
322
+ self.resource_permission_service = ResourcePermissionService(dynamodb=self.db)
323
+
324
+ # Save to DB
325
+ result = self.resource_permission_service.grant_permission(
326
+ grantee_user_id=user_id,
327
+ tenant_id=tenant_id,
328
+ resource_type=resource_type,
329
+ resource_id=resource_id,
330
+ permissions=permissions,
331
+ granted_by=granted_by,
332
+ reason=reason,
333
+ expires_at=expires_at
334
+ )
335
+
336
+ # Invalidate cache for this user
337
+ cache_key = f"{user_id}:{tenant_id}"
338
+ self._request_cache.pop(cache_key, None)
339
+
340
+ if result.success:
341
+ return result.data
342
+
343
+ return grant
344
+
345
+ def revoke_resource_permission(
346
+ self,
347
+ user_id: str,
348
+ tenant_id: str,
349
+ resource_type: str,
350
+ resource_id: str
351
+ ):
352
+ """
353
+ Revoke resource-level permissions from a user.
354
+
355
+ Args:
356
+ user_id: User to revoke from
357
+ tenant_id: Tenant context
358
+ resource_type: Type of resource
359
+ resource_id: Resource ID
360
+ """
361
+ # Import here to avoid circular dependencies
362
+ if self.resource_permission_service is None:
363
+ from geek_cafe_saas_sdk.domains.auth.services import ResourcePermissionService
364
+ self.resource_permission_service = ResourcePermissionService(dynamodb=self.db)
365
+
366
+ # Revoke from DB
367
+ self.resource_permission_service.revoke_permission(
368
+ grantee_user_id=user_id,
369
+ resource_type=resource_type,
370
+ resource_id=resource_id,
371
+ tenant_id=tenant_id
372
+ )
373
+
374
+ # Invalidate cache for this user
375
+ cache_key = f"{user_id}:{tenant_id}"
376
+ self._request_cache.pop(cache_key, None)