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
@@ -6,6 +6,7 @@ from boto3_assist.dynamodb.dynamodb import DynamoDB, DynamoDBIndex
6
6
  from ..core.service_result import ServiceResult
7
7
  from ..core.service_errors import ValidationError, AccessDeniedError, NotFoundError
8
8
  from ..core.error_codes import ErrorCode
9
+ from ..core.request_context import RequestContext
9
10
  import os
10
11
  from aws_lambda_powertools import Logger
11
12
 
@@ -17,7 +18,22 @@ logger = Logger()
17
18
  class DatabaseService(ABC, Generic[T]):
18
19
  """Base service class for database operations."""
19
20
 
20
- def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
21
+ def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None, request_context: RequestContext):
22
+ """
23
+ Initialize DatabaseService.
24
+
25
+ Args:
26
+ dynamodb: DynamoDB client instance
27
+ table_name: DynamoDB table name
28
+ request_context: **REQUIRED** Security context with JWT token (no longer optional)
29
+
30
+ Raises:
31
+ ValueError: If table_name is not provided
32
+ TypeError: If request_context is not provided (enforced by Python)
33
+ """
34
+ if request_context is None:
35
+ raise AccessDeniedError("request_context is required for all database operations. All services must have security context.")
36
+
21
37
  self.dynamodb = dynamodb or DynamoDB()
22
38
  self.table_name = (
23
39
  table_name or os.getenv("DYNAMODB_TABLE_NAME")
@@ -27,6 +43,19 @@ class DatabaseService(ABC, Generic[T]):
27
43
  raise ValueError("Table name is required")
28
44
 
29
45
  self.LOG_DYNAMO_DB_QUERY = os.getenv("LOG_DYNAMO_DB_QUERY", False)
46
+ self._request_context = request_context
47
+
48
+ @property
49
+ def request_context(self) -> RequestContext:
50
+ """Get the request context (security token)."""
51
+ if self._request_context is None:
52
+ raise AccessDeniedError("No security context set for this service")
53
+ return self._request_context
54
+
55
+ @request_context.setter
56
+ def request_context(self, value: RequestContext):
57
+ """Set the request context (security token)."""
58
+ self._request_context = value
30
59
 
31
60
  @abstractmethod
32
61
  def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[T]:
@@ -114,8 +143,45 @@ class DatabaseService(ABC, Generic[T]):
114
143
  raise AccessDeniedError("Access denied to resource in different tenant")
115
144
 
116
145
  def _save_model(self, model: T) -> ServiceResult[T]:
117
- """Save model to database with enhanced error handling."""
146
+ """Save model to database with **MANDATORY** security validation and audit trail.
147
+
148
+ Args:
149
+ model: Model to save
150
+
151
+ Returns:
152
+ ServiceResult with saved model or error
153
+
154
+ Security:
155
+ - **ALWAYS** validates tenant access (no longer optional)
156
+ - **ALWAYS** auto-populates audit fields (created_by, updated_by)
157
+ - **ALWAYS** prevents cross-tenant resource creation
158
+ - request_context is required (enforced in __init__)
159
+ """
118
160
  try:
161
+ # MANDATORY security validation - always validate tenant access
162
+ # EXCEPTION: SYSTEM tenant can provision new tenants (signup flow)
163
+ from geek_cafe_saas_sdk.core.anonymous_context import AnonymousContextFactory
164
+ is_system_tenant = (self._request_context.authenticated_tenant_id ==
165
+ AnonymousContextFactory.SYSTEM_TENANT_ID)
166
+
167
+ if hasattr(model, 'tenant_id') and model.tenant_id:
168
+ # SYSTEM tenant can create resources in any tenant (provisioning)
169
+ if not is_system_tenant:
170
+ if not self._request_context.validate_tenant_access(model.tenant_id):
171
+ return ServiceResult.error_result(
172
+ ErrorCode.ACCESS_DENIED,
173
+ "Cannot save resources in other tenants"
174
+ )
175
+
176
+ # MANDATORY audit trail - always populate audit fields
177
+ # Set created_by_id if this is a new resource
178
+ if hasattr(model, 'created_by_id') and not model.created_by_id:
179
+ model.created_by_id = self._request_context.authenticated_user_id
180
+
181
+ # Always update updated_by_id
182
+ if hasattr(model, 'updated_by_id'):
183
+ model.updated_by_id = self._request_context.authenticated_user_id
184
+
119
185
  # The boto3_assist library handles all GSI key population automatically.
120
186
  self.dynamodb.save(table_name=self.table_name, item=model)
121
187
  return ServiceResult.success_result(model)
@@ -147,7 +213,7 @@ class DatabaseService(ABC, Generic[T]):
147
213
  return None
148
214
 
149
215
  def _get_model_by_id_with_tenant_check(
150
- self, resource_id: str, model_class, tenant_id: str, include_deleted: bool = True
216
+ self, resource_id: str, model_class, tenant_id: Optional[str] = None, include_deleted: bool = True
151
217
  ) -> Optional[T]:
152
218
  """
153
219
  Get model by ID with automatic tenant validation.
@@ -159,7 +225,7 @@ class DatabaseService(ABC, Generic[T]):
159
225
  Args:
160
226
  resource_id: The resource ID to fetch
161
227
  model_class: The model class to instantiate
162
- tenant_id: The tenant ID from JWT (authenticated user's tenant)
228
+ tenant_id: The tenant ID to validate (uses request_context if not provided)
163
229
  include_deleted: If True, returns deleted items. If False, returns None for deleted items.
164
230
  Default is True since get-by-id operations typically need to verify deletion,
165
231
  perform restores, or show audit history.
@@ -168,10 +234,15 @@ class DatabaseService(ABC, Generic[T]):
168
234
  The model if found and belongs to tenant, None otherwise
169
235
 
170
236
  Security:
237
+ - Automatically uses request_context if available
171
238
  - Returns None for resources in different tenants (prevents enumeration)
172
239
  - Optionally filters deleted resources based on include_deleted parameter
173
- - Single source of truth: tenant_id from JWT only
174
240
  """
241
+ # Use request_context tenant if tenant_id not explicitly provided
242
+ # request_context is always present (enforced in __init__)
243
+ if tenant_id is None:
244
+ tenant_id = self._request_context.authenticated_tenant_id
245
+
175
246
  model = self._get_model_by_id(resource_id, model_class)
176
247
 
177
248
  if not model:
@@ -1,6 +1,6 @@
1
1
  Metadata-Version: 2.4
2
2
  Name: geek_cafe_saas_sdk
3
- Version: 0.7.5
3
+ Version: 0.8.0
4
4
  Summary: Base Reusable Services for SaaS
5
5
  Project-URL: Homepage, https://github.com/geekcafe/geek-cafe-services
6
6
  Project-URL: Documentation, https://github.com/geekcafe/geek-cafe-services/blob/main/README.md
@@ -87,20 +87,20 @@ Description-Content-Type: text/markdown
87
87
  <!-- COVERAGE-BADGE:START -->
88
88
  ## Test Coverage
89
89
 
90
- ![Tests](https://img.shields.io/badge/tests-1145%20passed-brightgreen)
91
- ![Coverage](https://img.shields.io/badge/coverage-83.1%25-green)
90
+ ![Tests](https://img.shields.io/badge/tests-1296%20passed-brightgreen)
91
+ ![Coverage](https://img.shields.io/badge/coverage-82.5%25-green)
92
92
 
93
- **Overall Coverage:** 83.1% (9646/11611 statements)
93
+ **Overall Coverage:** 82.5% (13150/15936 statements)
94
94
 
95
95
  ### Coverage Summary
96
96
 
97
97
  | Metric | Value |
98
98
  |--------|-------|
99
- | Total Statements | 11,611 |
100
- | Covered Statements | 9,646 |
101
- | Missing Statements | 1,965 |
102
- | Coverage Percentage | 83.1% |
103
- | Total Tests | 1145 |
99
+ | Total Statements | 15,936 |
100
+ | Covered Statements | 13,150 |
101
+ | Missing Statements | 2,786 |
102
+ | Coverage Percentage | 82.5% |
103
+ | Total Tests | 1296 |
104
104
  | Test Status | ✅ All Passing |
105
105
 
106
106
  ### Files Needing Attention (< 80% coverage)
@@ -108,17 +108,17 @@ Description-Content-Type: text/markdown
108
108
  | Coverage | Missing Lines | File |
109
109
  |----------|---------------|------|
110
110
  | 17.5% | 47 | `lambda_handlers/_base/authorized_secure_handler.py` |
111
- | 25.3% | 133 | `domains/communities/services/community_member_service.py` |
111
+ | 24.4% | 133 | `domains/communities/services/community_member_service.py` |
112
+ | 46.2% | 85 | `domains/auth/services/resource_permission_service.py` |
112
113
  | 46.3% | 58 | `domains/auth/models/role.py` |
113
114
  | 46.9% | 34 | `domains/auth/models/permission.py` |
114
- | 46.9% | 85 | `domains/auth/services/resource_permission_service.py` |
115
+ | 52.2% | 118 | `domains/tenancy/services/subscription_service.py` |
115
116
  | 56.2% | 7 | `lambda_handlers/_base/secure_handler.py` |
116
117
  | 58.5% | 17 | `domains/messaging/handlers/contact_threads/update/app.py` |
117
- | 62.3% | 118 | `utilities/lambda_event_utility.py` |
118
- | 63.4% | 86 | `domains/communities/services/community_service.py` |
119
- | 64.0% | 41 | `domains/files/services/s3_file_service.py` |
118
+ | 60.6% | 85 | `domains/notifications/services/notification_service.py` |
119
+ | 61.8% | 21 | `core/request_context.py` |
120
120
 
121
- *... and 22 more files with < 80% coverage*
121
+ *... and 27 more files with < 80% coverage*
122
122
 
123
123
  ### Running Tests
124
124
 
@@ -130,7 +130,7 @@ Description-Content-Type: text/markdown
130
130
  open reports/coverage/index.html
131
131
  ```
132
132
 
133
- *Last updated: 2025-10-16 13:22:11*
133
+ *Last updated: 2025-10-20 09:57:49*
134
134
 
135
135
  ---
136
136