kailash 0.4.2__py3-none-any.whl → 0.6.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (64) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/client/__init__.py +12 -0
  3. kailash/client/enhanced_client.py +306 -0
  4. kailash/core/actors/__init__.py +16 -0
  5. kailash/core/actors/connection_actor.py +566 -0
  6. kailash/core/actors/supervisor.py +364 -0
  7. kailash/edge/__init__.py +16 -0
  8. kailash/edge/compliance.py +834 -0
  9. kailash/edge/discovery.py +659 -0
  10. kailash/edge/location.py +582 -0
  11. kailash/gateway/__init__.py +33 -0
  12. kailash/gateway/api.py +289 -0
  13. kailash/gateway/enhanced_gateway.py +357 -0
  14. kailash/gateway/resource_resolver.py +217 -0
  15. kailash/gateway/security.py +227 -0
  16. kailash/middleware/auth/models.py +2 -2
  17. kailash/middleware/database/base_models.py +1 -7
  18. kailash/middleware/database/repositories.py +3 -1
  19. kailash/middleware/gateway/__init__.py +22 -0
  20. kailash/middleware/gateway/checkpoint_manager.py +398 -0
  21. kailash/middleware/gateway/deduplicator.py +382 -0
  22. kailash/middleware/gateway/durable_gateway.py +417 -0
  23. kailash/middleware/gateway/durable_request.py +498 -0
  24. kailash/middleware/gateway/event_store.py +459 -0
  25. kailash/nodes/admin/audit_log.py +364 -6
  26. kailash/nodes/admin/permission_check.py +817 -33
  27. kailash/nodes/admin/role_management.py +1242 -108
  28. kailash/nodes/admin/schema_manager.py +438 -0
  29. kailash/nodes/admin/user_management.py +1209 -681
  30. kailash/nodes/api/http.py +95 -71
  31. kailash/nodes/base.py +281 -164
  32. kailash/nodes/base_async.py +30 -31
  33. kailash/nodes/code/__init__.py +8 -1
  34. kailash/nodes/code/async_python.py +1035 -0
  35. kailash/nodes/code/python.py +1 -0
  36. kailash/nodes/data/async_sql.py +12 -25
  37. kailash/nodes/data/sql.py +20 -11
  38. kailash/nodes/data/workflow_connection_pool.py +643 -0
  39. kailash/nodes/rag/__init__.py +1 -4
  40. kailash/resources/__init__.py +40 -0
  41. kailash/resources/factory.py +533 -0
  42. kailash/resources/health.py +319 -0
  43. kailash/resources/reference.py +288 -0
  44. kailash/resources/registry.py +392 -0
  45. kailash/runtime/async_local.py +711 -302
  46. kailash/testing/__init__.py +34 -0
  47. kailash/testing/async_test_case.py +353 -0
  48. kailash/testing/async_utils.py +345 -0
  49. kailash/testing/fixtures.py +458 -0
  50. kailash/testing/mock_registry.py +495 -0
  51. kailash/utils/resource_manager.py +420 -0
  52. kailash/workflow/__init__.py +8 -0
  53. kailash/workflow/async_builder.py +621 -0
  54. kailash/workflow/async_patterns.py +766 -0
  55. kailash/workflow/builder.py +93 -10
  56. kailash/workflow/cyclic_runner.py +111 -41
  57. kailash/workflow/graph.py +7 -2
  58. kailash/workflow/resilience.py +11 -1
  59. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/METADATA +12 -7
  60. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/RECORD +64 -28
  61. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
  62. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
  63. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
  64. {kailash-0.4.2.dist-info → kailash-0.6.0.dist-info}/top_level.txt +0 -0
@@ -1,76 +1,88 @@
1
- """Enterprise user management node with 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,591 +414,1059 @@ class UserManagementNode(Node):
354
414
  raise NodeExecutionError(f"User management operation failed: {str(e)}")
355
415
 
356
416
  def _init_dependencies(self, inputs: Dict[str, Any]):
357
- """Initialize database and 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
- )
417
+ """Initialize database and schema manager dependencies."""
418
+ # Skip initialization if already initialized (for testing)
419
+ if hasattr(self, "_db_node") and self._db_node is not None:
420
+ return
421
+
422
+ db_config = inputs["database_config"]
370
423
 
371
- # Initialize async database node
372
- self._db_node = AsyncSQLDatabaseNode(name="user_management_db", **db_config)
424
+ # Initialize database node
425
+ self._db_node = SQLDatabaseNode(name="user_management_db", **db_config)
373
426
 
374
- # Initialize enhanced access manager
375
- self._access_manager = AccessControlManager(strategy="abac")
427
+ # Initialize schema manager and ensure schema exists
428
+ if not self._schema_manager:
429
+ self._schema_manager = AdminSchemaManager(db_config)
430
+
431
+ # Validate schema exists, create if needed
432
+ try:
433
+ validation = self._schema_manager.validate_schema()
434
+ if not validation["is_valid"]:
435
+ self.logger.info(
436
+ "Creating unified admin schema for user management..."
437
+ )
438
+ self._schema_manager.create_full_schema(drop_existing=False)
439
+ self.logger.info("Unified admin schema created successfully")
440
+ except Exception as e:
441
+ self.logger.warning(f"Schema validation/creation failed: {e}")
376
442
 
377
- 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)."""
443
+ def _create_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
444
+ """Create a new user."""
379
445
  user_data = inputs["user_data"]
380
- tenant_id = inputs.get("tenant_id", "default")
446
+ tenant_id = inputs["tenant_id"]
381
447
 
382
448
  # 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
- )
449
+ if "email" not in user_data:
450
+ raise NodeValidationError("Email is required for user creation")
451
+
452
+ # Generate user ID if not provided
453
+ user_id = user_data.get("user_id", str(uuid4()))
454
+
455
+ # Prepare user data with defaults
456
+ user = User(
457
+ user_id=user_id,
458
+ email=user_data["email"],
459
+ username=user_data.get("username"),
460
+ first_name=user_data.get("first_name"),
461
+ last_name=user_data.get("last_name"),
462
+ display_name=user_data.get("display_name"),
463
+ roles=user_data.get("roles", []),
464
+ attributes=user_data.get("attributes", {}),
465
+ status=UserStatus(user_data.get("status", "active")),
466
+ tenant_id=tenant_id,
467
+ external_auth_id=user_data.get("external_auth_id"),
468
+ auth_provider=user_data.get("auth_provider", "local"),
469
+ )
470
+
471
+ # Insert user into database with conflict resolution
472
+ insert_query = """
473
+ INSERT INTO users (
474
+ user_id, email, username, password_hash, first_name, last_name,
475
+ display_name, roles, attributes, status, tenant_id,
476
+ external_auth_id, auth_provider
477
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
478
+ ON CONFLICT (user_id) DO UPDATE SET
479
+ email = EXCLUDED.email,
480
+ username = EXCLUDED.username,
481
+ first_name = EXCLUDED.first_name,
482
+ last_name = EXCLUDED.last_name,
483
+ display_name = EXCLUDED.display_name,
484
+ roles = EXCLUDED.roles,
485
+ attributes = EXCLUDED.attributes,
486
+ status = EXCLUDED.status,
487
+ external_auth_id = EXCLUDED.external_auth_id,
488
+ auth_provider = EXCLUDED.auth_provider,
489
+ updated_at = CURRENT_TIMESTAMP
490
+ """
491
+
492
+ try:
493
+ self._db_node.run(
494
+ query=insert_query,
495
+ parameters=[
496
+ user.user_id,
497
+ user.email,
498
+ user.username,
499
+ inputs.get("password_hash"),
500
+ user.first_name,
501
+ user.last_name,
502
+ user.display_name,
503
+ json.dumps(user.roles),
504
+ json.dumps(user.attributes),
505
+ user.status.value,
506
+ user.tenant_id,
507
+ user.external_auth_id,
508
+ user.auth_provider,
509
+ ],
510
+ )
400
511
 
401
- # Generate user ID
402
- user_id = self._generate_user_id()
512
+ # Get the created user to return complete data
513
+ created_user = self._get_user_by_id(user.user_id, tenant_id)
403
514
 
404
- # 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"]
515
+ return {
516
+ "result": {
517
+ "user": created_user.to_dict(),
518
+ "operation": "create_user",
519
+ "timestamp": datetime.now(UTC).isoformat(),
520
+ }
521
+ }
410
522
 
411
- if len(password) < policy["min_length"]:
523
+ except Exception as e:
524
+ if "duplicate key" in str(e).lower():
412
525
  raise NodeValidationError(
413
- f"Password must be at least {policy['min_length']} characters"
526
+ f"User with email {user.email} already exists"
414
527
  )
528
+ raise NodeExecutionError(f"Failed to create user: {str(e)}")
529
+
530
+ def _get_user_by_id(self, user_id: str, tenant_id: str) -> User:
531
+ """Get user by ID and tenant."""
532
+ query = """
533
+ SELECT user_id, email, username, first_name, last_name, display_name,
534
+ roles, attributes, status, tenant_id, external_auth_id, auth_provider,
535
+ created_at, updated_at, last_login_at
536
+ FROM users
537
+ WHERE user_id = $1 AND tenant_id = $2
538
+ """
415
539
 
416
- if policy.get("require_uppercase") and not any(
417
- c.isupper() for c in password
418
- ):
419
- raise NodeValidationError("Password must contain uppercase letters")
540
+ result = self._db_node.run(
541
+ query=query, parameters=[user_id, tenant_id], result_format="dict"
542
+ )
420
543
 
421
- if policy.get("require_lowercase") and not any(
422
- c.islower() for c in password
423
- ):
424
- raise NodeValidationError("Password must contain lowercase letters")
544
+ user_rows = result.get("data", [])
545
+ if not user_rows:
546
+ raise NodeValidationError(f"User not found: {user_id}")
547
+
548
+ user_data = user_rows[0]
549
+ return User(
550
+ user_id=user_data["user_id"],
551
+ email=user_data["email"],
552
+ username=user_data["username"],
553
+ first_name=user_data["first_name"],
554
+ last_name=user_data["last_name"],
555
+ display_name=user_data["display_name"],
556
+ roles=user_data.get("roles", []),
557
+ attributes=user_data.get("attributes", {}),
558
+ status=UserStatus(user_data["status"]),
559
+ tenant_id=user_data["tenant_id"],
560
+ external_auth_id=user_data["external_auth_id"],
561
+ auth_provider=user_data["auth_provider"],
562
+ created_at=parse_datetime(user_data.get("created_at")),
563
+ updated_at=parse_datetime(user_data.get("updated_at")),
564
+ last_login_at=parse_datetime(user_data.get("last_login_at")),
565
+ )
425
566
 
426
- if policy.get("require_numbers") and not any(c.isdigit() for c in password):
427
- raise NodeValidationError("Password must contain numbers")
567
+ def _get_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
568
+ """Get a single user by ID, email, or username."""
569
+ tenant_id = inputs["tenant_id"]
570
+ user_id = inputs.get("user_id")
571
+ email = inputs.get("email")
572
+ username = inputs.get("username")
428
573
 
429
- if policy.get("require_special") and not any(
430
- c in "!@#$%^&*()_+-=[]{}|;:,.<>?" for c in password
431
- ):
432
- raise NodeValidationError("Password must contain special characters")
574
+ if not any([user_id, email, username]):
575
+ raise NodeValidationError("Must provide user_id, email, or username")
433
576
 
434
- password_hash = self._hash_password(password)
577
+ # Build query based on available identifiers
578
+ if user_id:
579
+ user = self._get_user_by_id(user_id, tenant_id)
580
+ elif email:
581
+ user = self._get_user_by_email(email, tenant_id)
582
+ else:
583
+ user = self._get_user_by_username(username, tenant_id)
435
584
 
436
- # 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"),
585
+ return {
586
+ "result": {
587
+ "user": user.to_dict(),
588
+ "operation": "get_user",
589
+ "timestamp": datetime.now(UTC).isoformat(),
590
+ }
452
591
  }
453
592
 
454
- # 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
- )
593
+ def _get_user_by_email(self, email: str, tenant_id: str) -> User:
594
+ """Get user by email and tenant."""
595
+ query = """
596
+ SELECT user_id, email, username, first_name, last_name, display_name,
597
+ roles, attributes, status, tenant_id, external_auth_id, auth_provider,
598
+ created_at, updated_at, last_login_at
599
+ FROM users
600
+ WHERE email = $1 AND tenant_id = $2
463
601
  """
464
602
 
465
- # 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)
603
+ result = self._db_node.run(
604
+ query=query, parameters=[email, tenant_id], result_format="dict"
605
+ )
487
606
 
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"],
607
+ user_rows = result.get("data", [])
608
+ if not user_rows:
609
+ raise NodeValidationError(f"User not found with email: {email}")
610
+
611
+ user_data = user_rows[0]
612
+ return User(
613
+ user_id=user_data["user_id"],
614
+ email=user_data["email"],
615
+ username=user_data["username"],
616
+ first_name=user_data["first_name"],
617
+ last_name=user_data["last_name"],
618
+ display_name=user_data["display_name"],
619
+ roles=user_data.get("roles", []),
620
+ attributes=user_data.get("attributes", {}),
621
+ status=UserStatus(user_data["status"]),
622
+ tenant_id=user_data["tenant_id"],
623
+ external_auth_id=user_data["external_auth_id"],
624
+ auth_provider=user_data["auth_provider"],
625
+ created_at=parse_datetime(user_data.get("created_at")),
626
+ updated_at=parse_datetime(user_data.get("updated_at")),
627
+ last_login_at=parse_datetime(user_data.get("last_login_at")),
500
628
  )
501
629
 
502
- # Handle initial role assignments
503
- if inputs.get("initial_roles"):
504
- # Role assignment would be handled by RoleManagementNode
505
- pass
630
+ def _get_user_by_username(self, username: str, tenant_id: str) -> User:
631
+ """Get user by username and tenant."""
632
+ query = """
633
+ SELECT user_id, email, username, first_name, last_name, display_name,
634
+ roles, attributes, status, tenant_id, external_auth_id, auth_provider,
635
+ created_at, updated_at, last_login_at
636
+ FROM users
637
+ WHERE username = $1 AND tenant_id = $2
638
+ """
506
639
 
507
- # 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']})")
640
+ result = self._db_node.run(
641
+ query=query, parameters=[username, tenant_id], result_format="dict"
642
+ )
511
643
 
512
- return {
513
- "success": True,
514
- "user": user_profile.__dict__,
515
- "message": f"User {user_record['username']} created successfully",
516
- }
644
+ user_rows = result.get("data", [])
645
+ if not user_rows:
646
+ raise NodeValidationError(f"User not found with username: {username}")
647
+
648
+ user_data = user_rows[0]
649
+ return User(
650
+ user_id=user_data["user_id"],
651
+ email=user_data["email"],
652
+ username=user_data["username"],
653
+ first_name=user_data["first_name"],
654
+ last_name=user_data["last_name"],
655
+ display_name=user_data["display_name"],
656
+ roles=user_data.get("roles", []),
657
+ attributes=user_data.get("attributes", {}),
658
+ status=UserStatus(user_data["status"]),
659
+ tenant_id=user_data["tenant_id"],
660
+ external_auth_id=user_data["external_auth_id"],
661
+ auth_provider=user_data["auth_provider"],
662
+ created_at=parse_datetime(user_data.get("created_at")),
663
+ updated_at=parse_datetime(user_data.get("updated_at")),
664
+ last_login_at=parse_datetime(user_data.get("last_login_at")),
665
+ )
517
666
 
518
- def _create_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
519
- """Create a new user with validation and audit logging."""
667
+ def _update_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
668
+ """Update an existing user."""
669
+ user_id = inputs["user_id"]
520
670
  user_data = inputs["user_data"]
521
- tenant_id = inputs.get("tenant_id", "default")
522
-
523
- # 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)
671
+ tenant_id = inputs["tenant_id"]
672
+
673
+ # Get existing user
674
+ existing_user = self._get_user_by_id(user_id, tenant_id)
675
+
676
+ # Build update query dynamically based on provided fields
677
+ update_fields = []
678
+ parameters = []
679
+ param_index = 1
680
+
681
+ updatable_fields = [
682
+ "email",
683
+ "username",
684
+ "first_name",
685
+ "last_name",
686
+ "display_name",
687
+ "status",
688
+ "external_auth_id",
689
+ "auth_provider",
690
+ ]
691
+
692
+ for field in updatable_fields:
693
+ if field in user_data:
694
+ update_fields.append(f"{field} = ${param_index}")
695
+ parameters.append(user_data[field])
696
+ param_index += 1
697
+
698
+ # Handle JSONB fields separately
699
+ if "roles" in user_data:
700
+ update_fields.append(f"roles = ${param_index}")
701
+ parameters.append(json.dumps(user_data["roles"]))
702
+ param_index += 1
703
+
704
+ if "attributes" in user_data:
705
+ update_fields.append(f"attributes = ${param_index}")
706
+ parameters.append(json.dumps(user_data["attributes"]))
707
+ param_index += 1
708
+
709
+ if "password_hash" in inputs:
710
+ update_fields.append(f"password_hash = ${param_index}")
711
+ parameters.append(inputs["password_hash"])
712
+ param_index += 1
713
+
714
+ if not update_fields:
715
+ raise NodeValidationError("No valid fields provided for update")
716
+
717
+ # Always update the updated_at timestamp
718
+ update_fields.append("updated_at = CURRENT_TIMESTAMP")
719
+
720
+ # Add WHERE clause parameters
721
+ parameters.extend([user_id, tenant_id])
722
+
723
+ update_query = f"""
724
+ UPDATE users
725
+ SET {', '.join(update_fields)}
726
+ WHERE user_id = ${param_index} AND tenant_id = ${param_index + 1}
727
+ """
544
728
 
545
- # 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"])
729
+ try:
730
+ self._db_node.run(query=update_query, parameters=parameters)
551
731
 
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
- }
732
+ # Get updated user
733
+ updated_user = self._get_user_by_id(user_id, tenant_id)
569
734
 
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
- """
735
+ return {
736
+ "result": {
737
+ "user": updated_user.to_dict(),
738
+ "operation": "update_user",
739
+ "timestamp": datetime.now(UTC).isoformat(),
740
+ }
741
+ }
580
742
 
581
- # 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
- }
743
+ except Exception as e:
744
+ raise NodeExecutionError(f"Failed to update user: {str(e)}")
601
745
 
602
- db_result = self._db_node.run(**query)
746
+ def _delete_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
747
+ """Delete (or soft-delete) a user."""
748
+ user_id = inputs["user_id"]
749
+ tenant_id = inputs["tenant_id"]
750
+ hard_delete = inputs.get("hard_delete", False)
751
+
752
+ # Get existing user to return in response
753
+ existing_user = self._get_user_by_id(user_id, tenant_id)
754
+
755
+ if hard_delete:
756
+ # Hard delete - remove from database
757
+ delete_query = "DELETE FROM users WHERE user_id = $1 AND tenant_id = $2"
758
+ else:
759
+ # Soft delete - mark as deleted
760
+ delete_query = """
761
+ UPDATE users
762
+ SET status = 'deleted', updated_at = CURRENT_TIMESTAMP
763
+ WHERE user_id = $1 AND tenant_id = $2
764
+ """
603
765
 
604
- # Create user profile response
605
- user_profile = UserProfile(
606
- user_id=user_id,
607
- email=user_record["email"],
608
- username=user_record["username"],
609
- first_name=user_record["first_name"],
610
- last_name=user_record["last_name"],
611
- status=UserStatus(user_record["status"]),
612
- roles=user_record["roles"],
613
- attributes=user_record["attributes"],
614
- created_at=user_record["created_at"],
615
- updated_at=user_record["updated_at"],
616
- tenant_id=tenant_id,
617
- )
766
+ try:
767
+ self._db_node.run(query=delete_query, parameters=[user_id, tenant_id])
618
768
 
619
- 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(),
769
+ return {
770
+ "result": {
771
+ "deleted_user": existing_user.to_dict(),
772
+ "hard_delete": hard_delete,
773
+ "operation": "delete_user",
774
+ "timestamp": datetime.now(UTC).isoformat(),
775
+ }
636
776
  }
637
- }
638
777
 
639
- 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)
778
+ except Exception as e:
779
+ raise NodeExecutionError(f"Failed to delete user: {str(e)}")
645
780
 
646
- if not user_id and not email:
647
- raise NodeValidationError("Either user_id or email must be provided")
781
+ def _list_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
782
+ """List users with filtering and pagination."""
783
+ tenant_id = inputs["tenant_id"]
784
+ status = inputs.get("status")
785
+ limit = inputs.get("limit", 50)
786
+ offset = inputs.get("offset", 0)
787
+ include_deleted = inputs.get("include_deleted", False)
648
788
 
649
- # Build query
789
+ # Build query with filters
650
790
  where_conditions = ["tenant_id = $1"]
651
- params = [tenant_id]
652
- param_count = 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
+ """
653
815
 
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
816
+ count_query = f"""
817
+ SELECT COUNT(*) as total_count
671
818
  FROM users
672
819
  WHERE {' AND '.join(where_conditions)}
673
- LIMIT 1
674
820
  """
675
821
 
676
- # Execute query
677
- self._db_node.config.update(
678
- {"query": query, "params": params, "fetch_mode": "one"}
679
- )
822
+ try:
823
+ # Get users
824
+ result = self._db_node.run(
825
+ query=list_query, parameters=parameters, result_format="dict"
826
+ )
827
+
828
+ # Get total count
829
+ count_result = self._db_node.run(
830
+ query=count_query, parameters=parameters[:-2], result_format="dict"
831
+ )
680
832
 
681
- db_result = self._db_node.run(**query)
833
+ users = []
834
+ for user_data in result.get("data", []):
835
+ user = User(
836
+ user_id=user_data["user_id"],
837
+ email=user_data["email"],
838
+ username=user_data["username"],
839
+ first_name=user_data["first_name"],
840
+ last_name=user_data["last_name"],
841
+ display_name=user_data["display_name"],
842
+ roles=user_data.get("roles", []),
843
+ attributes=user_data.get("attributes", {}),
844
+ status=UserStatus(user_data["status"]),
845
+ tenant_id=user_data["tenant_id"],
846
+ external_auth_id=user_data["external_auth_id"],
847
+ auth_provider=user_data["auth_provider"],
848
+ created_at=user_data.get("created_at"),
849
+ updated_at=user_data.get("updated_at"),
850
+ last_login_at=user_data.get("last_login_at"),
851
+ )
852
+ users.append(user.to_dict())
853
+
854
+ total_count = count_result.get("data", [{}])[0].get("total_count", 0)
682
855
 
683
- if not db_result.get("result", {}).get("data"):
684
856
  return {
685
857
  "result": {
686
- "user": None,
687
- "found": False,
688
- "operation": "read",
858
+ "users": users,
859
+ "pagination": {
860
+ "total_count": total_count,
861
+ "limit": limit,
862
+ "offset": offset,
863
+ "has_more": offset + limit < total_count,
864
+ },
865
+ "filters": {
866
+ "status": status,
867
+ "include_deleted": include_deleted,
868
+ },
869
+ "operation": "list_users",
689
870
  "timestamp": datetime.now(UTC).isoformat(),
690
871
  }
691
872
  }
692
873
 
693
- user_data = db_result["result"]["data"]
874
+ except Exception as e:
875
+ raise NodeExecutionError(f"Failed to list users: {str(e)}")
694
876
 
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
- }
877
+ def _activate_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
878
+ """Activate a user."""
879
+ return self._change_user_status(inputs, UserStatus.ACTIVE, "activate_user")
716
880
 
717
- def _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)
881
+ def _deactivate_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
882
+ """Deactivate a user."""
883
+ return self._change_user_status(inputs, UserStatus.INACTIVE, "deactivate_user")
884
+
885
+ def _change_user_status(
886
+ self, inputs: Dict[str, Any], new_status: UserStatus, operation: str
887
+ ) -> Dict[str, Any]:
888
+ """Helper method to change user status."""
889
+ user_id = inputs["user_id"]
890
+ tenant_id = inputs["tenant_id"]
891
+
892
+ update_query = """
893
+ UPDATE users
894
+ SET status = $1, updated_at = CURRENT_TIMESTAMP
895
+ WHERE user_id = $2 AND tenant_id = $3
896
+ """
725
897
 
726
- # 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
- """
898
+ try:
899
+ self._db_node.run(
900
+ query=update_query, parameters=[new_status.value, user_id, tenant_id]
761
901
  )
762
- params.append(f"%{search_query}%")
763
902
 
764
- # 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")
903
+ # Get updated user
904
+ updated_user = self._get_user_by_id(user_id, tenant_id)
769
905
 
770
- offset = (page - 1) * size
906
+ return {
907
+ "result": {
908
+ "user": updated_user.to_dict(),
909
+ "operation": operation,
910
+ "timestamp": datetime.now(UTC).isoformat(),
911
+ }
912
+ }
771
913
 
772
- # Count query
773
- count_query = f"""
774
- SELECT COUNT(*) as total
775
- FROM users
776
- WHERE {' AND '.join(where_conditions)}
914
+ except Exception as e:
915
+ raise NodeExecutionError(f"Failed to {operation}: {str(e)}")
916
+
917
+ def _set_password(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
918
+ """Set user password hash."""
919
+ user_id = inputs["user_id"]
920
+ tenant_id = inputs["tenant_id"]
921
+ password_hash = inputs["password_hash"]
922
+
923
+ update_query = """
924
+ UPDATE users
925
+ SET password_hash = $1, updated_at = CURRENT_TIMESTAMP
926
+ WHERE user_id = $2 AND tenant_id = $3
777
927
  """
778
928
 
779
- # 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
783
- FROM users
784
- WHERE {' AND '.join(where_conditions)}
785
- ORDER BY {sort_field} {sort_direction}
786
- LIMIT {size} OFFSET {offset}
787
- """
929
+ try:
930
+ self._db_node.run(
931
+ query=update_query, parameters=[password_hash, user_id, tenant_id]
932
+ )
788
933
 
789
- # Execute count query
790
- self._db_node.config.update(
791
- {"query": count_query, "params": params, "fetch_mode": "one"}
792
- )
793
- count_result = self._db_node.run()
794
- total_count = count_result["result"]["data"]["total"]
934
+ return {
935
+ "result": {
936
+ "user_id": user_id,
937
+ "password_updated": True,
938
+ "operation": "set_password",
939
+ "timestamp": datetime.now(UTC).isoformat(),
940
+ }
941
+ }
795
942
 
796
- # Execute data query
797
- self._db_node.config.update(
798
- {"query": data_query, "params": params, "fetch_mode": "all"}
799
- )
800
- data_result = self._db_node.run()
801
- users = data_result["result"]["data"]
943
+ except Exception as e:
944
+ raise NodeExecutionError(f"Failed to set password: {str(e)}")
945
+
946
+ def _update_profile(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
947
+ """Update user profile fields."""
948
+ # This is essentially the same as update_user but with a different operation name
949
+ result = self._update_user(inputs)
950
+ result["result"]["operation"] = "update_profile"
951
+ return result
952
+
953
+ def _bulk_create(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
954
+ """Create multiple users in bulk."""
955
+ users_data = inputs["users_data"]
956
+ tenant_id = inputs["tenant_id"]
802
957
 
803
- # Calculate pagination info
804
- total_pages = (total_count + size - 1) // size
805
- has_next = page < total_pages
806
- has_prev = page > 1
958
+ if not isinstance(users_data, list):
959
+ raise NodeValidationError("users_data must be a list")
960
+
961
+ created_users = []
962
+ failed_users = []
963
+
964
+ for i, user_data in enumerate(users_data):
965
+ try:
966
+ # Create each user individually for better error handling
967
+ create_inputs = {
968
+ "operation": "create_user",
969
+ "user_data": user_data,
970
+ "tenant_id": tenant_id,
971
+ "database_config": inputs["database_config"],
972
+ }
973
+
974
+ result = self._create_user(create_inputs)
975
+ created_users.append(result["result"]["user"])
976
+
977
+ except Exception as e:
978
+ failed_users.append(
979
+ {
980
+ "index": i,
981
+ "user_data": user_data,
982
+ "error": str(e),
983
+ }
984
+ )
807
985
 
808
986
  return {
809
987
  "result": {
810
- "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,
988
+ "bulk_result": {
989
+ "created_count": len(created_users),
990
+ "failed_count": len(failed_users),
991
+ "total_count": len(users_data),
992
+ "created_users": created_users,
993
+ "failed_users": failed_users,
818
994
  },
819
- "filters_applied": filters,
820
- "search_query": search_query,
821
- "operation": "list",
995
+ "operation": "bulk_create",
822
996
  "timestamp": datetime.now(UTC).isoformat(),
823
997
  }
824
998
  }
825
999
 
826
- def _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")
1000
+ def _bulk_update(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1001
+ """Update multiple users in bulk."""
1002
+ users_data = inputs["users_data"]
1003
+ tenant_id = inputs["tenant_id"]
830
1004
 
831
- if not isinstance(user_data_list, list):
832
- raise NodeValidationError("user_data must be a list for bulk operations")
1005
+ if not isinstance(users_data, list):
1006
+ raise NodeValidationError("users_data must be a list")
833
1007
 
834
- results = {"created": [], "failed": [], "stats": {"created": 0, "failed": 0}}
1008
+ updated_users = []
1009
+ failed_users = []
835
1010
 
836
- for i, user_data in enumerate(user_data_list):
1011
+ for i, user_data in enumerate(users_data):
837
1012
  try:
838
- # Create individual user
839
- create_inputs = {
840
- "operation": "create",
1013
+ if "user_id" not in user_data:
1014
+ raise NodeValidationError("user_id is required for bulk update")
1015
+
1016
+ update_inputs = {
1017
+ "operation": "update_user",
1018
+ "user_id": user_data.pop("user_id"),
841
1019
  "user_data": user_data,
842
1020
  "tenant_id": tenant_id,
843
- "validate_email": inputs.get("validate_email", True),
844
- "validate_username": inputs.get("validate_username", True),
1021
+ "database_config": inputs["database_config"],
845
1022
  }
846
1023
 
847
- result = self._create_user(create_inputs)
848
- results["created"].append(
849
- {"index": i, "user": result["result"]["user"]}
1024
+ result = self._update_user(update_inputs)
1025
+ updated_users.append(result["result"]["user"])
1026
+
1027
+ except Exception as e:
1028
+ failed_users.append(
1029
+ {
1030
+ "index": i,
1031
+ "user_data": user_data,
1032
+ "error": str(e),
1033
+ }
850
1034
  )
851
- results["stats"]["created"] += 1
1035
+
1036
+ return {
1037
+ "result": {
1038
+ "bulk_result": {
1039
+ "updated_count": len(updated_users),
1040
+ "failed_count": len(failed_users),
1041
+ "total_count": len(users_data),
1042
+ "updated_users": updated_users,
1043
+ "failed_users": failed_users,
1044
+ },
1045
+ "operation": "bulk_update",
1046
+ "timestamp": datetime.now(UTC).isoformat(),
1047
+ }
1048
+ }
1049
+
1050
+ def _bulk_delete(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1051
+ """Delete multiple users in bulk."""
1052
+ user_ids = inputs.get("user_ids", [])
1053
+ tenant_id = inputs["tenant_id"]
1054
+ hard_delete = inputs.get("hard_delete", False)
1055
+
1056
+ if not isinstance(user_ids, list):
1057
+ raise NodeValidationError("user_ids must be a list")
1058
+
1059
+ deleted_users = []
1060
+ failed_users = []
1061
+
1062
+ for user_id in user_ids:
1063
+ try:
1064
+ delete_inputs = {
1065
+ "operation": "delete_user",
1066
+ "user_id": user_id,
1067
+ "tenant_id": tenant_id,
1068
+ "hard_delete": hard_delete,
1069
+ "database_config": inputs["database_config"],
1070
+ }
1071
+
1072
+ result = self._delete_user(delete_inputs)
1073
+ deleted_users.append(result["result"]["deleted_user"])
852
1074
 
853
1075
  except Exception as e:
854
- results["failed"].append(
855
- {"index": i, "user_data": user_data, "error": str(e)}
1076
+ failed_users.append(
1077
+ {
1078
+ "user_id": user_id,
1079
+ "error": str(e),
1080
+ }
856
1081
  )
857
- results["stats"]["failed"] += 1
858
1082
 
859
1083
  return {
860
1084
  "result": {
861
- "operation": "bulk_create",
862
- "results": results,
1085
+ "bulk_result": {
1086
+ "deleted_count": len(deleted_users),
1087
+ "failed_count": len(failed_users),
1088
+ "total_count": len(user_ids),
1089
+ "deleted_users": deleted_users,
1090
+ "failed_users": failed_users,
1091
+ "hard_delete": hard_delete,
1092
+ },
1093
+ "operation": "bulk_delete",
1094
+ "timestamp": datetime.now(UTC).isoformat(),
1095
+ }
1096
+ }
1097
+
1098
+ def _get_user_roles(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1099
+ """Get roles assigned to a user."""
1100
+ user_id = inputs["user_id"]
1101
+ tenant_id = inputs["tenant_id"]
1102
+
1103
+ # Get user with roles
1104
+ user = self._get_user_by_id(user_id, tenant_id)
1105
+
1106
+ # Get detailed role information
1107
+ if user.roles:
1108
+ placeholders = ",".join([f"${i+1}" for i in range(len(user.roles))])
1109
+ role_query = f"""
1110
+ SELECT role_id, name, description, permissions, parent_roles, attributes
1111
+ FROM roles
1112
+ WHERE role_id IN ({placeholders}) AND tenant_id = ${len(user.roles) + 1}
1113
+ """
1114
+
1115
+ result = self._db_node.run(
1116
+ query=role_query,
1117
+ parameters=user.roles + [tenant_id],
1118
+ result_format="dict",
1119
+ )
1120
+ role_details = result.get("data", [])
1121
+ else:
1122
+ role_details = []
1123
+
1124
+ return {
1125
+ "result": {
1126
+ "user_id": user_id,
1127
+ "roles": user.roles,
1128
+ "role_details": role_details,
1129
+ "operation": "get_user_roles",
863
1130
  "timestamp": datetime.now(UTC).isoformat(),
864
1131
  }
865
1132
  }
866
1133
 
867
- # Utility methods
868
- def _generate_user_id(self) -> str:
869
- """Generate unique user ID."""
870
- import uuid
1134
+ def _get_user_permissions(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1135
+ """Get effective permissions for a user."""
1136
+ user_id = inputs["user_id"]
1137
+ tenant_id = inputs["tenant_id"]
871
1138
 
872
- return str(uuid.uuid4())
1139
+ # This would integrate with PermissionCheckNode to get effective permissions
1140
+ # For now, return a basic implementation
1141
+ user = self._get_user_by_id(user_id, tenant_id)
873
1142
 
874
- 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}"
1143
+ return {
1144
+ "result": {
1145
+ "user_id": user_id,
1146
+ "roles": user.roles,
1147
+ "attributes": user.attributes,
1148
+ "operation": "get_user_permissions",
1149
+ "note": "Use PermissionCheckNode.get_user_permissions for complete permission evaluation",
1150
+ "timestamp": datetime.now(UTC).isoformat(),
1151
+ }
1152
+ }
879
1153
 
880
- def _validate_email(self, email: str) -> bool:
881
- """Validate email format."""
882
- import re
1154
+ def _search_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1155
+ """Search users by query."""
1156
+ search_query = inputs["search_query"]
1157
+ tenant_id = inputs["tenant_id"]
1158
+ limit = inputs.get("limit", 50)
1159
+ offset = inputs.get("offset", 0)
1160
+
1161
+ # Simple text search across email, username, first_name, last_name
1162
+ query = """
1163
+ SELECT user_id, email, username, first_name, last_name, display_name,
1164
+ roles, attributes, status, tenant_id, external_auth_id, auth_provider,
1165
+ created_at, updated_at, last_login_at
1166
+ FROM users
1167
+ WHERE tenant_id = $1 AND status != 'deleted' AND (
1168
+ email ILIKE $2 OR
1169
+ username ILIKE $2 OR
1170
+ first_name ILIKE $2 OR
1171
+ last_name ILIKE $2 OR
1172
+ display_name ILIKE $2
1173
+ )
1174
+ ORDER BY created_at DESC
1175
+ LIMIT $3 OFFSET $4
1176
+ """
883
1177
 
884
- pattern = r"^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$"
885
- return bool(re.match(pattern, email))
1178
+ search_pattern = f"%{search_query}%"
886
1179
 
887
- def _validate_username(self, username: str) -> bool:
888
- """Validate username format."""
889
- import re
1180
+ try:
1181
+ result = self._db_node.run(
1182
+ query=query,
1183
+ parameters=[tenant_id, search_pattern, limit, offset],
1184
+ result_format="dict",
1185
+ )
890
1186
 
891
- # Username: alphanumeric, dots, hyphens, underscores, 3-50 chars
892
- pattern = r"^[a-zA-Z0-9._-]{3,50}$"
893
- return bool(re.match(pattern, username))
1187
+ users = []
1188
+ for user_data in result.get("data", []):
1189
+ user = User(
1190
+ user_id=user_data["user_id"],
1191
+ email=user_data["email"],
1192
+ username=user_data["username"],
1193
+ first_name=user_data["first_name"],
1194
+ last_name=user_data["last_name"],
1195
+ display_name=user_data["display_name"],
1196
+ roles=user_data.get("roles", []),
1197
+ attributes=user_data.get("attributes", {}),
1198
+ status=UserStatus(user_data["status"]),
1199
+ tenant_id=user_data["tenant_id"],
1200
+ external_auth_id=user_data["external_auth_id"],
1201
+ auth_provider=user_data["auth_provider"],
1202
+ created_at=user_data.get("created_at"),
1203
+ updated_at=user_data.get("updated_at"),
1204
+ last_login_at=user_data.get("last_login_at"),
1205
+ )
1206
+ users.append(user.to_dict())
894
1207
 
895
- # Additional operations (update, delete, etc.) would follow similar patterns
896
- def _update_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
897
- """Update user information."""
898
- # Implementation similar to create but with UPDATE query
899
- raise NotImplementedError("Update operation will be implemented")
1208
+ return {
1209
+ "result": {
1210
+ "users": users,
1211
+ "search_query": search_query,
1212
+ "count": len(users),
1213
+ "pagination": {
1214
+ "limit": limit,
1215
+ "offset": offset,
1216
+ },
1217
+ "operation": "search_users",
1218
+ "timestamp": datetime.now(UTC).isoformat(),
1219
+ }
1220
+ }
900
1221
 
901
- def _delete_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
902
- """Soft delete user."""
903
- # Implementation with status change to 'deleted'
904
- raise NotImplementedError("Delete operation will be implemented")
1222
+ except Exception as e:
1223
+ raise NodeExecutionError(f"Failed to search users: {str(e)}")
1224
+
1225
+ def _export_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1226
+ """Export users in specified format."""
1227
+ tenant_id = inputs["tenant_id"]
1228
+ export_format = inputs.get("export_format", "json")
1229
+ include_deleted = inputs.get("include_deleted", False)
1230
+
1231
+ # Get all users for export
1232
+ list_inputs = {
1233
+ **inputs,
1234
+ "operation": "list_users",
1235
+ "limit": 10000, # Large limit for export
1236
+ "offset": 0,
1237
+ "include_deleted": include_deleted,
1238
+ }
1239
+
1240
+ result = self._list_users(list_inputs)
1241
+ users = result["result"]["users"]
1242
+
1243
+ if export_format == "json":
1244
+ export_data = {
1245
+ "users": users,
1246
+ "export_metadata": {
1247
+ "tenant_id": tenant_id,
1248
+ "export_time": datetime.now(UTC).isoformat(),
1249
+ "total_users": len(users),
1250
+ "include_deleted": include_deleted,
1251
+ },
1252
+ }
1253
+ elif export_format == "csv":
1254
+ # Convert to CSV format (simplified)
1255
+ csv_headers = [
1256
+ "user_id",
1257
+ "email",
1258
+ "username",
1259
+ "first_name",
1260
+ "last_name",
1261
+ "status",
1262
+ "roles",
1263
+ "created_at",
1264
+ ]
1265
+ csv_rows = []
1266
+ for user in users:
1267
+ csv_rows.append(
1268
+ [
1269
+ user.get("user_id", ""),
1270
+ user.get("email", ""),
1271
+ user.get("username", ""),
1272
+ user.get("first_name", ""),
1273
+ user.get("last_name", ""),
1274
+ user.get("status", ""),
1275
+ ",".join(user.get("roles", [])),
1276
+ user.get("created_at", ""),
1277
+ ]
1278
+ )
1279
+
1280
+ export_data = {
1281
+ "format": "csv",
1282
+ "headers": csv_headers,
1283
+ "rows": csv_rows,
1284
+ "export_metadata": {
1285
+ "tenant_id": tenant_id,
1286
+ "export_time": datetime.now(UTC).isoformat(),
1287
+ "total_users": len(users),
1288
+ "include_deleted": include_deleted,
1289
+ },
1290
+ }
1291
+ else:
1292
+ raise NodeValidationError(f"Unsupported export format: {export_format}")
1293
+
1294
+ return {
1295
+ "result": {
1296
+ "export_data": export_data,
1297
+ "operation": "export_users",
1298
+ "timestamp": datetime.now(UTC).isoformat(),
1299
+ }
1300
+ }
1301
+
1302
+ def _generate_reset_token(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1303
+ """Generate a password reset token for a user."""
1304
+ user_id = inputs["user_id"]
1305
+ tenant_id = inputs["tenant_id"]
1306
+
1307
+ # Generate a secure reset token
1308
+ token = str(uuid4())
1309
+ expires_at = datetime.now(UTC) + timedelta(hours=1)
1310
+
1311
+ # Store token in database (using user_sessions table)
1312
+ store_token_query = """
1313
+ INSERT INTO user_sessions (
1314
+ session_id, user_id, tenant_id,
1315
+ session_token_hash, expires_at, created_at,
1316
+ last_accessed, ip_address, user_agent
1317
+ ) VALUES (
1318
+ :session_id, :user_id, :tenant_id,
1319
+ :token_hash, :expires_at, :created_at,
1320
+ :last_accessed, :ip_address, :user_agent
1321
+ )
1322
+ """
1323
+
1324
+ result = self._db_node.run(
1325
+ operation="execute",
1326
+ query=store_token_query,
1327
+ parameters={
1328
+ "session_id": token,
1329
+ "user_id": user_id,
1330
+ "tenant_id": tenant_id,
1331
+ "token_hash": hashlib.sha256(f"reset_{token}".encode()).hexdigest(),
1332
+ "expires_at": expires_at,
1333
+ "created_at": datetime.now(UTC),
1334
+ "last_accessed": datetime.now(UTC),
1335
+ "ip_address": "127.0.0.1",
1336
+ "user_agent": "password_reset_token",
1337
+ },
1338
+ )
905
1339
 
906
- def _change_password(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
907
- """Change user password."""
908
- # Implementation with password hashing and audit
909
- raise NotImplementedError("Change password operation will be implemented")
1340
+ return {
1341
+ "token": token,
1342
+ "expires_at": expires_at.isoformat(),
1343
+ "user_id": user_id,
1344
+ }
910
1345
 
911
1346
  def _reset_password(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
912
- """Reset user password with token generation."""
913
- # Implementation with secure token generation
914
- raise NotImplementedError("Reset password operation will be implemented")
1347
+ """Reset user password using a valid token."""
1348
+ token = inputs["token"]
1349
+ new_password = inputs["new_password"]
1350
+ tenant_id = inputs["tenant_id"]
1351
+
1352
+ # Verify token and get user_id
1353
+ verify_query = """
1354
+ SELECT user_id FROM user_sessions
1355
+ WHERE session_id = :token
1356
+ AND tenant_id = :tenant_id
1357
+ AND user_agent = 'password_reset_token'
1358
+ AND expires_at > CURRENT_TIMESTAMP
1359
+ """
915
1360
 
916
- def _deactivate_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
917
- """Deactivate user account."""
918
- # Implementation with status change to 'inactive'
919
- raise NotImplementedError("Deactivate operation will be implemented")
1361
+ result = self._db_node.run(
1362
+ operation="query",
1363
+ query=verify_query,
1364
+ parameters={"token": token, "tenant_id": tenant_id},
1365
+ )
920
1366
 
921
- def _activate_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
922
- """Activate user account."""
923
- # Implementation with status change to 'active'
924
- raise NotImplementedError("Activate operation will be implemented")
1367
+ if not result.get("data", []):
1368
+ raise NodeExecutionError("Invalid or expired reset token")
925
1369
 
926
- def _restore_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
927
- """Restore soft-deleted user."""
928
- # Implementation with status change from 'deleted'
929
- raise NotImplementedError("Restore operation will be implemented")
1370
+ user_id = result["data"][0]["user_id"]
930
1371
 
931
- def _search_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
932
- """Advanced user search with full-text capabilities."""
933
- # Implementation with advanced search features
934
- raise NotImplementedError("Search operation will be implemented")
935
-
936
- def _bulk_update_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
937
- """Bulk update multiple users."""
938
- # Implementation with transaction support
939
- raise NotImplementedError("Bulk update operation will be implemented")
940
-
941
- def _bulk_delete_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
942
- """Bulk delete multiple users."""
943
- # Implementation with transaction support
944
- raise NotImplementedError("Bulk delete operation will be implemented")
1372
+ # Update password
1373
+ password_hash = hashlib.sha256(new_password.encode()).hexdigest()
1374
+ update_query = """
1375
+ UPDATE users
1376
+ SET password_hash = :password_hash,
1377
+ updated_at = CURRENT_TIMESTAMP
1378
+ WHERE user_id = :user_id
1379
+ AND tenant_id = :tenant_id
1380
+ """
1381
+
1382
+ update_result = self._db_node.run(
1383
+ operation="execute",
1384
+ query=update_query,
1385
+ parameters={
1386
+ "password_hash": password_hash,
1387
+ "user_id": user_id,
1388
+ "tenant_id": tenant_id,
1389
+ },
1390
+ )
1391
+
1392
+ # Delete the used token
1393
+ delete_token_query = """
1394
+ DELETE FROM user_sessions
1395
+ WHERE session_id = :token
1396
+ """
1397
+
1398
+ self._db_node.run(
1399
+ operation="execute", query=delete_token_query, parameters={"token": token}
1400
+ )
1401
+
1402
+ return {
1403
+ "success": True,
1404
+ "user_id": user_id,
1405
+ "message": "Password reset successfully",
1406
+ }
1407
+
1408
+ def _authenticate(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
1409
+ """Authenticate a user with username/email and password."""
1410
+ username = inputs.get("username")
1411
+ email = inputs.get("email")
1412
+ password = inputs["password"]
1413
+ tenant_id = inputs["tenant_id"]
1414
+
1415
+ # Build query based on provided credentials
1416
+ if username:
1417
+ auth_query = """
1418
+ SELECT user_id, password_hash, status
1419
+ FROM users
1420
+ WHERE username = :username
1421
+ AND tenant_id = :tenant_id
1422
+ """
1423
+ params = {"username": username, "tenant_id": tenant_id}
1424
+ elif email:
1425
+ auth_query = """
1426
+ SELECT user_id, password_hash, status
1427
+ FROM users
1428
+ WHERE email = :email
1429
+ AND tenant_id = :tenant_id
1430
+ """
1431
+ params = {"email": email, "tenant_id": tenant_id}
1432
+ else:
1433
+ raise NodeValidationError("Either username or email must be provided")
1434
+
1435
+ result = self._db_node.run(
1436
+ operation="query", query=auth_query, parameters=params
1437
+ )
1438
+
1439
+ if not result.get("data", []):
1440
+ return {"authenticated": False, "message": "User not found"}
1441
+
1442
+ user_data = result["data"][0]
1443
+ stored_hash = user_data["password_hash"]
1444
+ provided_hash = hashlib.sha256(password.encode()).hexdigest()
1445
+
1446
+ if stored_hash != provided_hash:
1447
+ return {"authenticated": False, "message": "Invalid password"}
1448
+
1449
+ if user_data["status"] != "active":
1450
+ return {
1451
+ "authenticated": False,
1452
+ "message": f"User account is {user_data['status']}",
1453
+ }
1454
+
1455
+ # Update last login
1456
+ update_login_query = """
1457
+ UPDATE users
1458
+ SET last_login_at = CURRENT_TIMESTAMP
1459
+ WHERE user_id = :user_id
1460
+ """
1461
+
1462
+ self._db_node.run(
1463
+ operation="execute",
1464
+ query=update_login_query,
1465
+ parameters={"user_id": user_data["user_id"]},
1466
+ )
1467
+
1468
+ return {
1469
+ "authenticated": True,
1470
+ "user_id": user_data["user_id"],
1471
+ "message": "Authentication successful",
1472
+ }