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