kailash 0.5.0__py3-none-any.whl → 0.6.1__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.
Files changed (74) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control/__init__.py +1 -1
  3. kailash/client/__init__.py +12 -0
  4. kailash/client/enhanced_client.py +306 -0
  5. kailash/core/actors/__init__.py +16 -0
  6. kailash/core/actors/adaptive_pool_controller.py +630 -0
  7. kailash/core/actors/connection_actor.py +566 -0
  8. kailash/core/actors/supervisor.py +364 -0
  9. kailash/core/ml/__init__.py +1 -0
  10. kailash/core/ml/query_patterns.py +544 -0
  11. kailash/core/monitoring/__init__.py +19 -0
  12. kailash/core/monitoring/connection_metrics.py +488 -0
  13. kailash/core/optimization/__init__.py +1 -0
  14. kailash/core/resilience/__init__.py +17 -0
  15. kailash/core/resilience/circuit_breaker.py +382 -0
  16. kailash/edge/__init__.py +16 -0
  17. kailash/edge/compliance.py +834 -0
  18. kailash/edge/discovery.py +659 -0
  19. kailash/edge/location.py +582 -0
  20. kailash/gateway/__init__.py +33 -0
  21. kailash/gateway/api.py +289 -0
  22. kailash/gateway/enhanced_gateway.py +357 -0
  23. kailash/gateway/resource_resolver.py +217 -0
  24. kailash/gateway/security.py +227 -0
  25. kailash/middleware/auth/access_control.py +6 -6
  26. kailash/middleware/auth/models.py +2 -2
  27. kailash/middleware/communication/ai_chat.py +7 -7
  28. kailash/middleware/communication/api_gateway.py +5 -15
  29. kailash/middleware/database/base_models.py +1 -7
  30. kailash/middleware/gateway/__init__.py +22 -0
  31. kailash/middleware/gateway/checkpoint_manager.py +398 -0
  32. kailash/middleware/gateway/deduplicator.py +382 -0
  33. kailash/middleware/gateway/durable_gateway.py +417 -0
  34. kailash/middleware/gateway/durable_request.py +498 -0
  35. kailash/middleware/gateway/event_store.py +499 -0
  36. kailash/middleware/mcp/enhanced_server.py +2 -2
  37. kailash/nodes/admin/permission_check.py +817 -33
  38. kailash/nodes/admin/role_management.py +1242 -108
  39. kailash/nodes/admin/schema_manager.py +438 -0
  40. kailash/nodes/admin/user_management.py +1124 -1582
  41. kailash/nodes/code/__init__.py +8 -1
  42. kailash/nodes/code/async_python.py +1035 -0
  43. kailash/nodes/code/python.py +1 -0
  44. kailash/nodes/data/async_sql.py +9 -3
  45. kailash/nodes/data/query_pipeline.py +641 -0
  46. kailash/nodes/data/query_router.py +895 -0
  47. kailash/nodes/data/sql.py +20 -11
  48. kailash/nodes/data/workflow_connection_pool.py +1071 -0
  49. kailash/nodes/monitoring/__init__.py +3 -5
  50. kailash/nodes/monitoring/connection_dashboard.py +822 -0
  51. kailash/nodes/rag/__init__.py +2 -7
  52. kailash/resources/__init__.py +40 -0
  53. kailash/resources/factory.py +533 -0
  54. kailash/resources/health.py +319 -0
  55. kailash/resources/reference.py +288 -0
  56. kailash/resources/registry.py +392 -0
  57. kailash/runtime/async_local.py +711 -302
  58. kailash/testing/__init__.py +34 -0
  59. kailash/testing/async_test_case.py +353 -0
  60. kailash/testing/async_utils.py +345 -0
  61. kailash/testing/fixtures.py +458 -0
  62. kailash/testing/mock_registry.py +495 -0
  63. kailash/workflow/__init__.py +8 -0
  64. kailash/workflow/async_builder.py +621 -0
  65. kailash/workflow/async_patterns.py +766 -0
  66. kailash/workflow/cyclic_runner.py +107 -16
  67. kailash/workflow/graph.py +7 -2
  68. kailash/workflow/resilience.py +11 -1
  69. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/METADATA +19 -4
  70. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/RECORD +74 -28
  71. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/WHEEL +0 -0
  72. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/entry_points.txt +0 -0
  73. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/licenses/LICENSE +0 -0
  74. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/top_level.txt +0 -0
@@ -1,76 +1,88 @@
1
- """Enterprise user management node with comprehensive CRUD operations.
1
+ """Enterprise user management node with complete user lifecycle support.
2
2
 
3
- This node provides Django Admin-level user management capabilities with enhanced
4
- features for enterprise environments. Built on Session 065's async database
5
- infrastructure for high-performance operations.
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 (create, read, update, delete, restore)
9
- - Bulk operations with validation and rollback
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
- - Comprehensive audit logging
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
- - User search, filtering, and pagination
15
+ - Comprehensive audit logging
16
+ - User status and lifecycle tracking
16
17
  """
17
18
 
18
19
  import hashlib
19
20
  import json
20
- import secrets
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 AsyncSQLDatabaseNode
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
- @dataclass
33
- class UserConfig:
34
- """Configuration for user management node."""
35
-
36
- abac_enabled: bool = True
37
- audit_enabled: bool = True
38
- multi_tenant: bool = True
39
- password_policy: Dict[str, Any] = None
40
-
41
- def __post_init__(self):
42
- if self.password_policy is None:
43
- self.password_policy = {
44
- "min_length": 8,
45
- "require_uppercase": True,
46
- "require_lowercase": True,
47
- "require_numbers": True,
48
- "require_special": False,
49
- "history_count": 3,
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
- CREATE = "create"
57
- READ = "read"
58
- UPDATE = "update"
59
- DELETE = "delete"
60
- RESTORE = "restore"
61
- LIST = "list"
62
- SEARCH = "search"
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
- CHANGE_PASSWORD = "change_password"
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
- DEACTIVATE = "deactivate"
69
- ACTIVATE = "activate"
81
+ AUTHENTICATE = "authenticate"
70
82
 
71
83
 
72
84
  class UserStatus(Enum):
73
- """User account status."""
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 UserProfile:
84
- """Enhanced user profile with ABAC attributes."""
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
- status: UserStatus
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
- created_at: datetime
95
- updated_at: datetime
96
- last_login: Optional[datetime] = None
97
- password_changed_at: Optional[datetime] = None
98
- tenant_id: Optional[str] = None
99
-
100
- def to_user_context(self) -> UserContext:
101
- """Convert to UserContext for permission checks."""
102
- return UserContext(
103
- user_id=self.user_id,
104
- tenant_id=self.tenant_id or "default",
105
- email=self.email,
106
- roles=self.roles,
107
- attributes=self.attributes,
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 Django Admin-inspired features.
150
+ """Enterprise user management node with complete lifecycle support.
114
151
 
115
152
  This node provides comprehensive user management capabilities including:
116
- - User CRUD operations with validation
117
- - Bulk operations with transaction support
118
- - Password management with security policies
119
- - User search and filtering
120
- - Attribute management for ABAC
121
- - Audit logging integration
122
- - Multi-tenant support
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 operation to perform
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
- user_id: User ID for single-user operations
128
- user_ids: List of user IDs for bulk operations
129
- search_query: Search query for user lookup
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
- include_deleted: Whether to include soft-deleted users
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="create",
179
+ ... operation="create_user",
139
180
  ... user_data={
140
- ... "email": "john@company.com",
141
- ... "username": "john.smith",
181
+ ... "email": "john.doe@company.com",
182
+ ... "username": "johndoe",
142
183
  ... "first_name": "John",
143
- ... "last_name": "Smith",
144
- ... "roles": ["analyst"],
145
- ... "attributes": {
146
- ... "department": "finance",
147
- ... "clearance": "confidential"
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
- >>> # Bulk user operations
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
- ... user_data=[
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["stats"]["created"]
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._access_manager = None
170
- self._config = UserConfig(
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 data for create/update
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="user_id",
211
- type=str,
264
+ name="users_data",
265
+ type=list,
212
266
  required=False,
213
- description="User ID for single-user operations",
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
- # Search and filtering
275
+ # Filtering and search
223
276
  NodeParameter(
224
- name="search_query",
277
+ name="status",
225
278
  type=str,
226
279
  required=False,
227
- description="Search query for user lookup",
280
+ choices=[status.value for status in UserStatus],
281
+ description="User status filter",
228
282
  ),
229
283
  NodeParameter(
230
- name="filters",
231
- type=dict,
284
+ name="search_query",
285
+ type=str,
232
286
  required=False,
233
- description="Filters for user listing",
287
+ description="Search query for user search",
234
288
  ),
289
+ # Pagination
235
290
  NodeParameter(
236
- name="pagination",
237
- type=dict,
291
+ name="limit",
292
+ type=int,
238
293
  required=False,
239
- description="Pagination parameters (page, size, sort)",
294
+ default=50,
295
+ description="Result limit for list operations",
240
296
  ),
241
- # Multi-tenancy
242
297
  NodeParameter(
243
- name="tenant_id",
244
- type=str,
298
+ name="offset",
299
+ type=int,
245
300
  required=False,
246
- description="Tenant ID for multi-tenant isolation",
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="Include soft-deleted users in results",
310
+ description="Whether to include deleted users",
255
311
  ),
256
- # Database configuration
257
312
  NodeParameter(
258
- name="database_config",
259
- type=dict,
313
+ name="export_format",
314
+ type=str,
260
315
  required=False,
261
- description="Database connection configuration",
316
+ default="json",
317
+ choices=["json", "csv"],
318
+ description="Format for user export",
262
319
  ),
263
- # Password options
320
+ # Password reset parameters
264
321
  NodeParameter(
265
- name="password",
322
+ name="token",
266
323
  type=str,
267
324
  required=False,
268
- description="Password for create/change operations",
325
+ description="Password reset token",
269
326
  ),
270
327
  NodeParameter(
271
- name="force_password_change",
272
- type=bool,
328
+ name="new_password",
329
+ type=str,
273
330
  required=False,
274
- default=False,
275
- description="Force password change on next login",
331
+ description="New password for reset",
276
332
  ),
277
- # Validation options
278
333
  NodeParameter(
279
- name="validate_email",
280
- type=bool,
334
+ name="password",
335
+ type=str,
281
336
  required=False,
282
- default=True,
283
- description="Validate email format and uniqueness",
337
+ description="Password for authentication",
284
338
  ),
339
+ # Security
285
340
  NodeParameter(
286
- name="validate_username",
287
- type=bool,
341
+ name="password_hash",
342
+ type=str,
288
343
  required=False,
289
- default=True,
290
- description="Validate username format and uniqueness",
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 (sync wrapper for async_run)."""
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 database and access manager
368
+ # Initialize dependencies
319
369
  self._init_dependencies(inputs)
320
370
 
321
371
  # Route to appropriate operation
322
- if operation == UserOperation.CREATE:
323
- return await self._create_user_async(inputs)
324
- elif operation == UserOperation.READ:
325
- return await self._read_user_async(inputs)
326
- elif operation == UserOperation.UPDATE:
327
- return await self._update_user_async(inputs)
328
- elif operation == UserOperation.DELETE:
329
- return await self._delete_user_async(inputs)
330
- elif operation == UserOperation.RESTORE:
331
- return await self._restore_user_async(inputs)
332
- elif operation == UserOperation.LIST:
333
- return await self._list_users_async(inputs)
334
- elif operation == UserOperation.SEARCH:
335
- return await self._search_users_async(inputs)
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 await self._bulk_create_users_async(inputs)
391
+ return self._bulk_create(inputs)
338
392
  elif operation == UserOperation.BULK_UPDATE:
339
- return await self._bulk_update_users_async(inputs)
393
+ return self._bulk_update(inputs)
340
394
  elif operation == UserOperation.BULK_DELETE:
341
- return await self._bulk_delete_users_async(inputs)
342
- elif operation == UserOperation.CHANGE_PASSWORD:
343
- return await self._change_password_async(inputs)
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 await self._reset_password_async(inputs)
346
- elif operation == UserOperation.DEACTIVATE:
347
- return await self._deactivate_user_async(inputs)
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 access manager dependencies."""
358
- # Get database config
359
- db_config = inputs.get(
360
- "database_config",
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
- # Execute database insert using async method
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
- # Handle initial role assignments
503
- if inputs.get("initial_roles"):
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
- # Audit log
508
- if self._config.audit_enabled:
509
- # In production, this would use AuditLogNode
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
- return {
513
- "success": True,
514
- "user": user_profile.__dict__,
515
- "message": f"User {user_record['username']} created successfully",
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 with validation and audit logging."""
444
+ """Create a new user."""
520
445
  user_data = inputs["user_data"]
521
- tenant_id = inputs.get("tenant_id", "default")
446
+ tenant_id = inputs["tenant_id"]
522
447
 
523
448
  # Validate required fields
524
- required_fields = ["email", "username", "first_name", "last_name"]
525
- for field in required_fields:
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
- db_result = self._db_node.run(**query)
452
+ # Generate user ID if not provided
453
+ user_id = user_data.get("user_id", str(uuid4()))
603
454
 
604
- # Create user profile response
605
- user_profile = UserProfile(
455
+ # Prepare user data with defaults
456
+ user = User(
606
457
  user_id=user_id,
607
- email=user_record["email"],
608
- username=user_record["username"],
609
- first_name=user_record["first_name"],
610
- last_name=user_record["last_name"],
611
- status=UserStatus(user_record["status"]),
612
- roles=user_record["roles"],
613
- attributes=user_record["attributes"],
614
- created_at=user_record["created_at"],
615
- updated_at=user_record["updated_at"],
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
- return {
620
- "result": {
621
- "user": {
622
- "user_id": user_profile.user_id,
623
- "email": user_profile.email,
624
- "username": user_profile.username,
625
- "first_name": user_profile.first_name,
626
- "last_name": user_profile.last_name,
627
- "status": user_profile.status.value,
628
- "roles": user_profile.roles,
629
- "attributes": user_profile.attributes,
630
- "created_at": user_profile.created_at.isoformat(),
631
- "tenant_id": user_profile.tenant_id,
632
- },
633
- "operation": "create",
634
- "success": True,
635
- "timestamp": datetime.now(UTC).isoformat(),
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
- # Execute query
677
- self._db_node.config.update(
678
- {"query": query, "params": params, "fetch_mode": "one"}
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
- db_result = self._db_node.run(**query)
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": None,
687
- "found": False,
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
- user_data = db_result["result"]["data"]
694
-
695
- return {
696
- "result": {
697
- "user": {
698
- "user_id": user_data["user_id"],
699
- "email": user_data["email"],
700
- "username": user_data["username"],
701
- "first_name": user_data["first_name"],
702
- "last_name": user_data["last_name"],
703
- "status": user_data["status"],
704
- "roles": user_data["roles"],
705
- "attributes": user_data["attributes"],
706
- "created_at": user_data["created_at"],
707
- "updated_at": user_data["updated_at"],
708
- "last_login": user_data["last_login"],
709
- "tenant_id": user_data["tenant_id"],
710
- },
711
- "found": True,
712
- "operation": "read",
713
- "timestamp": datetime.now(UTC).isoformat(),
714
- }
715
- }
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 {' AND '.join(where_conditions)}
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
- # Execute count query
790
- self._db_node.config.update(
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
- # Execute data query
797
- self._db_node.config.update(
798
- {"query": data_query, "params": params, "fetch_mode": "all"}
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 _bulk_create_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
827
- """Create multiple users with transaction support."""
828
- user_data_list = inputs["user_data"]
829
- tenant_id = inputs.get("tenant_id", "default")
830
-
831
- if not isinstance(user_data_list, list):
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
- result = self._create_user(create_inputs)
848
- results["created"].append(
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
- except Exception as e:
854
- results["failed"].append(
855
- {"index": i, "user_data": user_data, "error": str(e)}
856
- )
857
- results["stats"]["failed"] += 1
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
- "operation": "bulk_create",
862
- "results": results,
587
+ "user": user.to_dict(),
588
+ "operation": "get_user",
863
589
  "timestamp": datetime.now(UTC).isoformat(),
864
590
  }
865
591
  }
866
592
 
867
- # Utility methods
868
- def _generate_user_id(self) -> str:
869
- """Generate unique user ID."""
870
- import uuid
871
-
872
- return str(uuid.uuid4())
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
- def _hash_password(self, password: str) -> str:
875
- """Hash password using SHA-256 with salt."""
876
- salt = secrets.token_hex(32)
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
- def _validate_email(self, email: str) -> bool:
881
- """Validate email format."""
882
- import re
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
- pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
885
- return bool(re.match(pattern, email))
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
- def _validate_username(self, username: str) -> bool:
888
- """Validate username format."""
889
- import re
640
+ result = self._db_node.run(
641
+ query=query, parameters=[username, tenant_id], result_format="dict"
642
+ )
890
643
 
891
- # Username: alphanumeric, dots, hyphens, underscores, 3-50 chars
892
- pattern = r"^[a-zA-Z0-9._-]{3,50}$"
893
- return bool(re.match(pattern, username))
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 information."""
898
- user_id = inputs.get("user_id")
899
- user_data = inputs.get("user_data", {})
900
- tenant_id = inputs.get("tenant_id", "default")
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
- if not user_id:
903
- raise NodeValidationError("user_id is required for update operation")
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
- params = []
908
- param_count = 1
678
+ parameters = []
679
+ param_index = 1
909
680
 
910
- # Update allowed fields
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
- "roles",
918
- "attributes",
919
- "phone",
920
- "department",
688
+ "external_auth_id",
689
+ "auth_provider",
921
690
  ]
922
691
 
923
- for field, value in user_data.items():
924
- if field in allowed_fields:
925
- # Validate specific fields
926
- if field == "email" and inputs.get("validate_email", True):
927
- if not self._validate_email(value):
928
- raise NodeValidationError(f"Invalid email format: {value}")
929
- elif field == "username" and inputs.get("validate_username", True):
930
- if not self._validate_username(value):
931
- raise NodeValidationError(f"Invalid username format: {value}")
932
- elif field == "status":
933
- if value not in [s.value for s in UserStatus]:
934
- raise NodeValidationError(f"Invalid status: {value}")
935
-
936
- update_fields.append(f"{field} = ${param_count}")
937
- params.append(value)
938
- param_count += 1
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
- return {"success": False, "message": "No valid fields to update"}
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 updated_at
944
- update_fields.append(f"updated_at = ${param_count}")
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 = ${param_count} AND tenant_id = ${param_count + 1}
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
- self._ensure_db_node(inputs)
958
- result = self._db_node.execute(query=update_query, params=params)
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
- updated_user = result["rows"][0]
732
+ # Get updated user
733
+ updated_user = self._get_user_by_id(user_id, tenant_id)
964
734
 
965
- # Audit log
966
- if self._config.audit_enabled:
967
- print(f"[AUDIT] user_updated: {user_id}")
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
- return {
970
- "success": True,
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
- """Soft delete user."""
977
- user_id = inputs.get("user_id")
978
- tenant_id = inputs.get("tenant_id", "default")
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
- if not user_id:
982
- raise NodeValidationError("user_id is required for delete operation")
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
- # Permanent deletion - use with caution
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 - change status to 'deleted'
759
+ # Soft delete - mark as deleted
995
760
  delete_query = """
996
761
  UPDATE users
997
- SET status = 'deleted',
998
- updated_at = $3,
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
- params = [user_id, tenant_id]
1006
- if not hard_delete:
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
- if not result.get("rows"):
1012
- return {"success": False, "message": "User not found or already deleted"}
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
- deleted_user = result["rows"][0]
778
+ except Exception as e:
779
+ raise NodeExecutionError(f"Failed to delete user: {str(e)}")
1015
780
 
1016
- # Audit log
1017
- if self._config.audit_enabled:
1018
- action = "hard_deleted" if hard_delete else "soft_deleted"
1019
- print(f"[AUDIT] user_{action}: {user_id}")
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
- return {
1022
- "success": True,
1023
- "user": deleted_user,
1024
- "message": f"User {user_id} deleted successfully",
1025
- "hard_delete": hard_delete,
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
- def _change_password(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1029
- """Change user password."""
1030
- user_id = inputs.get("user_id")
1031
- current_password = inputs.get("current_password")
1032
- new_password = inputs.get("new_password")
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
- self._ensure_db_node(inputs)
822
+ try:
823
+ # Get users
824
+ result = self._db_node.run(
825
+ query=list_query, parameters=parameters, result_format="dict"
826
+ )
1046
827
 
1047
- # Verify current password if required
1048
- if not skip_current_check:
1049
- verify_query = """
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
- if not result.get("rows"):
1059
- return {"success": False, "message": "User not found"}
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
- stored_hash = result["rows"][0]["password_hash"]
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
- # Validate new password against policy
1066
- policy = self._config.password_policy
1067
- if len(new_password) < policy["min_length"]:
1068
- raise NodeValidationError(
1069
- f"Password must be at least {policy['min_length']} characters"
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
- if policy.get("require_uppercase") and not any(
1073
- c.isupper() for c in new_password
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
- for row in history_result.get("rows", []):
1105
- if self._verify_password(new_password, row["password_hash"]):
1106
- return {
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
- # Hash new password
1112
- new_hash = self._hash_password(new_password)
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 password_hash = $1,
1118
- password_changed_at = $2,
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
- now = datetime.now(UTC)
1126
- result = self._db_node.execute(
1127
- query=update_query, params=[new_hash, now, user_id, tenant_id]
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
- self._ensure_db_node(inputs)
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
- user_data = result["rows"][0]
1183
- user_id = user_data["user_id"]
906
+ return {
907
+ "result": {
908
+ "user": updated_user.to_dict(),
909
+ "operation": operation,
910
+ "timestamp": datetime.now(UTC).isoformat(),
911
+ }
912
+ }
1184
913
 
1185
- if generate_token:
1186
- # Generate secure reset token
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
- # Store reset token
1192
- token_query = """
1193
- INSERT INTO password_reset_tokens (user_id, tenant_id, token_hash, expires_at, created_at)
1194
- VALUES ($1, $2, $3, $4, $5)
1195
- ON CONFLICT (user_id, tenant_id)
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
- self._db_node.execute(
1200
- query=token_query,
1201
- params=[user_id, tenant_id, token_hash, expiry_time, datetime.now(UTC)],
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
- # Force password change on next login
1205
- update_query = """
1206
- UPDATE users
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
- "success": True,
1220
- "user": user_data,
1221
- "reset_token": reset_token,
1222
- "expires_at": expiry_time.isoformat(),
1223
- "message": "Password reset token generated",
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
- elif new_password:
1227
- # Direct password reset (admin action)
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
- update_query = """
1239
- UPDATE users
1240
- SET password_hash = $1,
1241
- password_changed_at = $2,
1242
- updated_at = $2,
1243
- force_password_change = $3
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
- force_change = inputs.get("force_password_change", True)
1249
- now = datetime.now(UTC)
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
- result = self._db_node.execute(
1252
- query=update_query,
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
- if not result.get("rows"):
1257
- return {"success": False, "message": "Failed to reset password"}
961
+ created_users = []
962
+ failed_users = []
1258
963
 
1259
- # Audit log
1260
- if self._config.audit_enabled:
1261
- print(f"[AUDIT] password_reset_admin: {user_id}")
1262
-
1263
- return {
1264
- "success": True,
1265
- "user": result["rows"][0],
1266
- "message": "Password reset successfully",
1267
- "force_password_change": force_change,
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
- else:
1271
- raise NodeValidationError(
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
- def _deactivate_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1276
- """Deactivate user account."""
1277
- user_id = inputs.get("user_id")
1278
- tenant_id = inputs.get("tenant_id", "default")
1279
- reason = inputs.get("reason", "Manual deactivation")
1280
- deactivated_by = inputs.get("deactivated_by", "system")
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
- if not user_id:
1283
- raise NodeValidationError("user_id is required for deactivate operation")
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
- self._ensure_db_node(inputs)
1000
+ def _bulk_update(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1001
+ """Update multiple users in bulk."""
1002
+ users_data = inputs["users_data"]
1003
+ tenant_id = inputs["tenant_id"]
1286
1004
 
1287
- # Update user status to inactive
1288
- update_query = """
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
- now = datetime.now(UTC)
1300
- result = self._db_node.execute(
1301
- query=update_query, params=[now, reason, deactivated_by, user_id, tenant_id]
1302
- )
1008
+ updated_users = []
1009
+ failed_users = []
1303
1010
 
1304
- if not result.get("rows"):
1305
- # Check if user exists but is already inactive
1306
- check_query = """
1307
- SELECT status FROM users
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
- if check_result.get("rows"):
1315
- current_status = check_result["rows"][0]["status"]
1316
- return {
1317
- "success": False,
1318
- "message": f"User is already {current_status}",
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
- deactivated_user = result["rows"][0]
1024
+ result = self._update_user(update_inputs)
1025
+ updated_users.append(result["result"]["user"])
1324
1026
 
1325
- # Revoke active sessions
1326
- session_query = """
1327
- UPDATE user_sessions
1328
- SET status = 'revoked', revoked_at = $1
1329
- WHERE user_id = $2 AND tenant_id = $3 AND status = 'active'
1330
- """
1331
- self._db_node.execute(query=session_query, params=[now, user_id, tenant_id])
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
- "success": True,
1339
- "user": deactivated_user,
1340
- "message": f"User {user_id} deactivated successfully",
1341
- "reason": reason,
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 _activate_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1345
- """Activate user account."""
1346
- user_id = inputs.get("user_id")
1347
- tenant_id = inputs.get("tenant_id", "default")
1348
- activated_by = inputs.get("activated_by", "system")
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
- self._ensure_db_node(inputs)
1056
+ if not isinstance(user_ids, list):
1057
+ raise NodeValidationError("user_ids must be a list")
1355
1058
 
1356
- # Update user status to active
1357
- if clear_deactivation_data:
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
- now = datetime.now(UTC)
1382
- result = self._db_node.execute(
1383
- query=update_query, params=[now, activated_by, user_id, tenant_id]
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
- if not result.get("rows"):
1387
- # Check if user exists but is already active
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
- if check_result.get("rows"):
1397
- current_status = check_result["rows"][0]["status"]
1398
- if current_status == "active":
1399
- return {"success": False, "message": "User is already active"}
1400
- elif current_status == "deleted":
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
- else:
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
- "success": True,
1421
- "user": activated_user,
1422
- "message": f"User {user_id} activated successfully",
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 _ensure_db_node(self, inputs: Dict[str, Any]):
1426
- """Ensure database node is initialized."""
1427
- if not self._db_node:
1428
- self._init_dependencies(inputs)
1429
-
1430
- def _verify_password(self, password: str, password_hash: str) -> bool:
1431
- """Verify password against hash."""
1432
- if not password_hash or "$" not in password_hash:
1433
- return False
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
- salt, stored_hash = password_hash.split("$", 1)
1436
- test_hash = hashlib.sha256((password + salt).encode("utf-8")).hexdigest()
1437
- return test_hash == stored_hash
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
- def _restore_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1440
- """Restore soft-deleted user."""
1441
- user_id = inputs.get("user_id")
1442
- tenant_id = inputs.get("tenant_id", "default")
1443
- restored_by = inputs.get("restored_by", "system")
1444
- new_status = inputs.get("new_status", "active")
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
- if not user_id:
1447
- raise NodeValidationError("user_id is required for restore operation")
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
- if new_status not in ["active", "inactive", "pending"]:
1450
- raise NodeValidationError(
1451
- f"Invalid new_status: {new_status}. Must be active, inactive, or pending"
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
- self._ensure_db_node(inputs)
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
- # Check if user exists and is deleted
1457
- check_query = """
1458
- SELECT user_id, email, username, status, deleted_at
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 user_id = $1 AND tenant_id = $2
1461
- """
1462
- check_result = self._db_node.execute(
1463
- query=check_query, params=[user_id, tenant_id]
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
- if not check_result.get("rows"):
1467
- return {"success": False, "message": "User not found"}
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
- "success": False,
1473
- "message": f"User is not deleted. Current status: {user_data['status']}",
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
- # Restore user
1477
- restore_query = """
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
- now = datetime.now(UTC)
1490
- result = self._db_node.execute(
1491
- query=restore_query,
1492
- params=[new_status, now, restored_by, user_id, tenant_id],
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
- if not result.get("rows"):
1496
- return {"success": False, "message": "Failed to restore user"}
1240
+ result = self._list_users(list_inputs)
1241
+ users = result["result"]["users"]
1497
1242
 
1498
- restored_user = result["rows"][0]
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
- # Audit log
1501
- if self._config.audit_enabled:
1502
- print(f"[AUDIT] user_restored: {user_id} (new_status: {new_status})")
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
- "success": True,
1506
- "user": restored_user,
1507
- "message": f"User {user_id} restored successfully",
1508
- "new_status": new_status,
1509
- "previous_deleted_at": user_data["deleted_at"],
1295
+ "result": {
1296
+ "export_data": export_data,
1297
+ "operation": "export_users",
1298
+ "timestamp": datetime.now(UTC).isoformat(),
1299
+ }
1510
1300
  }
1511
1301
 
1512
- def _search_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1513
- """Advanced user search with full-text capabilities."""
1514
- search_query = inputs.get("search_query", "")
1515
- tenant_id = inputs.get("tenant_id", "default")
1516
- filters = inputs.get("filters", {})
1517
- search_fields = inputs.get(
1518
- "search_fields", ["email", "username", "first_name", "last_name"]
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
- pagination = inputs.get(
1521
- "pagination", {"page": 1, "size": 20, "sort": "relevance"}
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
- self._ensure_db_node(inputs)
1340
+ return {
1341
+ "token": token,
1342
+ "expires_at": expires_at.isoformat(),
1343
+ "user_id": user_id,
1344
+ }
1527
1345
 
1528
- # Build search conditions
1529
- where_conditions = ["tenant_id = $1"]
1530
- params = [tenant_id]
1531
- param_count = 1
1532
-
1533
- if not include_deleted:
1534
- where_conditions.append("status != 'deleted'")
1535
-
1536
- # Apply filters
1537
- if "status" in filters:
1538
- param_count += 1
1539
- if isinstance(filters["status"], list):
1540
- where_conditions.append(f"status = ANY(${param_count})")
1541
- params.append(filters["status"])
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
- order_by = "relevance DESC, created_at DESC"
1612
- else:
1613
- relevance_score = "0 as relevance"
1614
- order_by = f"{sort_field} {sort_direction}"
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
- # Count query
1617
- count_query = f"""
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
- # Data query
1624
- data_query = f"""
1625
- SELECT user_id, email, username, first_name, last_name,
1626
- status, roles, attributes, created_at, updated_at, last_login,
1627
- {relevance_score}
1628
- FROM users
1629
- WHERE {' AND '.join(where_conditions)}
1630
- ORDER BY {order_by}
1631
- LIMIT {size} OFFSET {offset}
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
- # Execute count query
1635
- count_result = self._db_node.execute(query=count_query, params=params)
1636
- total_count = (
1637
- count_result["rows"][0]["total"] if count_result.get("rows") else 0
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
- # Execute data query
1641
- data_result = self._db_node.execute(query=data_query, params=params)
1642
- users = data_result.get("rows", [])
1643
-
1644
- # Calculate pagination info
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
- # Audit log search action
1650
- if self._config.audit_enabled and search_query:
1651
- print(f"[AUDIT] user_search: query='{search_query}', results={len(users)}")
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
- "users": users,
1656
- "pagination": {
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 _bulk_update_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1674
- """Bulk update multiple users."""
1675
- user_updates = inputs.get("user_updates", [])
1676
- tenant_id = inputs.get("tenant_id", "default")
1677
- updated_by = inputs.get("updated_by", "system")
1678
- transaction_mode = inputs.get("transaction_mode", "all_or_none")
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
- results = {"updated": [], "failed": [], "stats": {"updated": 0, "failed": 0}}
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
- # Start transaction if all_or_none mode
1691
- if transaction_mode == "all_or_none":
1692
- self._db_node.execute(query="BEGIN")
1435
+ result = self._db_node.run(
1436
+ operation="query", query=auth_query, parameters=params
1437
+ )
1693
1438
 
1694
- try:
1695
- for i, update_data in enumerate(user_updates):
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
- for field, value in update_data.items():
1722
- if field in allowed_fields:
1723
- # Validate specific fields
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
- if transaction_mode == "all_or_none":
1787
- # Rollback and return error
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
- "success": True,
1812
- "results": results,
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
- except Exception as e:
1818
- if transaction_mode == "all_or_none":
1819
- self._db_node.execute(query="ROLLBACK")
1820
- raise NodeExecutionError(f"Bulk update failed: {str(e)}")
1821
-
1822
- def _bulk_delete_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
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
- return {
1920
- "success": True,
1921
- "results": results,
1922
- "message": f"Bulk delete completed: {results['stats']['deleted']} deleted, {results['stats']['failed']} failed",
1923
- "hard_delete": hard_delete,
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
- except Exception as e:
1928
- if transaction_mode == "all_or_none":
1929
- self._db_node.execute(query="ROLLBACK")
1930
- raise NodeExecutionError(f"Bulk delete failed: {str(e)}")
1468
+ return {
1469
+ "authenticated": True,
1470
+ "user_id": user_data["user_id"],
1471
+ "message": "Authentication successful",
1472
+ }