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,213 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Copyright 2024-2025 Geek Cafe, LLC
|
|
3
|
+
MIT License. See Project Root for the license information.
|
|
4
|
+
|
|
5
|
+
Role model for RBAC (Role-Based Access Control).
|
|
6
|
+
Roles aggregate permissions and can be assigned to users.
|
|
7
|
+
"""
|
|
8
|
+
|
|
9
|
+
from typing import List, Dict, Any
|
|
10
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
11
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
class Role(BaseModel):
|
|
15
|
+
"""
|
|
16
|
+
Role definition model.
|
|
17
|
+
|
|
18
|
+
Groups permissions together for easy assignment to users.
|
|
19
|
+
Supports hierarchical roles (platform vs tenant-level).
|
|
20
|
+
|
|
21
|
+
Examples:
|
|
22
|
+
- platform_admin: Full system access
|
|
23
|
+
- tenant_admin: Full access within tenant
|
|
24
|
+
- tenant_user: Standard user permissions
|
|
25
|
+
- tenant_organizer: Enhanced event management
|
|
26
|
+
|
|
27
|
+
Access Patterns:
|
|
28
|
+
- Get role by code (primary key)
|
|
29
|
+
- List all roles (scan/query all)
|
|
30
|
+
- List roles by scope (GSI1: global vs tenant)
|
|
31
|
+
- List roles by tenant (GSI2: tenant-specific custom roles)
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
def __init__(self):
|
|
35
|
+
super().__init__()
|
|
36
|
+
|
|
37
|
+
# Core fields
|
|
38
|
+
self._code: str | None = None # Unique code: "tenant_admin"
|
|
39
|
+
self._name: str | None = None # Display name: "Tenant Administrator"
|
|
40
|
+
self._description: str | None = None
|
|
41
|
+
|
|
42
|
+
# Permissions
|
|
43
|
+
self._permissions: List[str] = [] # List of permission codes
|
|
44
|
+
|
|
45
|
+
# Scope
|
|
46
|
+
self._scope: str = "tenant" # "global" or "tenant"
|
|
47
|
+
self._tenant_id: str | None = None # If tenant-specific custom role
|
|
48
|
+
|
|
49
|
+
# Hierarchy
|
|
50
|
+
self._inherits_from: List[str] = [] # Role codes to inherit from
|
|
51
|
+
self._level: int = 0 # Hierarchical level (higher = more power)
|
|
52
|
+
|
|
53
|
+
# Metadata
|
|
54
|
+
self._is_system: bool = True # System roles can't be deleted
|
|
55
|
+
self._is_assignable: bool = True # Can be assigned to users
|
|
56
|
+
self._metadata: Dict[str, Any] = {}
|
|
57
|
+
|
|
58
|
+
self._setup_indexes()
|
|
59
|
+
|
|
60
|
+
def _setup_indexes(self):
|
|
61
|
+
"""Setup DynamoDB indexes for role queries."""
|
|
62
|
+
|
|
63
|
+
# Primary index: role by code
|
|
64
|
+
primary: DynamoDBIndex = DynamoDBIndex()
|
|
65
|
+
primary.name = "primary"
|
|
66
|
+
primary.partition_key.attribute_name = "pk"
|
|
67
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
68
|
+
("role", self.code)
|
|
69
|
+
)
|
|
70
|
+
primary.sort_key.attribute_name = "sk"
|
|
71
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
72
|
+
("role", self.code)
|
|
73
|
+
)
|
|
74
|
+
self.indexes.add_primary(primary)
|
|
75
|
+
|
|
76
|
+
# GSI1: Roles by scope (global vs tenant)
|
|
77
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
78
|
+
gsi.name = "gsi1"
|
|
79
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
80
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
81
|
+
("role_scope", self.scope)
|
|
82
|
+
)
|
|
83
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
84
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
85
|
+
("level", str(self.level).zfill(5)),
|
|
86
|
+
("code", self.code)
|
|
87
|
+
)
|
|
88
|
+
self.indexes.add_secondary(gsi)
|
|
89
|
+
|
|
90
|
+
# GSI2: Custom roles by tenant
|
|
91
|
+
gsi: DynamoDBIndex = DynamoDBIndex()
|
|
92
|
+
gsi.name = "gsi2"
|
|
93
|
+
gsi.partition_key.attribute_name = f"{gsi.name}_pk"
|
|
94
|
+
gsi.partition_key.value = lambda: DynamoDBKey.build_key(
|
|
95
|
+
("tenant", self.tenant_id or "system")
|
|
96
|
+
)
|
|
97
|
+
gsi.sort_key.attribute_name = f"{gsi.name}_sk"
|
|
98
|
+
gsi.sort_key.value = lambda: DynamoDBKey.build_key(
|
|
99
|
+
("role", self.code)
|
|
100
|
+
)
|
|
101
|
+
self.indexes.add_secondary(gsi)
|
|
102
|
+
|
|
103
|
+
# Code
|
|
104
|
+
@property
|
|
105
|
+
def code(self) -> str | None:
|
|
106
|
+
"""Role code (e.g., 'tenant_admin')."""
|
|
107
|
+
return self._code
|
|
108
|
+
|
|
109
|
+
@code.setter
|
|
110
|
+
def code(self, value: str | None):
|
|
111
|
+
self._code = value
|
|
112
|
+
|
|
113
|
+
# Name
|
|
114
|
+
@property
|
|
115
|
+
def name(self) -> str | None:
|
|
116
|
+
"""Display name."""
|
|
117
|
+
return self._name
|
|
118
|
+
|
|
119
|
+
@name.setter
|
|
120
|
+
def name(self, value: str | None):
|
|
121
|
+
self._name = value
|
|
122
|
+
|
|
123
|
+
# Description
|
|
124
|
+
@property
|
|
125
|
+
def description(self) -> str | None:
|
|
126
|
+
"""Role description."""
|
|
127
|
+
return self._description
|
|
128
|
+
|
|
129
|
+
@description.setter
|
|
130
|
+
def description(self, value: str | None):
|
|
131
|
+
self._description = value
|
|
132
|
+
|
|
133
|
+
# Permissions
|
|
134
|
+
@property
|
|
135
|
+
def permissions(self) -> List[str]:
|
|
136
|
+
"""List of permission codes."""
|
|
137
|
+
return self._permissions
|
|
138
|
+
|
|
139
|
+
@permissions.setter
|
|
140
|
+
def permissions(self, value: List[str]):
|
|
141
|
+
self._permissions = value if value else []
|
|
142
|
+
|
|
143
|
+
# Scope
|
|
144
|
+
@property
|
|
145
|
+
def scope(self) -> str:
|
|
146
|
+
"""Role scope: 'global' or 'tenant'."""
|
|
147
|
+
return self._scope
|
|
148
|
+
|
|
149
|
+
@scope.setter
|
|
150
|
+
def scope(self, value: str):
|
|
151
|
+
if value not in ["global", "tenant"]:
|
|
152
|
+
raise ValueError("Scope must be 'global' or 'tenant'")
|
|
153
|
+
self._scope = value
|
|
154
|
+
|
|
155
|
+
# Tenant ID
|
|
156
|
+
@property
|
|
157
|
+
def tenant_id(self) -> str | None:
|
|
158
|
+
"""Tenant ID for tenant-specific custom roles."""
|
|
159
|
+
return self._tenant_id
|
|
160
|
+
|
|
161
|
+
@tenant_id.setter
|
|
162
|
+
def tenant_id(self, value: str | None):
|
|
163
|
+
self._tenant_id = value
|
|
164
|
+
|
|
165
|
+
# Inherits From
|
|
166
|
+
@property
|
|
167
|
+
def inherits_from(self) -> List[str]:
|
|
168
|
+
"""Role codes this role inherits from."""
|
|
169
|
+
return self._inherits_from
|
|
170
|
+
|
|
171
|
+
@inherits_from.setter
|
|
172
|
+
def inherits_from(self, value: List[str]):
|
|
173
|
+
self._inherits_from = value if value else []
|
|
174
|
+
|
|
175
|
+
# Level
|
|
176
|
+
@property
|
|
177
|
+
def level(self) -> int:
|
|
178
|
+
"""Hierarchical level (higher = more power)."""
|
|
179
|
+
return self._level
|
|
180
|
+
|
|
181
|
+
@level.setter
|
|
182
|
+
def level(self, value: int):
|
|
183
|
+
self._level = value
|
|
184
|
+
|
|
185
|
+
# Is System
|
|
186
|
+
@property
|
|
187
|
+
def is_system(self) -> bool:
|
|
188
|
+
"""Whether this is a system role (cannot be deleted)."""
|
|
189
|
+
return self._is_system
|
|
190
|
+
|
|
191
|
+
@is_system.setter
|
|
192
|
+
def is_system(self, value: bool):
|
|
193
|
+
self._is_system = value
|
|
194
|
+
|
|
195
|
+
# Is Assignable
|
|
196
|
+
@property
|
|
197
|
+
def is_assignable(self) -> bool:
|
|
198
|
+
"""Whether this role can be assigned to users."""
|
|
199
|
+
return self._is_assignable
|
|
200
|
+
|
|
201
|
+
@is_assignable.setter
|
|
202
|
+
def is_assignable(self, value: bool):
|
|
203
|
+
self._is_assignable = value
|
|
204
|
+
|
|
205
|
+
# Metadata
|
|
206
|
+
@property
|
|
207
|
+
def metadata(self) -> Dict[str, Any]:
|
|
208
|
+
"""Additional metadata."""
|
|
209
|
+
return self._metadata
|
|
210
|
+
|
|
211
|
+
@metadata.setter
|
|
212
|
+
def metadata(self, value: Dict[str, Any]):
|
|
213
|
+
self._metadata = value if value else {}
|
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
from boto3_assist.dynamodb.dynamodb_index import DynamoDBIndex, DynamoDBKey
|
|
2
|
+
from boto3_assist.utilities.string_utility import StringUtility
|
|
3
|
+
import datetime as dt
|
|
4
|
+
from typing import List, Optional, Dict, Any
|
|
5
|
+
from geek_cafe_saas_sdk.models.base_model import BaseModel
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
class User(BaseModel):
|
|
9
|
+
"""
|
|
10
|
+
User model for event scheduling system.
|
|
11
|
+
|
|
12
|
+
Represents users with roles, tenant association, and profile information.
|
|
13
|
+
"""
|
|
14
|
+
|
|
15
|
+
def __init__(self):
|
|
16
|
+
super().__init__()
|
|
17
|
+
self._email: str | None = None
|
|
18
|
+
self._first_name: str | None = None
|
|
19
|
+
self._last_name: str | None = None
|
|
20
|
+
self._roles: List[str] = ["tenant_user"]
|
|
21
|
+
self._avatar: str | None = None
|
|
22
|
+
|
|
23
|
+
# Cognito integration
|
|
24
|
+
self._cognito_user_name: str | None = None
|
|
25
|
+
|
|
26
|
+
# User status and lifecycle
|
|
27
|
+
self._status: str = "active" # active|invited|disabled
|
|
28
|
+
self._invited_utc_ts: float | None = None
|
|
29
|
+
self._activated_utc_ts: float | None = None
|
|
30
|
+
self._disabled_utc_ts: float | None = None
|
|
31
|
+
|
|
32
|
+
self._setup_indexes()
|
|
33
|
+
|
|
34
|
+
def _setup_indexes(self):
|
|
35
|
+
"""Setup DynamoDB indexes for user queries."""
|
|
36
|
+
|
|
37
|
+
# Primary index: users by ID
|
|
38
|
+
primary: DynamoDBIndex = DynamoDBIndex()
|
|
39
|
+
primary.name = "primary"
|
|
40
|
+
primary.partition_key.attribute_name = "pk"
|
|
41
|
+
primary.partition_key.value = lambda: DynamoDBKey.build_key(("user", self.id))
|
|
42
|
+
primary.sort_key.attribute_name = "sk"
|
|
43
|
+
primary.sort_key.value = lambda: DynamoDBKey.build_key(("user", self.id))
|
|
44
|
+
self.indexes.add_primary(primary)
|
|
45
|
+
|
|
46
|
+
## GSI: 1 - Users by email (for uniqueness and login)
|
|
47
|
+
gsi1: DynamoDBIndex = DynamoDBIndex()
|
|
48
|
+
gsi1.name = "gsi1"
|
|
49
|
+
gsi1.partition_key.attribute_name = f"{gsi1.name}_pk"
|
|
50
|
+
gsi1.partition_key.value = lambda: DynamoDBKey.build_key(("email", self.email))
|
|
51
|
+
gsi1.sort_key.attribute_name = f"{gsi1.name}_sk"
|
|
52
|
+
gsi1.sort_key.value = lambda: DynamoDBKey.build_key(("email", self.email))
|
|
53
|
+
self.indexes.add_secondary(gsi1)
|
|
54
|
+
|
|
55
|
+
## GSI: 2 - Users by tenant
|
|
56
|
+
gsi2: DynamoDBIndex = DynamoDBIndex()
|
|
57
|
+
gsi2.name = "gsi2"
|
|
58
|
+
gsi2.partition_key.attribute_name = f"{gsi2.name}_pk"
|
|
59
|
+
gsi2.partition_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id))
|
|
60
|
+
gsi2.sort_key.attribute_name = f"{gsi2.name}_sk"
|
|
61
|
+
gsi2.sort_key.value = lambda: DynamoDBKey.build_key(("ts", self.created_utc_ts))
|
|
62
|
+
self.indexes.add_secondary(gsi2)
|
|
63
|
+
|
|
64
|
+
## GSI: 3 - Users by role (for admin queries)
|
|
65
|
+
gsi3: DynamoDBIndex = DynamoDBIndex()
|
|
66
|
+
gsi3.name = "gsi3"
|
|
67
|
+
gsi3.partition_key.attribute_name = f"{gsi3.name}_pk"
|
|
68
|
+
gsi3.partition_key.value = lambda: DynamoDBKey.build_key(("role", self.primary_role))
|
|
69
|
+
gsi3.sort_key.attribute_name = f"{gsi3.name}_sk"
|
|
70
|
+
gsi3.sort_key.value = lambda: DynamoDBKey.build_key(("tenant", self.tenant_id), ("ts", self.created_utc_ts))
|
|
71
|
+
self.indexes.add_secondary(gsi3)
|
|
72
|
+
|
|
73
|
+
## GSI: 4 - All users (for admin listing)
|
|
74
|
+
gsi4: DynamoDBIndex = DynamoDBIndex()
|
|
75
|
+
gsi4.name = "gsi4"
|
|
76
|
+
gsi4.partition_key.attribute_name = f"{gsi4.name}_pk"
|
|
77
|
+
gsi4.partition_key.value = lambda: DynamoDBKey.build_key(("user", "all"))
|
|
78
|
+
gsi4.sort_key.attribute_name = f"{gsi4.name}_sk"
|
|
79
|
+
gsi4.sort_key.value = lambda: DynamoDBKey.build_key(("ts", self.created_utc_ts))
|
|
80
|
+
self.indexes.add_secondary(gsi4)
|
|
81
|
+
|
|
82
|
+
@property
|
|
83
|
+
def email(self) -> str | None:
|
|
84
|
+
"""User's email address."""
|
|
85
|
+
return self._email
|
|
86
|
+
|
|
87
|
+
@email.setter
|
|
88
|
+
def email(self, value: str | None):
|
|
89
|
+
"""Set email and ensure it's lowercase."""
|
|
90
|
+
if value:
|
|
91
|
+
self._email = value.lower()
|
|
92
|
+
else:
|
|
93
|
+
self._email = value
|
|
94
|
+
|
|
95
|
+
@property
|
|
96
|
+
def first_name(self) -> str | None:
|
|
97
|
+
"""User's first name."""
|
|
98
|
+
return self._first_name
|
|
99
|
+
|
|
100
|
+
@first_name.setter
|
|
101
|
+
def first_name(self, value: str | None):
|
|
102
|
+
self._first_name = value
|
|
103
|
+
|
|
104
|
+
@property
|
|
105
|
+
def last_name(self) -> str | None:
|
|
106
|
+
"""User's last name."""
|
|
107
|
+
return self._last_name
|
|
108
|
+
|
|
109
|
+
@last_name.setter
|
|
110
|
+
def last_name(self, value: str | None):
|
|
111
|
+
self._last_name = value
|
|
112
|
+
|
|
113
|
+
@property
|
|
114
|
+
def full_name(self) -> str:
|
|
115
|
+
"""User's full name."""
|
|
116
|
+
if self.first_name and self.last_name:
|
|
117
|
+
return f"{self.first_name} {self.last_name}"
|
|
118
|
+
elif self.first_name:
|
|
119
|
+
return self.first_name
|
|
120
|
+
elif self.last_name:
|
|
121
|
+
return self.last_name
|
|
122
|
+
return ""
|
|
123
|
+
|
|
124
|
+
@property
|
|
125
|
+
def roles(self) -> List[str]:
|
|
126
|
+
"""User's roles."""
|
|
127
|
+
return self._roles
|
|
128
|
+
|
|
129
|
+
@roles.setter
|
|
130
|
+
def roles(self, value: List[str]):
|
|
131
|
+
"""Set roles, ensuring it's always a list."""
|
|
132
|
+
if value is None:
|
|
133
|
+
self._roles = ["tenant_user"]
|
|
134
|
+
elif isinstance(value, list):
|
|
135
|
+
self._roles = value if value else ["tenant_user"]
|
|
136
|
+
else:
|
|
137
|
+
self._roles = ["tenant_user"]
|
|
138
|
+
|
|
139
|
+
@property
|
|
140
|
+
def primary_role(self) -> str:
|
|
141
|
+
"""Primary role (first in the list, or 'tenant_user' if empty)."""
|
|
142
|
+
return self._roles[0] if self._roles else "tenant_user"
|
|
143
|
+
|
|
144
|
+
@property
|
|
145
|
+
def is_admin(self) -> bool:
|
|
146
|
+
"""Check if user has any admin role."""
|
|
147
|
+
admin_roles = {"platform_admin", "tenant_admin"}
|
|
148
|
+
return any(role in admin_roles for role in self._roles)
|
|
149
|
+
|
|
150
|
+
@property
|
|
151
|
+
def is_organizer(self) -> bool:
|
|
152
|
+
"""Check if user has organizer role."""
|
|
153
|
+
return "tenant_organizer" in self._roles
|
|
154
|
+
|
|
155
|
+
@property
|
|
156
|
+
def avatar(self) -> str | None:
|
|
157
|
+
"""User's avatar URL."""
|
|
158
|
+
return self._avatar
|
|
159
|
+
|
|
160
|
+
@avatar.setter
|
|
161
|
+
def avatar(self, value: str | None):
|
|
162
|
+
self._avatar = value
|
|
163
|
+
|
|
164
|
+
@property
|
|
165
|
+
def cognito_user_name(self) -> str | None:
|
|
166
|
+
"""Cognito username (sub/UUID from Cognito User Pool)."""
|
|
167
|
+
return self._cognito_user_name
|
|
168
|
+
|
|
169
|
+
@cognito_user_name.setter
|
|
170
|
+
def cognito_user_name(self, value: str | None):
|
|
171
|
+
self._cognito_user_name = value
|
|
172
|
+
|
|
173
|
+
def has_role(self, role: str) -> bool:
|
|
174
|
+
"""Check if user has a specific role."""
|
|
175
|
+
return role in self._roles
|
|
176
|
+
|
|
177
|
+
def add_role(self, role: str):
|
|
178
|
+
"""Add a role to the user if not already present."""
|
|
179
|
+
if role not in self._roles:
|
|
180
|
+
self._roles.append(role)
|
|
181
|
+
|
|
182
|
+
def remove_role(self, role: str):
|
|
183
|
+
"""Remove a role from the user."""
|
|
184
|
+
if role in self._roles:
|
|
185
|
+
self._roles.remove(role)
|
|
186
|
+
# Ensure at least 'tenant_user' role remains
|
|
187
|
+
if not self._roles:
|
|
188
|
+
self._roles = ["tenant_user"]
|
|
189
|
+
|
|
190
|
+
# Status
|
|
191
|
+
@property
|
|
192
|
+
def status(self) -> str:
|
|
193
|
+
"""User status (active|invited|disabled)."""
|
|
194
|
+
return self._status
|
|
195
|
+
|
|
196
|
+
@status.setter
|
|
197
|
+
def status(self, value: str):
|
|
198
|
+
if value not in ["active", "invited", "disabled"]:
|
|
199
|
+
raise ValueError(f"Invalid status: {value}. Must be active, invited, or disabled.")
|
|
200
|
+
self._status = value
|
|
201
|
+
|
|
202
|
+
# Invited timestamp
|
|
203
|
+
@property
|
|
204
|
+
def invited_utc_ts(self) -> float | None:
|
|
205
|
+
"""Timestamp when user was invited."""
|
|
206
|
+
return self._invited_utc_ts
|
|
207
|
+
|
|
208
|
+
@invited_utc_ts.setter
|
|
209
|
+
def invited_utc_ts(self, value: float | None):
|
|
210
|
+
self._invited_utc_ts = value
|
|
211
|
+
|
|
212
|
+
# Activated timestamp
|
|
213
|
+
@property
|
|
214
|
+
def activated_utc_ts(self) -> float | None:
|
|
215
|
+
"""Timestamp when user activated their account."""
|
|
216
|
+
return self._activated_utc_ts
|
|
217
|
+
|
|
218
|
+
@activated_utc_ts.setter
|
|
219
|
+
def activated_utc_ts(self, value: float | None):
|
|
220
|
+
self._activated_utc_ts = value
|
|
221
|
+
|
|
222
|
+
# Disabled timestamp
|
|
223
|
+
@property
|
|
224
|
+
def disabled_utc_ts(self) -> float | None:
|
|
225
|
+
"""Timestamp when user was disabled."""
|
|
226
|
+
return self._disabled_utc_ts
|
|
227
|
+
|
|
228
|
+
@disabled_utc_ts.setter
|
|
229
|
+
def disabled_utc_ts(self, value: float | None):
|
|
230
|
+
self._disabled_utc_ts = value
|
|
231
|
+
|
|
232
|
+
# Status helper methods
|
|
233
|
+
def is_active(self) -> bool:
|
|
234
|
+
"""Check if user is active."""
|
|
235
|
+
return self._status == "active"
|
|
236
|
+
|
|
237
|
+
def is_invited(self) -> bool:
|
|
238
|
+
"""Check if user is in invited state (pending activation)."""
|
|
239
|
+
return self._status == "invited"
|
|
240
|
+
|
|
241
|
+
def is_disabled(self) -> bool:
|
|
242
|
+
"""Check if user is disabled."""
|
|
243
|
+
return self._status == "disabled"
|
|
244
|
+
|
|
245
|
+
def invite(self):
|
|
246
|
+
"""
|
|
247
|
+
Mark user as invited (pending activation).
|
|
248
|
+
|
|
249
|
+
Sets status to 'invited' and records invitation timestamp.
|
|
250
|
+
Used for invite workflow where user must accept/activate.
|
|
251
|
+
"""
|
|
252
|
+
self._status = "invited"
|
|
253
|
+
self._invited_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
254
|
+
|
|
255
|
+
def activate(self):
|
|
256
|
+
"""
|
|
257
|
+
Activate an invited user.
|
|
258
|
+
|
|
259
|
+
Sets status to 'active' and records activation timestamp.
|
|
260
|
+
Called when user accepts invite or completes signup.
|
|
261
|
+
"""
|
|
262
|
+
self._status = "active"
|
|
263
|
+
self._activated_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
264
|
+
|
|
265
|
+
def disable(self):
|
|
266
|
+
"""
|
|
267
|
+
Disable user account.
|
|
268
|
+
|
|
269
|
+
Sets status to 'disabled' and records disabled timestamp.
|
|
270
|
+
Disabled users cannot log in or access the system.
|
|
271
|
+
"""
|
|
272
|
+
self._status = "disabled"
|
|
273
|
+
self._disabled_utc_ts = dt.datetime.now(dt.UTC).timestamp()
|
|
274
|
+
|
|
275
|
+
def enable(self):
|
|
276
|
+
"""
|
|
277
|
+
Re-enable a disabled user account.
|
|
278
|
+
|
|
279
|
+
Sets status back to 'active'.
|
|
280
|
+
"""
|
|
281
|
+
if self._status == "disabled":
|
|
282
|
+
self._status = "active"
|
|
283
|
+
self._disabled_utc_ts = None
|
|
284
|
+
|
|
285
|
+
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
# Auth Domain Services
|
|
2
|
+
|
|
3
|
+
from .user_service import UserService
|
|
4
|
+
from .authorization_service import AuthorizationService, AuthorizationContext
|
|
5
|
+
from .resource_permission_service import ResourcePermissionService
|
|
6
|
+
from .permission_registry import permission_registry, PermissionDefinition, RoleDefinition
|
|
7
|
+
|
|
8
|
+
__all__ = [
|
|
9
|
+
"UserService",
|
|
10
|
+
"AuthorizationService",
|
|
11
|
+
"AuthorizationContext",
|
|
12
|
+
"ResourcePermissionService",
|
|
13
|
+
"permission_registry",
|
|
14
|
+
"PermissionDefinition",
|
|
15
|
+
"RoleDefinition",
|
|
16
|
+
]
|