kailash 0.3.2__py3-none-any.whl → 0.4.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.
- kailash/__init__.py +33 -1
- kailash/access_control/__init__.py +129 -0
- kailash/access_control/managers.py +461 -0
- kailash/access_control/rule_evaluators.py +467 -0
- kailash/access_control_abac.py +825 -0
- kailash/config/__init__.py +27 -0
- kailash/config/database_config.py +359 -0
- kailash/database/__init__.py +28 -0
- kailash/database/execution_pipeline.py +499 -0
- kailash/middleware/__init__.py +306 -0
- kailash/middleware/auth/__init__.py +33 -0
- kailash/middleware/auth/access_control.py +436 -0
- kailash/middleware/auth/auth_manager.py +422 -0
- kailash/middleware/auth/jwt_auth.py +477 -0
- kailash/middleware/auth/kailash_jwt_auth.py +616 -0
- kailash/middleware/communication/__init__.py +37 -0
- kailash/middleware/communication/ai_chat.py +989 -0
- kailash/middleware/communication/api_gateway.py +802 -0
- kailash/middleware/communication/events.py +470 -0
- kailash/middleware/communication/realtime.py +710 -0
- kailash/middleware/core/__init__.py +21 -0
- kailash/middleware/core/agent_ui.py +890 -0
- kailash/middleware/core/schema.py +643 -0
- kailash/middleware/core/workflows.py +396 -0
- kailash/middleware/database/__init__.py +63 -0
- kailash/middleware/database/base.py +113 -0
- kailash/middleware/database/base_models.py +525 -0
- kailash/middleware/database/enums.py +106 -0
- kailash/middleware/database/migrations.py +12 -0
- kailash/{api/database.py → middleware/database/models.py} +183 -291
- kailash/middleware/database/repositories.py +685 -0
- kailash/middleware/database/session_manager.py +19 -0
- kailash/middleware/mcp/__init__.py +38 -0
- kailash/middleware/mcp/client_integration.py +585 -0
- kailash/middleware/mcp/enhanced_server.py +576 -0
- kailash/nodes/__init__.py +25 -3
- kailash/nodes/admin/__init__.py +35 -0
- kailash/nodes/admin/audit_log.py +794 -0
- kailash/nodes/admin/permission_check.py +864 -0
- kailash/nodes/admin/role_management.py +823 -0
- kailash/nodes/admin/security_event.py +1519 -0
- kailash/nodes/admin/user_management.py +944 -0
- kailash/nodes/ai/a2a.py +24 -7
- kailash/nodes/ai/ai_providers.py +1 -0
- kailash/nodes/ai/embedding_generator.py +11 -11
- kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
- kailash/nodes/ai/llm_agent.py +407 -2
- kailash/nodes/ai/self_organizing.py +85 -10
- kailash/nodes/api/auth.py +287 -6
- kailash/nodes/api/rest.py +151 -0
- kailash/nodes/auth/__init__.py +17 -0
- kailash/nodes/auth/directory_integration.py +1228 -0
- kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
- kailash/nodes/auth/mfa.py +2338 -0
- kailash/nodes/auth/risk_assessment.py +872 -0
- kailash/nodes/auth/session_management.py +1093 -0
- kailash/nodes/auth/sso.py +1040 -0
- kailash/nodes/base.py +344 -13
- kailash/nodes/base_cycle_aware.py +4 -2
- kailash/nodes/base_with_acl.py +1 -1
- kailash/nodes/code/python.py +283 -10
- kailash/nodes/compliance/__init__.py +9 -0
- kailash/nodes/compliance/data_retention.py +1888 -0
- kailash/nodes/compliance/gdpr.py +2004 -0
- kailash/nodes/data/__init__.py +22 -2
- kailash/nodes/data/async_connection.py +469 -0
- kailash/nodes/data/async_sql.py +757 -0
- kailash/nodes/data/async_vector.py +598 -0
- kailash/nodes/data/readers.py +767 -0
- kailash/nodes/data/retrieval.py +360 -1
- kailash/nodes/data/sharepoint_graph.py +397 -21
- kailash/nodes/data/sql.py +94 -5
- kailash/nodes/data/streaming.py +68 -8
- kailash/nodes/data/vector_db.py +54 -4
- kailash/nodes/enterprise/__init__.py +13 -0
- kailash/nodes/enterprise/batch_processor.py +741 -0
- kailash/nodes/enterprise/data_lineage.py +497 -0
- kailash/nodes/logic/convergence.py +31 -9
- kailash/nodes/logic/operations.py +14 -3
- kailash/nodes/mixins/__init__.py +8 -0
- kailash/nodes/mixins/event_emitter.py +201 -0
- kailash/nodes/mixins/mcp.py +9 -4
- kailash/nodes/mixins/security.py +165 -0
- kailash/nodes/monitoring/__init__.py +7 -0
- kailash/nodes/monitoring/performance_benchmark.py +2497 -0
- kailash/nodes/rag/__init__.py +284 -0
- kailash/nodes/rag/advanced.py +1615 -0
- kailash/nodes/rag/agentic.py +773 -0
- kailash/nodes/rag/conversational.py +999 -0
- kailash/nodes/rag/evaluation.py +875 -0
- kailash/nodes/rag/federated.py +1188 -0
- kailash/nodes/rag/graph.py +721 -0
- kailash/nodes/rag/multimodal.py +671 -0
- kailash/nodes/rag/optimized.py +933 -0
- kailash/nodes/rag/privacy.py +1059 -0
- kailash/nodes/rag/query_processing.py +1335 -0
- kailash/nodes/rag/realtime.py +764 -0
- kailash/nodes/rag/registry.py +547 -0
- kailash/nodes/rag/router.py +837 -0
- kailash/nodes/rag/similarity.py +1854 -0
- kailash/nodes/rag/strategies.py +566 -0
- kailash/nodes/rag/workflows.py +575 -0
- kailash/nodes/security/__init__.py +19 -0
- kailash/nodes/security/abac_evaluator.py +1411 -0
- kailash/nodes/security/audit_log.py +91 -0
- kailash/nodes/security/behavior_analysis.py +1893 -0
- kailash/nodes/security/credential_manager.py +401 -0
- kailash/nodes/security/rotating_credentials.py +760 -0
- kailash/nodes/security/security_event.py +132 -0
- kailash/nodes/security/threat_detection.py +1103 -0
- kailash/nodes/testing/__init__.py +9 -0
- kailash/nodes/testing/credential_testing.py +499 -0
- kailash/nodes/transform/__init__.py +10 -2
- kailash/nodes/transform/chunkers.py +592 -1
- kailash/nodes/transform/processors.py +484 -14
- kailash/nodes/validation.py +321 -0
- kailash/runtime/access_controlled.py +1 -1
- kailash/runtime/async_local.py +41 -7
- kailash/runtime/docker.py +1 -1
- kailash/runtime/local.py +474 -55
- kailash/runtime/parallel.py +1 -1
- kailash/runtime/parallel_cyclic.py +1 -1
- kailash/runtime/testing.py +210 -2
- kailash/utils/migrations/__init__.py +25 -0
- kailash/utils/migrations/generator.py +433 -0
- kailash/utils/migrations/models.py +231 -0
- kailash/utils/migrations/runner.py +489 -0
- kailash/utils/secure_logging.py +342 -0
- kailash/workflow/__init__.py +16 -0
- kailash/workflow/cyclic_runner.py +3 -4
- kailash/workflow/graph.py +70 -2
- kailash/workflow/resilience.py +249 -0
- kailash/workflow/templates.py +726 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
- kailash-0.4.0.dist-info/RECORD +223 -0
- kailash/api/__init__.py +0 -17
- kailash/api/__main__.py +0 -6
- kailash/api/studio_secure.py +0 -893
- kailash/mcp/__main__.py +0 -13
- kailash/mcp/server_new.py +0 -336
- kailash/mcp/servers/__init__.py +0 -12
- kailash-0.3.2.dist-info/RECORD +0 -136
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,823 @@
|
|
1
|
+
"""Enterprise role management node with hierarchical RBAC support.
|
2
|
+
|
3
|
+
This node provides comprehensive role-based access control (RBAC) management
|
4
|
+
with support for role hierarchies, inheritance, and dynamic permission assignment.
|
5
|
+
Integrates with Session 065's ABAC system for enhanced access control.
|
6
|
+
|
7
|
+
Features:
|
8
|
+
- Hierarchical role management with inheritance
|
9
|
+
- Dynamic permission assignment and revocation
|
10
|
+
- Role templates and bulk operations
|
11
|
+
- Permission dependency validation
|
12
|
+
- Role-based data filtering
|
13
|
+
- Multi-tenant role isolation
|
14
|
+
- Integration with ABAC attributes
|
15
|
+
- Comprehensive audit logging
|
16
|
+
"""
|
17
|
+
|
18
|
+
from dataclasses import dataclass
|
19
|
+
from datetime import UTC, datetime
|
20
|
+
from enum import Enum
|
21
|
+
from typing import Any, Dict, List, Optional, Set
|
22
|
+
|
23
|
+
from kailash.access_control import (
|
24
|
+
AccessControlManager,
|
25
|
+
NodePermission,
|
26
|
+
PermissionEffect,
|
27
|
+
PermissionRule,
|
28
|
+
WorkflowPermission,
|
29
|
+
)
|
30
|
+
from kailash.nodes.base import Node, NodeParameter, register_node
|
31
|
+
from kailash.nodes.data import AsyncSQLDatabaseNode
|
32
|
+
from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
|
33
|
+
|
34
|
+
|
35
|
+
class RoleOperation(Enum):
|
36
|
+
"""Supported role management operations."""
|
37
|
+
|
38
|
+
CREATE_ROLE = "create_role"
|
39
|
+
UPDATE_ROLE = "update_role"
|
40
|
+
DELETE_ROLE = "delete_role"
|
41
|
+
LIST_ROLES = "list_roles"
|
42
|
+
GET_ROLE = "get_role"
|
43
|
+
ASSIGN_USER = "assign_user"
|
44
|
+
UNASSIGN_USER = "unassign_user"
|
45
|
+
ADD_PERMISSION = "add_permission"
|
46
|
+
REMOVE_PERMISSION = "remove_permission"
|
47
|
+
BULK_ASSIGN = "bulk_assign"
|
48
|
+
BULK_UNASSIGN = "bulk_unassign"
|
49
|
+
GET_USER_ROLES = "get_user_roles"
|
50
|
+
GET_ROLE_USERS = "get_role_users"
|
51
|
+
VALIDATE_HIERARCHY = "validate_hierarchy"
|
52
|
+
GET_EFFECTIVE_PERMISSIONS = "get_effective_permissions"
|
53
|
+
|
54
|
+
|
55
|
+
class RoleType(Enum):
|
56
|
+
"""Types of roles in the system."""
|
57
|
+
|
58
|
+
SYSTEM = "system" # Built-in system roles
|
59
|
+
CUSTOM = "custom" # Custom organization roles
|
60
|
+
TEMPLATE = "template" # Role templates for reuse
|
61
|
+
TEMPORARY = "temporary" # Time-limited roles
|
62
|
+
|
63
|
+
|
64
|
+
@dataclass
|
65
|
+
class Role:
|
66
|
+
"""Enhanced role definition with hierarchy support."""
|
67
|
+
|
68
|
+
role_id: str
|
69
|
+
name: str
|
70
|
+
description: str
|
71
|
+
role_type: RoleType
|
72
|
+
permissions: Set[str]
|
73
|
+
parent_roles: Set[str] # For role hierarchy
|
74
|
+
child_roles: Set[str] # Derived roles
|
75
|
+
attributes: Dict[str, Any] # ABAC attributes
|
76
|
+
is_active: bool
|
77
|
+
created_at: datetime
|
78
|
+
updated_at: datetime
|
79
|
+
tenant_id: str
|
80
|
+
created_by: str
|
81
|
+
|
82
|
+
def get_all_permissions(self, role_hierarchy: Dict[str, "Role"]) -> Set[str]:
|
83
|
+
"""Get all permissions including inherited from parent roles."""
|
84
|
+
all_permissions = self.permissions.copy()
|
85
|
+
|
86
|
+
# Add permissions from parent roles (recursive)
|
87
|
+
for parent_id in self.parent_roles:
|
88
|
+
if parent_id in role_hierarchy:
|
89
|
+
parent_role = role_hierarchy[parent_id]
|
90
|
+
all_permissions.update(parent_role.get_all_permissions(role_hierarchy))
|
91
|
+
|
92
|
+
return all_permissions
|
93
|
+
|
94
|
+
|
95
|
+
@register_node()
|
96
|
+
class RoleManagementNode(Node):
|
97
|
+
"""Enterprise role management node with hierarchical RBAC.
|
98
|
+
|
99
|
+
This node provides comprehensive role management capabilities including:
|
100
|
+
- Hierarchical role creation and management
|
101
|
+
- Permission assignment with inheritance
|
102
|
+
- User-role assignment and bulk operations
|
103
|
+
- Role validation and dependency checking
|
104
|
+
- Integration with ABAC attributes
|
105
|
+
- Multi-tenant role isolation
|
106
|
+
|
107
|
+
Parameters:
|
108
|
+
operation: Type of role operation to perform
|
109
|
+
role_data: Role configuration data
|
110
|
+
role_id: Role ID for single-role operations
|
111
|
+
role_ids: List of role IDs for bulk operations
|
112
|
+
user_id: User ID for assignment operations
|
113
|
+
user_ids: List of user IDs for bulk assignment
|
114
|
+
permission: Permission to add/remove
|
115
|
+
permissions: List of permissions for bulk operations
|
116
|
+
tenant_id: Tenant isolation
|
117
|
+
validate_hierarchy: Whether to validate role hierarchy
|
118
|
+
include_inherited: Include inherited permissions in results
|
119
|
+
|
120
|
+
Example:
|
121
|
+
>>> # Create hierarchical role structure
|
122
|
+
>>> node = RoleManagementNode(
|
123
|
+
... operation="create_role",
|
124
|
+
... role_data={
|
125
|
+
... "name": "Senior Analyst",
|
126
|
+
... "description": "Senior financial analyst with elevated permissions",
|
127
|
+
... "parent_roles": ["analyst"],
|
128
|
+
... "permissions": ["advanced_reports", "data_export"],
|
129
|
+
... "attributes": {
|
130
|
+
... "seniority": "senior",
|
131
|
+
... "clearance_required": "confidential"
|
132
|
+
... }
|
133
|
+
... }
|
134
|
+
... )
|
135
|
+
>>> result = node.run()
|
136
|
+
>>> role_id = result["role"]["role_id"]
|
137
|
+
|
138
|
+
>>> # Bulk user assignment
|
139
|
+
>>> node = RoleManagementNode(
|
140
|
+
... operation="bulk_assign",
|
141
|
+
... role_id="senior_analyst",
|
142
|
+
... user_ids=["user1", "user2", "user3"],
|
143
|
+
... validate_hierarchy=True
|
144
|
+
... )
|
145
|
+
>>> result = node.run()
|
146
|
+
>>> assigned_count = result["stats"]["assigned"]
|
147
|
+
"""
|
148
|
+
|
149
|
+
def __init__(self, **config):
|
150
|
+
super().__init__(**config)
|
151
|
+
self._db_node = None
|
152
|
+
self._access_manager = None
|
153
|
+
|
154
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
155
|
+
"""Define parameters for role management operations."""
|
156
|
+
return {
|
157
|
+
param.name: param
|
158
|
+
for param in [
|
159
|
+
# Operation type
|
160
|
+
NodeParameter(
|
161
|
+
name="operation",
|
162
|
+
type=str,
|
163
|
+
required=True,
|
164
|
+
description="Role management operation to perform",
|
165
|
+
choices=[op.value for op in RoleOperation],
|
166
|
+
),
|
167
|
+
# Role data
|
168
|
+
NodeParameter(
|
169
|
+
name="role_data",
|
170
|
+
type=dict,
|
171
|
+
required=False,
|
172
|
+
description="Role configuration data",
|
173
|
+
),
|
174
|
+
# Single role operations
|
175
|
+
NodeParameter(
|
176
|
+
name="role_id",
|
177
|
+
type=str,
|
178
|
+
required=False,
|
179
|
+
description="Role ID for single-role operations",
|
180
|
+
),
|
181
|
+
# Bulk operations
|
182
|
+
NodeParameter(
|
183
|
+
name="role_ids",
|
184
|
+
type=list,
|
185
|
+
required=False,
|
186
|
+
description="List of role IDs for bulk operations",
|
187
|
+
),
|
188
|
+
# User assignment
|
189
|
+
NodeParameter(
|
190
|
+
name="user_id",
|
191
|
+
type=str,
|
192
|
+
required=False,
|
193
|
+
description="User ID for assignment operations",
|
194
|
+
),
|
195
|
+
NodeParameter(
|
196
|
+
name="user_ids",
|
197
|
+
type=list,
|
198
|
+
required=False,
|
199
|
+
description="List of user IDs for bulk assignment",
|
200
|
+
),
|
201
|
+
# Permission management
|
202
|
+
NodeParameter(
|
203
|
+
name="permission",
|
204
|
+
type=str,
|
205
|
+
required=False,
|
206
|
+
description="Permission to add/remove",
|
207
|
+
),
|
208
|
+
NodeParameter(
|
209
|
+
name="permissions",
|
210
|
+
type=list,
|
211
|
+
required=False,
|
212
|
+
description="List of permissions for bulk operations",
|
213
|
+
),
|
214
|
+
# Multi-tenancy
|
215
|
+
NodeParameter(
|
216
|
+
name="tenant_id",
|
217
|
+
type=str,
|
218
|
+
required=False,
|
219
|
+
description="Tenant ID for multi-tenant isolation",
|
220
|
+
),
|
221
|
+
# Validation options
|
222
|
+
NodeParameter(
|
223
|
+
name="validate_hierarchy",
|
224
|
+
type=bool,
|
225
|
+
required=False,
|
226
|
+
default=True,
|
227
|
+
description="Whether to validate role hierarchy",
|
228
|
+
),
|
229
|
+
NodeParameter(
|
230
|
+
name="include_inherited",
|
231
|
+
type=bool,
|
232
|
+
required=False,
|
233
|
+
default=True,
|
234
|
+
description="Include inherited permissions in results",
|
235
|
+
),
|
236
|
+
# Database configuration
|
237
|
+
NodeParameter(
|
238
|
+
name="database_config",
|
239
|
+
type=dict,
|
240
|
+
required=False,
|
241
|
+
description="Database connection configuration",
|
242
|
+
),
|
243
|
+
# Search and filtering
|
244
|
+
NodeParameter(
|
245
|
+
name="filters",
|
246
|
+
type=dict,
|
247
|
+
required=False,
|
248
|
+
description="Filters for role listing",
|
249
|
+
),
|
250
|
+
NodeParameter(
|
251
|
+
name="search_query",
|
252
|
+
type=str,
|
253
|
+
required=False,
|
254
|
+
description="Search query for roles",
|
255
|
+
),
|
256
|
+
]
|
257
|
+
}
|
258
|
+
|
259
|
+
def run(self, **inputs) -> Dict[str, Any]:
|
260
|
+
"""Execute role management operation."""
|
261
|
+
try:
|
262
|
+
operation = RoleOperation(inputs["operation"])
|
263
|
+
|
264
|
+
# Initialize dependencies
|
265
|
+
self._init_dependencies(inputs)
|
266
|
+
|
267
|
+
# Route to appropriate operation
|
268
|
+
if operation == RoleOperation.CREATE_ROLE:
|
269
|
+
return self._create_role(inputs)
|
270
|
+
elif operation == RoleOperation.UPDATE_ROLE:
|
271
|
+
return self._update_role(inputs)
|
272
|
+
elif operation == RoleOperation.DELETE_ROLE:
|
273
|
+
return self._delete_role(inputs)
|
274
|
+
elif operation == RoleOperation.LIST_ROLES:
|
275
|
+
return self._list_roles(inputs)
|
276
|
+
elif operation == RoleOperation.GET_ROLE:
|
277
|
+
return self._get_role(inputs)
|
278
|
+
elif operation == RoleOperation.ASSIGN_USER:
|
279
|
+
return self._assign_user(inputs)
|
280
|
+
elif operation == RoleOperation.UNASSIGN_USER:
|
281
|
+
return self._unassign_user(inputs)
|
282
|
+
elif operation == RoleOperation.ADD_PERMISSION:
|
283
|
+
return self._add_permission(inputs)
|
284
|
+
elif operation == RoleOperation.REMOVE_PERMISSION:
|
285
|
+
return self._remove_permission(inputs)
|
286
|
+
elif operation == RoleOperation.BULK_ASSIGN:
|
287
|
+
return self._bulk_assign(inputs)
|
288
|
+
elif operation == RoleOperation.BULK_UNASSIGN:
|
289
|
+
return self._bulk_unassign(inputs)
|
290
|
+
elif operation == RoleOperation.GET_USER_ROLES:
|
291
|
+
return self._get_user_roles(inputs)
|
292
|
+
elif operation == RoleOperation.GET_ROLE_USERS:
|
293
|
+
return self._get_role_users(inputs)
|
294
|
+
elif operation == RoleOperation.VALIDATE_HIERARCHY:
|
295
|
+
return self._validate_hierarchy(inputs)
|
296
|
+
elif operation == RoleOperation.GET_EFFECTIVE_PERMISSIONS:
|
297
|
+
return self._get_effective_permissions(inputs)
|
298
|
+
else:
|
299
|
+
raise NodeExecutionError(f"Unknown operation: {operation}")
|
300
|
+
|
301
|
+
except Exception as e:
|
302
|
+
raise NodeExecutionError(f"Role management operation failed: {str(e)}")
|
303
|
+
|
304
|
+
def _init_dependencies(self, inputs: Dict[str, Any]):
|
305
|
+
"""Initialize database and access manager dependencies."""
|
306
|
+
# Get database config
|
307
|
+
db_config = inputs.get(
|
308
|
+
"database_config",
|
309
|
+
{
|
310
|
+
"database_type": "postgresql",
|
311
|
+
"host": "localhost",
|
312
|
+
"port": 5432,
|
313
|
+
"database": "kailash_admin",
|
314
|
+
"user": "admin",
|
315
|
+
"password": "admin",
|
316
|
+
},
|
317
|
+
)
|
318
|
+
|
319
|
+
# Initialize async database node
|
320
|
+
self._db_node = AsyncSQLDatabaseNode(name="role_management_db", **db_config)
|
321
|
+
|
322
|
+
# Initialize enhanced access manager
|
323
|
+
self._access_manager = AccessControlManager(strategy="abac")
|
324
|
+
|
325
|
+
def _create_role(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
326
|
+
"""Create a new role with hierarchy validation."""
|
327
|
+
role_data = inputs["role_data"]
|
328
|
+
tenant_id = inputs.get("tenant_id", "default")
|
329
|
+
validate_hierarchy = inputs.get("validate_hierarchy", True)
|
330
|
+
|
331
|
+
# Validate required fields
|
332
|
+
required_fields = ["name", "description"]
|
333
|
+
for field in required_fields:
|
334
|
+
if field not in role_data:
|
335
|
+
raise NodeValidationError(f"Missing required field: {field}")
|
336
|
+
|
337
|
+
# Generate role ID
|
338
|
+
role_id = self._generate_role_id(role_data["name"])
|
339
|
+
now = datetime.now(UTC)
|
340
|
+
|
341
|
+
# Validate parent roles exist if specified
|
342
|
+
parent_roles = set(role_data.get("parent_roles", []))
|
343
|
+
if parent_roles and validate_hierarchy:
|
344
|
+
self._validate_parent_roles_exist(parent_roles, tenant_id)
|
345
|
+
|
346
|
+
# Validate no circular dependencies
|
347
|
+
if parent_roles and validate_hierarchy:
|
348
|
+
self._validate_no_circular_dependency(role_id, parent_roles, tenant_id)
|
349
|
+
|
350
|
+
# Prepare role record
|
351
|
+
role_record = {
|
352
|
+
"role_id": role_id,
|
353
|
+
"name": role_data["name"],
|
354
|
+
"description": role_data["description"],
|
355
|
+
"role_type": role_data.get("role_type", RoleType.CUSTOM.value),
|
356
|
+
"permissions": list(role_data.get("permissions", [])),
|
357
|
+
"parent_roles": list(parent_roles),
|
358
|
+
"attributes": role_data.get("attributes", {}),
|
359
|
+
"is_active": role_data.get("is_active", True),
|
360
|
+
"tenant_id": tenant_id,
|
361
|
+
"created_at": now,
|
362
|
+
"updated_at": now,
|
363
|
+
"created_by": inputs.get("created_by", "system"),
|
364
|
+
}
|
365
|
+
|
366
|
+
# Insert role into database
|
367
|
+
insert_query = """
|
368
|
+
INSERT INTO roles (
|
369
|
+
role_id, name, description, role_type, permissions, parent_roles,
|
370
|
+
attributes, is_active, tenant_id, created_at, updated_at, created_by
|
371
|
+
) VALUES (
|
372
|
+
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
|
373
|
+
)
|
374
|
+
"""
|
375
|
+
|
376
|
+
# Execute database insert
|
377
|
+
self._db_node.config.update(
|
378
|
+
{
|
379
|
+
"query": insert_query,
|
380
|
+
"params": [
|
381
|
+
role_record["role_id"],
|
382
|
+
role_record["name"],
|
383
|
+
role_record["description"],
|
384
|
+
role_record["role_type"],
|
385
|
+
role_record["permissions"],
|
386
|
+
role_record["parent_roles"],
|
387
|
+
role_record["attributes"],
|
388
|
+
role_record["is_active"],
|
389
|
+
role_record["tenant_id"],
|
390
|
+
role_record["created_at"],
|
391
|
+
role_record["updated_at"],
|
392
|
+
role_record["created_by"],
|
393
|
+
],
|
394
|
+
}
|
395
|
+
)
|
396
|
+
|
397
|
+
db_result = self._db_node.run()
|
398
|
+
|
399
|
+
# Update child_roles for parent roles
|
400
|
+
if parent_roles:
|
401
|
+
self._update_child_roles(parent_roles, role_id, tenant_id, "add")
|
402
|
+
|
403
|
+
return {
|
404
|
+
"result": {
|
405
|
+
"role": {
|
406
|
+
"role_id": role_id,
|
407
|
+
"name": role_record["name"],
|
408
|
+
"description": role_record["description"],
|
409
|
+
"role_type": role_record["role_type"],
|
410
|
+
"permissions": role_record["permissions"],
|
411
|
+
"parent_roles": role_record["parent_roles"],
|
412
|
+
"attributes": role_record["attributes"],
|
413
|
+
"is_active": role_record["is_active"],
|
414
|
+
"tenant_id": role_record["tenant_id"],
|
415
|
+
"created_at": role_record["created_at"].isoformat(),
|
416
|
+
},
|
417
|
+
"operation": "create_role",
|
418
|
+
"success": True,
|
419
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
420
|
+
}
|
421
|
+
}
|
422
|
+
|
423
|
+
def _assign_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
424
|
+
"""Assign role to user with validation."""
|
425
|
+
user_id = inputs["user_id"]
|
426
|
+
role_id = inputs["role_id"]
|
427
|
+
tenant_id = inputs.get("tenant_id", "default")
|
428
|
+
validate_hierarchy = inputs.get("validate_hierarchy", True)
|
429
|
+
|
430
|
+
# Validate role exists and is active
|
431
|
+
if validate_hierarchy:
|
432
|
+
role = self._get_role_by_id(role_id, tenant_id)
|
433
|
+
if not role:
|
434
|
+
raise NodeValidationError(f"Role not found: {role_id}")
|
435
|
+
if not role["is_active"]:
|
436
|
+
raise NodeValidationError(f"Role is not active: {role_id}")
|
437
|
+
|
438
|
+
# Check if assignment already exists
|
439
|
+
existing_query = """
|
440
|
+
SELECT 1 FROM user_roles
|
441
|
+
WHERE user_id = $1 AND role_id = $2 AND tenant_id = $3
|
442
|
+
"""
|
443
|
+
|
444
|
+
self._db_node.config.update(
|
445
|
+
{
|
446
|
+
"query": existing_query,
|
447
|
+
"params": [user_id, role_id, tenant_id],
|
448
|
+
"fetch_mode": "one",
|
449
|
+
}
|
450
|
+
)
|
451
|
+
|
452
|
+
existing = self._db_node.run()
|
453
|
+
|
454
|
+
if existing.get("result", {}).get("data"):
|
455
|
+
return {
|
456
|
+
"result": {
|
457
|
+
"assignment": {
|
458
|
+
"user_id": user_id,
|
459
|
+
"role_id": role_id,
|
460
|
+
"already_assigned": True,
|
461
|
+
},
|
462
|
+
"operation": "assign_user",
|
463
|
+
"success": True,
|
464
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
465
|
+
}
|
466
|
+
}
|
467
|
+
|
468
|
+
# Create assignment
|
469
|
+
now = datetime.now(UTC)
|
470
|
+
insert_query = """
|
471
|
+
INSERT INTO user_roles (user_id, role_id, tenant_id, assigned_at, assigned_by)
|
472
|
+
VALUES ($1, $2, $3, $4, $5)
|
473
|
+
"""
|
474
|
+
|
475
|
+
self._db_node.config.update(
|
476
|
+
{
|
477
|
+
"query": insert_query,
|
478
|
+
"params": [
|
479
|
+
user_id,
|
480
|
+
role_id,
|
481
|
+
tenant_id,
|
482
|
+
now,
|
483
|
+
inputs.get("assigned_by", "system"),
|
484
|
+
],
|
485
|
+
}
|
486
|
+
)
|
487
|
+
|
488
|
+
db_result = self._db_node.run()
|
489
|
+
|
490
|
+
return {
|
491
|
+
"result": {
|
492
|
+
"assignment": {
|
493
|
+
"user_id": user_id,
|
494
|
+
"role_id": role_id,
|
495
|
+
"tenant_id": tenant_id,
|
496
|
+
"assigned_at": now.isoformat(),
|
497
|
+
"already_assigned": False,
|
498
|
+
},
|
499
|
+
"operation": "assign_user",
|
500
|
+
"success": True,
|
501
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
502
|
+
}
|
503
|
+
}
|
504
|
+
|
505
|
+
def _bulk_assign(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
506
|
+
"""Bulk assign role to multiple users."""
|
507
|
+
role_id = inputs["role_id"]
|
508
|
+
user_ids = inputs["user_ids"]
|
509
|
+
tenant_id = inputs.get("tenant_id", "default")
|
510
|
+
|
511
|
+
if not isinstance(user_ids, list):
|
512
|
+
raise NodeValidationError("user_ids must be a list for bulk operations")
|
513
|
+
|
514
|
+
results = {
|
515
|
+
"assigned": [],
|
516
|
+
"failed": [],
|
517
|
+
"stats": {"assigned": 0, "failed": 0, "already_assigned": 0},
|
518
|
+
}
|
519
|
+
|
520
|
+
for user_id in user_ids:
|
521
|
+
try:
|
522
|
+
assign_inputs = {
|
523
|
+
"operation": "assign_user",
|
524
|
+
"user_id": user_id,
|
525
|
+
"role_id": role_id,
|
526
|
+
"tenant_id": tenant_id,
|
527
|
+
"validate_hierarchy": inputs.get("validate_hierarchy", True),
|
528
|
+
}
|
529
|
+
|
530
|
+
result = self._assign_user(assign_inputs)
|
531
|
+
assignment = result["result"]["assignment"]
|
532
|
+
|
533
|
+
if assignment["already_assigned"]:
|
534
|
+
results["stats"]["already_assigned"] += 1
|
535
|
+
else:
|
536
|
+
results["stats"]["assigned"] += 1
|
537
|
+
|
538
|
+
results["assigned"].append(
|
539
|
+
{
|
540
|
+
"user_id": user_id,
|
541
|
+
"already_assigned": assignment["already_assigned"],
|
542
|
+
}
|
543
|
+
)
|
544
|
+
|
545
|
+
except Exception as e:
|
546
|
+
results["failed"].append({"user_id": user_id, "error": str(e)})
|
547
|
+
results["stats"]["failed"] += 1
|
548
|
+
|
549
|
+
return {
|
550
|
+
"result": {
|
551
|
+
"operation": "bulk_assign",
|
552
|
+
"role_id": role_id,
|
553
|
+
"results": results,
|
554
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
555
|
+
}
|
556
|
+
}
|
557
|
+
|
558
|
+
def _get_effective_permissions(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
559
|
+
"""Get effective permissions for a role including inherited permissions."""
|
560
|
+
role_id = inputs["role_id"]
|
561
|
+
tenant_id = inputs.get("tenant_id", "default")
|
562
|
+
include_inherited = inputs.get("include_inherited", True)
|
563
|
+
|
564
|
+
# Get role and build hierarchy
|
565
|
+
role_hierarchy = self._build_role_hierarchy(tenant_id)
|
566
|
+
|
567
|
+
if role_id not in role_hierarchy:
|
568
|
+
raise NodeValidationError(f"Role not found: {role_id}")
|
569
|
+
|
570
|
+
role = role_hierarchy[role_id]
|
571
|
+
|
572
|
+
# Get direct permissions
|
573
|
+
direct_permissions = set(role["permissions"])
|
574
|
+
|
575
|
+
# Get inherited permissions if requested
|
576
|
+
inherited_permissions = set()
|
577
|
+
all_permissions = direct_permissions.copy()
|
578
|
+
|
579
|
+
if include_inherited:
|
580
|
+
inherited_permissions = self._get_inherited_permissions(
|
581
|
+
role_id, role_hierarchy
|
582
|
+
)
|
583
|
+
all_permissions.update(inherited_permissions)
|
584
|
+
|
585
|
+
return {
|
586
|
+
"result": {
|
587
|
+
"role_id": role_id,
|
588
|
+
"direct_permissions": list(direct_permissions),
|
589
|
+
"inherited_permissions": list(inherited_permissions),
|
590
|
+
"all_permissions": list(all_permissions),
|
591
|
+
"permission_count": {
|
592
|
+
"direct": len(direct_permissions),
|
593
|
+
"inherited": len(inherited_permissions),
|
594
|
+
"total": len(all_permissions),
|
595
|
+
},
|
596
|
+
"operation": "get_effective_permissions",
|
597
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
598
|
+
}
|
599
|
+
}
|
600
|
+
|
601
|
+
# Utility methods
|
602
|
+
def _generate_role_id(self, name: str) -> str:
|
603
|
+
"""Generate role ID from name."""
|
604
|
+
import re
|
605
|
+
|
606
|
+
# Convert to lowercase, replace spaces/special chars with underscores
|
607
|
+
role_id = re.sub(r"[^a-zA-Z0-9_]", "_", name.lower())
|
608
|
+
role_id = re.sub(r"_+", "_", role_id) # Remove multiple underscores
|
609
|
+
role_id = role_id.strip("_") # Remove leading/trailing underscores
|
610
|
+
return role_id
|
611
|
+
|
612
|
+
def _validate_parent_roles_exist(self, parent_roles: Set[str], tenant_id: str):
|
613
|
+
"""Validate that all parent roles exist."""
|
614
|
+
if not parent_roles:
|
615
|
+
return
|
616
|
+
|
617
|
+
placeholders = ",".join(["$" + str(i + 2) for i in range(len(parent_roles))])
|
618
|
+
query = f"""
|
619
|
+
SELECT role_id FROM roles
|
620
|
+
WHERE tenant_id = $1 AND role_id IN ({placeholders}) AND is_active = true
|
621
|
+
"""
|
622
|
+
|
623
|
+
params = [tenant_id] + list(parent_roles)
|
624
|
+
|
625
|
+
self._db_node.config.update(
|
626
|
+
{"query": query, "params": params, "fetch_mode": "all"}
|
627
|
+
)
|
628
|
+
|
629
|
+
result = self._db_node.run()
|
630
|
+
existing_roles = {
|
631
|
+
row["role_id"] for row in result.get("result", {}).get("data", [])
|
632
|
+
}
|
633
|
+
|
634
|
+
missing_roles = parent_roles - existing_roles
|
635
|
+
if missing_roles:
|
636
|
+
raise NodeValidationError(
|
637
|
+
f"Parent roles not found: {', '.join(missing_roles)}"
|
638
|
+
)
|
639
|
+
|
640
|
+
def _validate_no_circular_dependency(
|
641
|
+
self, role_id: str, parent_roles: Set[str], tenant_id: str
|
642
|
+
):
|
643
|
+
"""Validate no circular dependencies in role hierarchy."""
|
644
|
+
# Build current hierarchy excluding the new role
|
645
|
+
role_hierarchy = self._build_role_hierarchy(tenant_id, exclude_role=role_id)
|
646
|
+
|
647
|
+
# Check if any parent role has this role as an ancestor
|
648
|
+
for parent_id in parent_roles:
|
649
|
+
if self._is_ancestor(role_id, parent_id, role_hierarchy):
|
650
|
+
raise NodeValidationError(
|
651
|
+
f"Circular dependency detected: {role_id} -> {parent_id}"
|
652
|
+
)
|
653
|
+
|
654
|
+
def _is_ancestor(
|
655
|
+
self, ancestor_id: str, role_id: str, role_hierarchy: Dict[str, Dict]
|
656
|
+
) -> bool:
|
657
|
+
"""Check if ancestor_id is an ancestor of role_id."""
|
658
|
+
if role_id not in role_hierarchy:
|
659
|
+
return False
|
660
|
+
|
661
|
+
role = role_hierarchy[role_id]
|
662
|
+
parent_roles = role.get("parent_roles", [])
|
663
|
+
|
664
|
+
if ancestor_id in parent_roles:
|
665
|
+
return True
|
666
|
+
|
667
|
+
# Recursively check parent roles
|
668
|
+
for parent_id in parent_roles:
|
669
|
+
if self._is_ancestor(ancestor_id, parent_id, role_hierarchy):
|
670
|
+
return True
|
671
|
+
|
672
|
+
return False
|
673
|
+
|
674
|
+
def _build_role_hierarchy(
|
675
|
+
self, tenant_id: str, exclude_role: Optional[str] = None
|
676
|
+
) -> Dict[str, Dict]:
|
677
|
+
"""Build complete role hierarchy for tenant."""
|
678
|
+
query = """
|
679
|
+
SELECT role_id, name, permissions, parent_roles, child_roles, is_active
|
680
|
+
FROM roles
|
681
|
+
WHERE tenant_id = $1
|
682
|
+
"""
|
683
|
+
params = [tenant_id]
|
684
|
+
|
685
|
+
if exclude_role:
|
686
|
+
query += " AND role_id != $2"
|
687
|
+
params.append(exclude_role)
|
688
|
+
|
689
|
+
self._db_node.config.update(
|
690
|
+
{"query": query, "params": params, "fetch_mode": "all"}
|
691
|
+
)
|
692
|
+
|
693
|
+
result = self._db_node.run()
|
694
|
+
roles_data = result.get("result", {}).get("data", [])
|
695
|
+
|
696
|
+
# Convert to hierarchy dict
|
697
|
+
hierarchy = {}
|
698
|
+
for role_data in roles_data:
|
699
|
+
hierarchy[role_data["role_id"]] = role_data
|
700
|
+
|
701
|
+
return hierarchy
|
702
|
+
|
703
|
+
def _get_inherited_permissions(
|
704
|
+
self, role_id: str, role_hierarchy: Dict[str, Dict]
|
705
|
+
) -> Set[str]:
|
706
|
+
"""Get all inherited permissions for a role."""
|
707
|
+
inherited = set()
|
708
|
+
|
709
|
+
if role_id not in role_hierarchy:
|
710
|
+
return inherited
|
711
|
+
|
712
|
+
role = role_hierarchy[role_id]
|
713
|
+
parent_roles = role.get("parent_roles", [])
|
714
|
+
|
715
|
+
for parent_id in parent_roles:
|
716
|
+
if parent_id in role_hierarchy:
|
717
|
+
parent_role = role_hierarchy[parent_id]
|
718
|
+
# Add parent's direct permissions
|
719
|
+
inherited.update(parent_role.get("permissions", []))
|
720
|
+
# Recursively add inherited permissions
|
721
|
+
inherited.update(
|
722
|
+
self._get_inherited_permissions(parent_id, role_hierarchy)
|
723
|
+
)
|
724
|
+
|
725
|
+
return inherited
|
726
|
+
|
727
|
+
def _get_role_by_id(self, role_id: str, tenant_id: str) -> Optional[Dict[str, Any]]:
|
728
|
+
"""Get role by ID."""
|
729
|
+
query = """
|
730
|
+
SELECT role_id, name, description, role_type, permissions, parent_roles,
|
731
|
+
attributes, is_active, created_at, updated_at
|
732
|
+
FROM roles
|
733
|
+
WHERE role_id = $1 AND tenant_id = $2
|
734
|
+
"""
|
735
|
+
|
736
|
+
self._db_node.config.update(
|
737
|
+
{"query": query, "params": [role_id, tenant_id], "fetch_mode": "one"}
|
738
|
+
)
|
739
|
+
|
740
|
+
result = self._db_node.run()
|
741
|
+
return result.get("result", {}).get("data")
|
742
|
+
|
743
|
+
def _update_child_roles(
|
744
|
+
self,
|
745
|
+
parent_role_ids: Set[str],
|
746
|
+
child_role_id: str,
|
747
|
+
tenant_id: str,
|
748
|
+
operation: str,
|
749
|
+
):
|
750
|
+
"""Update child_roles arrays for parent roles."""
|
751
|
+
if operation == "add":
|
752
|
+
query = """
|
753
|
+
UPDATE roles
|
754
|
+
SET child_roles = array_append(child_roles, $1),
|
755
|
+
updated_at = $2
|
756
|
+
WHERE role_id = ANY($3) AND tenant_id = $4
|
757
|
+
"""
|
758
|
+
else: # remove
|
759
|
+
query = """
|
760
|
+
UPDATE roles
|
761
|
+
SET child_roles = array_remove(child_roles, $1),
|
762
|
+
updated_at = $2
|
763
|
+
WHERE role_id = ANY($3) AND tenant_id = $4
|
764
|
+
"""
|
765
|
+
|
766
|
+
self._db_node.config.update(
|
767
|
+
{
|
768
|
+
"query": query,
|
769
|
+
"params": [
|
770
|
+
child_role_id,
|
771
|
+
datetime.now(UTC),
|
772
|
+
list(parent_role_ids),
|
773
|
+
tenant_id,
|
774
|
+
],
|
775
|
+
}
|
776
|
+
)
|
777
|
+
|
778
|
+
self._db_node.run()
|
779
|
+
|
780
|
+
# Additional operations would follow similar patterns
|
781
|
+
def _update_role(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
782
|
+
"""Update role information."""
|
783
|
+
raise NotImplementedError("Update role operation will be implemented")
|
784
|
+
|
785
|
+
def _delete_role(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
786
|
+
"""Delete role with dependency checking."""
|
787
|
+
raise NotImplementedError("Delete role operation will be implemented")
|
788
|
+
|
789
|
+
def _list_roles(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
790
|
+
"""List roles with filtering and pagination."""
|
791
|
+
raise NotImplementedError("List roles operation will be implemented")
|
792
|
+
|
793
|
+
def _get_role(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
794
|
+
"""Get detailed role information."""
|
795
|
+
raise NotImplementedError("Get role operation will be implemented")
|
796
|
+
|
797
|
+
def _unassign_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
798
|
+
"""Unassign role from user."""
|
799
|
+
raise NotImplementedError("Unassign user operation will be implemented")
|
800
|
+
|
801
|
+
def _add_permission(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
802
|
+
"""Add permission to role."""
|
803
|
+
raise NotImplementedError("Add permission operation will be implemented")
|
804
|
+
|
805
|
+
def _remove_permission(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
806
|
+
"""Remove permission from role."""
|
807
|
+
raise NotImplementedError("Remove permission operation will be implemented")
|
808
|
+
|
809
|
+
def _bulk_unassign(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
810
|
+
"""Bulk unassign role from multiple users."""
|
811
|
+
raise NotImplementedError("Bulk unassign operation will be implemented")
|
812
|
+
|
813
|
+
def _get_user_roles(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
814
|
+
"""Get all roles for a user."""
|
815
|
+
raise NotImplementedError("Get user roles operation will be implemented")
|
816
|
+
|
817
|
+
def _get_role_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
818
|
+
"""Get all users assigned to a role."""
|
819
|
+
raise NotImplementedError("Get role users operation will be implemented")
|
820
|
+
|
821
|
+
def _validate_hierarchy(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
822
|
+
"""Validate entire role hierarchy for consistency."""
|
823
|
+
raise NotImplementedError("Validate hierarchy operation will be implemented")
|