geek-cafe-saas-sdk 0.7.4__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.
- geek_cafe_saas_sdk/__init__.py +1 -1
- geek_cafe_saas_sdk/core/anonymous_context.py +321 -0
- geek_cafe_saas_sdk/core/request_context.py +184 -0
- geek_cafe_saas_sdk/decorators/__init__.py +1 -1
- geek_cafe_saas_sdk/decorators/auth.py +6 -6
- geek_cafe_saas_sdk/decorators/core.py +44 -44
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +1 -3
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +1 -3
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +15 -3
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +1 -4
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +0 -2
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +1 -3
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +3 -3
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +1 -2
- geek_cafe_saas_sdk/domains/events/services/event_service.py +6 -4
- geek_cafe_saas_sdk/domains/files/handlers/README.md +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/download/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/files/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_derived/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/create_main/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/download_bundle/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/get_lineage/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/handlers/lineage/prepare_bundle/app.py +1 -1
- geek_cafe_saas_sdk/domains/files/models/file.py +16 -6
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +34 -9
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +38 -3
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +33 -36
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +3 -2
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +35 -2
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +20 -3
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +1 -3
- geek_cafe_saas_sdk/domains/notifications/handlers/create_webhook/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/get_preferences/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/list_webhooks/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/mark_read/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/send/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/handlers/update_preferences/app.py +1 -1
- geek_cafe_saas_sdk/domains/notifications/services/notification_service.py +1 -2
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/billing_accounts/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payment_intents/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payments/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payments/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/payments/record/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/handlers/refunds/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/payments/services/payment_service.py +1 -2
- geek_cafe_saas_sdk/domains/subscriptions/handlers/README.md +2 -2
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/addons/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/discounts/validate/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/create/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/plans/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/aggregate/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/handlers/usage/record/app.py +1 -1
- geek_cafe_saas_sdk/domains/subscriptions/services/subscription_manager_service.py +0 -2
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +3 -3
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +3 -3
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +1 -1
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +1 -5
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +1 -5
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +10 -3
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +40 -6
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +157 -12
- geek_cafe_saas_sdk/middleware/authorization.py +1 -1
- geek_cafe_saas_sdk/middleware/cors.py +8 -8
- geek_cafe_saas_sdk/services/database_service.py +76 -5
- {geek_cafe_saas_sdk-0.7.4.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/METADATA +16 -16
- {geek_cafe_saas_sdk-0.7.4.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/RECORD +131 -129
- {geek_cafe_saas_sdk-0.7.4.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/WHEEL +0 -0
- {geek_cafe_saas_sdk-0.7.4.dist-info → geek_cafe_saas_sdk-0.8.0.dist-info}/licenses/LICENSE +0 -0
geek_cafe_saas_sdk/__init__.py
CHANGED
|
@@ -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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
361
|
+
def lambda_handler(event, context):
|
|
362
362
|
# Public endpoint - no auth check
|
|
363
363
|
return {'statusCode': 200}
|
|
364
364
|
|