geek-cafe-saas-sdk 0.7.5__py3-none-any.whl → 0.8.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 (131) hide show
  1. geek_cafe_saas_sdk/__init__.py +1 -1
  2. geek_cafe_saas_sdk/core/anonymous_context.py +321 -0
  3. geek_cafe_saas_sdk/core/request_context.py +184 -0
  4. geek_cafe_saas_sdk/decorators/__init__.py +1 -1
  5. geek_cafe_saas_sdk/decorators/auth.py +6 -6
  6. geek_cafe_saas_sdk/decorators/core.py +44 -44
  7. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +1 -3
  8. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +1 -3
  9. geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +15 -3
  10. geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +1 -1
  11. geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +1 -1
  12. geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +1 -1
  13. geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +1 -1
  14. geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +1 -1
  15. geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +1 -4
  16. geek_cafe_saas_sdk/domains/auth/services/user_service.py +0 -2
  17. geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +1 -1
  18. geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +1 -1
  19. geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +1 -1
  20. geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +1 -1
  21. geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +1 -1
  22. geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +1 -3
  23. geek_cafe_saas_sdk/domains/communities/services/community_service.py +3 -3
  24. geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +1 -1
  25. geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +1 -1
  26. geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +1 -1
  27. geek_cafe_saas_sdk/domains/events/handlers/create/app.py +1 -1
  28. geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +1 -1
  29. geek_cafe_saas_sdk/domains/events/handlers/get/app.py +1 -1
  30. geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +1 -1
  31. geek_cafe_saas_sdk/domains/events/handlers/list/app.py +1 -1
  32. geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +1 -1
  33. geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +1 -1
  34. geek_cafe_saas_sdk/domains/events/handlers/update/app.py +1 -1
  35. geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +1 -2
  36. geek_cafe_saas_sdk/domains/events/services/event_service.py +6 -4
  37. geek_cafe_saas_sdk/domains/files/handlers/README.md +1 -1
  38. geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +1 -1
  39. geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +1 -1
  40. geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +1 -1
  41. geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +1 -1
  42. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +1 -1
  43. geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +1 -1
  44. geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +1 -1
  45. geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +1 -1
  46. geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +1 -1
  47. geek_cafe_saas_sdk/domains/files/models/file.py +16 -6
  48. geek_cafe_saas_sdk/domains/files/services/directory_service.py +34 -9
  49. geek_cafe_saas_sdk/domains/files/services/file_system_service.py +38 -3
  50. geek_cafe_saas_sdk/domains/files/services/file_version_service.py +33 -36
  51. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +1 -1
  52. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +1 -1
  53. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +1 -1
  54. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +1 -1
  55. geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +1 -1
  56. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +1 -1
  57. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +1 -1
  58. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +1 -1
  59. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +1 -1
  60. geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +1 -1
  61. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +1 -1
  62. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +1 -1
  63. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +1 -1
  64. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +1 -1
  65. geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +1 -1
  66. geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +35 -2
  67. geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +20 -3
  68. geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +1 -3
  69. geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +1 -1
  70. geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +1 -1
  71. geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +1 -1
  72. geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +1 -1
  73. geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +1 -1
  74. geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +1 -1
  75. geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +1 -1
  76. geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +1 -1
  77. geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +1 -2
  78. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +1 -1
  79. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +1 -1
  80. geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +1 -1
  81. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +1 -1
  82. geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +1 -1
  83. geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +1 -1
  84. geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +1 -1
  85. geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +1 -1
  86. geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +1 -1
  87. geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +1 -1
  88. geek_cafe_saas_sdk/domains/payments/services/payment_service.py +1 -2
  89. geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +2 -2
  90. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +1 -1
  91. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +1 -1
  92. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +1 -1
  93. geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +1 -1
  94. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +1 -1
  95. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +1 -1
  96. geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +1 -1
  97. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +1 -1
  98. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +1 -1
  99. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +1 -1
  100. geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +1 -1
  101. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +1 -1
  102. geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +1 -1
  103. geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +0 -2
  104. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +1 -1
  105. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +1 -1
  106. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +1 -1
  107. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +1 -1
  108. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +1 -1
  109. geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +1 -1
  110. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +1 -1
  111. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +1 -1
  112. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +1 -1
  113. geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +1 -1
  114. geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +3 -3
  115. geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +3 -3
  116. geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +1 -1
  117. geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +1 -1
  118. geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +1 -1
  119. geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +1 -1
  120. geek_cafe_saas_sdk/domains/voting/services/vote_service.py +1 -5
  121. geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +1 -5
  122. geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +10 -3
  123. geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +40 -6
  124. geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +157 -12
  125. geek_cafe_saas_sdk/middleware/authorization.py +1 -1
  126. geek_cafe_saas_sdk/middleware/cors.py +8 -8
  127. geek_cafe_saas_sdk/services/database_service.py +76 -5
  128. {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/METADATA +16 -16
  129. {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/RECORD +131 -129
  130. {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/WHEEL +0 -0
  131. {geek_cafe_saas_sdk-0.7.5.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/licenses/LICENSE +0 -0
@@ -3,7 +3,7 @@ Geek Cafe Services - Base Reusable Services for SaaS
3
3
 
4
4
  Version 0.6.0 adds File System Service
5
5
  """
6
- __version__ = "0.7.5"
6
+ __version__ = "0.8.0"
7
7
 
8
8
  # Import main modules for easier access
9
9
  from . import services
@@ -0,0 +1,321 @@
1
+ """
2
+ Anonymous Context Factory for Public Operations.
3
+
4
+ Provides standardized RequestContext for:
5
+ - Anonymous/public operations (contact forms, surveys, voting)
6
+ - System operations (background jobs, scheduled tasks)
7
+
8
+ Maintains security architecture while allowing public access.
9
+ """
10
+
11
+ import os
12
+ from typing import Dict, Any, Optional
13
+ from .request_context import RequestContext
14
+
15
+
16
+ class AnonymousContextFactory:
17
+ """
18
+ Factory for creating RequestContext for anonymous/public operations.
19
+
20
+ Use this for operations that don't require authentication but still
21
+ need security context for audit trails and validation.
22
+
23
+ Examples:
24
+ - Public contact forms
25
+ - Anonymous voting/polls
26
+ - Public surveys
27
+ - Newsletter signups
28
+ - A/B testing
29
+ """
30
+
31
+ # Standard user IDs for special contexts
32
+ ANONYMOUS_USER_ID = "anonymous"
33
+ SYSTEM_USER_ID = "system"
34
+ SYSTEM_TENANT_ID = "SYSTEM" # Used for tenant provisioning operations
35
+
36
+ @staticmethod
37
+ def create_anonymous_context(
38
+ tenant_id: str,
39
+ ip_address: Optional[str] = None,
40
+ session_id: Optional[str] = None,
41
+ additional_metadata: Optional[Dict[str, Any]] = None
42
+ ) -> RequestContext:
43
+ """
44
+ Create RequestContext for anonymous operations.
45
+
46
+ Anonymous users:
47
+ - Belong to a specific tenant
48
+ - Have limited permissions (public:submit)
49
+ - Tracked by IP and session for rate limiting
50
+ - Show as 'anonymous' in audit trail
51
+
52
+ Args:
53
+ tenant_id: Tenant ID (required - anonymous users still belong to tenant)
54
+ ip_address: Optional IP address for rate limiting/tracking
55
+ session_id: Optional session ID for duplicate prevention
56
+ additional_metadata: Extra metadata (e.g., referrer, user agent)
57
+
58
+ Returns:
59
+ RequestContext with anonymous user identity
60
+
61
+ Example:
62
+ >>> from geek_cafe_saas_sdk.core.anonymous_context import AnonymousContextFactory
63
+ >>>
64
+ >>> # In public Lambda handler
65
+ >>> context = AnonymousContextFactory.create_anonymous_context(
66
+ ... tenant_id='tenant_123',
67
+ ... ip_address='192.168.1.1',
68
+ ... session_id='sess_abc123'
69
+ ... )
70
+ >>>
71
+ >>> service = ContactThreadService(
72
+ ... dynamodb=db,
73
+ ... table_name=TABLE,
74
+ ... request_context=context
75
+ ... )
76
+ >>>
77
+ >>> result = service.create(
78
+ ... tenant_id='tenant_123',
79
+ ... user_id='anonymous',
80
+ ... payload={'message': 'Hello'}
81
+ ... )
82
+ """
83
+ if not tenant_id:
84
+ raise ValueError("tenant_id is required for anonymous context")
85
+
86
+ user_context = {
87
+ 'user_id': AnonymousContextFactory.ANONYMOUS_USER_ID,
88
+ 'tenant_id': tenant_id,
89
+ 'email': 'anonymous@public',
90
+ 'roles': ['public'],
91
+ 'permissions': ['public:submit'],
92
+ 'inboxes': [],
93
+
94
+ # Special flags
95
+ 'is_anonymous': True,
96
+ 'is_authenticated': False,
97
+
98
+ # Tracking for rate limiting and abuse prevention
99
+ 'ip_address': ip_address,
100
+ 'session_id': session_id,
101
+
102
+ # Additional context
103
+ 'metadata': additional_metadata or {}
104
+ }
105
+
106
+ return RequestContext(user_context)
107
+
108
+ @staticmethod
109
+ def create_system_context(
110
+ tenant_id: str,
111
+ operation_name: Optional[str] = None
112
+ ) -> RequestContext:
113
+ """
114
+ Create RequestContext for system operations.
115
+
116
+ System context:
117
+ - Used for background jobs and scheduled tasks
118
+ - Has elevated permissions (*:*:*)
119
+ - Shows as 'system' in audit trail
120
+ - Not rate limited
121
+
122
+ Args:
123
+ tenant_id: Tenant ID for the operation
124
+ operation_name: Optional name of the operation (for logging)
125
+
126
+ Returns:
127
+ RequestContext with system identity
128
+
129
+ Example:
130
+ >>> from geek_cafe_saas_sdk.core.anonymous_context import AnonymousContextFactory
131
+ >>>
132
+ >>> # In background job
133
+ >>> context = AnonymousContextFactory.create_system_context(
134
+ ... tenant_id='tenant_123',
135
+ ... operation_name='cleanup_old_data'
136
+ ... )
137
+ >>>
138
+ >>> service = AnalyticsService(
139
+ ... dynamodb=db,
140
+ ... table_name=TABLE,
141
+ ... request_context=context
142
+ ... )
143
+ >>>
144
+ >>> result = service.delete_old_records(days_old=90)
145
+ >>> # Audit trail shows: deleted_by='system'
146
+ """
147
+ if not tenant_id:
148
+ raise ValueError("tenant_id is required for system context")
149
+
150
+ user_context = {
151
+ 'user_id': AnonymousContextFactory.SYSTEM_USER_ID,
152
+ 'tenant_id': tenant_id,
153
+ 'email': 'system@internal',
154
+ 'roles': ['system'],
155
+ 'permissions': ['*:*:*'], # System has all permissions
156
+ 'inboxes': [],
157
+
158
+ # Special flags
159
+ 'is_system': True,
160
+ 'is_authenticated': True,
161
+
162
+ # Context
163
+ 'operation_name': operation_name,
164
+ 'metadata': {'operation': operation_name} if operation_name else {}
165
+ }
166
+
167
+ return RequestContext(user_context)
168
+
169
+ @staticmethod
170
+ def create_test_context(
171
+ user_id: str = "test_user",
172
+ tenant_id: str = "test_tenant",
173
+ roles: Optional[list] = None,
174
+ permissions: Optional[list] = None
175
+ ) -> RequestContext:
176
+ """
177
+ Create RequestContext for testing.
178
+
179
+ Convenience method for creating test contexts without full JWT setup.
180
+
181
+ Args:
182
+ user_id: Test user ID
183
+ tenant_id: Test tenant ID
184
+ roles: Optional list of roles
185
+ permissions: Optional list of permissions
186
+
187
+ Returns:
188
+ RequestContext for testing
189
+
190
+ Example:
191
+ >>> from geek_cafe_saas_sdk.core.anonymous_context import AnonymousContextFactory
192
+ >>>
193
+ >>> # In test
194
+ >>> context = AnonymousContextFactory.create_test_context(
195
+ ... user_id='user_123',
196
+ ... tenant_id='tenant_123',
197
+ ... permissions=['messages:create']
198
+ ... )
199
+ """
200
+ user_context = {
201
+ 'user_id': user_id,
202
+ 'tenant_id': tenant_id,
203
+ 'email': f'{user_id}@test.com',
204
+ 'roles': roles or [],
205
+ 'permissions': permissions or [],
206
+ 'inboxes': [],
207
+ 'is_test': True
208
+ }
209
+
210
+ return RequestContext(user_context)
211
+
212
+ @staticmethod
213
+ def create_provisioning_context(
214
+ operation_name: str = "tenant_provisioning"
215
+ ) -> RequestContext:
216
+ """
217
+ Create RequestContext for tenant provisioning operations (signup flow).
218
+
219
+ Provisioning context:
220
+ - Used for creating new tenants during signup
221
+ - Has SYSTEM tenant_id to bypass tenant isolation
222
+ - Has elevated permissions for tenant creation
223
+ - Shows as 'system' in audit trail
224
+
225
+ Args:
226
+ operation_name: Name of the provisioning operation (for logging)
227
+
228
+ Returns:
229
+ RequestContext with system identity for provisioning
230
+
231
+ Example:
232
+ >>> from geek_cafe_saas_sdk.core.anonymous_context import AnonymousContextFactory
233
+ >>>
234
+ >>> # In signup handler
235
+ >>> context = AnonymousContextFactory.create_provisioning_context(
236
+ ... operation_name='user_signup'
237
+ ... )
238
+ >>>
239
+ >>> tenant_service = TenantService(
240
+ ... dynamodb=db,
241
+ ... table_name=TABLE,
242
+ ... request_context=context
243
+ ... )
244
+ >>>
245
+ >>> result = tenant_service.create_with_user(
246
+ ... user_payload={...},
247
+ ... tenant_payload={...}
248
+ ... )
249
+ >>> # Creates new tenant, audit trail shows: created_by='system'
250
+ """
251
+ user_context = {
252
+ 'user_id': AnonymousContextFactory.SYSTEM_USER_ID,
253
+ 'tenant_id': AnonymousContextFactory.SYSTEM_TENANT_ID,
254
+ 'email': 'system@internal',
255
+ 'roles': ['system', 'provisioner'],
256
+ 'permissions': ['*:*:*'], # System has all permissions
257
+ 'inboxes': [],
258
+
259
+ # Special flags
260
+ 'is_system': True,
261
+ 'is_provisioning': True,
262
+ 'is_authenticated': True,
263
+
264
+ # Context
265
+ 'operation_name': operation_name,
266
+ 'metadata': {'operation': operation_name, 'type': 'provisioning'}
267
+ }
268
+
269
+ return RequestContext(user_context)
270
+
271
+ @staticmethod
272
+ def is_anonymous(request_context: RequestContext) -> bool:
273
+ """
274
+ Check if request context is anonymous.
275
+
276
+ Args:
277
+ request_context: RequestContext to check
278
+
279
+ Returns:
280
+ True if anonymous, False otherwise
281
+ """
282
+ return request_context._user_context.get('is_anonymous', False)
283
+
284
+ @staticmethod
285
+ def is_system(request_context: RequestContext) -> bool:
286
+ """
287
+ Check if request context is system.
288
+
289
+ Args:
290
+ request_context: RequestContext to check
291
+
292
+ Returns:
293
+ True if system, False otherwise
294
+ """
295
+ return request_context._user_context.get('is_system', False)
296
+
297
+ @staticmethod
298
+ def get_ip_address(request_context: RequestContext) -> Optional[str]:
299
+ """
300
+ Get IP address from request context.
301
+
302
+ Args:
303
+ request_context: RequestContext
304
+
305
+ Returns:
306
+ IP address if available, None otherwise
307
+ """
308
+ return request_context._user_context.get('ip_address')
309
+
310
+ @staticmethod
311
+ def get_session_id(request_context: RequestContext) -> Optional[str]:
312
+ """
313
+ Get session ID from request context.
314
+
315
+ Args:
316
+ request_context: RequestContext
317
+
318
+ Returns:
319
+ Session ID if available, None otherwise
320
+ """
321
+ return request_context._user_context.get('session_id')
@@ -0,0 +1,184 @@
1
+ """
2
+ Request Context - Security Token Service for Geek Cafe SaaS SDK.
3
+
4
+ This module provides a centralized security context that tracks:
5
+ - Authenticated user (from JWT)
6
+ - Target resource (from API path parameters)
7
+ - Authorization helpers (roles, permissions, tenancy validation)
8
+ """
9
+
10
+ from typing import Dict, List, Optional, Any
11
+
12
+
13
+ class RequestContext:
14
+ """
15
+ Security token service - single source of truth for request authentication and authorization.
16
+
17
+ This class separates:
18
+ - WHO is making the request (authenticated_user_id, authenticated_tenant_id from JWT)
19
+ - WHAT they're trying to access (target_user_id, target_tenant_id from path)
20
+
21
+ This separation enables proper security validation:
22
+ - Can user A access resources belonging to user B?
23
+ - Can user from tenant X access resources in tenant Y?
24
+ - Does user have required role/permission?
25
+ """
26
+
27
+ def __init__(self, user_context: Optional[Dict[str, Any]] = None):
28
+ """
29
+ Initialize request context from JWT payload.
30
+
31
+ Args:
32
+ user_context: Dictionary from JWT containing:
33
+ - user_id: Authenticated user ID
34
+ - tenant_id: Authenticated user's tenant ID
35
+ - roles: List of role strings
36
+ - permissions: List of permission strings
37
+ - email: User email
38
+ - inboxes: List of inbox IDs (optional)
39
+ """
40
+ self._user_context = user_context or {}
41
+
42
+ # Authenticated user (from JWT)
43
+ self.authenticated_user_id: Optional[str] = self._user_context.get('user_id')
44
+ self.authenticated_tenant_id: Optional[str] = self._user_context.get('tenant_id')
45
+ self.authenticated_user_email: Optional[str] = self._user_context.get('email')
46
+ self.roles: List[str] = self._user_context.get('roles', [])
47
+ self.permissions: List[str] = self._user_context.get('permissions', [])
48
+ self.inboxes: List[str] = self._user_context.get('inboxes', [])
49
+
50
+ # Target resource (from path parameters - set by services)
51
+ self.target_user_id: Optional[str] = None
52
+ self.target_tenant_id: Optional[str] = None
53
+
54
+ def set_targets(self, tenant_id: Optional[str] = None, user_id: Optional[str] = None):
55
+ """
56
+ Set target resource IDs from path parameters.
57
+
58
+ Args:
59
+ tenant_id: Target tenant ID from path
60
+ user_id: Target user ID from path
61
+ """
62
+ if tenant_id:
63
+ self.target_tenant_id = tenant_id
64
+ if user_id:
65
+ self.target_user_id = user_id
66
+
67
+ # ========================================
68
+ # Tenancy Validation
69
+ # ========================================
70
+
71
+ def is_same_tenancy(self) -> bool:
72
+ """Check if authenticated user's tenant matches target tenant."""
73
+ if not self.target_tenant_id:
74
+ return True # No target specified, assume same
75
+ return self.authenticated_tenant_id == self.target_tenant_id
76
+
77
+ def validate_tenant_access(self, tenant_id: str) -> bool:
78
+ """
79
+ Validate user can access resources in target tenant.
80
+
81
+ Args:
82
+ tenant_id: Tenant ID to validate
83
+
84
+ Returns:
85
+ True if access allowed, False otherwise
86
+ """
87
+ # Platform admins can access any tenant
88
+ if self.is_platform_admin():
89
+ return True
90
+
91
+ # Regular users can only access their own tenant
92
+ return self.authenticated_tenant_id == tenant_id
93
+
94
+ # ========================================
95
+ # User Access Validation
96
+ # ========================================
97
+
98
+ def is_self_user(self) -> bool:
99
+ """Check if authenticated user is the same as target user."""
100
+ if not self.target_user_id:
101
+ return True # No target specified
102
+ return self.authenticated_user_id == self.target_user_id
103
+
104
+ def can_access_user_resource(self, resource_user_id: str) -> bool:
105
+ """
106
+ Check if user can access a resource owned by another user.
107
+
108
+ Args:
109
+ resource_user_id: Owner of the resource
110
+
111
+ Returns:
112
+ True if access allowed
113
+ """
114
+ # Self access
115
+ if self.authenticated_user_id == resource_user_id:
116
+ return True
117
+
118
+ # Admins can access
119
+ if self.is_platform_admin() or self.is_tenant_admin():
120
+ return True
121
+
122
+ return False
123
+
124
+ # ========================================
125
+ # Role Checks
126
+ # ========================================
127
+
128
+ def has_role(self, role: str) -> bool:
129
+ """Check if user has a specific role."""
130
+ return role in self.roles
131
+
132
+ def has_any_role(self, roles: List[str]) -> bool:
133
+ """Check if user has any of the specified roles."""
134
+ return any(role in self.roles for role in roles)
135
+
136
+ def is_platform_admin(self) -> bool:
137
+ """Check if user is a platform admin (can access all tenants)."""
138
+ return 'platform_admin' in self.roles
139
+
140
+ def is_tenant_admin(self) -> bool:
141
+ """Check if user is admin of their tenant."""
142
+ return 'tenant_admin' in self.roles
143
+
144
+ def is_admin(self) -> bool:
145
+ """Check if user is any kind of admin."""
146
+ return self.is_platform_admin() or self.is_tenant_admin()
147
+
148
+ # ========================================
149
+ # Permission Checks
150
+ # ========================================
151
+
152
+ def has_permission(self, permission: str) -> bool:
153
+ """Check if user has a specific permission."""
154
+ return permission in self.permissions
155
+
156
+ def has_any_permission(self, permissions: List[str]) -> bool:
157
+ """Check if user has any of the specified permissions."""
158
+ return any(perm in self.permissions for perm in permissions)
159
+
160
+ def has_all_permissions(self, permissions: List[str]) -> bool:
161
+ """Check if user has all specified permissions."""
162
+ return all(perm in self.permissions for perm in permissions)
163
+
164
+ # ========================================
165
+ # Utility Methods
166
+ # ========================================
167
+
168
+ def to_dict(self) -> Dict[str, Any]:
169
+ """Convert context to dictionary for logging/debugging."""
170
+ return {
171
+ 'authenticated_user_id': self.authenticated_user_id,
172
+ 'authenticated_tenant_id': self.authenticated_tenant_id,
173
+ 'authenticated_user_email': self.authenticated_user_email,
174
+ 'target_user_id': self.target_user_id,
175
+ 'target_tenant_id': self.target_tenant_id,
176
+ 'roles': self.roles,
177
+ 'permissions': self.permissions,
178
+ 'is_admin': self.is_admin(),
179
+ 'is_same_tenancy': self.is_same_tenancy(),
180
+ 'is_self_user': self.is_self_user(),
181
+ }
182
+
183
+ def __repr__(self) -> str:
184
+ return f"RequestContext(user={self.authenticated_user_id}, tenant={self.authenticated_tenant_id})"
@@ -16,7 +16,7 @@ Usage:
16
16
  @add_cors
17
17
  @parse_request_body(convert_case=True)
18
18
  @inject_service(MessageService)
19
- def handler(event, context, service):
19
+ def lambda_handler(event, context, service):
20
20
  return service.get_by_id(event['pathParameters']['id'])
21
21
 
22
22
  Design Principles:
@@ -51,14 +51,14 @@ def require_authorization(
51
51
 
52
52
  Usage:
53
53
  @require_authorization(operation=Operation.READ, resource_type="message")
54
- def handler(event, context):
54
+ def lambda_handler(event, context):
55
55
  # Authorization already checked
56
56
  message_id = event['pathParameters']['message_id']
57
57
  return {'statusCode': 200}
58
58
 
59
59
  # Auto-infer operation from HTTP method
60
60
  @require_authorization(resource_type="message")
61
- def handler(event, context):
61
+ def lambda_handler(event, context):
62
62
  # GET -> READ, POST -> CREATE, etc.
63
63
  pass
64
64
 
@@ -181,7 +181,7 @@ def require_admin(handler: Callable) -> Callable:
181
181
 
182
182
  Usage:
183
183
  @require_admin
184
- def handler(event, context):
184
+ def lambda_handler(event, context):
185
185
  # User is guaranteed to be an admin
186
186
  return {'statusCode': 200}
187
187
 
@@ -242,7 +242,7 @@ def require_tenant_admin(handler: Callable) -> Callable:
242
242
 
243
243
  Usage:
244
244
  @require_tenant_admin
245
- def handler(event, context):
245
+ def lambda_handler(event, context):
246
246
  # User is tenant admin for their tenant
247
247
  return {'statusCode': 200}
248
248
 
@@ -296,7 +296,7 @@ def require_platform_admin(handler: Callable) -> Callable:
296
296
 
297
297
  Usage:
298
298
  @require_platform_admin
299
- def handler(event, context):
299
+ def lambda_handler(event, context):
300
300
  # User is platform admin
301
301
  return {'statusCode': 200}
302
302
 
@@ -358,7 +358,7 @@ def public(handler: Callable) -> Callable:
358
358
 
359
359
  Usage:
360
360
  @public
361
- def handler(event, context):
361
+ def lambda_handler(event, context):
362
362
  # Public endpoint - no auth check
363
363
  return {'statusCode': 200}
364
364