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.
- geek_cafe_saas_sdk/__init__.py +9 -0
- geek_cafe_saas_sdk/core/__init__.py +11 -0
- geek_cafe_saas_sdk/core/audit_mixin.py +33 -0
- geek_cafe_saas_sdk/core/error_codes.py +132 -0
- geek_cafe_saas_sdk/core/service_errors.py +19 -0
- geek_cafe_saas_sdk/core/service_result.py +121 -0
- geek_cafe_saas_sdk/decorators/__init__.py +64 -0
- geek_cafe_saas_sdk/decorators/auth.py +373 -0
- geek_cafe_saas_sdk/decorators/core.py +358 -0
- geek_cafe_saas_sdk/domains/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/analytics/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics.py +219 -0
- geek_cafe_saas_sdk/domains/analytics/models/website_analytics_summary.py +220 -0
- geek_cafe_saas_sdk/domains/analytics/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_service.py +232 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_summary_service.py +212 -0
- geek_cafe_saas_sdk/domains/analytics/services/website_analytics_tally_service.py +610 -0
- geek_cafe_saas_sdk/domains/auth/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/auth/handlers/users/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/auth/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/auth/models/permission.py +134 -0
- geek_cafe_saas_sdk/domains/auth/models/resource_permission.py +245 -0
- geek_cafe_saas_sdk/domains/auth/models/role.py +213 -0
- geek_cafe_saas_sdk/domains/auth/models/user.py +285 -0
- geek_cafe_saas_sdk/domains/auth/services/__init__.py +16 -0
- geek_cafe_saas_sdk/domains/auth/services/authorization_service.py +376 -0
- geek_cafe_saas_sdk/domains/auth/services/permission_registry.py +464 -0
- geek_cafe_saas_sdk/domains/auth/services/resource_permission_service.py +408 -0
- geek_cafe_saas_sdk/domains/auth/services/user_service.py +274 -0
- geek_cafe_saas_sdk/domains/communities/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/create/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/list/app.py +36 -0
- geek_cafe_saas_sdk/domains/communities/handlers/communities/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/communities/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/models/community.py +326 -0
- geek_cafe_saas_sdk/domains/communities/models/community_member.py +227 -0
- geek_cafe_saas_sdk/domains/communities/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/communities/services/community_member_service.py +412 -0
- geek_cafe_saas_sdk/domains/communities/services/community_service.py +479 -0
- geek_cafe_saas_sdk/domains/events/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/events/handlers/attendees/app.py +67 -0
- geek_cafe_saas_sdk/domains/events/handlers/cancel/app.py +66 -0
- geek_cafe_saas_sdk/domains/events/handlers/check_in/app.py +60 -0
- geek_cafe_saas_sdk/domains/events/handlers/create/app.py +93 -0
- geek_cafe_saas_sdk/domains/events/handlers/delete/app.py +42 -0
- geek_cafe_saas_sdk/domains/events/handlers/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/events/handlers/invite/app.py +98 -0
- geek_cafe_saas_sdk/domains/events/handlers/list/app.py +125 -0
- geek_cafe_saas_sdk/domains/events/handlers/publish/app.py +49 -0
- geek_cafe_saas_sdk/domains/events/handlers/rsvp/app.py +83 -0
- geek_cafe_saas_sdk/domains/events/handlers/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/events/models/__init__.py +3 -0
- geek_cafe_saas_sdk/domains/events/models/event.py +681 -0
- geek_cafe_saas_sdk/domains/events/models/event_attendee.py +324 -0
- geek_cafe_saas_sdk/domains/events/services/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/events/services/event_attendee_service.py +571 -0
- geek_cafe_saas_sdk/domains/events/services/event_service.py +684 -0
- geek_cafe_saas_sdk/domains/files/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/models/directory.py +258 -0
- geek_cafe_saas_sdk/domains/files/models/file.py +312 -0
- geek_cafe_saas_sdk/domains/files/models/file_share.py +268 -0
- geek_cafe_saas_sdk/domains/files/models/file_version.py +216 -0
- geek_cafe_saas_sdk/domains/files/services/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/files/services/directory_service.py +701 -0
- geek_cafe_saas_sdk/domains/files/services/file_share_service.py +663 -0
- geek_cafe_saas_sdk/domains/files/services/file_system_service.py +575 -0
- geek_cafe_saas_sdk/domains/files/services/file_version_service.py +739 -0
- geek_cafe_saas_sdk/domains/files/services/s3_file_service.py +501 -0
- geek_cafe_saas_sdk/domains/messaging/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/create/app.py +86 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/list/app.py +97 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_channels/update/app.py +149 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/create/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/delete/app.py +65 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/get/app.py +64 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/list/app.py +102 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/chat_messages/update/app.py +127 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/create/app.py +94 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/delete/app.py +66 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/get/app.py +67 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/list/app.py +95 -0
- geek_cafe_saas_sdk/domains/messaging/handlers/contact_threads/update/app.py +156 -0
- geek_cafe_saas_sdk/domains/messaging/models/__init__.py +13 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel.py +337 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_channel_member.py +180 -0
- geek_cafe_saas_sdk/domains/messaging/models/chat_message.py +426 -0
- geek_cafe_saas_sdk/domains/messaging/models/contact_thread.py +392 -0
- geek_cafe_saas_sdk/domains/messaging/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_channel_service.py +700 -0
- geek_cafe_saas_sdk/domains/messaging/services/chat_message_service.py +491 -0
- geek_cafe_saas_sdk/domains/messaging/services/contact_thread_service.py +497 -0
- geek_cafe_saas_sdk/domains/tenancy/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/activate/app.py +52 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/active/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/cancel/app.py +55 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/list/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/subscriptions/record_payment/app.py +56 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/me/app.py +37 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/signup/app.py +61 -0
- geek_cafe_saas_sdk/domains/tenancy/handlers/tenants/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/tenancy/models/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/models/subscription.py +440 -0
- geek_cafe_saas_sdk/domains/tenancy/models/tenant.py +258 -0
- geek_cafe_saas_sdk/domains/tenancy/services/__init__.py +6 -0
- geek_cafe_saas_sdk/domains/tenancy/services/subscription_service.py +557 -0
- geek_cafe_saas_sdk/domains/tenancy/services/tenant_service.py +575 -0
- geek_cafe_saas_sdk/domains/voting/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/__init__.py +0 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/create/app.py +128 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/delete/app.py +41 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/get/app.py +39 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/list/app.py +38 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/summerize/README.md +3 -0
- geek_cafe_saas_sdk/domains/voting/handlers/votes/update/app.py +44 -0
- geek_cafe_saas_sdk/domains/voting/models/__init__.py +9 -0
- geek_cafe_saas_sdk/domains/voting/models/vote.py +231 -0
- geek_cafe_saas_sdk/domains/voting/models/vote_summary.py +193 -0
- geek_cafe_saas_sdk/domains/voting/services/__init__.py +11 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_service.py +264 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_summary_service.py +198 -0
- geek_cafe_saas_sdk/domains/voting/services/vote_tally_service.py +533 -0
- geek_cafe_saas_sdk/lambda_handlers/README.md +404 -0
- geek_cafe_saas_sdk/lambda_handlers/__init__.py +67 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/__init__.py +25 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/api_key_handler.py +129 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/authorized_secure_handler.py +218 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/base_handler.py +185 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/handler_factory.py +256 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/public_handler.py +53 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/secure_handler.py +89 -0
- geek_cafe_saas_sdk/lambda_handlers/_base/service_pool.py +94 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/create/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/delete/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/get/app.py +74 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/list/app.py +75 -0
- geek_cafe_saas_sdk/lambda_handlers/directories/move/app.py +79 -0
- geek_cafe_saas_sdk/lambda_handlers/files/delete/app.py +121 -0
- geek_cafe_saas_sdk/lambda_handlers/files/download/app.py +187 -0
- geek_cafe_saas_sdk/lambda_handlers/files/get/app.py +127 -0
- geek_cafe_saas_sdk/lambda_handlers/files/list/app.py +108 -0
- geek_cafe_saas_sdk/lambda_handlers/files/share/app.py +83 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/list/app.py +84 -0
- geek_cafe_saas_sdk/lambda_handlers/files/shares/revoke/app.py +76 -0
- geek_cafe_saas_sdk/lambda_handlers/files/update/app.py +143 -0
- geek_cafe_saas_sdk/lambda_handlers/files/upload/app.py +151 -0
- geek_cafe_saas_sdk/middleware/__init__.py +36 -0
- geek_cafe_saas_sdk/middleware/auth.py +85 -0
- geek_cafe_saas_sdk/middleware/authorization.py +523 -0
- geek_cafe_saas_sdk/middleware/cors.py +63 -0
- geek_cafe_saas_sdk/middleware/error_handling.py +114 -0
- geek_cafe_saas_sdk/middleware/validation.py +80 -0
- geek_cafe_saas_sdk/models/__init__.py +20 -0
- geek_cafe_saas_sdk/models/base_model.py +233 -0
- geek_cafe_saas_sdk/services/__init__.py +18 -0
- geek_cafe_saas_sdk/services/database_service.py +441 -0
- geek_cafe_saas_sdk/utilities/__init__.py +88 -0
- geek_cafe_saas_sdk/utilities/cognito_utility.py +568 -0
- geek_cafe_saas_sdk/utilities/custom_exceptions.py +183 -0
- geek_cafe_saas_sdk/utilities/datetime_utility.py +410 -0
- geek_cafe_saas_sdk/utilities/dictionary_utility.py +78 -0
- geek_cafe_saas_sdk/utilities/dynamodb_utils.py +151 -0
- geek_cafe_saas_sdk/utilities/environment_loader.py +149 -0
- geek_cafe_saas_sdk/utilities/environment_variables.py +228 -0
- geek_cafe_saas_sdk/utilities/http_body_parameters.py +44 -0
- geek_cafe_saas_sdk/utilities/http_path_parameters.py +60 -0
- geek_cafe_saas_sdk/utilities/http_status_code.py +63 -0
- geek_cafe_saas_sdk/utilities/jwt_utility.py +234 -0
- geek_cafe_saas_sdk/utilities/lambda_event_utility.py +776 -0
- geek_cafe_saas_sdk/utilities/logging_utility.py +64 -0
- geek_cafe_saas_sdk/utilities/message_query_helper.py +340 -0
- geek_cafe_saas_sdk/utilities/response.py +209 -0
- geek_cafe_saas_sdk/utilities/string_functions.py +180 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/METADATA +397 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/RECORD +194 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/WHEEL +4 -0
- geek_cafe_saas_sdk-0.6.0.dist-info/licenses/LICENSE +47 -0
|
@@ -0,0 +1,408 @@
|
|
|
1
|
+
# Resource Permission Service
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, List, Optional
|
|
4
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
5
|
+
from geek_cafe_saas_sdk.services.database_service import DatabaseService
|
|
6
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
7
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
|
|
8
|
+
from geek_cafe_saas_sdk.domains.auth.models import ResourcePermission
|
|
9
|
+
import datetime as dt
|
|
10
|
+
import time
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class ResourcePermissionService(DatabaseService[ResourcePermission]):
|
|
14
|
+
"""
|
|
15
|
+
Service for ResourcePermission operations (ABAC).
|
|
16
|
+
|
|
17
|
+
Manages resource-level permission grants for fine-grained sharing.
|
|
18
|
+
"""
|
|
19
|
+
|
|
20
|
+
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
21
|
+
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
22
|
+
|
|
23
|
+
# Required abstract methods from DatabaseService
|
|
24
|
+
def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[ResourcePermission]:
|
|
25
|
+
"""Create method - delegates to grant_permission()."""
|
|
26
|
+
required = ['resource_type', 'resource_id', 'permissions', 'grantee_user_id']
|
|
27
|
+
for field in required:
|
|
28
|
+
if field not in kwargs:
|
|
29
|
+
return ServiceResult.error_result(f"{field} is required", "VALIDATION_ERROR")
|
|
30
|
+
|
|
31
|
+
return self.grant_permission(
|
|
32
|
+
grantee_user_id=kwargs['grantee_user_id'],
|
|
33
|
+
tenant_id=tenant_id,
|
|
34
|
+
resource_type=kwargs['resource_type'],
|
|
35
|
+
resource_id=kwargs['resource_id'],
|
|
36
|
+
permissions=kwargs['permissions'],
|
|
37
|
+
granted_by=user_id,
|
|
38
|
+
reason=kwargs.get('reason'),
|
|
39
|
+
expires_at=kwargs.get('expires_at')
|
|
40
|
+
)
|
|
41
|
+
|
|
42
|
+
def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[ResourcePermission]:
|
|
43
|
+
"""Get a specific grant by ID."""
|
|
44
|
+
try:
|
|
45
|
+
grant = self._get_model_by_id(resource_id, ResourcePermission)
|
|
46
|
+
|
|
47
|
+
if not grant:
|
|
48
|
+
raise NotFoundError(f"Grant with ID {resource_id} not found")
|
|
49
|
+
|
|
50
|
+
# Validate tenant access
|
|
51
|
+
if grant.tenant_id != tenant_id:
|
|
52
|
+
raise AccessDeniedError("Access denied: different tenant")
|
|
53
|
+
|
|
54
|
+
return ServiceResult.success_result(grant)
|
|
55
|
+
|
|
56
|
+
except Exception as e:
|
|
57
|
+
return self._handle_service_exception(e, 'get_by_id', resource_id=resource_id)
|
|
58
|
+
|
|
59
|
+
def update(self, resource_id: str, tenant_id: str, user_id: str, updates: Dict[str, Any]) -> ServiceResult[ResourcePermission]:
|
|
60
|
+
"""Update a grant (e.g., change permissions or expiration)."""
|
|
61
|
+
try:
|
|
62
|
+
grant = self._get_model_by_id(resource_id, ResourcePermission)
|
|
63
|
+
|
|
64
|
+
if not grant:
|
|
65
|
+
raise NotFoundError(f"Grant with ID {resource_id} not found")
|
|
66
|
+
|
|
67
|
+
# Validate tenant access
|
|
68
|
+
if grant.tenant_id != tenant_id:
|
|
69
|
+
raise AccessDeniedError("Access denied: different tenant")
|
|
70
|
+
|
|
71
|
+
# Apply updates
|
|
72
|
+
if 'permissions' in updates:
|
|
73
|
+
grant.permissions = updates['permissions']
|
|
74
|
+
if 'expires_at' in updates:
|
|
75
|
+
grant.expires_at = updates['expires_at']
|
|
76
|
+
if 'reason' in updates:
|
|
77
|
+
grant.reason = updates['reason']
|
|
78
|
+
if 'metadata' in updates:
|
|
79
|
+
grant.metadata = updates['metadata']
|
|
80
|
+
|
|
81
|
+
grant.prep_for_save()
|
|
82
|
+
return self._save_model(grant)
|
|
83
|
+
|
|
84
|
+
except Exception as e:
|
|
85
|
+
return self._handle_service_exception(e, 'update', resource_id=resource_id)
|
|
86
|
+
|
|
87
|
+
def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
|
|
88
|
+
"""Revoke a grant."""
|
|
89
|
+
try:
|
|
90
|
+
grant = self._get_model_by_id(resource_id, ResourcePermission)
|
|
91
|
+
|
|
92
|
+
if not grant:
|
|
93
|
+
return ServiceResult.success_result(True) # Already revoked
|
|
94
|
+
|
|
95
|
+
# Validate tenant access
|
|
96
|
+
if grant.tenant_id != tenant_id:
|
|
97
|
+
raise AccessDeniedError("Access denied: different tenant")
|
|
98
|
+
|
|
99
|
+
# Hard delete (or could soft delete with deleted_at timestamp)
|
|
100
|
+
result = self._delete_model(grant)
|
|
101
|
+
|
|
102
|
+
if result.success:
|
|
103
|
+
return ServiceResult.success_result(True)
|
|
104
|
+
return result
|
|
105
|
+
|
|
106
|
+
except Exception as e:
|
|
107
|
+
return self._handle_service_exception(e, 'delete', resource_id=resource_id)
|
|
108
|
+
|
|
109
|
+
# Grant Management Methods
|
|
110
|
+
|
|
111
|
+
def grant_permission(
|
|
112
|
+
self,
|
|
113
|
+
grantee_user_id: str,
|
|
114
|
+
tenant_id: str,
|
|
115
|
+
resource_type: str,
|
|
116
|
+
resource_id: str,
|
|
117
|
+
permissions: List[str],
|
|
118
|
+
granted_by: str,
|
|
119
|
+
reason: Optional[str] = None,
|
|
120
|
+
expires_at: Optional[int] = None,
|
|
121
|
+
metadata: Optional[Dict[str, Any]] = None
|
|
122
|
+
) -> ServiceResult[ResourcePermission]:
|
|
123
|
+
"""
|
|
124
|
+
Grant resource-level permissions to a user.
|
|
125
|
+
|
|
126
|
+
Args:
|
|
127
|
+
grantee_user_id: User to grant permissions to
|
|
128
|
+
tenant_id: Tenant context
|
|
129
|
+
resource_type: Type of resource (event, community, chat_channel, etc.)
|
|
130
|
+
resource_id: ID of specific resource
|
|
131
|
+
permissions: List of permissions to grant (e.g., ["read", "write"])
|
|
132
|
+
granted_by: User ID granting the permission
|
|
133
|
+
reason: Optional reason for the grant
|
|
134
|
+
expires_at: Optional expiration timestamp
|
|
135
|
+
metadata: Optional additional metadata
|
|
136
|
+
"""
|
|
137
|
+
try:
|
|
138
|
+
# Validate inputs
|
|
139
|
+
if not permissions or len(permissions) == 0:
|
|
140
|
+
raise ValidationError("At least one permission is required")
|
|
141
|
+
|
|
142
|
+
# Check if grant already exists
|
|
143
|
+
existing = self.get_grant(grantee_user_id, resource_type, resource_id, tenant_id)
|
|
144
|
+
|
|
145
|
+
if existing.success and existing.data:
|
|
146
|
+
# Update existing grant
|
|
147
|
+
grant = existing.data
|
|
148
|
+
grant.permissions = permissions
|
|
149
|
+
grant.granted_by = granted_by
|
|
150
|
+
grant.granted_at = int(time.time())
|
|
151
|
+
grant.expires_at = expires_at
|
|
152
|
+
grant.reason = reason
|
|
153
|
+
if metadata:
|
|
154
|
+
grant.metadata = metadata
|
|
155
|
+
else:
|
|
156
|
+
# Create new grant
|
|
157
|
+
grant = ResourcePermission()
|
|
158
|
+
grant.user_id = grantee_user_id
|
|
159
|
+
grant.tenant_id = tenant_id
|
|
160
|
+
grant.resource_type = resource_type
|
|
161
|
+
grant.resource_id = resource_id
|
|
162
|
+
grant.permissions = permissions
|
|
163
|
+
grant.granted_by = granted_by
|
|
164
|
+
grant.granted_at = int(time.time())
|
|
165
|
+
grant.expires_at = expires_at
|
|
166
|
+
grant.reason = reason
|
|
167
|
+
if metadata:
|
|
168
|
+
grant.metadata = metadata
|
|
169
|
+
|
|
170
|
+
grant.prep_for_save()
|
|
171
|
+
return self._save_model(grant)
|
|
172
|
+
|
|
173
|
+
except Exception as e:
|
|
174
|
+
return self._handle_service_exception(
|
|
175
|
+
e, 'grant_permission',
|
|
176
|
+
user_id=grantee_user_id,
|
|
177
|
+
resource_type=resource_type,
|
|
178
|
+
resource_id=resource_id
|
|
179
|
+
)
|
|
180
|
+
|
|
181
|
+
def revoke_permission(
|
|
182
|
+
self,
|
|
183
|
+
grantee_user_id: str,
|
|
184
|
+
resource_type: str,
|
|
185
|
+
resource_id: str,
|
|
186
|
+
tenant_id: str
|
|
187
|
+
) -> ServiceResult[bool]:
|
|
188
|
+
"""
|
|
189
|
+
Revoke all permissions for a user on a resource.
|
|
190
|
+
|
|
191
|
+
Args:
|
|
192
|
+
grantee_user_id: User to revoke from
|
|
193
|
+
resource_type: Type of resource
|
|
194
|
+
resource_id: ID of resource
|
|
195
|
+
tenant_id: Tenant context
|
|
196
|
+
"""
|
|
197
|
+
try:
|
|
198
|
+
grant_result = self.get_grant(grantee_user_id, resource_type, resource_id, tenant_id)
|
|
199
|
+
|
|
200
|
+
if not grant_result.success or not grant_result.data:
|
|
201
|
+
return ServiceResult.success_result(True) # Already revoked
|
|
202
|
+
|
|
203
|
+
grant = grant_result.data
|
|
204
|
+
result = self._delete_model(grant)
|
|
205
|
+
|
|
206
|
+
if result.success:
|
|
207
|
+
return ServiceResult.success_result(True)
|
|
208
|
+
return result
|
|
209
|
+
|
|
210
|
+
except Exception as e:
|
|
211
|
+
return self._handle_service_exception(
|
|
212
|
+
e, 'revoke_permission',
|
|
213
|
+
user_id=grantee_user_id,
|
|
214
|
+
resource_type=resource_type,
|
|
215
|
+
resource_id=resource_id
|
|
216
|
+
)
|
|
217
|
+
|
|
218
|
+
def get_grant(
|
|
219
|
+
self,
|
|
220
|
+
user_id: str,
|
|
221
|
+
resource_type: str,
|
|
222
|
+
resource_id: str,
|
|
223
|
+
tenant_id: str
|
|
224
|
+
) -> ServiceResult[ResourcePermission]:
|
|
225
|
+
"""
|
|
226
|
+
Get a specific grant for user on resource.
|
|
227
|
+
|
|
228
|
+
Args:
|
|
229
|
+
user_id: User ID
|
|
230
|
+
resource_type: Type of resource
|
|
231
|
+
resource_id: ID of resource
|
|
232
|
+
tenant_id: Tenant context
|
|
233
|
+
"""
|
|
234
|
+
try:
|
|
235
|
+
# Query using GSI1 (user + resource)
|
|
236
|
+
temp = ResourcePermission()
|
|
237
|
+
temp.user_id = user_id
|
|
238
|
+
temp.resource_type = resource_type
|
|
239
|
+
temp.resource_id = resource_id
|
|
240
|
+
temp.tenant_id = tenant_id
|
|
241
|
+
|
|
242
|
+
result = self._query_by_index(temp, "gsi1", limit=1)
|
|
243
|
+
|
|
244
|
+
if not result.success:
|
|
245
|
+
return result
|
|
246
|
+
|
|
247
|
+
if not result.data or len(result.data) == 0:
|
|
248
|
+
return ServiceResult.error_result("Grant not found", "NOT_FOUND")
|
|
249
|
+
|
|
250
|
+
grant = result.data[0]
|
|
251
|
+
|
|
252
|
+
# Check if expired
|
|
253
|
+
if grant.expires_at and grant.expires_at < int(time.time()):
|
|
254
|
+
return ServiceResult.error_result("Grant has expired", "EXPIRED")
|
|
255
|
+
|
|
256
|
+
return ServiceResult.success_result(grant)
|
|
257
|
+
|
|
258
|
+
except Exception as e:
|
|
259
|
+
return self._handle_service_exception(
|
|
260
|
+
e, 'get_grant',
|
|
261
|
+
user_id=user_id,
|
|
262
|
+
resource_type=resource_type,
|
|
263
|
+
resource_id=resource_id
|
|
264
|
+
)
|
|
265
|
+
|
|
266
|
+
def list_user_grants(
|
|
267
|
+
self,
|
|
268
|
+
user_id: str,
|
|
269
|
+
tenant_id: str,
|
|
270
|
+
limit: int = 50
|
|
271
|
+
) -> ServiceResult[List[ResourcePermission]]:
|
|
272
|
+
"""
|
|
273
|
+
List all grants for a user.
|
|
274
|
+
|
|
275
|
+
Args:
|
|
276
|
+
user_id: User ID
|
|
277
|
+
tenant_id: Tenant context
|
|
278
|
+
limit: Max results
|
|
279
|
+
"""
|
|
280
|
+
try:
|
|
281
|
+
temp = ResourcePermission()
|
|
282
|
+
temp.user_id = user_id
|
|
283
|
+
temp.tenant_id = tenant_id
|
|
284
|
+
|
|
285
|
+
result = self._query_by_index(temp, "gsi2", limit=limit)
|
|
286
|
+
|
|
287
|
+
if not result.success:
|
|
288
|
+
return result
|
|
289
|
+
|
|
290
|
+
# Filter out expired grants
|
|
291
|
+
current_time = int(time.time())
|
|
292
|
+
active_grants = [
|
|
293
|
+
grant for grant in result.data
|
|
294
|
+
if not grant.expires_at or grant.expires_at > current_time
|
|
295
|
+
]
|
|
296
|
+
|
|
297
|
+
return ServiceResult.success_result(active_grants)
|
|
298
|
+
|
|
299
|
+
except Exception as e:
|
|
300
|
+
return self._handle_service_exception(
|
|
301
|
+
e, 'list_user_grants',
|
|
302
|
+
user_id=user_id
|
|
303
|
+
)
|
|
304
|
+
|
|
305
|
+
def list_resource_grants(
|
|
306
|
+
self,
|
|
307
|
+
resource_type: str,
|
|
308
|
+
resource_id: str,
|
|
309
|
+
tenant_id: str,
|
|
310
|
+
limit: int = 50
|
|
311
|
+
) -> ServiceResult[List[ResourcePermission]]:
|
|
312
|
+
"""
|
|
313
|
+
List all grants on a resource (who has access).
|
|
314
|
+
|
|
315
|
+
Args:
|
|
316
|
+
resource_type: Type of resource
|
|
317
|
+
resource_id: ID of resource
|
|
318
|
+
tenant_id: Tenant context
|
|
319
|
+
limit: Max results
|
|
320
|
+
"""
|
|
321
|
+
try:
|
|
322
|
+
temp = ResourcePermission()
|
|
323
|
+
temp.resource_type = resource_type
|
|
324
|
+
temp.resource_id = resource_id
|
|
325
|
+
temp.tenant_id = tenant_id
|
|
326
|
+
|
|
327
|
+
result = self._query_by_index(temp, "gsi3", limit=limit)
|
|
328
|
+
|
|
329
|
+
if not result.success:
|
|
330
|
+
return result
|
|
331
|
+
|
|
332
|
+
# Filter out expired grants
|
|
333
|
+
current_time = int(time.time())
|
|
334
|
+
active_grants = [
|
|
335
|
+
grant for grant in result.data
|
|
336
|
+
if not grant.expires_at or grant.expires_at > current_time
|
|
337
|
+
]
|
|
338
|
+
|
|
339
|
+
return ServiceResult.success_result(active_grants)
|
|
340
|
+
|
|
341
|
+
except Exception as e:
|
|
342
|
+
return self._handle_service_exception(
|
|
343
|
+
e, 'list_resource_grants',
|
|
344
|
+
resource_type=resource_type,
|
|
345
|
+
resource_id=resource_id
|
|
346
|
+
)
|
|
347
|
+
|
|
348
|
+
def get_user_permissions_on_resource(
|
|
349
|
+
self,
|
|
350
|
+
user_id: str,
|
|
351
|
+
resource_type: str,
|
|
352
|
+
resource_id: str,
|
|
353
|
+
tenant_id: str
|
|
354
|
+
) -> List[str]:
|
|
355
|
+
"""
|
|
356
|
+
Get list of permissions user has on a resource.
|
|
357
|
+
|
|
358
|
+
Args:
|
|
359
|
+
user_id: User ID
|
|
360
|
+
resource_type: Type of resource
|
|
361
|
+
resource_id: ID of resource
|
|
362
|
+
tenant_id: Tenant context
|
|
363
|
+
|
|
364
|
+
Returns:
|
|
365
|
+
List of permission strings (e.g., ["read", "write"])
|
|
366
|
+
"""
|
|
367
|
+
try:
|
|
368
|
+
grant_result = self.get_grant(user_id, resource_type, resource_id, tenant_id)
|
|
369
|
+
|
|
370
|
+
if grant_result.success and grant_result.data:
|
|
371
|
+
return grant_result.data.permissions
|
|
372
|
+
|
|
373
|
+
return []
|
|
374
|
+
|
|
375
|
+
except:
|
|
376
|
+
return []
|
|
377
|
+
|
|
378
|
+
def has_permission_on_resource(
|
|
379
|
+
self,
|
|
380
|
+
user_id: str,
|
|
381
|
+
resource_type: str,
|
|
382
|
+
resource_id: str,
|
|
383
|
+
permission: str,
|
|
384
|
+
tenant_id: str
|
|
385
|
+
) -> bool:
|
|
386
|
+
"""
|
|
387
|
+
Check if user has a specific permission on a resource.
|
|
388
|
+
|
|
389
|
+
Args:
|
|
390
|
+
user_id: User ID
|
|
391
|
+
resource_type: Type of resource
|
|
392
|
+
resource_id: ID of resource
|
|
393
|
+
permission: Permission to check (e.g., "write")
|
|
394
|
+
tenant_id: Tenant context
|
|
395
|
+
|
|
396
|
+
Returns:
|
|
397
|
+
True if user has permission, False otherwise
|
|
398
|
+
"""
|
|
399
|
+
try:
|
|
400
|
+
perms = self.get_user_permissions_on_resource(
|
|
401
|
+
user_id, resource_type, resource_id, tenant_id
|
|
402
|
+
)
|
|
403
|
+
|
|
404
|
+
# Check for wildcard or specific permission
|
|
405
|
+
return "*" in perms or permission in perms
|
|
406
|
+
|
|
407
|
+
except:
|
|
408
|
+
return False
|
|
@@ -0,0 +1,274 @@
|
|
|
1
|
+
# User Service
|
|
2
|
+
|
|
3
|
+
from typing import Dict, Any, Optional, List
|
|
4
|
+
from boto3_assist.dynamodb.dynamodb import DynamoDB
|
|
5
|
+
from geek_cafe_saas_sdk.services.database_service import DatabaseService
|
|
6
|
+
from geek_cafe_saas_sdk.core.service_result import ServiceResult
|
|
7
|
+
from geek_cafe_saas_sdk.core.service_errors import ValidationError, NotFoundError, AccessDeniedError
|
|
8
|
+
from geek_cafe_saas_sdk.utilities.dynamodb_utils import build_projection_with_reserved_keywords
|
|
9
|
+
from geek_cafe_saas_sdk.domains.auth.models import User
|
|
10
|
+
import datetime as dt
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
class UserService(DatabaseService[User]):
|
|
14
|
+
|
|
15
|
+
def __init__(self, *, dynamodb: DynamoDB = None, table_name: str = None):
|
|
16
|
+
super().__init__(dynamodb=dynamodb, table_name=table_name)
|
|
17
|
+
|
|
18
|
+
def create(self, tenant_id: str, user_id: str, **kwargs) -> ServiceResult[User]:
|
|
19
|
+
"""Create a new user."""
|
|
20
|
+
try:
|
|
21
|
+
# Validate required fields
|
|
22
|
+
required_fields = ['email', 'first_name', 'last_name']
|
|
23
|
+
self._validate_required_fields(kwargs, required_fields)
|
|
24
|
+
|
|
25
|
+
# Create user instance using map() approach
|
|
26
|
+
user = User().map(kwargs)
|
|
27
|
+
user.tenant_id = tenant_id
|
|
28
|
+
user.user_id = user_id # Set creator
|
|
29
|
+
user.created_by_id = user_id
|
|
30
|
+
|
|
31
|
+
# Prepare for save (sets ID and timestamps)
|
|
32
|
+
user.prep_for_save()
|
|
33
|
+
|
|
34
|
+
# Save to database
|
|
35
|
+
return self._save_model(user)
|
|
36
|
+
|
|
37
|
+
except Exception as e:
|
|
38
|
+
return self._handle_service_exception(e, 'create_user', tenant_id=tenant_id, user_id=user_id)
|
|
39
|
+
|
|
40
|
+
def get_by_id(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[User]:
|
|
41
|
+
"""Get user by ID with access control."""
|
|
42
|
+
try:
|
|
43
|
+
user = self._get_model_by_id(resource_id, User)
|
|
44
|
+
|
|
45
|
+
if not user:
|
|
46
|
+
raise NotFoundError(f"User with ID {resource_id} not found")
|
|
47
|
+
|
|
48
|
+
# Check if deleted
|
|
49
|
+
if user.is_deleted():
|
|
50
|
+
raise NotFoundError(f"User with ID {resource_id} not found")
|
|
51
|
+
|
|
52
|
+
# Validate tenant access
|
|
53
|
+
if hasattr(user, 'tenant_id'):
|
|
54
|
+
self._validate_tenant_access(user.tenant_id, tenant_id)
|
|
55
|
+
|
|
56
|
+
return ServiceResult.success_result(user)
|
|
57
|
+
|
|
58
|
+
except Exception as e:
|
|
59
|
+
return self._handle_service_exception(e, 'get_user', resource_id=resource_id, tenant_id=tenant_id)
|
|
60
|
+
|
|
61
|
+
def get_by_email(self, email: str, tenant_id: str, user_id: str) -> ServiceResult[User]:
|
|
62
|
+
"""Get user by email using GSI1."""
|
|
63
|
+
try:
|
|
64
|
+
# Create a temporary user instance to get the GSI key
|
|
65
|
+
temp_user = User()
|
|
66
|
+
temp_user.email = email
|
|
67
|
+
|
|
68
|
+
result = self._query_by_index(
|
|
69
|
+
temp_user,
|
|
70
|
+
"gsi1",
|
|
71
|
+
ascending=False
|
|
72
|
+
)
|
|
73
|
+
|
|
74
|
+
if not result.success or not result.data:
|
|
75
|
+
raise NotFoundError(f"User with email {email} not found")
|
|
76
|
+
|
|
77
|
+
# Get the first (most recent) result
|
|
78
|
+
user = result.data[0]
|
|
79
|
+
|
|
80
|
+
# Check if deleted
|
|
81
|
+
if user.is_deleted():
|
|
82
|
+
raise NotFoundError(f"User with email {email} not found")
|
|
83
|
+
|
|
84
|
+
# Validate tenant access
|
|
85
|
+
self._validate_tenant_access(user.tenant_id, tenant_id)
|
|
86
|
+
|
|
87
|
+
return ServiceResult.success_result(user)
|
|
88
|
+
|
|
89
|
+
except Exception as e:
|
|
90
|
+
return self._handle_service_exception(e, 'get_user_by_email', email=email, tenant_id=tenant_id)
|
|
91
|
+
|
|
92
|
+
def get_users_by_tenant(self, tenant_id: str, user_id: str, limit: int = 50) -> ServiceResult[List[User]]:
|
|
93
|
+
"""Get all users for a tenant using GSI2."""
|
|
94
|
+
try:
|
|
95
|
+
# Create a temporary user instance to get the GSI key
|
|
96
|
+
temp_user = User()
|
|
97
|
+
temp_user.tenant_id = tenant_id
|
|
98
|
+
|
|
99
|
+
result = self._query_by_index(
|
|
100
|
+
temp_user,
|
|
101
|
+
"gsi2",
|
|
102
|
+
ascending=False, # Most recent first
|
|
103
|
+
limit=limit
|
|
104
|
+
)
|
|
105
|
+
|
|
106
|
+
if not result.success:
|
|
107
|
+
return result
|
|
108
|
+
|
|
109
|
+
# Filter out deleted users and validate tenant access
|
|
110
|
+
active_users = []
|
|
111
|
+
for user in result.data:
|
|
112
|
+
if not user.is_deleted() and user.tenant_id == tenant_id:
|
|
113
|
+
active_users.append(user)
|
|
114
|
+
|
|
115
|
+
return ServiceResult.success_result(active_users)
|
|
116
|
+
|
|
117
|
+
except Exception as e:
|
|
118
|
+
return self._handle_service_exception(e, 'get_users_by_tenant', tenant_id=tenant_id)
|
|
119
|
+
|
|
120
|
+
def get_users_by_role(self, role: str, tenant_id: str, user_id: str, limit: int = 50) -> ServiceResult[List[User]]:
|
|
121
|
+
"""Get users by role within a tenant using GSI3."""
|
|
122
|
+
try:
|
|
123
|
+
# Create a temporary user instance to get the GSI key
|
|
124
|
+
temp_user = User()
|
|
125
|
+
temp_user._roles = [role] # Set the primary role
|
|
126
|
+
|
|
127
|
+
result = self._query_by_index(
|
|
128
|
+
temp_user,
|
|
129
|
+
"gsi3",
|
|
130
|
+
ascending=False, # Most recent first
|
|
131
|
+
limit=limit
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
if not result.success:
|
|
135
|
+
return result
|
|
136
|
+
|
|
137
|
+
# Filter out deleted users and validate tenant access
|
|
138
|
+
active_users = []
|
|
139
|
+
for user in result.data:
|
|
140
|
+
if not user.is_deleted() and user.tenant_id == tenant_id and user.has_role(role):
|
|
141
|
+
active_users.append(user)
|
|
142
|
+
|
|
143
|
+
return ServiceResult.success_result(active_users)
|
|
144
|
+
|
|
145
|
+
except Exception as e:
|
|
146
|
+
return self._handle_service_exception(e, 'get_users_by_role', role=role, tenant_id=tenant_id)
|
|
147
|
+
|
|
148
|
+
def restore_user(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[User]:
|
|
149
|
+
"""Restore a soft-deleted user (admin only)."""
|
|
150
|
+
try:
|
|
151
|
+
# Check permissions (admin only)
|
|
152
|
+
if not self._is_admin_user(user_id, tenant_id):
|
|
153
|
+
raise AccessDeniedError("Access denied: insufficient permissions")
|
|
154
|
+
|
|
155
|
+
# Get existing user (even if deleted)
|
|
156
|
+
user = self._get_model_by_id(resource_id, User)
|
|
157
|
+
|
|
158
|
+
if not user:
|
|
159
|
+
raise NotFoundError(f"User with ID {resource_id} not found")
|
|
160
|
+
|
|
161
|
+
# Validate tenant access
|
|
162
|
+
if hasattr(user, 'tenant_id'):
|
|
163
|
+
self._validate_tenant_access(user.tenant_id, tenant_id)
|
|
164
|
+
|
|
165
|
+
# Check if actually deleted
|
|
166
|
+
if not user.is_deleted():
|
|
167
|
+
return ServiceResult.success_result(user) # Already active
|
|
168
|
+
|
|
169
|
+
# Restore: clear deleted timestamp and metadata
|
|
170
|
+
user.deleted_utc_ts = None
|
|
171
|
+
user.deleted_by_id = None
|
|
172
|
+
user.updated_by_id = user_id
|
|
173
|
+
user.prep_for_save() # Updates timestamp
|
|
174
|
+
|
|
175
|
+
# Save the restored user
|
|
176
|
+
save_result = self._save_model(user)
|
|
177
|
+
if save_result.success:
|
|
178
|
+
return ServiceResult.success_result(user)
|
|
179
|
+
else:
|
|
180
|
+
return save_result
|
|
181
|
+
|
|
182
|
+
except Exception as e:
|
|
183
|
+
return self._handle_service_exception(e, 'restore_user', resource_id=resource_id, tenant_id=tenant_id)
|
|
184
|
+
|
|
185
|
+
def update(self, resource_id: str, tenant_id: str, user_id: str,
|
|
186
|
+
updates: Dict[str, Any]) -> ServiceResult[User]:
|
|
187
|
+
try:
|
|
188
|
+
# Get existing user
|
|
189
|
+
user = self._get_model_by_id(resource_id, User)
|
|
190
|
+
|
|
191
|
+
if not user:
|
|
192
|
+
raise NotFoundError(f"User with ID {resource_id} not found")
|
|
193
|
+
|
|
194
|
+
# Validate tenant access
|
|
195
|
+
if hasattr(user, 'tenant_id'):
|
|
196
|
+
self._validate_tenant_access(user.tenant_id, tenant_id)
|
|
197
|
+
|
|
198
|
+
# Check if user can update (admin or self)
|
|
199
|
+
if not (user_id == resource_id or self._is_admin_user(user_id, tenant_id)):
|
|
200
|
+
raise AccessDeniedError("Access denied: insufficient permissions")
|
|
201
|
+
|
|
202
|
+
# Prevent non-admins from updating roles
|
|
203
|
+
if 'roles' in updates and not self._is_admin_user(user_id, tenant_id):
|
|
204
|
+
raise AccessDeniedError("Access denied: only admins can update roles")
|
|
205
|
+
|
|
206
|
+
# Apply updates
|
|
207
|
+
for field, value in updates.items():
|
|
208
|
+
if hasattr(user, field) and field not in ['id', 'created_utc_ts', 'tenant_id', 'organizer_id']:
|
|
209
|
+
if field == 'email':
|
|
210
|
+
user.email = value
|
|
211
|
+
elif field == 'first_name':
|
|
212
|
+
user.first_name = value
|
|
213
|
+
elif field == 'last_name':
|
|
214
|
+
user.last_name = value
|
|
215
|
+
elif field == 'roles':
|
|
216
|
+
user.roles = value
|
|
217
|
+
elif field == 'avatar':
|
|
218
|
+
user.avatar = value
|
|
219
|
+
|
|
220
|
+
# Update metadata
|
|
221
|
+
user.updated_by_id = user_id
|
|
222
|
+
user.prep_for_save() # Updates timestamp
|
|
223
|
+
|
|
224
|
+
# Save updated user
|
|
225
|
+
return self._save_model(user)
|
|
226
|
+
|
|
227
|
+
except Exception as e:
|
|
228
|
+
return self._handle_service_exception(e, 'update_user', resource_id=resource_id, tenant_id=tenant_id)
|
|
229
|
+
|
|
230
|
+
def delete(self, resource_id: str, tenant_id: str, user_id: str) -> ServiceResult[bool]:
|
|
231
|
+
"""Soft delete user with access control."""
|
|
232
|
+
try:
|
|
233
|
+
# Get existing user
|
|
234
|
+
user = self._get_model_by_id(resource_id, User)
|
|
235
|
+
|
|
236
|
+
if not user:
|
|
237
|
+
raise NotFoundError(f"User with ID {resource_id} not found")
|
|
238
|
+
|
|
239
|
+
# Check if already deleted
|
|
240
|
+
if user.is_deleted():
|
|
241
|
+
return ServiceResult.success_result(True)
|
|
242
|
+
|
|
243
|
+
# Validate tenant access
|
|
244
|
+
if hasattr(user, 'tenant_id'):
|
|
245
|
+
self._validate_tenant_access(user.tenant_id, tenant_id)
|
|
246
|
+
|
|
247
|
+
# Check permissions (admin or self)
|
|
248
|
+
if not (user_id == resource_id or self._is_admin_user(user_id, tenant_id)):
|
|
249
|
+
raise AccessDeniedError("Access denied: insufficient permissions")
|
|
250
|
+
|
|
251
|
+
# Prevent deleting self
|
|
252
|
+
if user_id == resource_id:
|
|
253
|
+
raise ValidationError("Cannot delete your own account")
|
|
254
|
+
|
|
255
|
+
# Soft delete: set deleted timestamp and metadata
|
|
256
|
+
user.deleted_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
257
|
+
user.deleted_by_id = user_id
|
|
258
|
+
user.prep_for_save() # Updates timestamp
|
|
259
|
+
|
|
260
|
+
# Save the updated user
|
|
261
|
+
save_result = self._save_model(user)
|
|
262
|
+
if save_result.success:
|
|
263
|
+
return ServiceResult.success_result(True)
|
|
264
|
+
else:
|
|
265
|
+
return save_result
|
|
266
|
+
|
|
267
|
+
except Exception as e:
|
|
268
|
+
return self._handle_service_exception(e, 'delete_user', resource_id=resource_id, tenant_id=tenant_id)
|
|
269
|
+
|
|
270
|
+
def _is_admin_user(self, user_id: str, tenant_id: str) -> bool:
|
|
271
|
+
"""Check if user has admin role (placeholder - will be implemented when UserService is available)."""
|
|
272
|
+
# For now, assume no admin privileges
|
|
273
|
+
# This will be enhanced when we have user service integration
|
|
274
|
+
return False
|
|
File without changes
|
|
File without changes
|