kailash 0.4.2__py3-none-any.whl → 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.
- kailash/__init__.py +1 -1
- kailash/client/__init__.py +12 -0
- kailash/client/enhanced_client.py +306 -0
- kailash/core/actors/__init__.py +16 -0
- kailash/core/actors/connection_actor.py +566 -0
- kailash/core/actors/supervisor.py +364 -0
- kailash/edge/__init__.py +16 -0
- kailash/edge/compliance.py +834 -0
- kailash/edge/discovery.py +659 -0
- kailash/edge/location.py +582 -0
- kailash/gateway/__init__.py +33 -0
- kailash/gateway/api.py +289 -0
- kailash/gateway/enhanced_gateway.py +357 -0
- kailash/gateway/resource_resolver.py +217 -0
- kailash/gateway/security.py +227 -0
- kailash/middleware/auth/models.py +2 -2
- kailash/middleware/database/base_models.py +1 -7
- kailash/middleware/database/repositories.py +3 -1
- kailash/middleware/gateway/__init__.py +22 -0
- kailash/middleware/gateway/checkpoint_manager.py +398 -0
- kailash/middleware/gateway/deduplicator.py +382 -0
- kailash/middleware/gateway/durable_gateway.py +417 -0
- kailash/middleware/gateway/durable_request.py +498 -0
- kailash/middleware/gateway/event_store.py +459 -0
- kailash/nodes/admin/audit_log.py +364 -6
- kailash/nodes/admin/permission_check.py +817 -33
- kailash/nodes/admin/role_management.py +1242 -108
- kailash/nodes/admin/schema_manager.py +438 -0
- kailash/nodes/admin/user_management.py +1209 -681
- kailash/nodes/api/http.py +95 -71
- kailash/nodes/base.py +281 -164
- kailash/nodes/base_async.py +30 -31
- kailash/nodes/code/__init__.py +8 -1
- kailash/nodes/code/async_python.py +1035 -0
- kailash/nodes/code/python.py +1 -0
- kailash/nodes/data/async_sql.py +12 -25
- kailash/nodes/data/sql.py +20 -11
- kailash/nodes/data/workflow_connection_pool.py +643 -0
- kailash/nodes/rag/__init__.py +1 -4
- kailash/resources/__init__.py +40 -0
- kailash/resources/factory.py +533 -0
- kailash/resources/health.py +319 -0
- kailash/resources/reference.py +288 -0
- kailash/resources/registry.py +392 -0
- kailash/runtime/async_local.py +711 -302
- kailash/testing/__init__.py +34 -0
- kailash/testing/async_test_case.py +353 -0
- kailash/testing/async_utils.py +345 -0
- kailash/testing/fixtures.py +458 -0
- kailash/testing/mock_registry.py +495 -0
- kailash/utils/resource_manager.py +420 -0
- kailash/workflow/__init__.py +8 -0
- kailash/workflow/async_builder.py +621 -0
- kailash/workflow/async_patterns.py +766 -0
- kailash/workflow/builder.py +93 -10
- kailash/workflow/cyclic_runner.py +111 -41
- kailash/workflow/graph.py +7 -2
- kailash/workflow/resilience.py +11 -1
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/METADATA +12 -7
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/RECORD +64 -28
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/top_level.txt +0 -0
@@ -1,76 +1,88 @@
|
|
1
|
-
"""Enterprise user management node with
|
1
|
+
"""Enterprise user management node with complete user lifecycle support.
|
2
2
|
|
3
|
-
This node provides
|
4
|
-
|
5
|
-
|
3
|
+
This node provides comprehensive user management capabilities for the unified admin system,
|
4
|
+
including user creation, update, deletion, and lifecycle management. Integrates seamlessly
|
5
|
+
with RoleManagementNode and PermissionCheckNode for complete RBAC/ABAC functionality.
|
6
6
|
|
7
7
|
Features:
|
8
|
-
- Complete user lifecycle (
|
9
|
-
-
|
10
|
-
- Password management with security policies
|
11
|
-
- User attribute management for ABAC
|
8
|
+
- Complete user lifecycle management (CRUD operations)
|
9
|
+
- JSONB-based role and attribute management
|
12
10
|
- Multi-tenant user isolation
|
13
|
-
-
|
11
|
+
- User authentication and session management
|
12
|
+
- Bulk user operations for enterprise scenarios
|
13
|
+
- User profile and metadata management
|
14
14
|
- Integration with external identity providers
|
15
|
-
-
|
15
|
+
- Comprehensive audit logging
|
16
|
+
- User status and lifecycle tracking
|
16
17
|
"""
|
17
18
|
|
18
19
|
import hashlib
|
19
20
|
import json
|
20
|
-
import
|
21
|
+
import logging
|
21
22
|
from dataclasses import dataclass
|
22
23
|
from datetime import UTC, datetime, timedelta
|
23
24
|
from enum import Enum
|
24
|
-
from typing import Any, Dict, List, Optional, Union
|
25
|
+
from typing import Any, Dict, List, Optional, Set, Union
|
26
|
+
from uuid import uuid4
|
25
27
|
|
26
|
-
from kailash.access_control import AccessControlManager, UserContext
|
27
28
|
from kailash.nodes.base import Node, NodeParameter, register_node
|
28
|
-
from kailash.nodes.data import
|
29
|
+
from kailash.nodes.data import SQLDatabaseNode
|
29
30
|
from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
|
30
31
|
|
32
|
+
from .schema_manager import AdminSchemaManager
|
31
33
|
|
32
|
-
|
33
|
-
|
34
|
-
"""
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
42
|
-
|
43
|
-
|
44
|
-
|
45
|
-
|
46
|
-
"
|
47
|
-
"
|
48
|
-
"
|
49
|
-
"
|
50
|
-
|
34
|
+
|
35
|
+
def parse_datetime(value: Union[str, datetime, None]) -> Optional[datetime]:
|
36
|
+
"""Parse datetime from various formats."""
|
37
|
+
if value is None:
|
38
|
+
return None
|
39
|
+
if isinstance(value, datetime):
|
40
|
+
return value
|
41
|
+
if isinstance(value, str):
|
42
|
+
try:
|
43
|
+
# Try ISO format first
|
44
|
+
return datetime.fromisoformat(value.replace("Z", "+00:00"))
|
45
|
+
except ValueError:
|
46
|
+
# Try other common formats
|
47
|
+
for fmt in [
|
48
|
+
"%Y-%m-%d %H:%M:%S.%f",
|
49
|
+
"%Y-%m-%d %H:%M:%S",
|
50
|
+
"%Y-%m-%dT%H:%M:%S.%f",
|
51
|
+
"%Y-%m-%dT%H:%M:%S",
|
52
|
+
]:
|
53
|
+
try:
|
54
|
+
return datetime.strptime(value, fmt)
|
55
|
+
except ValueError:
|
56
|
+
continue
|
57
|
+
return None
|
51
58
|
|
52
59
|
|
53
60
|
class UserOperation(Enum):
|
54
61
|
"""Supported user management operations."""
|
55
62
|
|
56
|
-
|
57
|
-
|
58
|
-
|
59
|
-
|
60
|
-
|
61
|
-
|
62
|
-
|
63
|
+
CREATE_USER = "create_user"
|
64
|
+
UPDATE_USER = "update_user"
|
65
|
+
DELETE_USER = "delete_user"
|
66
|
+
GET_USER = "get_user"
|
67
|
+
LIST_USERS = "list_users"
|
68
|
+
ACTIVATE_USER = "activate_user"
|
69
|
+
DEACTIVATE_USER = "deactivate_user"
|
70
|
+
SET_PASSWORD = "set_password"
|
71
|
+
UPDATE_PROFILE = "update_profile"
|
63
72
|
BULK_CREATE = "bulk_create"
|
64
73
|
BULK_UPDATE = "bulk_update"
|
65
74
|
BULK_DELETE = "bulk_delete"
|
66
|
-
|
75
|
+
GET_USER_ROLES = "get_user_roles"
|
76
|
+
GET_USER_PERMISSIONS = "get_user_permissions"
|
77
|
+
SEARCH_USERS = "search_users"
|
78
|
+
EXPORT_USERS = "export_users"
|
79
|
+
GENERATE_RESET_TOKEN = "generate_reset_token"
|
67
80
|
RESET_PASSWORD = "reset_password"
|
68
|
-
|
69
|
-
ACTIVATE = "activate"
|
81
|
+
AUTHENTICATE = "authenticate"
|
70
82
|
|
71
83
|
|
72
84
|
class UserStatus(Enum):
|
73
|
-
"""User
|
85
|
+
"""User status values."""
|
74
86
|
|
75
87
|
ACTIVE = "active"
|
76
88
|
INACTIVE = "inactive"
|
@@ -80,110 +92,134 @@ class UserStatus(Enum):
|
|
80
92
|
|
81
93
|
|
82
94
|
@dataclass
|
83
|
-
class
|
84
|
-
"""
|
95
|
+
class User:
|
96
|
+
"""Complete user definition with all attributes."""
|
85
97
|
|
86
98
|
user_id: str
|
87
99
|
email: str
|
88
|
-
username: str
|
89
|
-
first_name: str
|
90
|
-
last_name: str
|
91
|
-
|
100
|
+
username: Optional[str]
|
101
|
+
first_name: Optional[str]
|
102
|
+
last_name: Optional[str]
|
103
|
+
display_name: Optional[str]
|
92
104
|
roles: List[str]
|
93
105
|
attributes: Dict[str, Any]
|
94
|
-
|
95
|
-
|
96
|
-
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
|
103
|
-
|
104
|
-
|
105
|
-
|
106
|
-
|
107
|
-
|
108
|
-
|
106
|
+
status: UserStatus
|
107
|
+
tenant_id: str
|
108
|
+
external_auth_id: Optional[str] = None
|
109
|
+
auth_provider: str = "local"
|
110
|
+
created_at: Optional[datetime] = None
|
111
|
+
updated_at: Optional[datetime] = None
|
112
|
+
last_login_at: Optional[datetime] = None
|
113
|
+
|
114
|
+
def to_dict(self) -> Dict[str, Any]:
|
115
|
+
"""Convert to dictionary for JSON serialization."""
|
116
|
+
|
117
|
+
def format_datetime(dt: Union[datetime, str, None]) -> Optional[str]:
|
118
|
+
"""Format datetime handling both datetime objects and strings."""
|
119
|
+
if dt is None:
|
120
|
+
return None
|
121
|
+
if isinstance(dt, datetime):
|
122
|
+
return dt.isoformat()
|
123
|
+
if isinstance(dt, str):
|
124
|
+
# Already a string, return as-is or try to parse and format
|
125
|
+
parsed = parse_datetime(dt)
|
126
|
+
return parsed.isoformat() if parsed else dt
|
127
|
+
return None
|
128
|
+
|
129
|
+
return {
|
130
|
+
"user_id": self.user_id,
|
131
|
+
"email": self.email,
|
132
|
+
"username": self.username,
|
133
|
+
"first_name": self.first_name,
|
134
|
+
"last_name": self.last_name,
|
135
|
+
"display_name": self.display_name,
|
136
|
+
"roles": self.roles,
|
137
|
+
"attributes": self.attributes,
|
138
|
+
"status": self.status.value,
|
139
|
+
"tenant_id": self.tenant_id,
|
140
|
+
"external_auth_id": self.external_auth_id,
|
141
|
+
"auth_provider": self.auth_provider,
|
142
|
+
"created_at": format_datetime(self.created_at),
|
143
|
+
"updated_at": format_datetime(self.updated_at),
|
144
|
+
"last_login_at": format_datetime(self.last_login_at),
|
145
|
+
}
|
109
146
|
|
110
147
|
|
111
148
|
@register_node()
|
112
149
|
class UserManagementNode(Node):
|
113
|
-
"""Enterprise user management node with
|
150
|
+
"""Enterprise user management node with complete lifecycle support.
|
114
151
|
|
115
152
|
This node provides comprehensive user management capabilities including:
|
116
|
-
-
|
117
|
-
-
|
118
|
-
-
|
119
|
-
- User
|
120
|
-
-
|
121
|
-
-
|
122
|
-
-
|
153
|
+
- Complete user CRUD operations
|
154
|
+
- JSONB-based role and attribute management
|
155
|
+
- Multi-tenant user isolation
|
156
|
+
- User profile and metadata management
|
157
|
+
- Bulk operations for enterprise scenarios
|
158
|
+
- Integration with authentication systems
|
159
|
+
- Comprehensive audit logging
|
123
160
|
|
124
161
|
Parameters:
|
125
|
-
operation: Type of
|
162
|
+
operation: Type of user management operation
|
163
|
+
user_id: User ID for single user operations
|
126
164
|
user_data: User data for create/update operations
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
filters: Filters for user listing
|
131
|
-
pagination: Pagination parameters
|
165
|
+
users_data: List of user data for bulk operations
|
166
|
+
email: Email address for user lookup
|
167
|
+
username: Username for user lookup
|
132
168
|
tenant_id: Tenant isolation
|
133
|
-
|
169
|
+
status: User status filter
|
170
|
+
search_query: Search query for user search
|
171
|
+
limit: Result limit for list operations
|
172
|
+
offset: Result offset for pagination
|
173
|
+
include_deleted: Whether to include deleted users
|
174
|
+
export_format: Format for user export
|
134
175
|
|
135
176
|
Example:
|
136
|
-
>>> # Create new user
|
177
|
+
>>> # Create a new user
|
137
178
|
>>> node = UserManagementNode(
|
138
|
-
... operation="
|
179
|
+
... operation="create_user",
|
139
180
|
... user_data={
|
140
|
-
... "email": "john@company.com",
|
141
|
-
... "username": "
|
181
|
+
... "email": "john.doe@company.com",
|
182
|
+
... "username": "johndoe",
|
142
183
|
... "first_name": "John",
|
143
|
-
... "last_name": "
|
144
|
-
... "roles": ["
|
145
|
-
... "attributes": {
|
146
|
-
...
|
147
|
-
...
|
148
|
-
... }
|
149
|
-
... }
|
184
|
+
... "last_name": "Doe",
|
185
|
+
... "roles": ["employee", "developer"],
|
186
|
+
... "attributes": {"department": "engineering", "level": "senior"}
|
187
|
+
... },
|
188
|
+
... tenant_id="company"
|
150
189
|
... )
|
151
190
|
>>> result = node.run()
|
152
191
|
>>> user_id = result["user"]["user_id"]
|
153
192
|
|
154
|
-
>>> #
|
193
|
+
>>> # Update user profile
|
194
|
+
>>> node = UserManagementNode(
|
195
|
+
... operation="update_profile",
|
196
|
+
... user_id="user123",
|
197
|
+
... user_data={
|
198
|
+
... "display_name": "John D. Smith",
|
199
|
+
... "attributes": {"department": "engineering", "level": "lead"}
|
200
|
+
... },
|
201
|
+
... tenant_id="company"
|
202
|
+
... )
|
203
|
+
>>> result = node.run()
|
204
|
+
|
205
|
+
>>> # Bulk create users
|
155
206
|
>>> node = UserManagementNode(
|
156
207
|
... operation="bulk_create",
|
157
|
-
...
|
158
|
-
... {"email": "user1@company.com",
|
159
|
-
... {"email": "user2@company.com",
|
160
|
-
... ]
|
208
|
+
... users_data=[
|
209
|
+
... {"email": "user1@company.com", "roles": ["employee"]},
|
210
|
+
... {"email": "user2@company.com", "roles": ["manager"]},
|
211
|
+
... ],
|
212
|
+
... tenant_id="company"
|
161
213
|
... )
|
162
214
|
>>> result = node.run()
|
163
|
-
>>> created_count = result["
|
215
|
+
>>> created_count = result["bulk_result"]["created_count"]
|
164
216
|
"""
|
165
217
|
|
166
218
|
def __init__(self, **config):
|
167
219
|
super().__init__(**config)
|
168
220
|
self._db_node = None
|
169
|
-
self.
|
170
|
-
self.
|
171
|
-
abac_enabled=config.get("abac_enabled", True),
|
172
|
-
audit_enabled=config.get("audit_enabled", True),
|
173
|
-
multi_tenant=config.get("multi_tenant", True),
|
174
|
-
password_policy=config.get(
|
175
|
-
"password_policy",
|
176
|
-
{
|
177
|
-
"min_length": 8,
|
178
|
-
"require_uppercase": True,
|
179
|
-
"require_lowercase": True,
|
180
|
-
"require_numbers": True,
|
181
|
-
"require_special": False,
|
182
|
-
"history_count": 3,
|
183
|
-
},
|
184
|
-
),
|
185
|
-
)
|
186
|
-
self._audit_logger = None
|
221
|
+
self._schema_manager = None
|
222
|
+
self.logger = logging.getLogger(__name__)
|
187
223
|
|
188
224
|
def get_parameters(self) -> Dict[str, NodeParameter]:
|
189
225
|
"""Define parameters for user management operations."""
|
@@ -198,52 +234,72 @@ class UserManagementNode(Node):
|
|
198
234
|
description="User management operation to perform",
|
199
235
|
choices=[op.value for op in UserOperation],
|
200
236
|
),
|
201
|
-
# User
|
237
|
+
# User identification
|
238
|
+
NodeParameter(
|
239
|
+
name="user_id",
|
240
|
+
type=str,
|
241
|
+
required=False,
|
242
|
+
description="User ID for single user operations",
|
243
|
+
),
|
244
|
+
NodeParameter(
|
245
|
+
name="email",
|
246
|
+
type=str,
|
247
|
+
required=False,
|
248
|
+
description="Email address for user lookup",
|
249
|
+
),
|
250
|
+
NodeParameter(
|
251
|
+
name="username",
|
252
|
+
type=str,
|
253
|
+
required=False,
|
254
|
+
description="Username for user lookup",
|
255
|
+
),
|
256
|
+
# User data
|
202
257
|
NodeParameter(
|
203
258
|
name="user_data",
|
204
259
|
type=dict,
|
205
260
|
required=False,
|
206
261
|
description="User data for create/update operations",
|
207
262
|
),
|
208
|
-
# Single user operations
|
209
263
|
NodeParameter(
|
210
|
-
name="
|
211
|
-
type=
|
264
|
+
name="users_data",
|
265
|
+
type=list,
|
212
266
|
required=False,
|
213
|
-
description="
|
267
|
+
description="List of user data for bulk operations",
|
214
268
|
),
|
215
|
-
# Bulk operations
|
216
269
|
NodeParameter(
|
217
270
|
name="user_ids",
|
218
271
|
type=list,
|
219
272
|
required=False,
|
220
|
-
description="List of user IDs for bulk operations",
|
273
|
+
description="List of user IDs for bulk delete operations",
|
221
274
|
),
|
222
|
-
#
|
275
|
+
# Filtering and search
|
223
276
|
NodeParameter(
|
224
|
-
name="
|
277
|
+
name="status",
|
225
278
|
type=str,
|
226
279
|
required=False,
|
227
|
-
|
280
|
+
choices=[status.value for status in UserStatus],
|
281
|
+
description="User status filter",
|
228
282
|
),
|
229
283
|
NodeParameter(
|
230
|
-
name="
|
231
|
-
type=
|
284
|
+
name="search_query",
|
285
|
+
type=str,
|
232
286
|
required=False,
|
233
|
-
description="
|
287
|
+
description="Search query for user search",
|
234
288
|
),
|
289
|
+
# Pagination
|
235
290
|
NodeParameter(
|
236
|
-
name="
|
237
|
-
type=
|
291
|
+
name="limit",
|
292
|
+
type=int,
|
238
293
|
required=False,
|
239
|
-
|
294
|
+
default=50,
|
295
|
+
description="Result limit for list operations",
|
240
296
|
),
|
241
|
-
# Multi-tenancy
|
242
297
|
NodeParameter(
|
243
|
-
name="
|
244
|
-
type=
|
298
|
+
name="offset",
|
299
|
+
type=int,
|
245
300
|
required=False,
|
246
|
-
|
301
|
+
default=0,
|
302
|
+
description="Result offset for pagination",
|
247
303
|
),
|
248
304
|
# Options
|
249
305
|
NodeParameter(
|
@@ -251,102 +307,106 @@ class UserManagementNode(Node):
|
|
251
307
|
type=bool,
|
252
308
|
required=False,
|
253
309
|
default=False,
|
254
|
-
description="
|
310
|
+
description="Whether to include deleted users",
|
255
311
|
),
|
256
|
-
# Database configuration
|
257
312
|
NodeParameter(
|
258
|
-
name="
|
259
|
-
type=
|
313
|
+
name="export_format",
|
314
|
+
type=str,
|
260
315
|
required=False,
|
261
|
-
|
316
|
+
default="json",
|
317
|
+
choices=["json", "csv"],
|
318
|
+
description="Format for user export",
|
262
319
|
),
|
263
|
-
# Password
|
320
|
+
# Password reset parameters
|
264
321
|
NodeParameter(
|
265
|
-
name="
|
322
|
+
name="token",
|
266
323
|
type=str,
|
267
324
|
required=False,
|
268
|
-
description="Password
|
325
|
+
description="Password reset token",
|
269
326
|
),
|
270
327
|
NodeParameter(
|
271
|
-
name="
|
272
|
-
type=
|
328
|
+
name="new_password",
|
329
|
+
type=str,
|
273
330
|
required=False,
|
274
|
-
|
275
|
-
description="Force password change on next login",
|
331
|
+
description="New password for reset",
|
276
332
|
),
|
277
|
-
# Validation options
|
278
333
|
NodeParameter(
|
279
|
-
name="
|
280
|
-
type=
|
334
|
+
name="password",
|
335
|
+
type=str,
|
281
336
|
required=False,
|
282
|
-
|
283
|
-
description="Validate email format and uniqueness",
|
337
|
+
description="Password for authentication",
|
284
338
|
),
|
339
|
+
# Security
|
285
340
|
NodeParameter(
|
286
|
-
name="
|
287
|
-
type=
|
341
|
+
name="password_hash",
|
342
|
+
type=str,
|
288
343
|
required=False,
|
289
|
-
|
290
|
-
|
344
|
+
description="Password hash for user creation/update",
|
345
|
+
),
|
346
|
+
# Multi-tenancy
|
347
|
+
NodeParameter(
|
348
|
+
name="tenant_id",
|
349
|
+
type=str,
|
350
|
+
required=True,
|
351
|
+
description="Tenant ID for multi-tenant isolation",
|
352
|
+
),
|
353
|
+
# Database configuration
|
354
|
+
NodeParameter(
|
355
|
+
name="database_config",
|
356
|
+
type=dict,
|
357
|
+
required=True,
|
358
|
+
description="Database connection configuration",
|
291
359
|
),
|
292
360
|
]
|
293
361
|
}
|
294
362
|
|
295
363
|
def run(self, **inputs) -> Dict[str, Any]:
|
296
|
-
"""Execute user management operation
|
297
|
-
import asyncio
|
298
|
-
|
299
|
-
try:
|
300
|
-
# Check if we're already in an event loop
|
301
|
-
loop = asyncio.get_running_loop()
|
302
|
-
# If we are, we need to handle this differently
|
303
|
-
import concurrent.futures
|
304
|
-
|
305
|
-
# Run in a thread pool to avoid blocking the event loop
|
306
|
-
with concurrent.futures.ThreadPoolExecutor() as executor:
|
307
|
-
future = executor.submit(asyncio.run, self.async_run(**inputs))
|
308
|
-
return future.result()
|
309
|
-
except RuntimeError:
|
310
|
-
# No event loop running, we can use asyncio.run()
|
311
|
-
return asyncio.run(self.async_run(**inputs))
|
312
|
-
|
313
|
-
async def async_run(self, **inputs) -> Dict[str, Any]:
|
314
|
-
"""Execute user management operation asynchronously."""
|
364
|
+
"""Execute user management operation."""
|
315
365
|
try:
|
316
366
|
operation = UserOperation(inputs["operation"])
|
317
367
|
|
318
|
-
# Initialize
|
368
|
+
# Initialize dependencies
|
319
369
|
self._init_dependencies(inputs)
|
320
370
|
|
321
371
|
# Route to appropriate operation
|
322
|
-
if operation == UserOperation.
|
323
|
-
return
|
324
|
-
elif operation == UserOperation.
|
325
|
-
return
|
326
|
-
elif operation == UserOperation.
|
327
|
-
return
|
328
|
-
elif operation == UserOperation.
|
329
|
-
return
|
330
|
-
elif operation == UserOperation.
|
331
|
-
return
|
332
|
-
elif operation == UserOperation.
|
333
|
-
return
|
334
|
-
elif operation == UserOperation.
|
335
|
-
return
|
372
|
+
if operation == UserOperation.CREATE_USER:
|
373
|
+
return self._create_user(inputs)
|
374
|
+
elif operation == UserOperation.UPDATE_USER:
|
375
|
+
return self._update_user(inputs)
|
376
|
+
elif operation == UserOperation.DELETE_USER:
|
377
|
+
return self._delete_user(inputs)
|
378
|
+
elif operation == UserOperation.GET_USER:
|
379
|
+
return self._get_user(inputs)
|
380
|
+
elif operation == UserOperation.LIST_USERS:
|
381
|
+
return self._list_users(inputs)
|
382
|
+
elif operation == UserOperation.ACTIVATE_USER:
|
383
|
+
return self._activate_user(inputs)
|
384
|
+
elif operation == UserOperation.DEACTIVATE_USER:
|
385
|
+
return self._deactivate_user(inputs)
|
386
|
+
elif operation == UserOperation.SET_PASSWORD:
|
387
|
+
return self._set_password(inputs)
|
388
|
+
elif operation == UserOperation.UPDATE_PROFILE:
|
389
|
+
return self._update_profile(inputs)
|
336
390
|
elif operation == UserOperation.BULK_CREATE:
|
337
|
-
return
|
391
|
+
return self._bulk_create(inputs)
|
338
392
|
elif operation == UserOperation.BULK_UPDATE:
|
339
|
-
return
|
393
|
+
return self._bulk_update(inputs)
|
340
394
|
elif operation == UserOperation.BULK_DELETE:
|
341
|
-
return
|
342
|
-
elif operation == UserOperation.
|
343
|
-
return
|
395
|
+
return self._bulk_delete(inputs)
|
396
|
+
elif operation == UserOperation.GET_USER_ROLES:
|
397
|
+
return self._get_user_roles(inputs)
|
398
|
+
elif operation == UserOperation.GET_USER_PERMISSIONS:
|
399
|
+
return self._get_user_permissions(inputs)
|
400
|
+
elif operation == UserOperation.SEARCH_USERS:
|
401
|
+
return self._search_users(inputs)
|
402
|
+
elif operation == UserOperation.EXPORT_USERS:
|
403
|
+
return self._export_users(inputs)
|
404
|
+
elif operation == UserOperation.GENERATE_RESET_TOKEN:
|
405
|
+
return self._generate_reset_token(inputs)
|
344
406
|
elif operation == UserOperation.RESET_PASSWORD:
|
345
|
-
return
|
346
|
-
elif operation == UserOperation.
|
347
|
-
return
|
348
|
-
elif operation == UserOperation.ACTIVATE:
|
349
|
-
return await self._activate_user_async(inputs)
|
407
|
+
return self._reset_password(inputs)
|
408
|
+
elif operation == UserOperation.AUTHENTICATE:
|
409
|
+
return self._authenticate(inputs)
|
350
410
|
else:
|
351
411
|
raise NodeExecutionError(f"Unknown operation: {operation}")
|
352
412
|
|
@@ -354,591 +414,1059 @@ class UserManagementNode(Node):
|
|
354
414
|
raise NodeExecutionError(f"User management operation failed: {str(e)}")
|
355
415
|
|
356
416
|
def _init_dependencies(self, inputs: Dict[str, Any]):
|
357
|
-
"""Initialize database and
|
358
|
-
#
|
359
|
-
|
360
|
-
|
361
|
-
|
362
|
-
|
363
|
-
"host": "localhost",
|
364
|
-
"port": 5432,
|
365
|
-
"database": "kailash_admin",
|
366
|
-
"user": "admin",
|
367
|
-
"password": "admin",
|
368
|
-
},
|
369
|
-
)
|
417
|
+
"""Initialize database and schema manager dependencies."""
|
418
|
+
# Skip initialization if already initialized (for testing)
|
419
|
+
if hasattr(self, "_db_node") and self._db_node is not None:
|
420
|
+
return
|
421
|
+
|
422
|
+
db_config = inputs["database_config"]
|
370
423
|
|
371
|
-
# Initialize
|
372
|
-
self._db_node =
|
424
|
+
# Initialize database node
|
425
|
+
self._db_node = SQLDatabaseNode(name="user_management_db", **db_config)
|
373
426
|
|
374
|
-
# Initialize
|
375
|
-
self.
|
427
|
+
# Initialize schema manager and ensure schema exists
|
428
|
+
if not self._schema_manager:
|
429
|
+
self._schema_manager = AdminSchemaManager(db_config)
|
430
|
+
|
431
|
+
# Validate schema exists, create if needed
|
432
|
+
try:
|
433
|
+
validation = self._schema_manager.validate_schema()
|
434
|
+
if not validation["is_valid"]:
|
435
|
+
self.logger.info(
|
436
|
+
"Creating unified admin schema for user management..."
|
437
|
+
)
|
438
|
+
self._schema_manager.create_full_schema(drop_existing=False)
|
439
|
+
self.logger.info("Unified admin schema created successfully")
|
440
|
+
except Exception as e:
|
441
|
+
self.logger.warning(f"Schema validation/creation failed: {e}")
|
376
442
|
|
377
|
-
|
378
|
-
"""Create a new user
|
443
|
+
def _create_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
444
|
+
"""Create a new user."""
|
379
445
|
user_data = inputs["user_data"]
|
380
|
-
tenant_id = inputs
|
446
|
+
tenant_id = inputs["tenant_id"]
|
381
447
|
|
382
448
|
# Validate required fields
|
383
|
-
|
384
|
-
|
385
|
-
|
386
|
-
|
387
|
-
|
388
|
-
|
389
|
-
|
390
|
-
|
391
|
-
|
392
|
-
|
393
|
-
|
394
|
-
|
395
|
-
|
396
|
-
|
397
|
-
|
398
|
-
|
399
|
-
|
449
|
+
if "email" not in user_data:
|
450
|
+
raise NodeValidationError("Email is required for user creation")
|
451
|
+
|
452
|
+
# Generate user ID if not provided
|
453
|
+
user_id = user_data.get("user_id", str(uuid4()))
|
454
|
+
|
455
|
+
# Prepare user data with defaults
|
456
|
+
user = User(
|
457
|
+
user_id=user_id,
|
458
|
+
email=user_data["email"],
|
459
|
+
username=user_data.get("username"),
|
460
|
+
first_name=user_data.get("first_name"),
|
461
|
+
last_name=user_data.get("last_name"),
|
462
|
+
display_name=user_data.get("display_name"),
|
463
|
+
roles=user_data.get("roles", []),
|
464
|
+
attributes=user_data.get("attributes", {}),
|
465
|
+
status=UserStatus(user_data.get("status", "active")),
|
466
|
+
tenant_id=tenant_id,
|
467
|
+
external_auth_id=user_data.get("external_auth_id"),
|
468
|
+
auth_provider=user_data.get("auth_provider", "local"),
|
469
|
+
)
|
470
|
+
|
471
|
+
# Insert user into database with conflict resolution
|
472
|
+
insert_query = """
|
473
|
+
INSERT INTO users (
|
474
|
+
user_id, email, username, password_hash, first_name, last_name,
|
475
|
+
display_name, roles, attributes, status, tenant_id,
|
476
|
+
external_auth_id, auth_provider
|
477
|
+
) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
|
478
|
+
ON CONFLICT (user_id) DO UPDATE SET
|
479
|
+
email = EXCLUDED.email,
|
480
|
+
username = EXCLUDED.username,
|
481
|
+
first_name = EXCLUDED.first_name,
|
482
|
+
last_name = EXCLUDED.last_name,
|
483
|
+
display_name = EXCLUDED.display_name,
|
484
|
+
roles = EXCLUDED.roles,
|
485
|
+
attributes = EXCLUDED.attributes,
|
486
|
+
status = EXCLUDED.status,
|
487
|
+
external_auth_id = EXCLUDED.external_auth_id,
|
488
|
+
auth_provider = EXCLUDED.auth_provider,
|
489
|
+
updated_at = CURRENT_TIMESTAMP
|
490
|
+
"""
|
491
|
+
|
492
|
+
try:
|
493
|
+
self._db_node.run(
|
494
|
+
query=insert_query,
|
495
|
+
parameters=[
|
496
|
+
user.user_id,
|
497
|
+
user.email,
|
498
|
+
user.username,
|
499
|
+
inputs.get("password_hash"),
|
500
|
+
user.first_name,
|
501
|
+
user.last_name,
|
502
|
+
user.display_name,
|
503
|
+
json.dumps(user.roles),
|
504
|
+
json.dumps(user.attributes),
|
505
|
+
user.status.value,
|
506
|
+
user.tenant_id,
|
507
|
+
user.external_auth_id,
|
508
|
+
user.auth_provider,
|
509
|
+
],
|
510
|
+
)
|
400
511
|
|
401
|
-
|
402
|
-
|
512
|
+
# Get the created user to return complete data
|
513
|
+
created_user = self._get_user_by_id(user.user_id, tenant_id)
|
403
514
|
|
404
|
-
|
405
|
-
|
406
|
-
|
407
|
-
|
408
|
-
|
409
|
-
|
515
|
+
return {
|
516
|
+
"result": {
|
517
|
+
"user": created_user.to_dict(),
|
518
|
+
"operation": "create_user",
|
519
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
520
|
+
}
|
521
|
+
}
|
410
522
|
|
411
|
-
|
523
|
+
except Exception as e:
|
524
|
+
if "duplicate key" in str(e).lower():
|
412
525
|
raise NodeValidationError(
|
413
|
-
f"
|
526
|
+
f"User with email {user.email} already exists"
|
414
527
|
)
|
528
|
+
raise NodeExecutionError(f"Failed to create user: {str(e)}")
|
529
|
+
|
530
|
+
def _get_user_by_id(self, user_id: str, tenant_id: str) -> User:
|
531
|
+
"""Get user by ID and tenant."""
|
532
|
+
query = """
|
533
|
+
SELECT user_id, email, username, first_name, last_name, display_name,
|
534
|
+
roles, attributes, status, tenant_id, external_auth_id, auth_provider,
|
535
|
+
created_at, updated_at, last_login_at
|
536
|
+
FROM users
|
537
|
+
WHERE user_id = $1 AND tenant_id = $2
|
538
|
+
"""
|
415
539
|
|
416
|
-
|
417
|
-
|
418
|
-
|
419
|
-
raise NodeValidationError("Password must contain uppercase letters")
|
540
|
+
result = self._db_node.run(
|
541
|
+
query=query, parameters=[user_id, tenant_id], result_format="dict"
|
542
|
+
)
|
420
543
|
|
421
|
-
|
422
|
-
|
423
|
-
)
|
424
|
-
|
544
|
+
user_rows = result.get("data", [])
|
545
|
+
if not user_rows:
|
546
|
+
raise NodeValidationError(f"User not found: {user_id}")
|
547
|
+
|
548
|
+
user_data = user_rows[0]
|
549
|
+
return User(
|
550
|
+
user_id=user_data["user_id"],
|
551
|
+
email=user_data["email"],
|
552
|
+
username=user_data["username"],
|
553
|
+
first_name=user_data["first_name"],
|
554
|
+
last_name=user_data["last_name"],
|
555
|
+
display_name=user_data["display_name"],
|
556
|
+
roles=user_data.get("roles", []),
|
557
|
+
attributes=user_data.get("attributes", {}),
|
558
|
+
status=UserStatus(user_data["status"]),
|
559
|
+
tenant_id=user_data["tenant_id"],
|
560
|
+
external_auth_id=user_data["external_auth_id"],
|
561
|
+
auth_provider=user_data["auth_provider"],
|
562
|
+
created_at=parse_datetime(user_data.get("created_at")),
|
563
|
+
updated_at=parse_datetime(user_data.get("updated_at")),
|
564
|
+
last_login_at=parse_datetime(user_data.get("last_login_at")),
|
565
|
+
)
|
425
566
|
|
426
|
-
|
427
|
-
|
567
|
+
def _get_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
568
|
+
"""Get a single user by ID, email, or username."""
|
569
|
+
tenant_id = inputs["tenant_id"]
|
570
|
+
user_id = inputs.get("user_id")
|
571
|
+
email = inputs.get("email")
|
572
|
+
username = inputs.get("username")
|
428
573
|
|
429
|
-
|
430
|
-
|
431
|
-
):
|
432
|
-
raise NodeValidationError("Password must contain special characters")
|
574
|
+
if not any([user_id, email, username]):
|
575
|
+
raise NodeValidationError("Must provide user_id, email, or username")
|
433
576
|
|
434
|
-
|
577
|
+
# Build query based on available identifiers
|
578
|
+
if user_id:
|
579
|
+
user = self._get_user_by_id(user_id, tenant_id)
|
580
|
+
elif email:
|
581
|
+
user = self._get_user_by_email(email, tenant_id)
|
582
|
+
else:
|
583
|
+
user = self._get_user_by_username(username, tenant_id)
|
435
584
|
|
436
|
-
|
437
|
-
|
438
|
-
|
439
|
-
|
440
|
-
|
441
|
-
|
442
|
-
"first_name": user_data["first_name"],
|
443
|
-
"last_name": user_data["last_name"],
|
444
|
-
"status": user_data.get("status", UserStatus.ACTIVE.value),
|
445
|
-
"roles": json.dumps(user_data.get("roles", ["user"])),
|
446
|
-
"attributes": json.dumps(user_data.get("attributes", {})),
|
447
|
-
"password_hash": password_hash,
|
448
|
-
"force_password_change": user_data.get("force_password_change", False),
|
449
|
-
"created_at": datetime.now(UTC),
|
450
|
-
"updated_at": datetime.now(UTC),
|
451
|
-
"created_by": inputs.get("metadata", {}).get("created_by", "system"),
|
585
|
+
return {
|
586
|
+
"result": {
|
587
|
+
"user": user.to_dict(),
|
588
|
+
"operation": "get_user",
|
589
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
590
|
+
}
|
452
591
|
}
|
453
592
|
|
454
|
-
|
455
|
-
|
456
|
-
|
457
|
-
|
458
|
-
|
459
|
-
|
460
|
-
|
461
|
-
|
462
|
-
)
|
593
|
+
def _get_user_by_email(self, email: str, tenant_id: str) -> User:
|
594
|
+
"""Get user by email and tenant."""
|
595
|
+
query = """
|
596
|
+
SELECT user_id, email, username, first_name, last_name, display_name,
|
597
|
+
roles, attributes, status, tenant_id, external_auth_id, auth_provider,
|
598
|
+
created_at, updated_at, last_login_at
|
599
|
+
FROM users
|
600
|
+
WHERE email = $1 AND tenant_id = $2
|
463
601
|
"""
|
464
602
|
|
465
|
-
|
466
|
-
|
467
|
-
|
468
|
-
"params": [
|
469
|
-
user_record["user_id"],
|
470
|
-
user_record["tenant_id"],
|
471
|
-
user_record["email"],
|
472
|
-
user_record["username"],
|
473
|
-
user_record["first_name"],
|
474
|
-
user_record["last_name"],
|
475
|
-
user_record["status"],
|
476
|
-
user_record["roles"],
|
477
|
-
user_record["attributes"],
|
478
|
-
user_record["password_hash"],
|
479
|
-
user_record["force_password_change"],
|
480
|
-
user_record["created_at"],
|
481
|
-
user_record["updated_at"],
|
482
|
-
user_record["created_by"],
|
483
|
-
],
|
484
|
-
}
|
485
|
-
|
486
|
-
db_result = await self._db_node.async_run(**query)
|
603
|
+
result = self._db_node.run(
|
604
|
+
query=query, parameters=[email, tenant_id], result_format="dict"
|
605
|
+
)
|
487
606
|
|
488
|
-
|
489
|
-
|
490
|
-
|
491
|
-
|
492
|
-
|
493
|
-
|
494
|
-
|
495
|
-
|
496
|
-
|
497
|
-
|
498
|
-
|
499
|
-
|
607
|
+
user_rows = result.get("data", [])
|
608
|
+
if not user_rows:
|
609
|
+
raise NodeValidationError(f"User not found with email: {email}")
|
610
|
+
|
611
|
+
user_data = user_rows[0]
|
612
|
+
return User(
|
613
|
+
user_id=user_data["user_id"],
|
614
|
+
email=user_data["email"],
|
615
|
+
username=user_data["username"],
|
616
|
+
first_name=user_data["first_name"],
|
617
|
+
last_name=user_data["last_name"],
|
618
|
+
display_name=user_data["display_name"],
|
619
|
+
roles=user_data.get("roles", []),
|
620
|
+
attributes=user_data.get("attributes", {}),
|
621
|
+
status=UserStatus(user_data["status"]),
|
622
|
+
tenant_id=user_data["tenant_id"],
|
623
|
+
external_auth_id=user_data["external_auth_id"],
|
624
|
+
auth_provider=user_data["auth_provider"],
|
625
|
+
created_at=parse_datetime(user_data.get("created_at")),
|
626
|
+
updated_at=parse_datetime(user_data.get("updated_at")),
|
627
|
+
last_login_at=parse_datetime(user_data.get("last_login_at")),
|
500
628
|
)
|
501
629
|
|
502
|
-
|
503
|
-
|
504
|
-
|
505
|
-
|
630
|
+
def _get_user_by_username(self, username: str, tenant_id: str) -> User:
|
631
|
+
"""Get user by username and tenant."""
|
632
|
+
query = """
|
633
|
+
SELECT user_id, email, username, first_name, last_name, display_name,
|
634
|
+
roles, attributes, status, tenant_id, external_auth_id, auth_provider,
|
635
|
+
created_at, updated_at, last_login_at
|
636
|
+
FROM users
|
637
|
+
WHERE username = $1 AND tenant_id = $2
|
638
|
+
"""
|
506
639
|
|
507
|
-
|
508
|
-
|
509
|
-
|
510
|
-
print(f"[AUDIT] user_created: {user_id} ({user_record['username']})")
|
640
|
+
result = self._db_node.run(
|
641
|
+
query=query, parameters=[username, tenant_id], result_format="dict"
|
642
|
+
)
|
511
643
|
|
512
|
-
|
513
|
-
|
514
|
-
"
|
515
|
-
|
516
|
-
|
644
|
+
user_rows = result.get("data", [])
|
645
|
+
if not user_rows:
|
646
|
+
raise NodeValidationError(f"User not found with username: {username}")
|
647
|
+
|
648
|
+
user_data = user_rows[0]
|
649
|
+
return User(
|
650
|
+
user_id=user_data["user_id"],
|
651
|
+
email=user_data["email"],
|
652
|
+
username=user_data["username"],
|
653
|
+
first_name=user_data["first_name"],
|
654
|
+
last_name=user_data["last_name"],
|
655
|
+
display_name=user_data["display_name"],
|
656
|
+
roles=user_data.get("roles", []),
|
657
|
+
attributes=user_data.get("attributes", {}),
|
658
|
+
status=UserStatus(user_data["status"]),
|
659
|
+
tenant_id=user_data["tenant_id"],
|
660
|
+
external_auth_id=user_data["external_auth_id"],
|
661
|
+
auth_provider=user_data["auth_provider"],
|
662
|
+
created_at=parse_datetime(user_data.get("created_at")),
|
663
|
+
updated_at=parse_datetime(user_data.get("updated_at")),
|
664
|
+
last_login_at=parse_datetime(user_data.get("last_login_at")),
|
665
|
+
)
|
517
666
|
|
518
|
-
def
|
519
|
-
"""
|
667
|
+
def _update_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
668
|
+
"""Update an existing user."""
|
669
|
+
user_id = inputs["user_id"]
|
520
670
|
user_data = inputs["user_data"]
|
521
|
-
tenant_id = inputs
|
522
|
-
|
523
|
-
#
|
524
|
-
|
525
|
-
|
526
|
-
|
527
|
-
|
528
|
-
|
529
|
-
|
530
|
-
|
531
|
-
|
532
|
-
|
533
|
-
|
534
|
-
|
535
|
-
|
536
|
-
|
537
|
-
|
538
|
-
|
539
|
-
|
540
|
-
|
541
|
-
|
542
|
-
|
543
|
-
|
671
|
+
tenant_id = inputs["tenant_id"]
|
672
|
+
|
673
|
+
# Get existing user
|
674
|
+
existing_user = self._get_user_by_id(user_id, tenant_id)
|
675
|
+
|
676
|
+
# Build update query dynamically based on provided fields
|
677
|
+
update_fields = []
|
678
|
+
parameters = []
|
679
|
+
param_index = 1
|
680
|
+
|
681
|
+
updatable_fields = [
|
682
|
+
"email",
|
683
|
+
"username",
|
684
|
+
"first_name",
|
685
|
+
"last_name",
|
686
|
+
"display_name",
|
687
|
+
"status",
|
688
|
+
"external_auth_id",
|
689
|
+
"auth_provider",
|
690
|
+
]
|
691
|
+
|
692
|
+
for field in updatable_fields:
|
693
|
+
if field in user_data:
|
694
|
+
update_fields.append(f"{field} = ${param_index}")
|
695
|
+
parameters.append(user_data[field])
|
696
|
+
param_index += 1
|
697
|
+
|
698
|
+
# Handle JSONB fields separately
|
699
|
+
if "roles" in user_data:
|
700
|
+
update_fields.append(f"roles = ${param_index}")
|
701
|
+
parameters.append(json.dumps(user_data["roles"]))
|
702
|
+
param_index += 1
|
703
|
+
|
704
|
+
if "attributes" in user_data:
|
705
|
+
update_fields.append(f"attributes = ${param_index}")
|
706
|
+
parameters.append(json.dumps(user_data["attributes"]))
|
707
|
+
param_index += 1
|
708
|
+
|
709
|
+
if "password_hash" in inputs:
|
710
|
+
update_fields.append(f"password_hash = ${param_index}")
|
711
|
+
parameters.append(inputs["password_hash"])
|
712
|
+
param_index += 1
|
713
|
+
|
714
|
+
if not update_fields:
|
715
|
+
raise NodeValidationError("No valid fields provided for update")
|
716
|
+
|
717
|
+
# Always update the updated_at timestamp
|
718
|
+
update_fields.append("updated_at = CURRENT_TIMESTAMP")
|
719
|
+
|
720
|
+
# Add WHERE clause parameters
|
721
|
+
parameters.extend([user_id, tenant_id])
|
722
|
+
|
723
|
+
update_query = f"""
|
724
|
+
UPDATE users
|
725
|
+
SET {', '.join(update_fields)}
|
726
|
+
WHERE user_id = ${param_index} AND tenant_id = ${param_index + 1}
|
727
|
+
"""
|
544
728
|
|
545
|
-
|
546
|
-
|
547
|
-
if "password" in inputs:
|
548
|
-
password_hash = self._hash_password(inputs["password"])
|
549
|
-
elif "password" in user_data:
|
550
|
-
password_hash = self._hash_password(user_data["password"])
|
729
|
+
try:
|
730
|
+
self._db_node.run(query=update_query, parameters=parameters)
|
551
731
|
|
552
|
-
|
553
|
-
|
554
|
-
"user_id": user_id,
|
555
|
-
"tenant_id": tenant_id,
|
556
|
-
"email": user_data["email"],
|
557
|
-
"username": user_data["username"],
|
558
|
-
"first_name": user_data["first_name"],
|
559
|
-
"last_name": user_data["last_name"],
|
560
|
-
"status": user_data.get("status", UserStatus.ACTIVE.value),
|
561
|
-
"roles": user_data.get("roles", []),
|
562
|
-
"attributes": user_data.get("attributes", {}),
|
563
|
-
"password_hash": password_hash,
|
564
|
-
"force_password_change": inputs.get("force_password_change", False),
|
565
|
-
"created_at": now,
|
566
|
-
"updated_at": now,
|
567
|
-
"created_by": inputs.get("created_by", "system"),
|
568
|
-
}
|
732
|
+
# Get updated user
|
733
|
+
updated_user = self._get_user_by_id(user_id, tenant_id)
|
569
734
|
|
570
|
-
|
571
|
-
|
572
|
-
|
573
|
-
|
574
|
-
|
575
|
-
|
576
|
-
|
577
|
-
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
|
578
|
-
)
|
579
|
-
"""
|
735
|
+
return {
|
736
|
+
"result": {
|
737
|
+
"user": updated_user.to_dict(),
|
738
|
+
"operation": "update_user",
|
739
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
740
|
+
}
|
741
|
+
}
|
580
742
|
|
581
|
-
|
582
|
-
|
583
|
-
"query": insert_query,
|
584
|
-
"params": [
|
585
|
-
user_record["user_id"],
|
586
|
-
user_record["tenant_id"],
|
587
|
-
user_record["email"],
|
588
|
-
user_record["username"],
|
589
|
-
user_record["first_name"],
|
590
|
-
user_record["last_name"],
|
591
|
-
user_record["status"],
|
592
|
-
user_record["roles"],
|
593
|
-
user_record["attributes"],
|
594
|
-
user_record["password_hash"],
|
595
|
-
user_record["force_password_change"],
|
596
|
-
user_record["created_at"],
|
597
|
-
user_record["updated_at"],
|
598
|
-
user_record["created_by"],
|
599
|
-
],
|
600
|
-
}
|
743
|
+
except Exception as e:
|
744
|
+
raise NodeExecutionError(f"Failed to update user: {str(e)}")
|
601
745
|
|
602
|
-
|
746
|
+
def _delete_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
747
|
+
"""Delete (or soft-delete) a user."""
|
748
|
+
user_id = inputs["user_id"]
|
749
|
+
tenant_id = inputs["tenant_id"]
|
750
|
+
hard_delete = inputs.get("hard_delete", False)
|
751
|
+
|
752
|
+
# Get existing user to return in response
|
753
|
+
existing_user = self._get_user_by_id(user_id, tenant_id)
|
754
|
+
|
755
|
+
if hard_delete:
|
756
|
+
# Hard delete - remove from database
|
757
|
+
delete_query = "DELETE FROM users WHERE user_id = $1 AND tenant_id = $2"
|
758
|
+
else:
|
759
|
+
# Soft delete - mark as deleted
|
760
|
+
delete_query = """
|
761
|
+
UPDATE users
|
762
|
+
SET status = 'deleted', updated_at = CURRENT_TIMESTAMP
|
763
|
+
WHERE user_id = $1 AND tenant_id = $2
|
764
|
+
"""
|
603
765
|
|
604
|
-
|
605
|
-
|
606
|
-
user_id=user_id,
|
607
|
-
email=user_record["email"],
|
608
|
-
username=user_record["username"],
|
609
|
-
first_name=user_record["first_name"],
|
610
|
-
last_name=user_record["last_name"],
|
611
|
-
status=UserStatus(user_record["status"]),
|
612
|
-
roles=user_record["roles"],
|
613
|
-
attributes=user_record["attributes"],
|
614
|
-
created_at=user_record["created_at"],
|
615
|
-
updated_at=user_record["updated_at"],
|
616
|
-
tenant_id=tenant_id,
|
617
|
-
)
|
766
|
+
try:
|
767
|
+
self._db_node.run(query=delete_query, parameters=[user_id, tenant_id])
|
618
768
|
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
"
|
623
|
-
"
|
624
|
-
"
|
625
|
-
|
626
|
-
"last_name": user_profile.last_name,
|
627
|
-
"status": user_profile.status.value,
|
628
|
-
"roles": user_profile.roles,
|
629
|
-
"attributes": user_profile.attributes,
|
630
|
-
"created_at": user_profile.created_at.isoformat(),
|
631
|
-
"tenant_id": user_profile.tenant_id,
|
632
|
-
},
|
633
|
-
"operation": "create",
|
634
|
-
"success": True,
|
635
|
-
"timestamp": datetime.now(UTC).isoformat(),
|
769
|
+
return {
|
770
|
+
"result": {
|
771
|
+
"deleted_user": existing_user.to_dict(),
|
772
|
+
"hard_delete": hard_delete,
|
773
|
+
"operation": "delete_user",
|
774
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
775
|
+
}
|
636
776
|
}
|
637
|
-
}
|
638
777
|
|
639
|
-
|
640
|
-
|
641
|
-
user_id = inputs.get("user_id")
|
642
|
-
email = inputs.get("email")
|
643
|
-
tenant_id = inputs.get("tenant_id", "default")
|
644
|
-
include_deleted = inputs.get("include_deleted", False)
|
778
|
+
except Exception as e:
|
779
|
+
raise NodeExecutionError(f"Failed to delete user: {str(e)}")
|
645
780
|
|
646
|
-
|
647
|
-
|
781
|
+
def _list_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
782
|
+
"""List users with filtering and pagination."""
|
783
|
+
tenant_id = inputs["tenant_id"]
|
784
|
+
status = inputs.get("status")
|
785
|
+
limit = inputs.get("limit", 50)
|
786
|
+
offset = inputs.get("offset", 0)
|
787
|
+
include_deleted = inputs.get("include_deleted", False)
|
648
788
|
|
649
|
-
# Build query
|
789
|
+
# Build query with filters
|
650
790
|
where_conditions = ["tenant_id = $1"]
|
651
|
-
|
652
|
-
|
791
|
+
parameters = [tenant_id]
|
792
|
+
param_index = 2
|
793
|
+
|
794
|
+
if status:
|
795
|
+
where_conditions.append(f"status = ${param_index}")
|
796
|
+
parameters.append(status)
|
797
|
+
param_index += 1
|
798
|
+
elif not include_deleted:
|
799
|
+
where_conditions.append(f"status != ${param_index}")
|
800
|
+
parameters.append("deleted")
|
801
|
+
param_index += 1
|
802
|
+
|
803
|
+
# Add pagination
|
804
|
+
parameters.extend([limit, offset])
|
805
|
+
|
806
|
+
list_query = f"""
|
807
|
+
SELECT user_id, email, username, first_name, last_name, display_name,
|
808
|
+
roles, attributes, status, tenant_id, external_auth_id, auth_provider,
|
809
|
+
created_at, updated_at, last_login_at
|
810
|
+
FROM users
|
811
|
+
WHERE {' AND '.join(where_conditions)}
|
812
|
+
ORDER BY created_at DESC
|
813
|
+
LIMIT ${param_index} OFFSET ${param_index + 1}
|
814
|
+
"""
|
653
815
|
|
654
|
-
|
655
|
-
|
656
|
-
where_conditions.append(f"user_id = ${param_count}")
|
657
|
-
params.append(user_id)
|
658
|
-
|
659
|
-
if email:
|
660
|
-
param_count += 1
|
661
|
-
where_conditions.append(f"email = ${param_count}")
|
662
|
-
params.append(email)
|
663
|
-
|
664
|
-
if not include_deleted:
|
665
|
-
where_conditions.append("status != 'deleted'")
|
666
|
-
|
667
|
-
query = f"""
|
668
|
-
SELECT user_id, tenant_id, email, username, first_name, last_name,
|
669
|
-
status, roles, attributes, created_at, updated_at, last_login,
|
670
|
-
password_changed_at, force_password_change
|
816
|
+
count_query = f"""
|
817
|
+
SELECT COUNT(*) as total_count
|
671
818
|
FROM users
|
672
819
|
WHERE {' AND '.join(where_conditions)}
|
673
|
-
LIMIT 1
|
674
820
|
"""
|
675
821
|
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
822
|
+
try:
|
823
|
+
# Get users
|
824
|
+
result = self._db_node.run(
|
825
|
+
query=list_query, parameters=parameters, result_format="dict"
|
826
|
+
)
|
827
|
+
|
828
|
+
# Get total count
|
829
|
+
count_result = self._db_node.run(
|
830
|
+
query=count_query, parameters=parameters[:-2], result_format="dict"
|
831
|
+
)
|
680
832
|
|
681
|
-
|
833
|
+
users = []
|
834
|
+
for user_data in result.get("data", []):
|
835
|
+
user = User(
|
836
|
+
user_id=user_data["user_id"],
|
837
|
+
email=user_data["email"],
|
838
|
+
username=user_data["username"],
|
839
|
+
first_name=user_data["first_name"],
|
840
|
+
last_name=user_data["last_name"],
|
841
|
+
display_name=user_data["display_name"],
|
842
|
+
roles=user_data.get("roles", []),
|
843
|
+
attributes=user_data.get("attributes", {}),
|
844
|
+
status=UserStatus(user_data["status"]),
|
845
|
+
tenant_id=user_data["tenant_id"],
|
846
|
+
external_auth_id=user_data["external_auth_id"],
|
847
|
+
auth_provider=user_data["auth_provider"],
|
848
|
+
created_at=user_data.get("created_at"),
|
849
|
+
updated_at=user_data.get("updated_at"),
|
850
|
+
last_login_at=user_data.get("last_login_at"),
|
851
|
+
)
|
852
|
+
users.append(user.to_dict())
|
853
|
+
|
854
|
+
total_count = count_result.get("data", [{}])[0].get("total_count", 0)
|
682
855
|
|
683
|
-
if not db_result.get("result", {}).get("data"):
|
684
856
|
return {
|
685
857
|
"result": {
|
686
|
-
"
|
687
|
-
"
|
688
|
-
|
858
|
+
"users": users,
|
859
|
+
"pagination": {
|
860
|
+
"total_count": total_count,
|
861
|
+
"limit": limit,
|
862
|
+
"offset": offset,
|
863
|
+
"has_more": offset + limit < total_count,
|
864
|
+
},
|
865
|
+
"filters": {
|
866
|
+
"status": status,
|
867
|
+
"include_deleted": include_deleted,
|
868
|
+
},
|
869
|
+
"operation": "list_users",
|
689
870
|
"timestamp": datetime.now(UTC).isoformat(),
|
690
871
|
}
|
691
872
|
}
|
692
873
|
|
693
|
-
|
874
|
+
except Exception as e:
|
875
|
+
raise NodeExecutionError(f"Failed to list users: {str(e)}")
|
694
876
|
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
"user_id": user_data["user_id"],
|
699
|
-
"email": user_data["email"],
|
700
|
-
"username": user_data["username"],
|
701
|
-
"first_name": user_data["first_name"],
|
702
|
-
"last_name": user_data["last_name"],
|
703
|
-
"status": user_data["status"],
|
704
|
-
"roles": user_data["roles"],
|
705
|
-
"attributes": user_data["attributes"],
|
706
|
-
"created_at": user_data["created_at"],
|
707
|
-
"updated_at": user_data["updated_at"],
|
708
|
-
"last_login": user_data["last_login"],
|
709
|
-
"tenant_id": user_data["tenant_id"],
|
710
|
-
},
|
711
|
-
"found": True,
|
712
|
-
"operation": "read",
|
713
|
-
"timestamp": datetime.now(UTC).isoformat(),
|
714
|
-
}
|
715
|
-
}
|
877
|
+
def _activate_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
878
|
+
"""Activate a user."""
|
879
|
+
return self._change_user_status(inputs, UserStatus.ACTIVE, "activate_user")
|
716
880
|
|
717
|
-
def
|
718
|
-
"""
|
719
|
-
|
720
|
-
|
721
|
-
|
722
|
-
|
723
|
-
|
724
|
-
|
881
|
+
def _deactivate_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
882
|
+
"""Deactivate a user."""
|
883
|
+
return self._change_user_status(inputs, UserStatus.INACTIVE, "deactivate_user")
|
884
|
+
|
885
|
+
def _change_user_status(
|
886
|
+
self, inputs: Dict[str, Any], new_status: UserStatus, operation: str
|
887
|
+
) -> Dict[str, Any]:
|
888
|
+
"""Helper method to change user status."""
|
889
|
+
user_id = inputs["user_id"]
|
890
|
+
tenant_id = inputs["tenant_id"]
|
891
|
+
|
892
|
+
update_query = """
|
893
|
+
UPDATE users
|
894
|
+
SET status = $1, updated_at = CURRENT_TIMESTAMP
|
895
|
+
WHERE user_id = $2 AND tenant_id = $3
|
896
|
+
"""
|
725
897
|
|
726
|
-
|
727
|
-
|
728
|
-
|
729
|
-
param_count = 1
|
730
|
-
|
731
|
-
if not include_deleted:
|
732
|
-
where_conditions.append("status != 'deleted'")
|
733
|
-
|
734
|
-
# Apply filters
|
735
|
-
if "status" in filters:
|
736
|
-
param_count += 1
|
737
|
-
where_conditions.append(f"status = ${param_count}")
|
738
|
-
params.append(filters["status"])
|
739
|
-
|
740
|
-
if "role" in filters:
|
741
|
-
param_count += 1
|
742
|
-
where_conditions.append(f"${param_count} = ANY(roles)")
|
743
|
-
params.append(filters["role"])
|
744
|
-
|
745
|
-
if "department" in filters:
|
746
|
-
param_count += 1
|
747
|
-
where_conditions.append(f"attributes->>'department' = ${param_count}")
|
748
|
-
params.append(filters["department"])
|
749
|
-
|
750
|
-
# Search query
|
751
|
-
search_query = inputs.get("search_query")
|
752
|
-
if search_query:
|
753
|
-
param_count += 1
|
754
|
-
where_conditions.append(
|
755
|
-
f"""
|
756
|
-
(email ILIKE ${param_count} OR
|
757
|
-
username ILIKE ${param_count} OR
|
758
|
-
first_name ILIKE ${param_count} OR
|
759
|
-
last_name ILIKE ${param_count})
|
760
|
-
"""
|
898
|
+
try:
|
899
|
+
self._db_node.run(
|
900
|
+
query=update_query, parameters=[new_status.value, user_id, tenant_id]
|
761
901
|
)
|
762
|
-
params.append(f"%{search_query}%")
|
763
902
|
|
764
|
-
|
765
|
-
|
766
|
-
size = pagination.get("size", 20)
|
767
|
-
sort_field = pagination.get("sort", "created_at")
|
768
|
-
sort_direction = pagination.get("direction", "DESC")
|
903
|
+
# Get updated user
|
904
|
+
updated_user = self._get_user_by_id(user_id, tenant_id)
|
769
905
|
|
770
|
-
|
906
|
+
return {
|
907
|
+
"result": {
|
908
|
+
"user": updated_user.to_dict(),
|
909
|
+
"operation": operation,
|
910
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
911
|
+
}
|
912
|
+
}
|
771
913
|
|
772
|
-
|
773
|
-
|
774
|
-
|
775
|
-
|
776
|
-
|
914
|
+
except Exception as e:
|
915
|
+
raise NodeExecutionError(f"Failed to {operation}: {str(e)}")
|
916
|
+
|
917
|
+
def _set_password(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
918
|
+
"""Set user password hash."""
|
919
|
+
user_id = inputs["user_id"]
|
920
|
+
tenant_id = inputs["tenant_id"]
|
921
|
+
password_hash = inputs["password_hash"]
|
922
|
+
|
923
|
+
update_query = """
|
924
|
+
UPDATE users
|
925
|
+
SET password_hash = $1, updated_at = CURRENT_TIMESTAMP
|
926
|
+
WHERE user_id = $2 AND tenant_id = $3
|
777
927
|
"""
|
778
928
|
|
779
|
-
|
780
|
-
|
781
|
-
|
782
|
-
|
783
|
-
FROM users
|
784
|
-
WHERE {' AND '.join(where_conditions)}
|
785
|
-
ORDER BY {sort_field} {sort_direction}
|
786
|
-
LIMIT {size} OFFSET {offset}
|
787
|
-
"""
|
929
|
+
try:
|
930
|
+
self._db_node.run(
|
931
|
+
query=update_query, parameters=[password_hash, user_id, tenant_id]
|
932
|
+
)
|
788
933
|
|
789
|
-
|
790
|
-
|
791
|
-
|
792
|
-
|
793
|
-
|
794
|
-
|
934
|
+
return {
|
935
|
+
"result": {
|
936
|
+
"user_id": user_id,
|
937
|
+
"password_updated": True,
|
938
|
+
"operation": "set_password",
|
939
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
940
|
+
}
|
941
|
+
}
|
795
942
|
|
796
|
-
|
797
|
-
|
798
|
-
|
799
|
-
|
800
|
-
|
801
|
-
|
943
|
+
except Exception as e:
|
944
|
+
raise NodeExecutionError(f"Failed to set password: {str(e)}")
|
945
|
+
|
946
|
+
def _update_profile(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
947
|
+
"""Update user profile fields."""
|
948
|
+
# This is essentially the same as update_user but with a different operation name
|
949
|
+
result = self._update_user(inputs)
|
950
|
+
result["result"]["operation"] = "update_profile"
|
951
|
+
return result
|
952
|
+
|
953
|
+
def _bulk_create(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
954
|
+
"""Create multiple users in bulk."""
|
955
|
+
users_data = inputs["users_data"]
|
956
|
+
tenant_id = inputs["tenant_id"]
|
802
957
|
|
803
|
-
|
804
|
-
|
805
|
-
|
806
|
-
|
958
|
+
if not isinstance(users_data, list):
|
959
|
+
raise NodeValidationError("users_data must be a list")
|
960
|
+
|
961
|
+
created_users = []
|
962
|
+
failed_users = []
|
963
|
+
|
964
|
+
for i, user_data in enumerate(users_data):
|
965
|
+
try:
|
966
|
+
# Create each user individually for better error handling
|
967
|
+
create_inputs = {
|
968
|
+
"operation": "create_user",
|
969
|
+
"user_data": user_data,
|
970
|
+
"tenant_id": tenant_id,
|
971
|
+
"database_config": inputs["database_config"],
|
972
|
+
}
|
973
|
+
|
974
|
+
result = self._create_user(create_inputs)
|
975
|
+
created_users.append(result["result"]["user"])
|
976
|
+
|
977
|
+
except Exception as e:
|
978
|
+
failed_users.append(
|
979
|
+
{
|
980
|
+
"index": i,
|
981
|
+
"user_data": user_data,
|
982
|
+
"error": str(e),
|
983
|
+
}
|
984
|
+
)
|
807
985
|
|
808
986
|
return {
|
809
987
|
"result": {
|
810
|
-
"
|
811
|
-
|
812
|
-
"
|
813
|
-
"
|
814
|
-
"
|
815
|
-
"
|
816
|
-
"has_next": has_next,
|
817
|
-
"has_prev": has_prev,
|
988
|
+
"bulk_result": {
|
989
|
+
"created_count": len(created_users),
|
990
|
+
"failed_count": len(failed_users),
|
991
|
+
"total_count": len(users_data),
|
992
|
+
"created_users": created_users,
|
993
|
+
"failed_users": failed_users,
|
818
994
|
},
|
819
|
-
"
|
820
|
-
"search_query": search_query,
|
821
|
-
"operation": "list",
|
995
|
+
"operation": "bulk_create",
|
822
996
|
"timestamp": datetime.now(UTC).isoformat(),
|
823
997
|
}
|
824
998
|
}
|
825
999
|
|
826
|
-
def
|
827
|
-
"""
|
828
|
-
|
829
|
-
tenant_id = inputs
|
1000
|
+
def _bulk_update(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
1001
|
+
"""Update multiple users in bulk."""
|
1002
|
+
users_data = inputs["users_data"]
|
1003
|
+
tenant_id = inputs["tenant_id"]
|
830
1004
|
|
831
|
-
if not isinstance(
|
832
|
-
raise NodeValidationError("
|
1005
|
+
if not isinstance(users_data, list):
|
1006
|
+
raise NodeValidationError("users_data must be a list")
|
833
1007
|
|
834
|
-
|
1008
|
+
updated_users = []
|
1009
|
+
failed_users = []
|
835
1010
|
|
836
|
-
for i, user_data in enumerate(
|
1011
|
+
for i, user_data in enumerate(users_data):
|
837
1012
|
try:
|
838
|
-
|
839
|
-
|
840
|
-
|
1013
|
+
if "user_id" not in user_data:
|
1014
|
+
raise NodeValidationError("user_id is required for bulk update")
|
1015
|
+
|
1016
|
+
update_inputs = {
|
1017
|
+
"operation": "update_user",
|
1018
|
+
"user_id": user_data.pop("user_id"),
|
841
1019
|
"user_data": user_data,
|
842
1020
|
"tenant_id": tenant_id,
|
843
|
-
"
|
844
|
-
"validate_username": inputs.get("validate_username", True),
|
1021
|
+
"database_config": inputs["database_config"],
|
845
1022
|
}
|
846
1023
|
|
847
|
-
result = self.
|
848
|
-
|
849
|
-
|
1024
|
+
result = self._update_user(update_inputs)
|
1025
|
+
updated_users.append(result["result"]["user"])
|
1026
|
+
|
1027
|
+
except Exception as e:
|
1028
|
+
failed_users.append(
|
1029
|
+
{
|
1030
|
+
"index": i,
|
1031
|
+
"user_data": user_data,
|
1032
|
+
"error": str(e),
|
1033
|
+
}
|
850
1034
|
)
|
851
|
-
|
1035
|
+
|
1036
|
+
return {
|
1037
|
+
"result": {
|
1038
|
+
"bulk_result": {
|
1039
|
+
"updated_count": len(updated_users),
|
1040
|
+
"failed_count": len(failed_users),
|
1041
|
+
"total_count": len(users_data),
|
1042
|
+
"updated_users": updated_users,
|
1043
|
+
"failed_users": failed_users,
|
1044
|
+
},
|
1045
|
+
"operation": "bulk_update",
|
1046
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
1047
|
+
}
|
1048
|
+
}
|
1049
|
+
|
1050
|
+
def _bulk_delete(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
1051
|
+
"""Delete multiple users in bulk."""
|
1052
|
+
user_ids = inputs.get("user_ids", [])
|
1053
|
+
tenant_id = inputs["tenant_id"]
|
1054
|
+
hard_delete = inputs.get("hard_delete", False)
|
1055
|
+
|
1056
|
+
if not isinstance(user_ids, list):
|
1057
|
+
raise NodeValidationError("user_ids must be a list")
|
1058
|
+
|
1059
|
+
deleted_users = []
|
1060
|
+
failed_users = []
|
1061
|
+
|
1062
|
+
for user_id in user_ids:
|
1063
|
+
try:
|
1064
|
+
delete_inputs = {
|
1065
|
+
"operation": "delete_user",
|
1066
|
+
"user_id": user_id,
|
1067
|
+
"tenant_id": tenant_id,
|
1068
|
+
"hard_delete": hard_delete,
|
1069
|
+
"database_config": inputs["database_config"],
|
1070
|
+
}
|
1071
|
+
|
1072
|
+
result = self._delete_user(delete_inputs)
|
1073
|
+
deleted_users.append(result["result"]["deleted_user"])
|
852
1074
|
|
853
1075
|
except Exception as e:
|
854
|
-
|
855
|
-
{
|
1076
|
+
failed_users.append(
|
1077
|
+
{
|
1078
|
+
"user_id": user_id,
|
1079
|
+
"error": str(e),
|
1080
|
+
}
|
856
1081
|
)
|
857
|
-
results["stats"]["failed"] += 1
|
858
1082
|
|
859
1083
|
return {
|
860
1084
|
"result": {
|
861
|
-
"
|
862
|
-
|
1085
|
+
"bulk_result": {
|
1086
|
+
"deleted_count": len(deleted_users),
|
1087
|
+
"failed_count": len(failed_users),
|
1088
|
+
"total_count": len(user_ids),
|
1089
|
+
"deleted_users": deleted_users,
|
1090
|
+
"failed_users": failed_users,
|
1091
|
+
"hard_delete": hard_delete,
|
1092
|
+
},
|
1093
|
+
"operation": "bulk_delete",
|
1094
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
1095
|
+
}
|
1096
|
+
}
|
1097
|
+
|
1098
|
+
def _get_user_roles(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
1099
|
+
"""Get roles assigned to a user."""
|
1100
|
+
user_id = inputs["user_id"]
|
1101
|
+
tenant_id = inputs["tenant_id"]
|
1102
|
+
|
1103
|
+
# Get user with roles
|
1104
|
+
user = self._get_user_by_id(user_id, tenant_id)
|
1105
|
+
|
1106
|
+
# Get detailed role information
|
1107
|
+
if user.roles:
|
1108
|
+
placeholders = ",".join([f"${i+1}" for i in range(len(user.roles))])
|
1109
|
+
role_query = f"""
|
1110
|
+
SELECT role_id, name, description, permissions, parent_roles, attributes
|
1111
|
+
FROM roles
|
1112
|
+
WHERE role_id IN ({placeholders}) AND tenant_id = ${len(user.roles) + 1}
|
1113
|
+
"""
|
1114
|
+
|
1115
|
+
result = self._db_node.run(
|
1116
|
+
query=role_query,
|
1117
|
+
parameters=user.roles + [tenant_id],
|
1118
|
+
result_format="dict",
|
1119
|
+
)
|
1120
|
+
role_details = result.get("data", [])
|
1121
|
+
else:
|
1122
|
+
role_details = []
|
1123
|
+
|
1124
|
+
return {
|
1125
|
+
"result": {
|
1126
|
+
"user_id": user_id,
|
1127
|
+
"roles": user.roles,
|
1128
|
+
"role_details": role_details,
|
1129
|
+
"operation": "get_user_roles",
|
863
1130
|
"timestamp": datetime.now(UTC).isoformat(),
|
864
1131
|
}
|
865
1132
|
}
|
866
1133
|
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
1134
|
+
def _get_user_permissions(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
1135
|
+
"""Get effective permissions for a user."""
|
1136
|
+
user_id = inputs["user_id"]
|
1137
|
+
tenant_id = inputs["tenant_id"]
|
871
1138
|
|
872
|
-
|
1139
|
+
# This would integrate with PermissionCheckNode to get effective permissions
|
1140
|
+
# For now, return a basic implementation
|
1141
|
+
user = self._get_user_by_id(user_id, tenant_id)
|
873
1142
|
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
|
878
|
-
|
1143
|
+
return {
|
1144
|
+
"result": {
|
1145
|
+
"user_id": user_id,
|
1146
|
+
"roles": user.roles,
|
1147
|
+
"attributes": user.attributes,
|
1148
|
+
"operation": "get_user_permissions",
|
1149
|
+
"note": "Use PermissionCheckNode.get_user_permissions for complete permission evaluation",
|
1150
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
1151
|
+
}
|
1152
|
+
}
|
879
1153
|
|
880
|
-
def
|
881
|
-
"""
|
882
|
-
|
1154
|
+
def _search_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
1155
|
+
"""Search users by query."""
|
1156
|
+
search_query = inputs["search_query"]
|
1157
|
+
tenant_id = inputs["tenant_id"]
|
1158
|
+
limit = inputs.get("limit", 50)
|
1159
|
+
offset = inputs.get("offset", 0)
|
1160
|
+
|
1161
|
+
# Simple text search across email, username, first_name, last_name
|
1162
|
+
query = """
|
1163
|
+
SELECT user_id, email, username, first_name, last_name, display_name,
|
1164
|
+
roles, attributes, status, tenant_id, external_auth_id, auth_provider,
|
1165
|
+
created_at, updated_at, last_login_at
|
1166
|
+
FROM users
|
1167
|
+
WHERE tenant_id = $1 AND status != 'deleted' AND (
|
1168
|
+
email ILIKE $2 OR
|
1169
|
+
username ILIKE $2 OR
|
1170
|
+
first_name ILIKE $2 OR
|
1171
|
+
last_name ILIKE $2 OR
|
1172
|
+
display_name ILIKE $2
|
1173
|
+
)
|
1174
|
+
ORDER BY created_at DESC
|
1175
|
+
LIMIT $3 OFFSET $4
|
1176
|
+
"""
|
883
1177
|
|
884
|
-
|
885
|
-
return bool(re.match(pattern, email))
|
1178
|
+
search_pattern = f"%{search_query}%"
|
886
1179
|
|
887
|
-
|
888
|
-
|
889
|
-
|
1180
|
+
try:
|
1181
|
+
result = self._db_node.run(
|
1182
|
+
query=query,
|
1183
|
+
parameters=[tenant_id, search_pattern, limit, offset],
|
1184
|
+
result_format="dict",
|
1185
|
+
)
|
890
1186
|
|
891
|
-
|
892
|
-
|
893
|
-
|
1187
|
+
users = []
|
1188
|
+
for user_data in result.get("data", []):
|
1189
|
+
user = User(
|
1190
|
+
user_id=user_data["user_id"],
|
1191
|
+
email=user_data["email"],
|
1192
|
+
username=user_data["username"],
|
1193
|
+
first_name=user_data["first_name"],
|
1194
|
+
last_name=user_data["last_name"],
|
1195
|
+
display_name=user_data["display_name"],
|
1196
|
+
roles=user_data.get("roles", []),
|
1197
|
+
attributes=user_data.get("attributes", {}),
|
1198
|
+
status=UserStatus(user_data["status"]),
|
1199
|
+
tenant_id=user_data["tenant_id"],
|
1200
|
+
external_auth_id=user_data["external_auth_id"],
|
1201
|
+
auth_provider=user_data["auth_provider"],
|
1202
|
+
created_at=user_data.get("created_at"),
|
1203
|
+
updated_at=user_data.get("updated_at"),
|
1204
|
+
last_login_at=user_data.get("last_login_at"),
|
1205
|
+
)
|
1206
|
+
users.append(user.to_dict())
|
894
1207
|
|
895
|
-
|
896
|
-
|
897
|
-
|
898
|
-
|
899
|
-
|
1208
|
+
return {
|
1209
|
+
"result": {
|
1210
|
+
"users": users,
|
1211
|
+
"search_query": search_query,
|
1212
|
+
"count": len(users),
|
1213
|
+
"pagination": {
|
1214
|
+
"limit": limit,
|
1215
|
+
"offset": offset,
|
1216
|
+
},
|
1217
|
+
"operation": "search_users",
|
1218
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
1219
|
+
}
|
1220
|
+
}
|
900
1221
|
|
901
|
-
|
902
|
-
|
903
|
-
|
904
|
-
|
1222
|
+
except Exception as e:
|
1223
|
+
raise NodeExecutionError(f"Failed to search users: {str(e)}")
|
1224
|
+
|
1225
|
+
def _export_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
1226
|
+
"""Export users in specified format."""
|
1227
|
+
tenant_id = inputs["tenant_id"]
|
1228
|
+
export_format = inputs.get("export_format", "json")
|
1229
|
+
include_deleted = inputs.get("include_deleted", False)
|
1230
|
+
|
1231
|
+
# Get all users for export
|
1232
|
+
list_inputs = {
|
1233
|
+
**inputs,
|
1234
|
+
"operation": "list_users",
|
1235
|
+
"limit": 10000, # Large limit for export
|
1236
|
+
"offset": 0,
|
1237
|
+
"include_deleted": include_deleted,
|
1238
|
+
}
|
1239
|
+
|
1240
|
+
result = self._list_users(list_inputs)
|
1241
|
+
users = result["result"]["users"]
|
1242
|
+
|
1243
|
+
if export_format == "json":
|
1244
|
+
export_data = {
|
1245
|
+
"users": users,
|
1246
|
+
"export_metadata": {
|
1247
|
+
"tenant_id": tenant_id,
|
1248
|
+
"export_time": datetime.now(UTC).isoformat(),
|
1249
|
+
"total_users": len(users),
|
1250
|
+
"include_deleted": include_deleted,
|
1251
|
+
},
|
1252
|
+
}
|
1253
|
+
elif export_format == "csv":
|
1254
|
+
# Convert to CSV format (simplified)
|
1255
|
+
csv_headers = [
|
1256
|
+
"user_id",
|
1257
|
+
"email",
|
1258
|
+
"username",
|
1259
|
+
"first_name",
|
1260
|
+
"last_name",
|
1261
|
+
"status",
|
1262
|
+
"roles",
|
1263
|
+
"created_at",
|
1264
|
+
]
|
1265
|
+
csv_rows = []
|
1266
|
+
for user in users:
|
1267
|
+
csv_rows.append(
|
1268
|
+
[
|
1269
|
+
user.get("user_id", ""),
|
1270
|
+
user.get("email", ""),
|
1271
|
+
user.get("username", ""),
|
1272
|
+
user.get("first_name", ""),
|
1273
|
+
user.get("last_name", ""),
|
1274
|
+
user.get("status", ""),
|
1275
|
+
",".join(user.get("roles", [])),
|
1276
|
+
user.get("created_at", ""),
|
1277
|
+
]
|
1278
|
+
)
|
1279
|
+
|
1280
|
+
export_data = {
|
1281
|
+
"format": "csv",
|
1282
|
+
"headers": csv_headers,
|
1283
|
+
"rows": csv_rows,
|
1284
|
+
"export_metadata": {
|
1285
|
+
"tenant_id": tenant_id,
|
1286
|
+
"export_time": datetime.now(UTC).isoformat(),
|
1287
|
+
"total_users": len(users),
|
1288
|
+
"include_deleted": include_deleted,
|
1289
|
+
},
|
1290
|
+
}
|
1291
|
+
else:
|
1292
|
+
raise NodeValidationError(f"Unsupported export format: {export_format}")
|
1293
|
+
|
1294
|
+
return {
|
1295
|
+
"result": {
|
1296
|
+
"export_data": export_data,
|
1297
|
+
"operation": "export_users",
|
1298
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
1299
|
+
}
|
1300
|
+
}
|
1301
|
+
|
1302
|
+
def _generate_reset_token(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
1303
|
+
"""Generate a password reset token for a user."""
|
1304
|
+
user_id = inputs["user_id"]
|
1305
|
+
tenant_id = inputs["tenant_id"]
|
1306
|
+
|
1307
|
+
# Generate a secure reset token
|
1308
|
+
token = str(uuid4())
|
1309
|
+
expires_at = datetime.now(UTC) + timedelta(hours=1)
|
1310
|
+
|
1311
|
+
# Store token in database (using user_sessions table)
|
1312
|
+
store_token_query = """
|
1313
|
+
INSERT INTO user_sessions (
|
1314
|
+
session_id, user_id, tenant_id,
|
1315
|
+
session_token_hash, expires_at, created_at,
|
1316
|
+
last_accessed, ip_address, user_agent
|
1317
|
+
) VALUES (
|
1318
|
+
:session_id, :user_id, :tenant_id,
|
1319
|
+
:token_hash, :expires_at, :created_at,
|
1320
|
+
:last_accessed, :ip_address, :user_agent
|
1321
|
+
)
|
1322
|
+
"""
|
1323
|
+
|
1324
|
+
result = self._db_node.run(
|
1325
|
+
operation="execute",
|
1326
|
+
query=store_token_query,
|
1327
|
+
parameters={
|
1328
|
+
"session_id": token,
|
1329
|
+
"user_id": user_id,
|
1330
|
+
"tenant_id": tenant_id,
|
1331
|
+
"token_hash": hashlib.sha256(f"reset_{token}".encode()).hexdigest(),
|
1332
|
+
"expires_at": expires_at,
|
1333
|
+
"created_at": datetime.now(UTC),
|
1334
|
+
"last_accessed": datetime.now(UTC),
|
1335
|
+
"ip_address": "127.0.0.1",
|
1336
|
+
"user_agent": "password_reset_token",
|
1337
|
+
},
|
1338
|
+
)
|
905
1339
|
|
906
|
-
|
907
|
-
|
908
|
-
|
909
|
-
|
1340
|
+
return {
|
1341
|
+
"token": token,
|
1342
|
+
"expires_at": expires_at.isoformat(),
|
1343
|
+
"user_id": user_id,
|
1344
|
+
}
|
910
1345
|
|
911
1346
|
def _reset_password(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
912
|
-
"""Reset user password
|
913
|
-
|
914
|
-
|
1347
|
+
"""Reset user password using a valid token."""
|
1348
|
+
token = inputs["token"]
|
1349
|
+
new_password = inputs["new_password"]
|
1350
|
+
tenant_id = inputs["tenant_id"]
|
1351
|
+
|
1352
|
+
# Verify token and get user_id
|
1353
|
+
verify_query = """
|
1354
|
+
SELECT user_id FROM user_sessions
|
1355
|
+
WHERE session_id = :token
|
1356
|
+
AND tenant_id = :tenant_id
|
1357
|
+
AND user_agent = 'password_reset_token'
|
1358
|
+
AND expires_at > CURRENT_TIMESTAMP
|
1359
|
+
"""
|
915
1360
|
|
916
|
-
|
917
|
-
|
918
|
-
|
919
|
-
|
1361
|
+
result = self._db_node.run(
|
1362
|
+
operation="query",
|
1363
|
+
query=verify_query,
|
1364
|
+
parameters={"token": token, "tenant_id": tenant_id},
|
1365
|
+
)
|
920
1366
|
|
921
|
-
|
922
|
-
|
923
|
-
# Implementation with status change to 'active'
|
924
|
-
raise NotImplementedError("Activate operation will be implemented")
|
1367
|
+
if not result.get("data", []):
|
1368
|
+
raise NodeExecutionError("Invalid or expired reset token")
|
925
1369
|
|
926
|
-
|
927
|
-
"""Restore soft-deleted user."""
|
928
|
-
# Implementation with status change from 'deleted'
|
929
|
-
raise NotImplementedError("Restore operation will be implemented")
|
1370
|
+
user_id = result["data"][0]["user_id"]
|
930
1371
|
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
939
|
-
|
940
|
-
|
941
|
-
|
942
|
-
|
943
|
-
|
944
|
-
|
1372
|
+
# Update password
|
1373
|
+
password_hash = hashlib.sha256(new_password.encode()).hexdigest()
|
1374
|
+
update_query = """
|
1375
|
+
UPDATE users
|
1376
|
+
SET password_hash = :password_hash,
|
1377
|
+
updated_at = CURRENT_TIMESTAMP
|
1378
|
+
WHERE user_id = :user_id
|
1379
|
+
AND tenant_id = :tenant_id
|
1380
|
+
"""
|
1381
|
+
|
1382
|
+
update_result = self._db_node.run(
|
1383
|
+
operation="execute",
|
1384
|
+
query=update_query,
|
1385
|
+
parameters={
|
1386
|
+
"password_hash": password_hash,
|
1387
|
+
"user_id": user_id,
|
1388
|
+
"tenant_id": tenant_id,
|
1389
|
+
},
|
1390
|
+
)
|
1391
|
+
|
1392
|
+
# Delete the used token
|
1393
|
+
delete_token_query = """
|
1394
|
+
DELETE FROM user_sessions
|
1395
|
+
WHERE session_id = :token
|
1396
|
+
"""
|
1397
|
+
|
1398
|
+
self._db_node.run(
|
1399
|
+
operation="execute", query=delete_token_query, parameters={"token": token}
|
1400
|
+
)
|
1401
|
+
|
1402
|
+
return {
|
1403
|
+
"success": True,
|
1404
|
+
"user_id": user_id,
|
1405
|
+
"message": "Password reset successfully",
|
1406
|
+
}
|
1407
|
+
|
1408
|
+
def _authenticate(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
1409
|
+
"""Authenticate a user with username/email and password."""
|
1410
|
+
username = inputs.get("username")
|
1411
|
+
email = inputs.get("email")
|
1412
|
+
password = inputs["password"]
|
1413
|
+
tenant_id = inputs["tenant_id"]
|
1414
|
+
|
1415
|
+
# Build query based on provided credentials
|
1416
|
+
if username:
|
1417
|
+
auth_query = """
|
1418
|
+
SELECT user_id, password_hash, status
|
1419
|
+
FROM users
|
1420
|
+
WHERE username = :username
|
1421
|
+
AND tenant_id = :tenant_id
|
1422
|
+
"""
|
1423
|
+
params = {"username": username, "tenant_id": tenant_id}
|
1424
|
+
elif email:
|
1425
|
+
auth_query = """
|
1426
|
+
SELECT user_id, password_hash, status
|
1427
|
+
FROM users
|
1428
|
+
WHERE email = :email
|
1429
|
+
AND tenant_id = :tenant_id
|
1430
|
+
"""
|
1431
|
+
params = {"email": email, "tenant_id": tenant_id}
|
1432
|
+
else:
|
1433
|
+
raise NodeValidationError("Either username or email must be provided")
|
1434
|
+
|
1435
|
+
result = self._db_node.run(
|
1436
|
+
operation="query", query=auth_query, parameters=params
|
1437
|
+
)
|
1438
|
+
|
1439
|
+
if not result.get("data", []):
|
1440
|
+
return {"authenticated": False, "message": "User not found"}
|
1441
|
+
|
1442
|
+
user_data = result["data"][0]
|
1443
|
+
stored_hash = user_data["password_hash"]
|
1444
|
+
provided_hash = hashlib.sha256(password.encode()).hexdigest()
|
1445
|
+
|
1446
|
+
if stored_hash != provided_hash:
|
1447
|
+
return {"authenticated": False, "message": "Invalid password"}
|
1448
|
+
|
1449
|
+
if user_data["status"] != "active":
|
1450
|
+
return {
|
1451
|
+
"authenticated": False,
|
1452
|
+
"message": f"User account is {user_data['status']}",
|
1453
|
+
}
|
1454
|
+
|
1455
|
+
# Update last login
|
1456
|
+
update_login_query = """
|
1457
|
+
UPDATE users
|
1458
|
+
SET last_login_at = CURRENT_TIMESTAMP
|
1459
|
+
WHERE user_id = :user_id
|
1460
|
+
"""
|
1461
|
+
|
1462
|
+
self._db_node.run(
|
1463
|
+
operation="execute",
|
1464
|
+
query=update_login_query,
|
1465
|
+
parameters={"user_id": user_data["user_id"]},
|
1466
|
+
)
|
1467
|
+
|
1468
|
+
return {
|
1469
|
+
"authenticated": True,
|
1470
|
+
"user_id": user_data["user_id"],
|
1471
|
+
"message": "Authentication successful",
|
1472
|
+
}
|