kailash 0.6.1__py3-none-any.whl → 0.6.3__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 +1 -1
- kailash/core/actors/connection_actor.py +3 -3
- kailash/gateway/api.py +7 -5
- kailash/gateway/enhanced_gateway.py +1 -1
- kailash/{mcp → mcp_server}/__init__.py +12 -7
- kailash/{mcp → mcp_server}/ai_registry_server.py +2 -2
- kailash/{mcp/server_enhanced.py → mcp_server/server.py} +231 -48
- kailash/{mcp → mcp_server}/servers/ai_registry.py +2 -2
- kailash/{mcp → mcp_server}/utils/__init__.py +1 -6
- kailash/middleware/auth/access_control.py +5 -5
- kailash/middleware/gateway/checkpoint_manager.py +45 -8
- kailash/middleware/mcp/client_integration.py +1 -1
- kailash/middleware/mcp/enhanced_server.py +2 -2
- kailash/nodes/admin/permission_check.py +110 -30
- kailash/nodes/admin/schema.sql +387 -0
- kailash/nodes/admin/tenant_isolation.py +249 -0
- kailash/nodes/admin/transaction_utils.py +244 -0
- kailash/nodes/admin/user_management.py +37 -9
- kailash/nodes/ai/ai_providers.py +55 -3
- kailash/nodes/ai/iterative_llm_agent.py +1 -1
- kailash/nodes/ai/llm_agent.py +118 -16
- kailash/nodes/data/sql.py +24 -0
- kailash/resources/registry.py +6 -0
- kailash/runtime/async_local.py +7 -0
- kailash/utils/export.py +152 -0
- kailash/workflow/builder.py +42 -0
- kailash/workflow/graph.py +86 -17
- kailash/workflow/templates.py +4 -9
- {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/METADATA +3 -2
- {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/RECORD +40 -38
- kailash/mcp/server.py +0 -292
- /kailash/{mcp → mcp_server}/client.py +0 -0
- /kailash/{mcp → mcp_server}/client_new.py +0 -0
- /kailash/{mcp → mcp_server}/utils/cache.py +0 -0
- /kailash/{mcp → mcp_server}/utils/config.py +0 -0
- /kailash/{mcp → mcp_server}/utils/formatters.py +0 -0
- /kailash/{mcp → mcp_server}/utils/metrics.py +0 -0
- {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/WHEEL +0 -0
- {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/entry_points.txt +0 -0
- {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.6.1.dist-info → kailash-0.6.3.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,249 @@
|
|
1
|
+
"""Enhanced tenant isolation utilities for admin nodes.
|
2
|
+
|
3
|
+
This module provides robust tenant isolation mechanisms to ensure that
|
4
|
+
multi-tenant operations properly enforce data boundaries and prevent
|
5
|
+
cross-tenant access.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
from dataclasses import dataclass
|
10
|
+
from typing import Any, Dict, List, Optional, Set
|
11
|
+
|
12
|
+
from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
|
17
|
+
@dataclass
|
18
|
+
class TenantContext:
|
19
|
+
"""Represents the context for a specific tenant."""
|
20
|
+
|
21
|
+
tenant_id: str
|
22
|
+
permissions: Set[str]
|
23
|
+
user_ids: Set[str]
|
24
|
+
role_ids: Set[str]
|
25
|
+
resource_prefixes: Set[str]
|
26
|
+
|
27
|
+
|
28
|
+
class TenantIsolationManager:
|
29
|
+
"""Manages tenant isolation for admin operations."""
|
30
|
+
|
31
|
+
def __init__(self, db_node):
|
32
|
+
"""
|
33
|
+
Initialize tenant isolation manager.
|
34
|
+
|
35
|
+
Args:
|
36
|
+
db_node: Database node for tenant context queries
|
37
|
+
"""
|
38
|
+
self.db_node = db_node
|
39
|
+
self._tenant_cache = {}
|
40
|
+
|
41
|
+
def get_tenant_context(self, tenant_id: str) -> TenantContext:
|
42
|
+
"""
|
43
|
+
Get the context for a specific tenant.
|
44
|
+
|
45
|
+
Args:
|
46
|
+
tenant_id: The tenant ID
|
47
|
+
|
48
|
+
Returns:
|
49
|
+
TenantContext with tenant-specific data
|
50
|
+
"""
|
51
|
+
if tenant_id not in self._tenant_cache:
|
52
|
+
self._tenant_cache[tenant_id] = self._load_tenant_context(tenant_id)
|
53
|
+
|
54
|
+
return self._tenant_cache[tenant_id]
|
55
|
+
|
56
|
+
def _load_tenant_context(self, tenant_id: str) -> TenantContext:
|
57
|
+
"""Load tenant context from database."""
|
58
|
+
# Get all users for this tenant
|
59
|
+
users_query = """
|
60
|
+
SELECT user_id FROM users WHERE tenant_id = $1 AND status = 'active'
|
61
|
+
"""
|
62
|
+
users_result = self.db_node.run(
|
63
|
+
query=users_query, parameters=[tenant_id], result_format="dict"
|
64
|
+
)
|
65
|
+
user_ids = {row["user_id"] for row in users_result.get("data", [])}
|
66
|
+
|
67
|
+
# Get all roles for this tenant
|
68
|
+
roles_query = """
|
69
|
+
SELECT role_id FROM roles WHERE tenant_id = $1 AND is_active = true
|
70
|
+
"""
|
71
|
+
roles_result = self.db_node.run(
|
72
|
+
query=roles_query, parameters=[tenant_id], result_format="dict"
|
73
|
+
)
|
74
|
+
role_ids = {row["role_id"] for row in roles_result.get("data", [])}
|
75
|
+
|
76
|
+
# Get all permissions for this tenant (from roles)
|
77
|
+
permissions_query = """
|
78
|
+
SELECT DISTINCT unnest(
|
79
|
+
CASE
|
80
|
+
WHEN jsonb_typeof(permissions) = 'array'
|
81
|
+
THEN ARRAY(SELECT jsonb_array_elements_text(permissions))
|
82
|
+
ELSE ARRAY[]::text[]
|
83
|
+
END
|
84
|
+
) as permission
|
85
|
+
FROM roles
|
86
|
+
WHERE tenant_id = $1 AND is_active = true
|
87
|
+
"""
|
88
|
+
permissions_result = self.db_node.run(
|
89
|
+
query=permissions_query, parameters=[tenant_id], result_format="dict"
|
90
|
+
)
|
91
|
+
permissions = {row["permission"] for row in permissions_result.get("data", [])}
|
92
|
+
|
93
|
+
# Create resource prefixes for this tenant
|
94
|
+
resource_prefixes = {f"{tenant_id}:*", "*"}
|
95
|
+
|
96
|
+
return TenantContext(
|
97
|
+
tenant_id=tenant_id,
|
98
|
+
permissions=permissions,
|
99
|
+
user_ids=user_ids,
|
100
|
+
role_ids=role_ids,
|
101
|
+
resource_prefixes=resource_prefixes,
|
102
|
+
)
|
103
|
+
|
104
|
+
def validate_user_tenant_access(self, user_id: str, target_tenant_id: str) -> bool:
|
105
|
+
"""
|
106
|
+
Validate that a user has access within a specific tenant.
|
107
|
+
|
108
|
+
Args:
|
109
|
+
user_id: The user ID to check
|
110
|
+
target_tenant_id: The tenant being accessed
|
111
|
+
|
112
|
+
Returns:
|
113
|
+
True if access is allowed, False otherwise
|
114
|
+
"""
|
115
|
+
tenant_context = self.get_tenant_context(target_tenant_id)
|
116
|
+
return user_id in tenant_context.user_ids
|
117
|
+
|
118
|
+
def validate_role_tenant_access(self, role_id: str, target_tenant_id: str) -> bool:
|
119
|
+
"""
|
120
|
+
Validate that a role belongs to a specific tenant.
|
121
|
+
|
122
|
+
Args:
|
123
|
+
role_id: The role ID to check
|
124
|
+
target_tenant_id: The tenant being accessed
|
125
|
+
|
126
|
+
Returns:
|
127
|
+
True if role belongs to tenant, False otherwise
|
128
|
+
"""
|
129
|
+
tenant_context = self.get_tenant_context(target_tenant_id)
|
130
|
+
return role_id in tenant_context.role_ids
|
131
|
+
|
132
|
+
def check_cross_tenant_permission(
|
133
|
+
self,
|
134
|
+
user_id: str,
|
135
|
+
user_tenant_id: str,
|
136
|
+
resource_tenant_id: str,
|
137
|
+
permission: str,
|
138
|
+
) -> bool:
|
139
|
+
"""
|
140
|
+
Check if a user from one tenant can access resources in another tenant.
|
141
|
+
|
142
|
+
Args:
|
143
|
+
user_id: The user attempting access
|
144
|
+
user_tenant_id: The tenant the user belongs to
|
145
|
+
resource_tenant_id: The tenant of the resource being accessed
|
146
|
+
permission: The permission being requested
|
147
|
+
|
148
|
+
Returns:
|
149
|
+
True if cross-tenant access is allowed, False otherwise
|
150
|
+
"""
|
151
|
+
# For now, enforce strict tenant isolation
|
152
|
+
# Users can only access resources in their own tenant
|
153
|
+
if user_tenant_id != resource_tenant_id:
|
154
|
+
logger.debug(
|
155
|
+
f"Cross-tenant access denied: user {user_id} from {user_tenant_id} "
|
156
|
+
f"attempting to access {resource_tenant_id}"
|
157
|
+
)
|
158
|
+
return False
|
159
|
+
|
160
|
+
# Same tenant access - check if user exists in tenant
|
161
|
+
return self.validate_user_tenant_access(user_id, resource_tenant_id)
|
162
|
+
|
163
|
+
def enforce_tenant_isolation(
|
164
|
+
self, user_id: str, user_tenant_id: str, operation_tenant_id: str
|
165
|
+
) -> None:
|
166
|
+
"""
|
167
|
+
Enforce tenant isolation for an operation.
|
168
|
+
|
169
|
+
Args:
|
170
|
+
user_id: The user performing the operation
|
171
|
+
user_tenant_id: The tenant the user belongs to
|
172
|
+
operation_tenant_id: The tenant context for the operation
|
173
|
+
|
174
|
+
Raises:
|
175
|
+
NodeValidationError: If tenant isolation is violated
|
176
|
+
"""
|
177
|
+
if not self.check_cross_tenant_permission(
|
178
|
+
user_id, user_tenant_id, operation_tenant_id, "access"
|
179
|
+
):
|
180
|
+
raise NodeValidationError(
|
181
|
+
f"Tenant isolation violation: user {user_id} from tenant "
|
182
|
+
f"{user_tenant_id} cannot access tenant {operation_tenant_id}"
|
183
|
+
)
|
184
|
+
|
185
|
+
def get_tenant_scoped_permission(
|
186
|
+
self, permission: str, tenant_id: str, resource_id: Optional[str] = None
|
187
|
+
) -> str:
|
188
|
+
"""
|
189
|
+
Create a tenant-scoped permission string.
|
190
|
+
|
191
|
+
Args:
|
192
|
+
permission: Base permission (e.g., "read", "write")
|
193
|
+
tenant_id: Tenant ID
|
194
|
+
resource_id: Optional resource ID
|
195
|
+
|
196
|
+
Returns:
|
197
|
+
Tenant-scoped permission string
|
198
|
+
"""
|
199
|
+
if resource_id:
|
200
|
+
return f"{tenant_id}:{resource_id}:{permission}"
|
201
|
+
else:
|
202
|
+
return f"{tenant_id}:*:{permission}"
|
203
|
+
|
204
|
+
def clear_tenant_cache(self, tenant_id: Optional[str] = None) -> None:
|
205
|
+
"""
|
206
|
+
Clear the tenant context cache.
|
207
|
+
|
208
|
+
Args:
|
209
|
+
tenant_id: Specific tenant to clear, or None to clear all
|
210
|
+
"""
|
211
|
+
if tenant_id:
|
212
|
+
self._tenant_cache.pop(tenant_id, None)
|
213
|
+
else:
|
214
|
+
self._tenant_cache.clear()
|
215
|
+
|
216
|
+
logger.debug(f"Cleared tenant cache for {tenant_id or 'all tenants'}")
|
217
|
+
|
218
|
+
|
219
|
+
def enforce_tenant_boundary(tenant_id_param: str = "tenant_id"):
|
220
|
+
"""
|
221
|
+
Decorator to enforce tenant boundaries on admin node methods.
|
222
|
+
|
223
|
+
Args:
|
224
|
+
tenant_id_param: Name of the parameter containing the tenant ID
|
225
|
+
"""
|
226
|
+
|
227
|
+
def decorator(func):
|
228
|
+
def wrapper(self, *args, **kwargs):
|
229
|
+
# Extract tenant ID from parameters
|
230
|
+
tenant_id = kwargs.get(tenant_id_param)
|
231
|
+
if not tenant_id:
|
232
|
+
raise NodeValidationError(
|
233
|
+
f"Missing required parameter: {tenant_id_param}"
|
234
|
+
)
|
235
|
+
|
236
|
+
# Create tenant isolation manager if not exists
|
237
|
+
if not hasattr(self, "_tenant_isolation"):
|
238
|
+
self._tenant_isolation = TenantIsolationManager(self._db_node)
|
239
|
+
|
240
|
+
# Perform the operation within tenant context
|
241
|
+
try:
|
242
|
+
return func(self, *args, **kwargs)
|
243
|
+
except Exception as e:
|
244
|
+
logger.error(f"Tenant-scoped operation failed for {tenant_id}: {e}")
|
245
|
+
raise
|
246
|
+
|
247
|
+
return wrapper
|
248
|
+
|
249
|
+
return decorator
|
@@ -0,0 +1,244 @@
|
|
1
|
+
"""Transaction utilities for admin nodes to handle timing and persistence issues.
|
2
|
+
|
3
|
+
This module provides utilities to handle common transaction and timing issues
|
4
|
+
encountered in admin node operations, particularly around user creation,
|
5
|
+
role assignment, and permission checks.
|
6
|
+
"""
|
7
|
+
|
8
|
+
import logging
|
9
|
+
import time
|
10
|
+
from typing import Any, Callable, Dict, Optional, TypeVar
|
11
|
+
|
12
|
+
from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
|
13
|
+
|
14
|
+
logger = logging.getLogger(__name__)
|
15
|
+
|
16
|
+
T = TypeVar("T")
|
17
|
+
|
18
|
+
|
19
|
+
class TransactionHelper:
|
20
|
+
"""Helper class for handling database transaction timing and persistence issues."""
|
21
|
+
|
22
|
+
def __init__(self, db_node, max_retries: int = 3, retry_delay: float = 0.1):
|
23
|
+
"""
|
24
|
+
Initialize transaction helper.
|
25
|
+
|
26
|
+
Args:
|
27
|
+
db_node: Database node instance (SQLDatabaseNode)
|
28
|
+
max_retries: Maximum number of retries for transient failures
|
29
|
+
retry_delay: Delay between retries in seconds
|
30
|
+
"""
|
31
|
+
self.db_node = db_node
|
32
|
+
self.max_retries = max_retries
|
33
|
+
self.retry_delay = retry_delay
|
34
|
+
|
35
|
+
def execute_with_retry(self, operation: Callable[[], T], operation_name: str) -> T:
|
36
|
+
"""
|
37
|
+
Execute a database operation with retry logic.
|
38
|
+
|
39
|
+
Args:
|
40
|
+
operation: Function that performs the database operation
|
41
|
+
operation_name: Description of the operation for logging
|
42
|
+
|
43
|
+
Returns:
|
44
|
+
Result of the operation
|
45
|
+
|
46
|
+
Raises:
|
47
|
+
NodeExecutionError: If operation fails after all retries
|
48
|
+
"""
|
49
|
+
last_exception = None
|
50
|
+
|
51
|
+
for attempt in range(self.max_retries):
|
52
|
+
try:
|
53
|
+
result = operation()
|
54
|
+
if attempt > 0:
|
55
|
+
logger.info(f"{operation_name} succeeded on attempt {attempt + 1}")
|
56
|
+
return result
|
57
|
+
except Exception as e:
|
58
|
+
last_exception = e
|
59
|
+
if attempt < self.max_retries - 1:
|
60
|
+
logger.warning(
|
61
|
+
f"{operation_name} failed on attempt {attempt + 1}, retrying: {e}"
|
62
|
+
)
|
63
|
+
time.sleep(self.retry_delay * (2**attempt)) # Exponential backoff
|
64
|
+
else:
|
65
|
+
logger.error(
|
66
|
+
f"{operation_name} failed after {self.max_retries} attempts: {e}"
|
67
|
+
)
|
68
|
+
|
69
|
+
raise NodeExecutionError(
|
70
|
+
f"{operation_name} failed after {self.max_retries} attempts: {last_exception}"
|
71
|
+
)
|
72
|
+
|
73
|
+
def verify_operation_success(
|
74
|
+
self,
|
75
|
+
verification_query: str,
|
76
|
+
expected_result: Any,
|
77
|
+
operation_name: str,
|
78
|
+
timeout_seconds: float = 5.0,
|
79
|
+
) -> bool:
|
80
|
+
"""
|
81
|
+
Verify that a database operation was successful by checking the result.
|
82
|
+
|
83
|
+
Args:
|
84
|
+
verification_query: SQL query to verify the operation
|
85
|
+
expected_result: Expected result from the verification query
|
86
|
+
operation_name: Description of the operation for logging
|
87
|
+
timeout_seconds: Maximum time to wait for verification
|
88
|
+
|
89
|
+
Returns:
|
90
|
+
True if verification succeeds
|
91
|
+
|
92
|
+
Raises:
|
93
|
+
NodeValidationError: If verification fails after timeout
|
94
|
+
"""
|
95
|
+
start_time = time.time()
|
96
|
+
|
97
|
+
while time.time() - start_time < timeout_seconds:
|
98
|
+
try:
|
99
|
+
result = self.db_node.run(
|
100
|
+
query=verification_query, result_format="dict"
|
101
|
+
)
|
102
|
+
data = result.get("data", [])
|
103
|
+
|
104
|
+
if data and len(data) > 0:
|
105
|
+
# Operation was successful
|
106
|
+
logger.debug(f"{operation_name} verification succeeded")
|
107
|
+
return True
|
108
|
+
|
109
|
+
except Exception as e:
|
110
|
+
logger.debug(f"{operation_name} verification error: {e}")
|
111
|
+
|
112
|
+
# Wait before retrying
|
113
|
+
time.sleep(0.05) # 50ms
|
114
|
+
|
115
|
+
raise NodeValidationError(
|
116
|
+
f"{operation_name} verification failed after {timeout_seconds}s"
|
117
|
+
)
|
118
|
+
|
119
|
+
def create_user_with_verification(
|
120
|
+
self, user_data: Dict[str, Any], tenant_id: str
|
121
|
+
) -> Dict[str, Any]:
|
122
|
+
"""
|
123
|
+
Create a user and verify the creation was successful.
|
124
|
+
|
125
|
+
Args:
|
126
|
+
user_data: User data dictionary
|
127
|
+
tenant_id: Tenant ID
|
128
|
+
|
129
|
+
Returns:
|
130
|
+
User creation result
|
131
|
+
"""
|
132
|
+
user_id = user_data.get("user_id")
|
133
|
+
|
134
|
+
def create_operation():
|
135
|
+
# Perform the user creation
|
136
|
+
from .user_management import UserManagementNode
|
137
|
+
|
138
|
+
user_mgmt = UserManagementNode(database_url=self.db_node.connection_string)
|
139
|
+
return user_mgmt.run(
|
140
|
+
operation="create_user", user_data=user_data, tenant_id=tenant_id
|
141
|
+
)
|
142
|
+
|
143
|
+
# Execute creation with retry
|
144
|
+
result = self.execute_with_retry(
|
145
|
+
create_operation, f"User creation for {user_id}"
|
146
|
+
)
|
147
|
+
|
148
|
+
# Verify user was created
|
149
|
+
verification_query = """
|
150
|
+
SELECT user_id FROM users
|
151
|
+
WHERE user_id = $1 AND tenant_id = $2
|
152
|
+
"""
|
153
|
+
|
154
|
+
self.verify_operation_success(
|
155
|
+
verification_query,
|
156
|
+
user_id,
|
157
|
+
f"User {user_id} creation verification",
|
158
|
+
timeout_seconds=2.0,
|
159
|
+
)
|
160
|
+
|
161
|
+
return result
|
162
|
+
|
163
|
+
def assign_role_with_verification(
|
164
|
+
self, user_id: str, role_id: str, tenant_id: str
|
165
|
+
) -> Dict[str, Any]:
|
166
|
+
"""
|
167
|
+
Assign a role to a user and verify the assignment was successful.
|
168
|
+
|
169
|
+
Args:
|
170
|
+
user_id: User ID
|
171
|
+
role_id: Role ID
|
172
|
+
tenant_id: Tenant ID
|
173
|
+
|
174
|
+
Returns:
|
175
|
+
Role assignment result
|
176
|
+
"""
|
177
|
+
|
178
|
+
def assign_operation():
|
179
|
+
from .role_management import RoleManagementNode
|
180
|
+
|
181
|
+
role_mgmt = RoleManagementNode(database_url=self.db_node.connection_string)
|
182
|
+
return role_mgmt.run(
|
183
|
+
operation="assign_user",
|
184
|
+
user_id=user_id,
|
185
|
+
role_id=role_id,
|
186
|
+
tenant_id=tenant_id,
|
187
|
+
)
|
188
|
+
|
189
|
+
# Execute assignment with retry
|
190
|
+
result = self.execute_with_retry(
|
191
|
+
assign_operation, f"Role assignment {role_id} to {user_id}"
|
192
|
+
)
|
193
|
+
|
194
|
+
# Verify role was assigned
|
195
|
+
verification_query = """
|
196
|
+
SELECT user_id, role_id FROM user_role_assignments
|
197
|
+
WHERE user_id = $1 AND role_id = $2 AND tenant_id = $3 AND is_active = true
|
198
|
+
"""
|
199
|
+
|
200
|
+
self.verify_operation_success(
|
201
|
+
verification_query,
|
202
|
+
{"user_id": user_id, "role_id": role_id},
|
203
|
+
f"Role assignment {role_id} to {user_id} verification",
|
204
|
+
timeout_seconds=2.0,
|
205
|
+
)
|
206
|
+
|
207
|
+
return result
|
208
|
+
|
209
|
+
|
210
|
+
def with_transaction_retry(max_retries: int = 3, retry_delay: float = 0.1):
|
211
|
+
"""
|
212
|
+
Decorator to add retry logic to admin node operations.
|
213
|
+
|
214
|
+
Args:
|
215
|
+
max_retries: Maximum number of retries
|
216
|
+
retry_delay: Initial delay between retries
|
217
|
+
"""
|
218
|
+
|
219
|
+
def decorator(func):
|
220
|
+
def wrapper(*args, **kwargs):
|
221
|
+
last_exception = None
|
222
|
+
|
223
|
+
for attempt in range(max_retries):
|
224
|
+
try:
|
225
|
+
return func(*args, **kwargs)
|
226
|
+
except Exception as e:
|
227
|
+
last_exception = e
|
228
|
+
if attempt < max_retries - 1:
|
229
|
+
logger.warning(
|
230
|
+
f"{func.__name__} failed on attempt {attempt + 1}, retrying: {e}"
|
231
|
+
)
|
232
|
+
time.sleep(retry_delay * (2**attempt))
|
233
|
+
else:
|
234
|
+
logger.error(
|
235
|
+
f"{func.__name__} failed after {max_retries} attempts: {e}"
|
236
|
+
)
|
237
|
+
|
238
|
+
raise NodeExecutionError(
|
239
|
+
f"{func.__name__} failed after {max_retries} attempts: {last_exception}"
|
240
|
+
)
|
241
|
+
|
242
|
+
return wrapper
|
243
|
+
|
244
|
+
return decorator
|
@@ -25,6 +25,8 @@ from enum import Enum
|
|
25
25
|
from typing import Any, Dict, List, Optional, Set, Union
|
26
26
|
from uuid import uuid4
|
27
27
|
|
28
|
+
import bcrypt
|
29
|
+
|
28
30
|
from kailash.nodes.base import Node, NodeParameter, register_node
|
29
31
|
from kailash.nodes.data import SQLDatabaseNode
|
30
32
|
from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
|
@@ -32,6 +34,25 @@ from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
|
|
32
34
|
from .schema_manager import AdminSchemaManager
|
33
35
|
|
34
36
|
|
37
|
+
def hash_password(password: str) -> str:
|
38
|
+
"""Hash password using bcrypt with salt."""
|
39
|
+
if not password:
|
40
|
+
return ""
|
41
|
+
salt = bcrypt.gensalt()
|
42
|
+
hashed = bcrypt.hashpw(password.encode("utf-8"), salt)
|
43
|
+
return hashed.decode("utf-8")
|
44
|
+
|
45
|
+
|
46
|
+
def verify_password(password: str, hashed: str) -> bool:
|
47
|
+
"""Verify password against bcrypt hash."""
|
48
|
+
if not password or not hashed:
|
49
|
+
return False
|
50
|
+
try:
|
51
|
+
return bcrypt.checkpw(password.encode("utf-8"), hashed.encode("utf-8"))
|
52
|
+
except Exception:
|
53
|
+
return False
|
54
|
+
|
55
|
+
|
35
56
|
def parse_datetime(value: Union[str, datetime, None]) -> Optional[datetime]:
|
36
57
|
"""Parse datetime from various formats."""
|
37
58
|
if value is None:
|
@@ -496,7 +517,7 @@ class UserManagementNode(Node):
|
|
496
517
|
user.user_id,
|
497
518
|
user.email,
|
498
519
|
user.username,
|
499
|
-
inputs.get("
|
520
|
+
hash_password(inputs.get("password", "")),
|
500
521
|
user.first_name,
|
501
522
|
user.last_name,
|
502
523
|
user.display_name,
|
@@ -509,12 +530,15 @@ class UserManagementNode(Node):
|
|
509
530
|
],
|
510
531
|
)
|
511
532
|
|
512
|
-
#
|
513
|
-
|
533
|
+
# Return the user data that was successfully inserted
|
534
|
+
# Add timestamps that would be set by the database
|
535
|
+
user_dict = user.to_dict()
|
536
|
+
user_dict["created_at"] = datetime.now(UTC).isoformat()
|
537
|
+
user_dict["updated_at"] = datetime.now(UTC).isoformat()
|
514
538
|
|
515
539
|
return {
|
516
540
|
"result": {
|
517
|
-
"user":
|
541
|
+
"user": user_dict,
|
518
542
|
"operation": "create_user",
|
519
543
|
"timestamp": datetime.now(UTC).isoformat(),
|
520
544
|
}
|
@@ -918,7 +942,8 @@ class UserManagementNode(Node):
|
|
918
942
|
"""Set user password hash."""
|
919
943
|
user_id = inputs["user_id"]
|
920
944
|
tenant_id = inputs["tenant_id"]
|
921
|
-
|
945
|
+
password = inputs.get("password", "")
|
946
|
+
password_hash = hash_password(password)
|
922
947
|
|
923
948
|
update_query = """
|
924
949
|
UPDATE users
|
@@ -964,9 +989,13 @@ class UserManagementNode(Node):
|
|
964
989
|
for i, user_data in enumerate(users_data):
|
965
990
|
try:
|
966
991
|
# Create each user individually for better error handling
|
992
|
+
# Extract password from user_data if present
|
993
|
+
user_data_copy = user_data.copy()
|
994
|
+
password = user_data_copy.pop("password", "")
|
967
995
|
create_inputs = {
|
968
996
|
"operation": "create_user",
|
969
|
-
"user_data":
|
997
|
+
"user_data": user_data_copy,
|
998
|
+
"password": password,
|
970
999
|
"tenant_id": tenant_id,
|
971
1000
|
"database_config": inputs["database_config"],
|
972
1001
|
}
|
@@ -1370,7 +1399,7 @@ class UserManagementNode(Node):
|
|
1370
1399
|
user_id = result["data"][0]["user_id"]
|
1371
1400
|
|
1372
1401
|
# Update password
|
1373
|
-
password_hash =
|
1402
|
+
password_hash = hash_password(new_password)
|
1374
1403
|
update_query = """
|
1375
1404
|
UPDATE users
|
1376
1405
|
SET password_hash = :password_hash,
|
@@ -1441,9 +1470,8 @@ class UserManagementNode(Node):
|
|
1441
1470
|
|
1442
1471
|
user_data = result["data"][0]
|
1443
1472
|
stored_hash = user_data["password_hash"]
|
1444
|
-
provided_hash = hashlib.sha256(password.encode()).hexdigest()
|
1445
1473
|
|
1446
|
-
if
|
1474
|
+
if not verify_password(password, stored_hash):
|
1447
1475
|
return {"authenticated": False, "message": "Invalid password"}
|
1448
1476
|
|
1449
1477
|
if user_data["status"] != "active":
|