kailash 0.5.0__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/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/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 +1124 -1582
- 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 +9 -3
- 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/workflow/__init__.py +8 -0
- kailash/workflow/async_builder.py +621 -0
- kailash/workflow/async_patterns.py +766 -0
- kailash/workflow/cyclic_runner.py +107 -16
- kailash/workflow/graph.py +7 -2
- kailash/workflow/resilience.py +11 -1
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/METADATA +7 -4
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/RECORD +57 -22
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.5.0.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,1577 +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
|
-
"database_type": "postgresql",
|
363
|
-
"host": "localhost",
|
364
|
-
"port": 5432,
|
365
|
-
"database": "kailash_admin",
|
366
|
-
"user": "admin",
|
367
|
-
"password": "admin",
|
368
|
-
},
|
369
|
-
)
|
370
|
-
|
371
|
-
# Initialize async database node
|
372
|
-
self._db_node = AsyncSQLDatabaseNode(name="user_management_db", **db_config)
|
373
|
-
|
374
|
-
# Initialize enhanced access manager
|
375
|
-
self._access_manager = AccessControlManager(strategy="abac")
|
376
|
-
|
377
|
-
async def _create_user_async(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
378
|
-
"""Create a new user with validation and audit logging (async version)."""
|
379
|
-
user_data = inputs["user_data"]
|
380
|
-
tenant_id = inputs.get("tenant_id", "default")
|
381
|
-
|
382
|
-
# Validate required fields
|
383
|
-
required_fields = ["email", "username", "first_name", "last_name"]
|
384
|
-
for field in required_fields:
|
385
|
-
if field not in user_data:
|
386
|
-
raise NodeValidationError(f"Missing required field: {field}")
|
387
|
-
|
388
|
-
# Validate email format
|
389
|
-
if inputs.get("validate_email", True):
|
390
|
-
if not self._validate_email(user_data["email"]):
|
391
|
-
raise NodeValidationError(f"Invalid email format: {user_data['email']}")
|
392
|
-
|
393
|
-
# Validate username format
|
394
|
-
if inputs.get("validate_username", True):
|
395
|
-
if not self._validate_username(user_data["username"]):
|
396
|
-
raise NodeValidationError(
|
397
|
-
"Invalid username format. Username must be 3-50 characters, "
|
398
|
-
"alphanumeric with underscores/dashes allowed"
|
399
|
-
)
|
400
|
-
|
401
|
-
# Generate user ID
|
402
|
-
user_id = self._generate_user_id()
|
403
|
-
|
404
|
-
# Hash password if provided
|
405
|
-
password_hash = None
|
406
|
-
if "password" in user_data:
|
407
|
-
# Validate password against policy
|
408
|
-
policy = self._config.password_policy
|
409
|
-
password = user_data["password"]
|
410
|
-
|
411
|
-
if len(password) < policy["min_length"]:
|
412
|
-
raise NodeValidationError(
|
413
|
-
f"Password must be at least {policy['min_length']} characters"
|
414
|
-
)
|
415
|
-
|
416
|
-
if policy.get("require_uppercase") and not any(
|
417
|
-
c.isupper() for c in password
|
418
|
-
):
|
419
|
-
raise NodeValidationError("Password must contain uppercase letters")
|
420
|
-
|
421
|
-
if policy.get("require_lowercase") and not any(
|
422
|
-
c.islower() for c in password
|
423
|
-
):
|
424
|
-
raise NodeValidationError("Password must contain lowercase letters")
|
425
|
-
|
426
|
-
if policy.get("require_numbers") and not any(c.isdigit() for c in password):
|
427
|
-
raise NodeValidationError("Password must contain numbers")
|
428
|
-
|
429
|
-
if policy.get("require_special") and not any(
|
430
|
-
c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in password
|
431
|
-
):
|
432
|
-
raise NodeValidationError("Password must contain special characters")
|
433
|
-
|
434
|
-
password_hash = self._hash_password(password)
|
435
|
-
|
436
|
-
# Create user record
|
437
|
-
user_record = {
|
438
|
-
"user_id": user_id,
|
439
|
-
"tenant_id": tenant_id,
|
440
|
-
"email": user_data["email"],
|
441
|
-
"username": user_data["username"],
|
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"),
|
452
|
-
}
|
453
|
-
|
454
|
-
# Insert into database
|
455
|
-
insert_query = """
|
456
|
-
INSERT INTO users (
|
457
|
-
user_id, tenant_id, email, username, first_name, last_name,
|
458
|
-
status, roles, attributes, password_hash, force_password_change,
|
459
|
-
created_at, updated_at, created_by
|
460
|
-
) VALUES (
|
461
|
-
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
|
462
|
-
)
|
463
|
-
"""
|
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
|
464
421
|
|
465
|
-
|
466
|
-
query = {
|
467
|
-
"query": insert_query,
|
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)
|
487
|
-
|
488
|
-
# Create user profile response
|
489
|
-
user_profile = UserProfile(
|
490
|
-
user_id=user_id,
|
491
|
-
email=user_record["email"],
|
492
|
-
username=user_record["username"],
|
493
|
-
first_name=user_record["first_name"],
|
494
|
-
last_name=user_record["last_name"],
|
495
|
-
status=UserStatus(user_record["status"]),
|
496
|
-
roles=user_record["roles"],
|
497
|
-
attributes=user_record["attributes"],
|
498
|
-
created_at=user_record["created_at"],
|
499
|
-
updated_at=user_record["updated_at"],
|
500
|
-
)
|
422
|
+
db_config = inputs["database_config"]
|
501
423
|
|
502
|
-
#
|
503
|
-
|
504
|
-
# Role assignment would be handled by RoleManagementNode
|
505
|
-
pass
|
424
|
+
# Initialize database node
|
425
|
+
self._db_node = SQLDatabaseNode(name="user_management_db", **db_config)
|
506
426
|
|
507
|
-
#
|
508
|
-
if self.
|
509
|
-
|
510
|
-
print(f"[AUDIT] user_created: {user_id} ({user_record['username']})")
|
427
|
+
# Initialize schema manager and ensure schema exists
|
428
|
+
if not self._schema_manager:
|
429
|
+
self._schema_manager = AdminSchemaManager(db_config)
|
511
430
|
|
512
|
-
|
513
|
-
|
514
|
-
|
515
|
-
|
516
|
-
|
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}")
|
517
442
|
|
518
443
|
def _create_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
519
|
-
"""Create a new user
|
444
|
+
"""Create a new user."""
|
520
445
|
user_data = inputs["user_data"]
|
521
|
-
tenant_id = inputs
|
446
|
+
tenant_id = inputs["tenant_id"]
|
522
447
|
|
523
448
|
# Validate required fields
|
524
|
-
|
525
|
-
|
526
|
-
if field not in user_data:
|
527
|
-
raise NodeValidationError(f"Missing required field: {field}")
|
528
|
-
|
529
|
-
# Validate email format
|
530
|
-
if inputs.get("validate_email", True):
|
531
|
-
if not self._validate_email(user_data["email"]):
|
532
|
-
raise NodeValidationError(f"Invalid email format: {user_data['email']}")
|
533
|
-
|
534
|
-
# Validate username format
|
535
|
-
if inputs.get("validate_username", True):
|
536
|
-
if not self._validate_username(user_data["username"]):
|
537
|
-
raise NodeValidationError(
|
538
|
-
f"Invalid username format: {user_data['username']}"
|
539
|
-
)
|
540
|
-
|
541
|
-
# Generate user ID and timestamps
|
542
|
-
user_id = self._generate_user_id()
|
543
|
-
now = datetime.now(UTC)
|
544
|
-
|
545
|
-
# Hash password if provided
|
546
|
-
password_hash = None
|
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"])
|
551
|
-
|
552
|
-
# Prepare user record
|
553
|
-
user_record = {
|
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
|
-
}
|
569
|
-
|
570
|
-
# Insert user into database
|
571
|
-
insert_query = """
|
572
|
-
INSERT INTO users (
|
573
|
-
user_id, tenant_id, email, username, first_name, last_name,
|
574
|
-
status, roles, attributes, password_hash, force_password_change,
|
575
|
-
created_at, updated_at, created_by
|
576
|
-
) VALUES (
|
577
|
-
$1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13, $14
|
578
|
-
)
|
579
|
-
"""
|
580
|
-
|
581
|
-
# Execute database insert
|
582
|
-
query = {
|
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
|
-
}
|
449
|
+
if "email" not in user_data:
|
450
|
+
raise NodeValidationError("Email is required for user creation")
|
601
451
|
|
602
|
-
|
452
|
+
# Generate user ID if not provided
|
453
|
+
user_id = user_data.get("user_id", str(uuid4()))
|
603
454
|
|
604
|
-
#
|
605
|
-
|
455
|
+
# Prepare user data with defaults
|
456
|
+
user = User(
|
606
457
|
user_id=user_id,
|
607
|
-
email=
|
608
|
-
username=
|
609
|
-
first_name=
|
610
|
-
last_name=
|
611
|
-
|
612
|
-
roles=
|
613
|
-
attributes=
|
614
|
-
|
615
|
-
updated_at=user_record["updated_at"],
|
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")),
|
616
466
|
tenant_id=tenant_id,
|
467
|
+
external_auth_id=user_data.get("external_auth_id"),
|
468
|
+
auth_provider=user_data.get("auth_provider", "local"),
|
617
469
|
)
|
618
470
|
|
619
|
-
|
620
|
-
|
621
|
-
|
622
|
-
|
623
|
-
|
624
|
-
|
625
|
-
|
626
|
-
|
627
|
-
|
628
|
-
|
629
|
-
|
630
|
-
|
631
|
-
|
632
|
-
|
633
|
-
|
634
|
-
|
635
|
-
|
636
|
-
|
637
|
-
|
638
|
-
|
639
|
-
def _read_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
640
|
-
"""Read user information by ID or email."""
|
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)
|
645
|
-
|
646
|
-
if not user_id and not email:
|
647
|
-
raise NodeValidationError("Either user_id or email must be provided")
|
648
|
-
|
649
|
-
# Build query
|
650
|
-
where_conditions = ["tenant_id = $1"]
|
651
|
-
params = [tenant_id]
|
652
|
-
param_count = 1
|
653
|
-
|
654
|
-
if user_id:
|
655
|
-
param_count += 1
|
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
|
671
|
-
FROM users
|
672
|
-
WHERE {' AND '.join(where_conditions)}
|
673
|
-
LIMIT 1
|
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
|
674
490
|
"""
|
675
491
|
|
676
|
-
|
677
|
-
|
678
|
-
|
679
|
-
|
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
|
+
)
|
680
511
|
|
681
|
-
|
512
|
+
# Get the created user to return complete data
|
513
|
+
created_user = self._get_user_by_id(user.user_id, tenant_id)
|
682
514
|
|
683
|
-
if not db_result.get("result", {}).get("data"):
|
684
515
|
return {
|
685
516
|
"result": {
|
686
|
-
"user":
|
687
|
-
"
|
688
|
-
"operation": "read",
|
517
|
+
"user": created_user.to_dict(),
|
518
|
+
"operation": "create_user",
|
689
519
|
"timestamp": datetime.now(UTC).isoformat(),
|
690
520
|
}
|
691
521
|
}
|
692
522
|
|
693
|
-
|
694
|
-
|
695
|
-
|
696
|
-
|
697
|
-
|
698
|
-
|
699
|
-
|
700
|
-
|
701
|
-
|
702
|
-
|
703
|
-
|
704
|
-
|
705
|
-
|
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
|
-
}
|
716
|
-
|
717
|
-
def _list_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
718
|
-
"""List users with filtering, pagination, and search."""
|
719
|
-
tenant_id = inputs.get("tenant_id", "default")
|
720
|
-
filters = inputs.get("filters", {})
|
721
|
-
pagination = inputs.get(
|
722
|
-
"pagination", {"page": 1, "size": 20, "sort": "created_at"}
|
723
|
-
)
|
724
|
-
include_deleted = inputs.get("include_deleted", False)
|
725
|
-
|
726
|
-
# Build WHERE clause
|
727
|
-
where_conditions = ["tenant_id = $1"]
|
728
|
-
params = [tenant_id]
|
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
|
-
"""
|
761
|
-
)
|
762
|
-
params.append(f"%{search_query}%")
|
763
|
-
|
764
|
-
# Pagination
|
765
|
-
page = pagination.get("page", 1)
|
766
|
-
size = pagination.get("size", 20)
|
767
|
-
sort_field = pagination.get("sort", "created_at")
|
768
|
-
sort_direction = pagination.get("direction", "DESC")
|
769
|
-
|
770
|
-
offset = (page - 1) * size
|
771
|
-
|
772
|
-
# Count query
|
773
|
-
count_query = f"""
|
774
|
-
SELECT COUNT(*) as total
|
775
|
-
FROM users
|
776
|
-
WHERE {' AND '.join(where_conditions)}
|
777
|
-
"""
|
778
|
-
|
779
|
-
# Data query
|
780
|
-
data_query = f"""
|
781
|
-
SELECT user_id, email, username, first_name, last_name,
|
782
|
-
status, roles, attributes, created_at, updated_at, last_login
|
523
|
+
except Exception as e:
|
524
|
+
if "duplicate key" in str(e).lower():
|
525
|
+
raise NodeValidationError(
|
526
|
+
f"User with email {user.email} already exists"
|
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
|
783
536
|
FROM users
|
784
|
-
WHERE
|
785
|
-
ORDER BY {sort_field} {sort_direction}
|
786
|
-
LIMIT {size} OFFSET {offset}
|
537
|
+
WHERE user_id = $1 AND tenant_id = $2
|
787
538
|
"""
|
788
539
|
|
789
|
-
|
790
|
-
|
791
|
-
{"query": count_query, "params": params, "fetch_mode": "one"}
|
540
|
+
result = self._db_node.run(
|
541
|
+
query=query, parameters=[user_id, tenant_id], result_format="dict"
|
792
542
|
)
|
793
|
-
count_result = self._db_node.run()
|
794
|
-
total_count = count_result["result"]["data"]["total"]
|
795
543
|
|
796
|
-
|
797
|
-
|
798
|
-
|
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")),
|
799
565
|
)
|
800
|
-
data_result = self._db_node.run()
|
801
|
-
users = data_result["result"]["data"]
|
802
|
-
|
803
|
-
# Calculate pagination info
|
804
|
-
total_pages = (total_count + size - 1) // size
|
805
|
-
has_next = page < total_pages
|
806
|
-
has_prev = page > 1
|
807
|
-
|
808
|
-
return {
|
809
|
-
"result": {
|
810
|
-
"users": users,
|
811
|
-
"pagination": {
|
812
|
-
"page": page,
|
813
|
-
"size": size,
|
814
|
-
"total": total_count,
|
815
|
-
"total_pages": total_pages,
|
816
|
-
"has_next": has_next,
|
817
|
-
"has_prev": has_prev,
|
818
|
-
},
|
819
|
-
"filters_applied": filters,
|
820
|
-
"search_query": search_query,
|
821
|
-
"operation": "list",
|
822
|
-
"timestamp": datetime.now(UTC).isoformat(),
|
823
|
-
}
|
824
|
-
}
|
825
566
|
|
826
|
-
def
|
827
|
-
"""
|
828
|
-
|
829
|
-
|
830
|
-
|
831
|
-
|
832
|
-
raise NodeValidationError("user_data must be a list for bulk operations")
|
833
|
-
|
834
|
-
results = {"created": [], "failed": [], "stats": {"created": 0, "failed": 0}}
|
835
|
-
|
836
|
-
for i, user_data in enumerate(user_data_list):
|
837
|
-
try:
|
838
|
-
# Create individual user
|
839
|
-
create_inputs = {
|
840
|
-
"operation": "create",
|
841
|
-
"user_data": user_data,
|
842
|
-
"tenant_id": tenant_id,
|
843
|
-
"validate_email": inputs.get("validate_email", True),
|
844
|
-
"validate_username": inputs.get("validate_username", True),
|
845
|
-
}
|
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")
|
846
573
|
|
847
|
-
|
848
|
-
|
849
|
-
{"index": i, "user": result["result"]["user"]}
|
850
|
-
)
|
851
|
-
results["stats"]["created"] += 1
|
574
|
+
if not any([user_id, email, username]):
|
575
|
+
raise NodeValidationError("Must provide user_id, email, or username")
|
852
576
|
|
853
|
-
|
854
|
-
|
855
|
-
|
856
|
-
|
857
|
-
|
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)
|
858
584
|
|
859
585
|
return {
|
860
586
|
"result": {
|
861
|
-
"
|
862
|
-
"
|
587
|
+
"user": user.to_dict(),
|
588
|
+
"operation": "get_user",
|
863
589
|
"timestamp": datetime.now(UTC).isoformat(),
|
864
590
|
}
|
865
591
|
}
|
866
592
|
|
867
|
-
|
868
|
-
|
869
|
-
|
870
|
-
|
871
|
-
|
872
|
-
|
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
|
601
|
+
"""
|
873
602
|
|
874
|
-
|
875
|
-
|
876
|
-
|
877
|
-
password_hash = hashlib.sha256((password + salt).encode("utf-8")).hexdigest()
|
878
|
-
return f"{salt}${password_hash}"
|
603
|
+
result = self._db_node.run(
|
604
|
+
query=query, parameters=[email, tenant_id], result_format="dict"
|
605
|
+
)
|
879
606
|
|
880
|
-
|
881
|
-
|
882
|
-
|
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")),
|
628
|
+
)
|
883
629
|
|
884
|
-
|
885
|
-
|
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
|
+
"""
|
886
639
|
|
887
|
-
|
888
|
-
|
889
|
-
|
640
|
+
result = self._db_node.run(
|
641
|
+
query=query, parameters=[username, tenant_id], result_format="dict"
|
642
|
+
)
|
890
643
|
|
891
|
-
|
892
|
-
|
893
|
-
|
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
|
+
)
|
894
666
|
|
895
|
-
# Additional operations (update, delete, etc.) would follow similar patterns
|
896
667
|
def _update_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
897
|
-
"""Update user
|
898
|
-
user_id = inputs
|
899
|
-
user_data = inputs
|
900
|
-
tenant_id = inputs
|
668
|
+
"""Update an existing user."""
|
669
|
+
user_id = inputs["user_id"]
|
670
|
+
user_data = inputs["user_data"]
|
671
|
+
tenant_id = inputs["tenant_id"]
|
901
672
|
|
902
|
-
|
903
|
-
|
673
|
+
# Get existing user
|
674
|
+
existing_user = self._get_user_by_id(user_id, tenant_id)
|
904
675
|
|
905
|
-
# Build update fields
|
676
|
+
# Build update query dynamically based on provided fields
|
906
677
|
update_fields = []
|
907
|
-
|
908
|
-
|
678
|
+
parameters = []
|
679
|
+
param_index = 1
|
909
680
|
|
910
|
-
|
911
|
-
allowed_fields = [
|
681
|
+
updatable_fields = [
|
912
682
|
"email",
|
913
683
|
"username",
|
914
684
|
"first_name",
|
915
685
|
"last_name",
|
686
|
+
"display_name",
|
916
687
|
"status",
|
917
|
-
"
|
918
|
-
"
|
919
|
-
"phone",
|
920
|
-
"department",
|
688
|
+
"external_auth_id",
|
689
|
+
"auth_provider",
|
921
690
|
]
|
922
691
|
|
923
|
-
for field
|
924
|
-
if field in
|
925
|
-
|
926
|
-
|
927
|
-
|
928
|
-
|
929
|
-
|
930
|
-
|
931
|
-
|
932
|
-
|
933
|
-
|
934
|
-
|
935
|
-
|
936
|
-
|
937
|
-
|
938
|
-
|
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
|
939
713
|
|
940
714
|
if not update_fields:
|
941
|
-
|
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")
|
942
719
|
|
943
|
-
# Add
|
944
|
-
|
945
|
-
params.append(datetime.now(UTC))
|
946
|
-
param_count += 1
|
720
|
+
# Add WHERE clause parameters
|
721
|
+
parameters.extend([user_id, tenant_id])
|
947
722
|
|
948
|
-
# Build query
|
949
723
|
update_query = f"""
|
950
724
|
UPDATE users
|
951
725
|
SET {', '.join(update_fields)}
|
952
|
-
WHERE user_id = ${
|
953
|
-
RETURNING user_id, email, username, first_name, last_name, status, roles, attributes
|
726
|
+
WHERE user_id = ${param_index} AND tenant_id = ${param_index + 1}
|
954
727
|
"""
|
955
|
-
params.extend([user_id, tenant_id])
|
956
728
|
|
957
|
-
|
958
|
-
|
959
|
-
|
960
|
-
if not result.get("rows"):
|
961
|
-
return {"success": False, "message": "User not found"}
|
729
|
+
try:
|
730
|
+
self._db_node.run(query=update_query, parameters=parameters)
|
962
731
|
|
963
|
-
|
732
|
+
# Get updated user
|
733
|
+
updated_user = self._get_user_by_id(user_id, tenant_id)
|
964
734
|
|
965
|
-
|
966
|
-
|
967
|
-
|
735
|
+
return {
|
736
|
+
"result": {
|
737
|
+
"user": updated_user.to_dict(),
|
738
|
+
"operation": "update_user",
|
739
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
740
|
+
}
|
741
|
+
}
|
968
742
|
|
969
|
-
|
970
|
-
"
|
971
|
-
"user": updated_user,
|
972
|
-
"message": f"User {user_id} updated successfully",
|
973
|
-
}
|
743
|
+
except Exception as e:
|
744
|
+
raise NodeExecutionError(f"Failed to update user: {str(e)}")
|
974
745
|
|
975
746
|
def _delete_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
976
|
-
"""
|
977
|
-
user_id = inputs
|
978
|
-
tenant_id = inputs
|
747
|
+
"""Delete (or soft-delete) a user."""
|
748
|
+
user_id = inputs["user_id"]
|
749
|
+
tenant_id = inputs["tenant_id"]
|
979
750
|
hard_delete = inputs.get("hard_delete", False)
|
980
751
|
|
981
|
-
|
982
|
-
|
983
|
-
|
984
|
-
self._ensure_db_node(inputs)
|
752
|
+
# Get existing user to return in response
|
753
|
+
existing_user = self._get_user_by_id(user_id, tenant_id)
|
985
754
|
|
986
755
|
if hard_delete:
|
987
|
-
#
|
988
|
-
delete_query = ""
|
989
|
-
DELETE FROM users
|
990
|
-
WHERE user_id = $1 AND tenant_id = $2
|
991
|
-
RETURNING user_id, email, username
|
992
|
-
"""
|
756
|
+
# Hard delete - remove from database
|
757
|
+
delete_query = "DELETE FROM users WHERE user_id = $1 AND tenant_id = $2"
|
993
758
|
else:
|
994
|
-
# Soft delete -
|
759
|
+
# Soft delete - mark as deleted
|
995
760
|
delete_query = """
|
996
761
|
UPDATE users
|
997
|
-
SET status = 'deleted',
|
998
|
-
|
999
|
-
deleted_at = $3,
|
1000
|
-
deleted_by = $4
|
1001
|
-
WHERE user_id = $1 AND tenant_id = $2 AND status != 'deleted'
|
1002
|
-
RETURNING user_id, email, username, status
|
762
|
+
SET status = 'deleted', updated_at = CURRENT_TIMESTAMP
|
763
|
+
WHERE user_id = $1 AND tenant_id = $2
|
1003
764
|
"""
|
1004
765
|
|
1005
|
-
|
1006
|
-
|
1007
|
-
params.extend([datetime.now(UTC), inputs.get("deleted_by", "system")])
|
1008
|
-
|
1009
|
-
result = self._db_node.execute(query=delete_query, params=params)
|
766
|
+
try:
|
767
|
+
self._db_node.run(query=delete_query, parameters=[user_id, tenant_id])
|
1010
768
|
|
1011
|
-
|
1012
|
-
|
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
|
+
}
|
776
|
+
}
|
1013
777
|
|
1014
|
-
|
778
|
+
except Exception as e:
|
779
|
+
raise NodeExecutionError(f"Failed to delete user: {str(e)}")
|
1015
780
|
|
1016
|
-
|
1017
|
-
|
1018
|
-
|
1019
|
-
|
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)
|
1020
788
|
|
1021
|
-
|
1022
|
-
|
1023
|
-
|
1024
|
-
|
1025
|
-
|
1026
|
-
|
789
|
+
# Build query with filters
|
790
|
+
where_conditions = ["tenant_id = $1"]
|
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
|
+
"""
|
1027
815
|
|
1028
|
-
|
1029
|
-
|
1030
|
-
|
1031
|
-
|
1032
|
-
|
1033
|
-
tenant_id = inputs.get("tenant_id", "default")
|
1034
|
-
skip_current_check = inputs.get("skip_current_check", False)
|
1035
|
-
|
1036
|
-
if not user_id:
|
1037
|
-
raise NodeValidationError("user_id is required for password change")
|
1038
|
-
if not new_password:
|
1039
|
-
raise NodeValidationError("new_password is required")
|
1040
|
-
if not skip_current_check and not current_password:
|
1041
|
-
raise NodeValidationError(
|
1042
|
-
"current_password is required unless skip_current_check is True"
|
1043
|
-
)
|
816
|
+
count_query = f"""
|
817
|
+
SELECT COUNT(*) as total_count
|
818
|
+
FROM users
|
819
|
+
WHERE {' AND '.join(where_conditions)}
|
820
|
+
"""
|
1044
821
|
|
1045
|
-
|
822
|
+
try:
|
823
|
+
# Get users
|
824
|
+
result = self._db_node.run(
|
825
|
+
query=list_query, parameters=parameters, result_format="dict"
|
826
|
+
)
|
1046
827
|
|
1047
|
-
|
1048
|
-
|
1049
|
-
|
1050
|
-
SELECT password_hash
|
1051
|
-
FROM users
|
1052
|
-
WHERE user_id = $1 AND tenant_id = $2 AND status != 'deleted'
|
1053
|
-
"""
|
1054
|
-
result = self._db_node.execute(
|
1055
|
-
query=verify_query, params=[user_id, tenant_id]
|
828
|
+
# Get total count
|
829
|
+
count_result = self._db_node.run(
|
830
|
+
query=count_query, parameters=parameters[:-2], result_format="dict"
|
1056
831
|
)
|
1057
832
|
|
1058
|
-
|
1059
|
-
|
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())
|
1060
853
|
|
1061
|
-
|
1062
|
-
if stored_hash and not self._verify_password(current_password, stored_hash):
|
1063
|
-
return {"success": False, "message": "Current password is incorrect"}
|
854
|
+
total_count = count_result.get("data", [{}])[0].get("total_count", 0)
|
1064
855
|
|
1065
|
-
|
1066
|
-
|
1067
|
-
|
1068
|
-
|
1069
|
-
|
1070
|
-
|
856
|
+
return {
|
857
|
+
"result": {
|
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",
|
870
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
871
|
+
}
|
872
|
+
}
|
1071
873
|
|
1072
|
-
|
1073
|
-
|
1074
|
-
):
|
1075
|
-
raise NodeValidationError("Password must contain uppercase letters")
|
1076
|
-
|
1077
|
-
if policy.get("require_lowercase") and not any(
|
1078
|
-
c.islower() for c in new_password
|
1079
|
-
):
|
1080
|
-
raise NodeValidationError("Password must contain lowercase letters")
|
1081
|
-
|
1082
|
-
if policy.get("require_numbers") and not any(c.isdigit() for c in new_password):
|
1083
|
-
raise NodeValidationError("Password must contain numbers")
|
1084
|
-
|
1085
|
-
if policy.get("require_special") and not any(
|
1086
|
-
c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in new_password
|
1087
|
-
):
|
1088
|
-
raise NodeValidationError("Password must contain special characters")
|
1089
|
-
|
1090
|
-
# Check password history if configured
|
1091
|
-
if policy.get("history_count", 0) > 0:
|
1092
|
-
history_query = """
|
1093
|
-
SELECT password_hash
|
1094
|
-
FROM password_history
|
1095
|
-
WHERE user_id = $1 AND tenant_id = $2
|
1096
|
-
ORDER BY created_at DESC
|
1097
|
-
LIMIT $3
|
1098
|
-
"""
|
1099
|
-
history_result = self._db_node.execute(
|
1100
|
-
query=history_query,
|
1101
|
-
params=[user_id, tenant_id, policy["history_count"]],
|
1102
|
-
)
|
874
|
+
except Exception as e:
|
875
|
+
raise NodeExecutionError(f"Failed to list users: {str(e)}")
|
1103
876
|
|
1104
|
-
|
1105
|
-
|
1106
|
-
|
1107
|
-
"success": False,
|
1108
|
-
"message": f"Password cannot be reused from last {policy['history_count']} passwords",
|
1109
|
-
}
|
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")
|
1110
880
|
|
1111
|
-
|
1112
|
-
|
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"]
|
1113
891
|
|
1114
|
-
# Update password
|
1115
892
|
update_query = """
|
1116
893
|
UPDATE users
|
1117
|
-
SET
|
1118
|
-
|
1119
|
-
updated_at = $2,
|
1120
|
-
force_password_change = false
|
1121
|
-
WHERE user_id = $3 AND tenant_id = $4
|
1122
|
-
RETURNING user_id, email, username
|
894
|
+
SET status = $1, updated_at = CURRENT_TIMESTAMP
|
895
|
+
WHERE user_id = $2 AND tenant_id = $3
|
1123
896
|
"""
|
1124
897
|
|
1125
|
-
|
1126
|
-
|
1127
|
-
|
1128
|
-
)
|
1129
|
-
|
1130
|
-
if not result.get("rows"):
|
1131
|
-
return {"success": False, "message": "Failed to update password"}
|
1132
|
-
|
1133
|
-
# Store in password history
|
1134
|
-
if policy.get("history_count", 0) > 0:
|
1135
|
-
history_insert = """
|
1136
|
-
INSERT INTO password_history (user_id, tenant_id, password_hash, created_at)
|
1137
|
-
VALUES ($1, $2, $3, $4)
|
1138
|
-
"""
|
1139
|
-
self._db_node.execute(
|
1140
|
-
query=history_insert, params=[user_id, tenant_id, new_hash, now]
|
1141
|
-
)
|
1142
|
-
|
1143
|
-
# Audit log
|
1144
|
-
if self._config.audit_enabled:
|
1145
|
-
print(f"[AUDIT] password_changed: {user_id}")
|
1146
|
-
|
1147
|
-
return {
|
1148
|
-
"success": True,
|
1149
|
-
"user": result["rows"][0],
|
1150
|
-
"message": "Password changed successfully",
|
1151
|
-
}
|
1152
|
-
|
1153
|
-
def _reset_password(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
1154
|
-
"""Reset user password with token generation."""
|
1155
|
-
user_id = inputs.get("user_id")
|
1156
|
-
email = inputs.get("email")
|
1157
|
-
tenant_id = inputs.get("tenant_id", "default")
|
1158
|
-
generate_token = inputs.get("generate_token", True)
|
1159
|
-
new_password = inputs.get("new_password")
|
1160
|
-
token_expiry_hours = inputs.get("token_expiry_hours", 24)
|
1161
|
-
|
1162
|
-
if not user_id and not email:
|
1163
|
-
raise NodeValidationError(
|
1164
|
-
"Either user_id or email is required for password reset"
|
898
|
+
try:
|
899
|
+
self._db_node.run(
|
900
|
+
query=update_query, parameters=[new_status.value, user_id, tenant_id]
|
1165
901
|
)
|
1166
902
|
|
1167
|
-
|
1168
|
-
|
1169
|
-
# Find user
|
1170
|
-
if user_id:
|
1171
|
-
query = "SELECT user_id, email, username FROM users WHERE user_id = $1 AND tenant_id = $2 AND status != 'deleted'"
|
1172
|
-
params = [user_id, tenant_id]
|
1173
|
-
else:
|
1174
|
-
query = "SELECT user_id, email, username FROM users WHERE email = $1 AND tenant_id = $2 AND status != 'deleted'"
|
1175
|
-
params = [email, tenant_id]
|
1176
|
-
|
1177
|
-
result = self._db_node.execute(query=query, params=params)
|
1178
|
-
|
1179
|
-
if not result.get("rows"):
|
1180
|
-
return {"success": False, "message": "User not found"}
|
903
|
+
# Get updated user
|
904
|
+
updated_user = self._get_user_by_id(user_id, tenant_id)
|
1181
905
|
|
1182
|
-
|
1183
|
-
|
906
|
+
return {
|
907
|
+
"result": {
|
908
|
+
"user": updated_user.to_dict(),
|
909
|
+
"operation": operation,
|
910
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
911
|
+
}
|
912
|
+
}
|
1184
913
|
|
1185
|
-
|
1186
|
-
|
1187
|
-
reset_token = secrets.token_urlsafe(32)
|
1188
|
-
token_hash = hashlib.sha256(reset_token.encode()).hexdigest()
|
1189
|
-
expiry_time = datetime.now(UTC) + timedelta(hours=token_expiry_hours)
|
914
|
+
except Exception as e:
|
915
|
+
raise NodeExecutionError(f"Failed to {operation}: {str(e)}")
|
1190
916
|
|
1191
|
-
|
1192
|
-
|
1193
|
-
|
1194
|
-
|
1195
|
-
|
1196
|
-
DO UPDATE SET token_hash = $3, expires_at = $4, created_at = $5, used = false
|
1197
|
-
"""
|
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"]
|
1198
922
|
|
1199
|
-
|
1200
|
-
|
1201
|
-
|
1202
|
-
|
923
|
+
update_query = """
|
924
|
+
UPDATE users
|
925
|
+
SET password_hash = $1, updated_at = CURRENT_TIMESTAMP
|
926
|
+
WHERE user_id = $2 AND tenant_id = $3
|
927
|
+
"""
|
1203
928
|
|
1204
|
-
|
1205
|
-
|
1206
|
-
|
1207
|
-
SET force_password_change = true, updated_at = $1
|
1208
|
-
WHERE user_id = $2 AND tenant_id = $3
|
1209
|
-
"""
|
1210
|
-
self._db_node.execute(
|
1211
|
-
query=update_query, params=[datetime.now(UTC), user_id, tenant_id]
|
929
|
+
try:
|
930
|
+
self._db_node.run(
|
931
|
+
query=update_query, parameters=[password_hash, user_id, tenant_id]
|
1212
932
|
)
|
1213
933
|
|
1214
|
-
# Audit log
|
1215
|
-
if self._config.audit_enabled:
|
1216
|
-
print(f"[AUDIT] password_reset_requested: {user_id}")
|
1217
|
-
|
1218
934
|
return {
|
1219
|
-
"
|
1220
|
-
|
1221
|
-
|
1222
|
-
|
1223
|
-
|
935
|
+
"result": {
|
936
|
+
"user_id": user_id,
|
937
|
+
"password_updated": True,
|
938
|
+
"operation": "set_password",
|
939
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
940
|
+
}
|
1224
941
|
}
|
1225
942
|
|
1226
|
-
|
1227
|
-
|
1228
|
-
# Validate new password
|
1229
|
-
policy = self._config.password_policy
|
1230
|
-
if len(new_password) < policy["min_length"]:
|
1231
|
-
raise NodeValidationError(
|
1232
|
-
f"Password must be at least {policy['min_length']} characters"
|
1233
|
-
)
|
1234
|
-
|
1235
|
-
# Hash and update password
|
1236
|
-
new_hash = self._hash_password(new_password)
|
943
|
+
except Exception as e:
|
944
|
+
raise NodeExecutionError(f"Failed to set password: {str(e)}")
|
1237
945
|
|
1238
|
-
|
1239
|
-
|
1240
|
-
|
1241
|
-
|
1242
|
-
|
1243
|
-
|
1244
|
-
WHERE user_id = $4 AND tenant_id = $5
|
1245
|
-
RETURNING user_id, email, username
|
1246
|
-
"""
|
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
|
1247
952
|
|
1248
|
-
|
1249
|
-
|
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"]
|
1250
957
|
|
1251
|
-
|
1252
|
-
|
1253
|
-
params=[new_hash, now, force_change, user_id, tenant_id],
|
1254
|
-
)
|
958
|
+
if not isinstance(users_data, list):
|
959
|
+
raise NodeValidationError("users_data must be a list")
|
1255
960
|
|
1256
|
-
|
1257
|
-
|
961
|
+
created_users = []
|
962
|
+
failed_users = []
|
1258
963
|
|
1259
|
-
|
1260
|
-
|
1261
|
-
|
1262
|
-
|
1263
|
-
|
1264
|
-
|
1265
|
-
|
1266
|
-
|
1267
|
-
|
1268
|
-
}
|
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
|
+
}
|
1269
973
|
|
1270
|
-
|
1271
|
-
|
1272
|
-
"Either generate_token or new_password must be provided"
|
1273
|
-
)
|
974
|
+
result = self._create_user(create_inputs)
|
975
|
+
created_users.append(result["result"]["user"])
|
1274
976
|
|
1275
|
-
|
1276
|
-
|
1277
|
-
|
1278
|
-
|
1279
|
-
|
1280
|
-
|
977
|
+
except Exception as e:
|
978
|
+
failed_users.append(
|
979
|
+
{
|
980
|
+
"index": i,
|
981
|
+
"user_data": user_data,
|
982
|
+
"error": str(e),
|
983
|
+
}
|
984
|
+
)
|
1281
985
|
|
1282
|
-
|
1283
|
-
|
986
|
+
return {
|
987
|
+
"result": {
|
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,
|
994
|
+
},
|
995
|
+
"operation": "bulk_create",
|
996
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
997
|
+
}
|
998
|
+
}
|
1284
999
|
|
1285
|
-
|
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"]
|
1286
1004
|
|
1287
|
-
|
1288
|
-
|
1289
|
-
UPDATE users
|
1290
|
-
SET status = 'inactive',
|
1291
|
-
updated_at = $1,
|
1292
|
-
deactivated_at = $1,
|
1293
|
-
deactivation_reason = $2,
|
1294
|
-
deactivated_by = $3
|
1295
|
-
WHERE user_id = $4 AND tenant_id = $5 AND status = 'active'
|
1296
|
-
RETURNING user_id, email, username, status, first_name, last_name
|
1297
|
-
"""
|
1005
|
+
if not isinstance(users_data, list):
|
1006
|
+
raise NodeValidationError("users_data must be a list")
|
1298
1007
|
|
1299
|
-
|
1300
|
-
|
1301
|
-
query=update_query, params=[now, reason, deactivated_by, user_id, tenant_id]
|
1302
|
-
)
|
1008
|
+
updated_users = []
|
1009
|
+
failed_users = []
|
1303
1010
|
|
1304
|
-
|
1305
|
-
|
1306
|
-
|
1307
|
-
|
1308
|
-
WHERE user_id = $1 AND tenant_id = $2
|
1309
|
-
"""
|
1310
|
-
check_result = self._db_node.execute(
|
1311
|
-
query=check_query, params=[user_id, tenant_id]
|
1312
|
-
)
|
1011
|
+
for i, user_data in enumerate(users_data):
|
1012
|
+
try:
|
1013
|
+
if "user_id" not in user_data:
|
1014
|
+
raise NodeValidationError("user_id is required for bulk update")
|
1313
1015
|
|
1314
|
-
|
1315
|
-
|
1316
|
-
|
1317
|
-
"
|
1318
|
-
"
|
1016
|
+
update_inputs = {
|
1017
|
+
"operation": "update_user",
|
1018
|
+
"user_id": user_data.pop("user_id"),
|
1019
|
+
"user_data": user_data,
|
1020
|
+
"tenant_id": tenant_id,
|
1021
|
+
"database_config": inputs["database_config"],
|
1319
1022
|
}
|
1320
|
-
else:
|
1321
|
-
return {"success": False, "message": "User not found"}
|
1322
1023
|
|
1323
|
-
|
1024
|
+
result = self._update_user(update_inputs)
|
1025
|
+
updated_users.append(result["result"]["user"])
|
1324
1026
|
|
1325
|
-
|
1326
|
-
|
1327
|
-
|
1328
|
-
|
1329
|
-
|
1330
|
-
|
1331
|
-
|
1332
|
-
|
1333
|
-
# Audit log
|
1334
|
-
if self._config.audit_enabled:
|
1335
|
-
print(f"[AUDIT] user_deactivated: {user_id} (reason: {reason})")
|
1027
|
+
except Exception as e:
|
1028
|
+
failed_users.append(
|
1029
|
+
{
|
1030
|
+
"index": i,
|
1031
|
+
"user_data": user_data,
|
1032
|
+
"error": str(e),
|
1033
|
+
}
|
1034
|
+
)
|
1336
1035
|
|
1337
1036
|
return {
|
1338
|
-
"
|
1339
|
-
|
1340
|
-
|
1341
|
-
|
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
|
+
}
|
1342
1048
|
}
|
1343
1049
|
|
1344
|
-
def
|
1345
|
-
"""
|
1346
|
-
|
1347
|
-
tenant_id = inputs
|
1348
|
-
|
1349
|
-
clear_deactivation_data = inputs.get("clear_deactivation_data", True)
|
1350
|
-
|
1351
|
-
if not user_id:
|
1352
|
-
raise NodeValidationError("user_id is required for activate operation")
|
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)
|
1353
1055
|
|
1354
|
-
|
1056
|
+
if not isinstance(user_ids, list):
|
1057
|
+
raise NodeValidationError("user_ids must be a list")
|
1355
1058
|
|
1356
|
-
|
1357
|
-
|
1358
|
-
update_query = """
|
1359
|
-
UPDATE users
|
1360
|
-
SET status = 'active',
|
1361
|
-
updated_at = $1,
|
1362
|
-
activated_at = $1,
|
1363
|
-
activated_by = $2,
|
1364
|
-
deactivated_at = NULL,
|
1365
|
-
deactivation_reason = NULL,
|
1366
|
-
deactivated_by = NULL
|
1367
|
-
WHERE user_id = $3 AND tenant_id = $4 AND status IN ('inactive', 'pending')
|
1368
|
-
RETURNING user_id, email, username, status, first_name, last_name
|
1369
|
-
"""
|
1370
|
-
else:
|
1371
|
-
update_query = """
|
1372
|
-
UPDATE users
|
1373
|
-
SET status = 'active',
|
1374
|
-
updated_at = $1,
|
1375
|
-
activated_at = $1,
|
1376
|
-
activated_by = $2
|
1377
|
-
WHERE user_id = $3 AND tenant_id = $4 AND status IN ('inactive', 'pending')
|
1378
|
-
RETURNING user_id, email, username, status, first_name, last_name
|
1379
|
-
"""
|
1059
|
+
deleted_users = []
|
1060
|
+
failed_users = []
|
1380
1061
|
|
1381
|
-
|
1382
|
-
|
1383
|
-
|
1384
|
-
|
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
|
+
}
|
1385
1071
|
|
1386
|
-
|
1387
|
-
|
1388
|
-
check_query = """
|
1389
|
-
SELECT status FROM users
|
1390
|
-
WHERE user_id = $1 AND tenant_id = $2
|
1391
|
-
"""
|
1392
|
-
check_result = self._db_node.execute(
|
1393
|
-
query=check_query, params=[user_id, tenant_id]
|
1394
|
-
)
|
1072
|
+
result = self._delete_user(delete_inputs)
|
1073
|
+
deleted_users.append(result["result"]["deleted_user"])
|
1395
1074
|
|
1396
|
-
|
1397
|
-
|
1398
|
-
|
1399
|
-
|
1400
|
-
|
1401
|
-
return {
|
1402
|
-
"success": False,
|
1403
|
-
"message": "Cannot activate deleted user. Use restore operation instead.",
|
1404
|
-
}
|
1405
|
-
else:
|
1406
|
-
return {
|
1407
|
-
"success": False,
|
1408
|
-
"message": f"Cannot activate user with status: {current_status}",
|
1075
|
+
except Exception as e:
|
1076
|
+
failed_users.append(
|
1077
|
+
{
|
1078
|
+
"user_id": user_id,
|
1079
|
+
"error": str(e),
|
1409
1080
|
}
|
1410
|
-
|
1411
|
-
return {"success": False, "message": "User not found"}
|
1412
|
-
|
1413
|
-
activated_user = result["rows"][0]
|
1414
|
-
|
1415
|
-
# Audit log
|
1416
|
-
if self._config.audit_enabled:
|
1417
|
-
print(f"[AUDIT] user_activated: {user_id}")
|
1081
|
+
)
|
1418
1082
|
|
1419
1083
|
return {
|
1420
|
-
"
|
1421
|
-
|
1422
|
-
|
1084
|
+
"result": {
|
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
|
+
}
|
1423
1096
|
}
|
1424
1097
|
|
1425
|
-
def
|
1426
|
-
"""
|
1427
|
-
|
1428
|
-
|
1429
|
-
|
1430
|
-
|
1431
|
-
|
1432
|
-
|
1433
|
-
|
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
|
+
"""
|
1434
1114
|
|
1435
|
-
|
1436
|
-
|
1437
|
-
|
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 = []
|
1438
1123
|
|
1439
|
-
|
1440
|
-
|
1441
|
-
|
1442
|
-
|
1443
|
-
|
1444
|
-
|
1124
|
+
return {
|
1125
|
+
"result": {
|
1126
|
+
"user_id": user_id,
|
1127
|
+
"roles": user.roles,
|
1128
|
+
"role_details": role_details,
|
1129
|
+
"operation": "get_user_roles",
|
1130
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
1131
|
+
}
|
1132
|
+
}
|
1445
1133
|
|
1446
|
-
|
1447
|
-
|
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"]
|
1448
1138
|
|
1449
|
-
|
1450
|
-
|
1451
|
-
|
1452
|
-
)
|
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)
|
1453
1142
|
|
1454
|
-
|
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
|
+
}
|
1455
1153
|
|
1456
|
-
|
1457
|
-
|
1458
|
-
|
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
|
1459
1166
|
FROM users
|
1460
|
-
WHERE
|
1461
|
-
|
1462
|
-
|
1463
|
-
|
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
|
1464
1173
|
)
|
1174
|
+
ORDER BY created_at DESC
|
1175
|
+
LIMIT $3 OFFSET $4
|
1176
|
+
"""
|
1177
|
+
|
1178
|
+
search_pattern = f"%{search_query}%"
|
1465
1179
|
|
1466
|
-
|
1467
|
-
|
1180
|
+
try:
|
1181
|
+
result = self._db_node.run(
|
1182
|
+
query=query,
|
1183
|
+
parameters=[tenant_id, search_pattern, limit, offset],
|
1184
|
+
result_format="dict",
|
1185
|
+
)
|
1186
|
+
|
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())
|
1468
1207
|
|
1469
|
-
user_data = check_result["rows"][0]
|
1470
|
-
if user_data["status"] != "deleted":
|
1471
1208
|
return {
|
1472
|
-
"
|
1473
|
-
|
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
|
+
}
|
1474
1220
|
}
|
1475
1221
|
|
1476
|
-
|
1477
|
-
|
1478
|
-
UPDATE users
|
1479
|
-
SET status = $1,
|
1480
|
-
updated_at = $2,
|
1481
|
-
restored_at = $2,
|
1482
|
-
restored_by = $3,
|
1483
|
-
deleted_at = NULL,
|
1484
|
-
deleted_by = NULL
|
1485
|
-
WHERE user_id = $4 AND tenant_id = $5 AND status = 'deleted'
|
1486
|
-
RETURNING user_id, email, username, status, first_name, last_name
|
1487
|
-
"""
|
1222
|
+
except Exception as e:
|
1223
|
+
raise NodeExecutionError(f"Failed to search users: {str(e)}")
|
1488
1224
|
|
1489
|
-
|
1490
|
-
|
1491
|
-
|
1492
|
-
|
1493
|
-
)
|
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
|
+
}
|
1494
1239
|
|
1495
|
-
|
1496
|
-
|
1240
|
+
result = self._list_users(list_inputs)
|
1241
|
+
users = result["result"]["users"]
|
1497
1242
|
|
1498
|
-
|
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
|
+
)
|
1499
1279
|
|
1500
|
-
|
1501
|
-
|
1502
|
-
|
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}")
|
1503
1293
|
|
1504
1294
|
return {
|
1505
|
-
"
|
1506
|
-
|
1507
|
-
|
1508
|
-
|
1509
|
-
|
1295
|
+
"result": {
|
1296
|
+
"export_data": export_data,
|
1297
|
+
"operation": "export_users",
|
1298
|
+
"timestamp": datetime.now(UTC).isoformat(),
|
1299
|
+
}
|
1510
1300
|
}
|
1511
1301
|
|
1512
|
-
def
|
1513
|
-
"""
|
1514
|
-
|
1515
|
-
tenant_id = inputs
|
1516
|
-
|
1517
|
-
|
1518
|
-
|
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
|
1519
1321
|
)
|
1520
|
-
|
1521
|
-
|
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
|
+
},
|
1522
1338
|
)
|
1523
|
-
include_deleted = inputs.get("include_deleted", False)
|
1524
|
-
fuzzy_search = inputs.get("fuzzy_search", True)
|
1525
1339
|
|
1526
|
-
|
1340
|
+
return {
|
1341
|
+
"token": token,
|
1342
|
+
"expires_at": expires_at.isoformat(),
|
1343
|
+
"user_id": user_id,
|
1344
|
+
}
|
1527
1345
|
|
1528
|
-
|
1529
|
-
|
1530
|
-
|
1531
|
-
|
1532
|
-
|
1533
|
-
|
1534
|
-
|
1535
|
-
|
1536
|
-
|
1537
|
-
|
1538
|
-
|
1539
|
-
|
1540
|
-
|
1541
|
-
|
1542
|
-
else:
|
1543
|
-
where_conditions.append(f"status = ${param_count}")
|
1544
|
-
params.append(filters["status"])
|
1545
|
-
|
1546
|
-
if "roles" in filters:
|
1547
|
-
param_count += 1
|
1548
|
-
where_conditions.append(f"roles && ${param_count}")
|
1549
|
-
params.append(filters["roles"])
|
1550
|
-
|
1551
|
-
if "created_after" in filters:
|
1552
|
-
param_count += 1
|
1553
|
-
where_conditions.append(f"created_at >= ${param_count}")
|
1554
|
-
params.append(filters["created_after"])
|
1555
|
-
|
1556
|
-
if "created_before" in filters:
|
1557
|
-
param_count += 1
|
1558
|
-
where_conditions.append(f"created_at <= ${param_count}")
|
1559
|
-
params.append(filters["created_before"])
|
1560
|
-
|
1561
|
-
# Apply attribute filters
|
1562
|
-
if "attributes" in filters:
|
1563
|
-
for attr_key, attr_value in filters["attributes"].items():
|
1564
|
-
param_count += 1
|
1565
|
-
where_conditions.append(f"attributes->>'{attr_key}' = ${param_count}")
|
1566
|
-
params.append(attr_value)
|
1567
|
-
|
1568
|
-
# Build search query
|
1569
|
-
if search_query:
|
1570
|
-
search_conditions = []
|
1571
|
-
param_count += 1
|
1572
|
-
|
1573
|
-
if fuzzy_search:
|
1574
|
-
# Use ILIKE for fuzzy matching
|
1575
|
-
search_pattern = f"%{search_query}%"
|
1576
|
-
params.append(search_pattern)
|
1577
|
-
|
1578
|
-
for field in search_fields:
|
1579
|
-
search_conditions.append(f"{field} ILIKE ${param_count}")
|
1580
|
-
else:
|
1581
|
-
# Exact match
|
1582
|
-
params.append(search_query)
|
1583
|
-
|
1584
|
-
for field in search_fields:
|
1585
|
-
search_conditions.append(f"{field} = ${param_count}")
|
1586
|
-
|
1587
|
-
if search_conditions:
|
1588
|
-
where_conditions.append(f"({' OR '.join(search_conditions)})")
|
1589
|
-
|
1590
|
-
# Get pagination settings
|
1591
|
-
page = pagination.get("page", 1)
|
1592
|
-
size = pagination.get("size", 20)
|
1593
|
-
sort_field = pagination.get("sort", "relevance")
|
1594
|
-
sort_direction = pagination.get("direction", "DESC")
|
1595
|
-
|
1596
|
-
# Calculate offset
|
1597
|
-
offset = (page - 1) * size
|
1598
|
-
|
1599
|
-
# Build relevance scoring for sorting
|
1600
|
-
if sort_field == "relevance" and search_query:
|
1601
|
-
relevance_score = f"""
|
1602
|
-
CASE
|
1603
|
-
WHEN email = ${param_count} THEN 4
|
1604
|
-
WHEN username = ${param_count} THEN 3
|
1605
|
-
WHEN email ILIKE ${param_count} THEN 2
|
1606
|
-
WHEN username ILIKE ${param_count} OR first_name ILIKE ${param_count} OR last_name ILIKE ${param_count} THEN 1
|
1607
|
-
ELSE 0
|
1608
|
-
END as relevance
|
1609
|
-
"""
|
1346
|
+
def _reset_password(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
|
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
|
+
"""
|
1610
1360
|
|
1611
|
-
|
1612
|
-
|
1613
|
-
|
1614
|
-
|
1361
|
+
result = self._db_node.run(
|
1362
|
+
operation="query",
|
1363
|
+
query=verify_query,
|
1364
|
+
parameters={"token": token, "tenant_id": tenant_id},
|
1365
|
+
)
|
1615
1366
|
|
1616
|
-
|
1617
|
-
|
1618
|
-
SELECT COUNT(*) as total
|
1619
|
-
FROM users
|
1620
|
-
WHERE {' AND '.join(where_conditions)}
|
1621
|
-
"""
|
1367
|
+
if not result.get("data", []):
|
1368
|
+
raise NodeExecutionError("Invalid or expired reset token")
|
1622
1369
|
|
1623
|
-
|
1624
|
-
|
1625
|
-
|
1626
|
-
|
1627
|
-
|
1628
|
-
|
1629
|
-
|
1630
|
-
|
1631
|
-
|
1370
|
+
user_id = result["data"][0]["user_id"]
|
1371
|
+
|
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
|
1632
1380
|
"""
|
1633
1381
|
|
1634
|
-
|
1635
|
-
|
1636
|
-
|
1637
|
-
|
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
|
+
},
|
1638
1390
|
)
|
1639
1391
|
|
1640
|
-
#
|
1641
|
-
|
1642
|
-
|
1643
|
-
|
1644
|
-
|
1645
|
-
total_pages = (total_count + size - 1) // size if size > 0 else 0
|
1646
|
-
has_next = page < total_pages
|
1647
|
-
has_prev = page > 1
|
1392
|
+
# Delete the used token
|
1393
|
+
delete_token_query = """
|
1394
|
+
DELETE FROM user_sessions
|
1395
|
+
WHERE session_id = :token
|
1396
|
+
"""
|
1648
1397
|
|
1649
|
-
|
1650
|
-
|
1651
|
-
|
1398
|
+
self._db_node.run(
|
1399
|
+
operation="execute", query=delete_token_query, parameters={"token": token}
|
1400
|
+
)
|
1652
1401
|
|
1653
1402
|
return {
|
1654
1403
|
"success": True,
|
1655
|
-
"
|
1656
|
-
"
|
1657
|
-
"page": page,
|
1658
|
-
"size": size,
|
1659
|
-
"total": total_count,
|
1660
|
-
"total_pages": total_pages,
|
1661
|
-
"has_next": has_next,
|
1662
|
-
"has_prev": has_prev,
|
1663
|
-
},
|
1664
|
-
"search": {
|
1665
|
-
"query": search_query,
|
1666
|
-
"fields": search_fields,
|
1667
|
-
"fuzzy": fuzzy_search,
|
1668
|
-
},
|
1669
|
-
"filters_applied": filters,
|
1670
|
-
"message": f"Found {total_count} users matching criteria",
|
1404
|
+
"user_id": user_id,
|
1405
|
+
"message": "Password reset successfully",
|
1671
1406
|
}
|
1672
1407
|
|
1673
|
-
def
|
1674
|
-
"""
|
1675
|
-
|
1676
|
-
|
1677
|
-
|
1678
|
-
|
1679
|
-
|
1680
|
-
if not user_updates:
|
1681
|
-
raise NodeValidationError("user_updates list is required for bulk update")
|
1682
|
-
|
1683
|
-
if not isinstance(user_updates, list):
|
1684
|
-
raise NodeValidationError("user_updates must be a list")
|
1685
|
-
|
1686
|
-
self._ensure_db_node(inputs)
|
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"]
|
1687
1414
|
|
1688
|
-
|
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")
|
1689
1434
|
|
1690
|
-
|
1691
|
-
|
1692
|
-
|
1435
|
+
result = self._db_node.run(
|
1436
|
+
operation="query", query=auth_query, parameters=params
|
1437
|
+
)
|
1693
1438
|
|
1694
|
-
|
1695
|
-
|
1696
|
-
try:
|
1697
|
-
user_id = update_data.get("user_id")
|
1698
|
-
if not user_id:
|
1699
|
-
raise NodeValidationError(
|
1700
|
-
f"user_id missing in update at index {i}"
|
1701
|
-
)
|
1702
|
-
|
1703
|
-
# Build update fields
|
1704
|
-
update_fields = []
|
1705
|
-
params = []
|
1706
|
-
param_count = 1
|
1707
|
-
|
1708
|
-
# Update allowed fields
|
1709
|
-
allowed_fields = [
|
1710
|
-
"email",
|
1711
|
-
"username",
|
1712
|
-
"first_name",
|
1713
|
-
"last_name",
|
1714
|
-
"status",
|
1715
|
-
"roles",
|
1716
|
-
"attributes",
|
1717
|
-
"phone",
|
1718
|
-
"department",
|
1719
|
-
]
|
1439
|
+
if not result.get("data", []):
|
1440
|
+
return {"authenticated": False, "message": "User not found"}
|
1720
1441
|
|
1721
|
-
|
1722
|
-
|
1723
|
-
|
1724
|
-
if field == "email" and inputs.get("validate_email", True):
|
1725
|
-
if not self._validate_email(value):
|
1726
|
-
raise NodeValidationError(
|
1727
|
-
f"Invalid email format: {value}"
|
1728
|
-
)
|
1729
|
-
elif field == "username" and inputs.get(
|
1730
|
-
"validate_username", True
|
1731
|
-
):
|
1732
|
-
if not self._validate_username(value):
|
1733
|
-
raise NodeValidationError(
|
1734
|
-
f"Invalid username format: {value}"
|
1735
|
-
)
|
1736
|
-
elif field == "status":
|
1737
|
-
if value not in [s.value for s in UserStatus]:
|
1738
|
-
raise NodeValidationError(
|
1739
|
-
f"Invalid status: {value}"
|
1740
|
-
)
|
1741
|
-
|
1742
|
-
update_fields.append(f"{field} = ${param_count}")
|
1743
|
-
params.append(value)
|
1744
|
-
param_count += 1
|
1745
|
-
|
1746
|
-
if not update_fields:
|
1747
|
-
raise NodeValidationError(
|
1748
|
-
f"No valid fields to update at index {i}"
|
1749
|
-
)
|
1750
|
-
|
1751
|
-
# Add updated_at and updated_by
|
1752
|
-
update_fields.append(f"updated_at = ${param_count}")
|
1753
|
-
params.append(datetime.now(UTC))
|
1754
|
-
param_count += 1
|
1755
|
-
|
1756
|
-
update_fields.append(f"updated_by = ${param_count}")
|
1757
|
-
params.append(updated_by)
|
1758
|
-
param_count += 1
|
1759
|
-
|
1760
|
-
# Build query
|
1761
|
-
update_query = f"""
|
1762
|
-
UPDATE users
|
1763
|
-
SET {', '.join(update_fields)}
|
1764
|
-
WHERE user_id = ${param_count} AND tenant_id = ${param_count + 1}
|
1765
|
-
RETURNING user_id, email, username, status
|
1766
|
-
"""
|
1767
|
-
params.extend([user_id, tenant_id])
|
1768
|
-
|
1769
|
-
result = self._db_node.execute(query=update_query, params=params)
|
1770
|
-
|
1771
|
-
if result.get("rows"):
|
1772
|
-
results["updated"].append(
|
1773
|
-
{"index": i, "user": result["rows"][0]}
|
1774
|
-
)
|
1775
|
-
results["stats"]["updated"] += 1
|
1776
|
-
else:
|
1777
|
-
raise Exception("User not found or no changes made")
|
1778
|
-
|
1779
|
-
except Exception as e:
|
1780
|
-
error_info = {
|
1781
|
-
"index": i,
|
1782
|
-
"user_id": update_data.get("user_id"),
|
1783
|
-
"error": str(e),
|
1784
|
-
}
|
1442
|
+
user_data = result["data"][0]
|
1443
|
+
stored_hash = user_data["password_hash"]
|
1444
|
+
provided_hash = hashlib.sha256(password.encode()).hexdigest()
|
1785
1445
|
|
1786
|
-
|
1787
|
-
|
1788
|
-
self._db_node.execute(query="ROLLBACK")
|
1789
|
-
return {
|
1790
|
-
"success": False,
|
1791
|
-
"message": f"Bulk update failed at index {i}: {str(e)}",
|
1792
|
-
"error_detail": error_info,
|
1793
|
-
"stats": results["stats"],
|
1794
|
-
}
|
1795
|
-
else:
|
1796
|
-
# Continue with next update
|
1797
|
-
results["failed"].append(error_info)
|
1798
|
-
results["stats"]["failed"] += 1
|
1799
|
-
|
1800
|
-
# Commit transaction if all_or_none mode
|
1801
|
-
if transaction_mode == "all_or_none":
|
1802
|
-
self._db_node.execute(query="COMMIT")
|
1803
|
-
|
1804
|
-
# Audit log
|
1805
|
-
if self._config.audit_enabled:
|
1806
|
-
print(
|
1807
|
-
f"[AUDIT] bulk_user_update: updated={results['stats']['updated']}, failed={results['stats']['failed']}"
|
1808
|
-
)
|
1446
|
+
if stored_hash != provided_hash:
|
1447
|
+
return {"authenticated": False, "message": "Invalid password"}
|
1809
1448
|
|
1449
|
+
if user_data["status"] != "active":
|
1810
1450
|
return {
|
1811
|
-
"
|
1812
|
-
"
|
1813
|
-
"message": f"Bulk update completed: {results['stats']['updated']} updated, {results['stats']['failed']} failed",
|
1814
|
-
"transaction_mode": transaction_mode,
|
1451
|
+
"authenticated": False,
|
1452
|
+
"message": f"User account is {user_data['status']}",
|
1815
1453
|
}
|
1816
1454
|
|
1817
|
-
|
1818
|
-
|
1819
|
-
|
1820
|
-
|
1821
|
-
|
1822
|
-
|
1823
|
-
"""Bulk delete multiple users."""
|
1824
|
-
user_ids = inputs.get("user_ids", [])
|
1825
|
-
tenant_id = inputs.get("tenant_id", "default")
|
1826
|
-
hard_delete = inputs.get("hard_delete", False)
|
1827
|
-
deleted_by = inputs.get("deleted_by", "system")
|
1828
|
-
transaction_mode = inputs.get("transaction_mode", "all_or_none")
|
1829
|
-
|
1830
|
-
if not user_ids:
|
1831
|
-
raise NodeValidationError("user_ids list is required for bulk delete")
|
1832
|
-
|
1833
|
-
if not isinstance(user_ids, list):
|
1834
|
-
raise NodeValidationError("user_ids must be a list")
|
1835
|
-
|
1836
|
-
self._ensure_db_node(inputs)
|
1837
|
-
|
1838
|
-
results = {"deleted": [], "failed": [], "stats": {"deleted": 0, "failed": 0}}
|
1839
|
-
|
1840
|
-
# Start transaction if all_or_none mode
|
1841
|
-
if transaction_mode == "all_or_none":
|
1842
|
-
self._db_node.execute(query="BEGIN")
|
1843
|
-
|
1844
|
-
try:
|
1845
|
-
now = datetime.now(UTC)
|
1846
|
-
|
1847
|
-
for i, user_id in enumerate(user_ids):
|
1848
|
-
try:
|
1849
|
-
if hard_delete:
|
1850
|
-
# Permanent deletion
|
1851
|
-
delete_query = """
|
1852
|
-
DELETE FROM users
|
1853
|
-
WHERE user_id = $1 AND tenant_id = $2
|
1854
|
-
RETURNING user_id, email, username
|
1855
|
-
"""
|
1856
|
-
params = [user_id, tenant_id]
|
1857
|
-
else:
|
1858
|
-
# Soft delete
|
1859
|
-
delete_query = """
|
1860
|
-
UPDATE users
|
1861
|
-
SET status = 'deleted',
|
1862
|
-
updated_at = $1,
|
1863
|
-
deleted_at = $1,
|
1864
|
-
deleted_by = $2
|
1865
|
-
WHERE user_id = $3 AND tenant_id = $4 AND status != 'deleted'
|
1866
|
-
RETURNING user_id, email, username, status
|
1867
|
-
"""
|
1868
|
-
params = [now, deleted_by, user_id, tenant_id]
|
1869
|
-
|
1870
|
-
result = self._db_node.execute(query=delete_query, params=params)
|
1871
|
-
|
1872
|
-
if result.get("rows"):
|
1873
|
-
results["deleted"].append(
|
1874
|
-
{"index": i, "user": result["rows"][0]}
|
1875
|
-
)
|
1876
|
-
results["stats"]["deleted"] += 1
|
1877
|
-
|
1878
|
-
# Revoke sessions for soft delete
|
1879
|
-
if not hard_delete:
|
1880
|
-
session_query = """
|
1881
|
-
UPDATE user_sessions
|
1882
|
-
SET status = 'revoked', revoked_at = $1
|
1883
|
-
WHERE user_id = $2 AND tenant_id = $3 AND status = 'active'
|
1884
|
-
"""
|
1885
|
-
self._db_node.execute(
|
1886
|
-
query=session_query, params=[now, user_id, tenant_id]
|
1887
|
-
)
|
1888
|
-
else:
|
1889
|
-
raise Exception("User not found or already deleted")
|
1890
|
-
|
1891
|
-
except Exception as e:
|
1892
|
-
error_info = {"index": i, "user_id": user_id, "error": str(e)}
|
1893
|
-
|
1894
|
-
if transaction_mode == "all_or_none":
|
1895
|
-
# Rollback and return error
|
1896
|
-
self._db_node.execute(query="ROLLBACK")
|
1897
|
-
return {
|
1898
|
-
"success": False,
|
1899
|
-
"message": f"Bulk delete failed at index {i}: {str(e)}",
|
1900
|
-
"error_detail": error_info,
|
1901
|
-
"stats": results["stats"],
|
1902
|
-
}
|
1903
|
-
else:
|
1904
|
-
# Continue with next deletion
|
1905
|
-
results["failed"].append(error_info)
|
1906
|
-
results["stats"]["failed"] += 1
|
1907
|
-
|
1908
|
-
# Commit transaction if all_or_none mode
|
1909
|
-
if transaction_mode == "all_or_none":
|
1910
|
-
self._db_node.execute(query="COMMIT")
|
1911
|
-
|
1912
|
-
# Audit log
|
1913
|
-
if self._config.audit_enabled:
|
1914
|
-
action = "hard_deleted" if hard_delete else "soft_deleted"
|
1915
|
-
print(
|
1916
|
-
f"[AUDIT] bulk_user_{action}: deleted={results['stats']['deleted']}, failed={results['stats']['failed']}"
|
1917
|
-
)
|
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
|
+
"""
|
1918
1461
|
|
1919
|
-
|
1920
|
-
|
1921
|
-
|
1922
|
-
|
1923
|
-
|
1924
|
-
"transaction_mode": transaction_mode,
|
1925
|
-
}
|
1462
|
+
self._db_node.run(
|
1463
|
+
operation="execute",
|
1464
|
+
query=update_login_query,
|
1465
|
+
parameters={"user_id": user_data["user_id"]},
|
1466
|
+
)
|
1926
1467
|
|
1927
|
-
|
1928
|
-
|
1929
|
-
|
1930
|
-
|
1468
|
+
return {
|
1469
|
+
"authenticated": True,
|
1470
|
+
"user_id": user_data["user_id"],
|
1471
|
+
"message": "Authentication successful",
|
1472
|
+
}
|