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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (57) 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/gateway/__init__.py +22 -0
  19. kailash/middleware/gateway/checkpoint_manager.py +398 -0
  20. kailash/middleware/gateway/deduplicator.py +382 -0
  21. kailash/middleware/gateway/durable_gateway.py +417 -0
  22. kailash/middleware/gateway/durable_request.py +498 -0
  23. kailash/middleware/gateway/event_store.py +459 -0
  24. kailash/nodes/admin/permission_check.py +817 -33
  25. kailash/nodes/admin/role_management.py +1242 -108
  26. kailash/nodes/admin/schema_manager.py +438 -0
  27. kailash/nodes/admin/user_management.py +1124 -1582
  28. kailash/nodes/code/__init__.py +8 -1
  29. kailash/nodes/code/async_python.py +1035 -0
  30. kailash/nodes/code/python.py +1 -0
  31. kailash/nodes/data/async_sql.py +9 -3
  32. kailash/nodes/data/sql.py +20 -11
  33. kailash/nodes/data/workflow_connection_pool.py +643 -0
  34. kailash/nodes/rag/__init__.py +1 -4
  35. kailash/resources/__init__.py +40 -0
  36. kailash/resources/factory.py +533 -0
  37. kailash/resources/health.py +319 -0
  38. kailash/resources/reference.py +288 -0
  39. kailash/resources/registry.py +392 -0
  40. kailash/runtime/async_local.py +711 -302
  41. kailash/testing/__init__.py +34 -0
  42. kailash/testing/async_test_case.py +353 -0
  43. kailash/testing/async_utils.py +345 -0
  44. kailash/testing/fixtures.py +458 -0
  45. kailash/testing/mock_registry.py +495 -0
  46. kailash/workflow/__init__.py +8 -0
  47. kailash/workflow/async_builder.py +621 -0
  48. kailash/workflow/async_patterns.py +766 -0
  49. kailash/workflow/cyclic_runner.py +107 -16
  50. kailash/workflow/graph.py +7 -2
  51. kailash/workflow/resilience.py +11 -1
  52. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/METADATA +7 -4
  53. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/RECORD +57 -22
  54. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
  55. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
  56. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
  57. {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/top_level.txt +0 -0
@@ -15,10 +15,11 @@ Features:
15
15
  - Comprehensive audit logging
16
16
  """
17
17
 
18
+ import json
18
19
  from dataclasses import dataclass
19
20
  from datetime import UTC, datetime
20
21
  from enum import Enum
21
- from typing import Any, Dict, List, Optional, Set
22
+ from typing import Any, Dict, List, Optional, Set, Union
22
23
 
23
24
  from kailash.access_control import (
24
25
  AccessControlManager,
@@ -28,10 +29,48 @@ from kailash.access_control import (
28
29
  WorkflowPermission,
29
30
  )
30
31
  from kailash.nodes.base import Node, NodeParameter, register_node
31
- from kailash.nodes.data import AsyncSQLDatabaseNode
32
+ from kailash.nodes.data import SQLDatabaseNode
32
33
  from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
33
34
 
34
35
 
36
+ def parse_datetime(value: Union[str, datetime, None]) -> Optional[datetime]:
37
+ """Parse datetime from various formats."""
38
+ if value is None:
39
+ return None
40
+ if isinstance(value, datetime):
41
+ return value
42
+ if isinstance(value, str):
43
+ try:
44
+ # Try ISO format first
45
+ return datetime.fromisoformat(value.replace("Z", "+00:00"))
46
+ except ValueError:
47
+ # Try other common formats
48
+ for fmt in [
49
+ "%Y-%m-%d %H:%M:%S.%f",
50
+ "%Y-%m-%d %H:%M:%S",
51
+ "%Y-%m-%dT%H:%M:%S.%f",
52
+ "%Y-%m-%dT%H:%M:%S",
53
+ ]:
54
+ try:
55
+ return datetime.strptime(value, fmt)
56
+ except ValueError:
57
+ continue
58
+ return None
59
+
60
+
61
+ def format_datetime(dt: Union[datetime, str, None]) -> Optional[str]:
62
+ """Format datetime handling both datetime objects and strings."""
63
+ if dt is None:
64
+ return None
65
+ if isinstance(dt, datetime):
66
+ return dt.isoformat()
67
+ if isinstance(dt, str):
68
+ # Already a string, return as-is or try to parse and format
69
+ parsed = parse_datetime(dt)
70
+ return parsed.isoformat() if parsed else dt
71
+ return None
72
+
73
+
35
74
  class RoleOperation(Enum):
36
75
  """Supported role management operations."""
37
76
 
@@ -253,6 +292,79 @@ class RoleManagementNode(Node):
253
292
  required=False,
254
293
  description="Search query for roles",
255
294
  ),
295
+ # Pagination
296
+ NodeParameter(
297
+ name="limit",
298
+ type=int,
299
+ required=False,
300
+ default=50,
301
+ description="Maximum number of results to return",
302
+ ),
303
+ NodeParameter(
304
+ name="offset",
305
+ type=int,
306
+ required=False,
307
+ default=0,
308
+ description="Number of results to skip",
309
+ ),
310
+ # Additional options
311
+ NodeParameter(
312
+ name="include_users",
313
+ type=bool,
314
+ required=False,
315
+ default=False,
316
+ description="Include user assignments in role details",
317
+ ),
318
+ NodeParameter(
319
+ name="include_user_details",
320
+ type=bool,
321
+ required=False,
322
+ default=True,
323
+ description="Include detailed user information",
324
+ ),
325
+ NodeParameter(
326
+ name="include_inactive",
327
+ type=bool,
328
+ required=False,
329
+ default=False,
330
+ description="Include inactive roles/users",
331
+ ),
332
+ NodeParameter(
333
+ name="force",
334
+ type=bool,
335
+ required=False,
336
+ default=False,
337
+ description="Force operation even with dependencies",
338
+ ),
339
+ NodeParameter(
340
+ name="fix_issues",
341
+ type=bool,
342
+ required=False,
343
+ default=False,
344
+ description="Automatically fix validation issues",
345
+ ),
346
+ # Audit fields
347
+ NodeParameter(
348
+ name="created_by",
349
+ type=str,
350
+ required=False,
351
+ default="system",
352
+ description="User who created the role",
353
+ ),
354
+ NodeParameter(
355
+ name="assigned_by",
356
+ type=str,
357
+ required=False,
358
+ default="system",
359
+ description="User who assigned the role",
360
+ ),
361
+ NodeParameter(
362
+ name="unassigned_by",
363
+ type=str,
364
+ required=False,
365
+ default="system",
366
+ description="User who unassigned the role",
367
+ ),
256
368
  ]
257
369
  }
258
370
 
@@ -303,6 +415,10 @@ class RoleManagementNode(Node):
303
415
 
304
416
  def _init_dependencies(self, inputs: Dict[str, Any]):
305
417
  """Initialize database and access 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
+
306
422
  # Get database config
307
423
  db_config = inputs.get(
308
424
  "database_config",
@@ -316,8 +432,8 @@ class RoleManagementNode(Node):
316
432
  },
317
433
  )
318
434
 
319
- # Initialize async database node
320
- self._db_node = AsyncSQLDatabaseNode(name="role_management_db", **db_config)
435
+ # Initialize database node
436
+ self._db_node = SQLDatabaseNode(name="role_management_db", **db_config)
321
437
 
322
438
  # Initialize enhanced access manager
323
439
  self._access_manager = AccessControlManager(strategy="abac")
@@ -363,7 +479,7 @@ class RoleManagementNode(Node):
363
479
  "created_by": inputs.get("created_by", "system"),
364
480
  }
365
481
 
366
- # Insert role into database
482
+ # Insert role into database with conflict resolution
367
483
  insert_query = """
368
484
  INSERT INTO roles (
369
485
  role_id, name, description, role_type, permissions, parent_roles,
@@ -371,31 +487,37 @@ class RoleManagementNode(Node):
371
487
  ) VALUES (
372
488
  $1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12
373
489
  )
490
+ ON CONFLICT (role_id) DO UPDATE SET
491
+ name = EXCLUDED.name,
492
+ description = EXCLUDED.description,
493
+ role_type = EXCLUDED.role_type,
494
+ permissions = EXCLUDED.permissions,
495
+ parent_roles = EXCLUDED.parent_roles,
496
+ attributes = EXCLUDED.attributes,
497
+ is_active = EXCLUDED.is_active,
498
+ updated_at = EXCLUDED.updated_at,
499
+ created_by = EXCLUDED.created_by
374
500
  """
375
501
 
376
502
  # Execute database insert
377
- self._db_node.config.update(
378
- {
379
- "query": insert_query,
380
- "params": [
381
- role_record["role_id"],
382
- role_record["name"],
383
- role_record["description"],
384
- role_record["role_type"],
385
- role_record["permissions"],
386
- role_record["parent_roles"],
387
- role_record["attributes"],
388
- role_record["is_active"],
389
- role_record["tenant_id"],
390
- role_record["created_at"],
391
- role_record["updated_at"],
392
- role_record["created_by"],
393
- ],
394
- }
503
+ db_result = self._db_node.run(
504
+ query=insert_query,
505
+ parameters=[
506
+ role_record["role_id"],
507
+ role_record["name"],
508
+ role_record["description"],
509
+ role_record["role_type"],
510
+ json.dumps(role_record["permissions"]), # Serialize list to JSON
511
+ json.dumps(role_record["parent_roles"]), # Serialize list to JSON
512
+ json.dumps(role_record["attributes"]), # Serialize dict to JSON
513
+ role_record["is_active"],
514
+ role_record["tenant_id"],
515
+ role_record["created_at"],
516
+ role_record["updated_at"],
517
+ role_record["created_by"],
518
+ ],
395
519
  )
396
520
 
397
- db_result = self._db_node.run()
398
-
399
521
  # Update child_roles for parent roles
400
522
  if parent_roles:
401
523
  self._update_child_roles(parent_roles, role_id, tenant_id, "add")
@@ -412,7 +534,7 @@ class RoleManagementNode(Node):
412
534
  "attributes": role_record["attributes"],
413
535
  "is_active": role_record["is_active"],
414
536
  "tenant_id": role_record["tenant_id"],
415
- "created_at": role_record["created_at"].isoformat(),
537
+ "created_at": format_datetime(role_record["created_at"]),
416
538
  },
417
539
  "operation": "create_role",
418
540
  "success": True,
@@ -437,21 +559,17 @@ class RoleManagementNode(Node):
437
559
 
438
560
  # Check if assignment already exists
439
561
  existing_query = """
440
- SELECT 1 FROM user_roles
441
- WHERE user_id = $1 AND role_id = $2 AND tenant_id = $3
562
+ SELECT 1 FROM user_role_assignments
563
+ WHERE user_id = $1 AND role_id = $2 AND tenant_id = $3 AND is_active = true
442
564
  """
443
565
 
444
- self._db_node.config.update(
445
- {
446
- "query": existing_query,
447
- "params": [user_id, role_id, tenant_id],
448
- "fetch_mode": "one",
449
- }
566
+ existing = self._db_node.run(
567
+ query=existing_query,
568
+ parameters=[user_id, role_id, tenant_id],
569
+ result_format="dict",
450
570
  )
451
571
 
452
- existing = self._db_node.run()
453
-
454
- if existing.get("result", {}).get("data"):
572
+ if existing.get("data"):
455
573
  return {
456
574
  "result": {
457
575
  "assignment": {
@@ -465,28 +583,28 @@ class RoleManagementNode(Node):
465
583
  }
466
584
  }
467
585
 
468
- # Create assignment
586
+ # Create assignment with conflict resolution
469
587
  now = datetime.now(UTC)
470
588
  insert_query = """
471
- INSERT INTO user_roles (user_id, role_id, tenant_id, assigned_at, assigned_by)
589
+ INSERT INTO user_role_assignments (user_id, role_id, tenant_id, assigned_at, assigned_by)
472
590
  VALUES ($1, $2, $3, $4, $5)
591
+ ON CONFLICT (user_id, role_id, tenant_id) DO UPDATE SET
592
+ assigned_at = EXCLUDED.assigned_at,
593
+ assigned_by = EXCLUDED.assigned_by,
594
+ is_active = true
473
595
  """
474
596
 
475
- self._db_node.config.update(
476
- {
477
- "query": insert_query,
478
- "params": [
479
- user_id,
480
- role_id,
481
- tenant_id,
482
- now,
483
- inputs.get("assigned_by", "system"),
484
- ],
485
- }
597
+ db_result = self._db_node.run(
598
+ query=insert_query,
599
+ parameters=[
600
+ user_id,
601
+ role_id,
602
+ tenant_id,
603
+ now,
604
+ inputs.get("assigned_by", "system"),
605
+ ],
486
606
  )
487
607
 
488
- db_result = self._db_node.run()
489
-
490
608
  return {
491
609
  "result": {
492
610
  "assignment": {
@@ -622,14 +740,8 @@ class RoleManagementNode(Node):
622
740
 
623
741
  params = [tenant_id] + list(parent_roles)
624
742
 
625
- self._db_node.config.update(
626
- {"query": query, "params": params, "fetch_mode": "all"}
627
- )
628
-
629
- result = self._db_node.run()
630
- existing_roles = {
631
- row["role_id"] for row in result.get("result", {}).get("data", [])
632
- }
743
+ result = self._db_node.run(query=query, parameters=params, result_format="dict")
744
+ existing_roles = {row["role_id"] for row in result.get("data", [])}
633
745
 
634
746
  missing_roles = parent_roles - existing_roles
635
747
  if missing_roles:
@@ -686,12 +798,8 @@ class RoleManagementNode(Node):
686
798
  query += " AND role_id != $2"
687
799
  params.append(exclude_role)
688
800
 
689
- self._db_node.config.update(
690
- {"query": query, "params": params, "fetch_mode": "all"}
691
- )
692
-
693
- result = self._db_node.run()
694
- roles_data = result.get("result", {}).get("data", [])
801
+ result = self._db_node.run(query=query, parameters=params, fetch_mode="all")
802
+ roles_data = result.get("data", [])
695
803
 
696
804
  # Convert to hierarchy dict
697
805
  hierarchy = {}
@@ -733,12 +841,11 @@ class RoleManagementNode(Node):
733
841
  WHERE role_id = $1 AND tenant_id = $2
734
842
  """
735
843
 
736
- self._db_node.config.update(
737
- {"query": query, "params": [role_id, tenant_id], "fetch_mode": "one"}
844
+ result = self._db_node.run(
845
+ query=query, parameters=[role_id, tenant_id], result_format="dict"
738
846
  )
739
-
740
- result = self._db_node.run()
741
- return result.get("result", {}).get("data")
847
+ data = result.get("data", [])
848
+ return data[0] if data else None
742
849
 
743
850
  def _update_child_roles(
744
851
  self,
@@ -747,77 +854,1104 @@ class RoleManagementNode(Node):
747
854
  tenant_id: str,
748
855
  operation: str,
749
856
  ):
750
- """Update child_roles arrays for parent roles."""
751
- if operation == "add":
752
- query = """
753
- UPDATE roles
754
- SET child_roles = array_append(child_roles, $1),
755
- updated_at = $2
756
- WHERE role_id = ANY($3) AND tenant_id = $4
757
- """
758
- else: # remove
759
- query = """
760
- UPDATE roles
761
- SET child_roles = array_remove(child_roles, $1),
762
- updated_at = $2
763
- WHERE role_id = ANY($3) AND tenant_id = $4
857
+ """Update child_roles JSONB arrays for parent roles."""
858
+ # For now, let's just read the current child_roles, modify them in Python, and update
859
+ # This is simpler and more reliable than complex JSONB operations
860
+
861
+ for parent_role_id in parent_role_ids:
862
+ # Get current child roles
863
+ get_query = """
864
+ SELECT child_roles FROM roles
865
+ WHERE role_id = $1 AND tenant_id = $2
764
866
  """
765
867
 
766
- self._db_node.config.update(
767
- {
768
- "query": query,
769
- "params": [
770
- child_role_id,
771
- datetime.now(UTC),
772
- list(parent_role_ids),
773
- tenant_id,
774
- ],
775
- }
776
- )
868
+ result = self._db_node.run(
869
+ query=get_query,
870
+ parameters=[parent_role_id, tenant_id],
871
+ result_format="dict",
872
+ )
777
873
 
778
- self._db_node.run()
874
+ if result.get("data"):
875
+ current_child_roles = result["data"][0].get("child_roles", [])
876
+ if isinstance(current_child_roles, str):
877
+ current_child_roles = json.loads(current_child_roles)
878
+ elif current_child_roles is None:
879
+ current_child_roles = []
880
+
881
+ # Modify the list
882
+ if operation == "add":
883
+ if child_role_id not in current_child_roles:
884
+ current_child_roles.append(child_role_id)
885
+ else: # remove
886
+ if child_role_id in current_child_roles:
887
+ current_child_roles.remove(child_role_id)
888
+
889
+ # Update the database
890
+ update_query = """
891
+ UPDATE roles
892
+ SET child_roles = $1, updated_at = $2
893
+ WHERE role_id = $3 AND tenant_id = $4
894
+ """
895
+
896
+ self._db_node.run(
897
+ query=update_query,
898
+ parameters=[
899
+ json.dumps(current_child_roles),
900
+ datetime.now(UTC),
901
+ parent_role_id,
902
+ tenant_id,
903
+ ],
904
+ )
779
905
 
780
906
  # Additional operations would follow similar patterns
781
907
  def _update_role(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
782
908
  """Update role information."""
783
- raise NotImplementedError("Update role operation will be implemented")
909
+ role_id = inputs["role_id"]
910
+ role_data = inputs["role_data"]
911
+ tenant_id = inputs.get("tenant_id", "default")
912
+ validate_hierarchy = inputs.get("validate_hierarchy", True)
913
+
914
+ # Validate role exists
915
+ existing_role = self._get_role_by_id(role_id, tenant_id)
916
+ if not existing_role:
917
+ raise NodeValidationError(f"Role not found: {role_id}")
918
+
919
+ # Validate parent roles if being updated
920
+ if "parent_roles" in role_data and validate_hierarchy:
921
+ parent_roles = set(role_data["parent_roles"])
922
+ self._validate_parent_roles_exist(parent_roles, tenant_id)
923
+ self._validate_no_circular_dependency(role_id, parent_roles, tenant_id)
924
+
925
+ # Build update fields
926
+ update_fields = []
927
+ params = []
928
+ param_count = 1
929
+
930
+ updatable_fields = {
931
+ "name": "name",
932
+ "description": "description",
933
+ "permissions": "permissions",
934
+ "parent_roles": "parent_roles",
935
+ "attributes": "attributes",
936
+ "is_active": "is_active",
937
+ }
938
+
939
+ for field, db_field in updatable_fields.items():
940
+ if field in role_data:
941
+ update_fields.append(f"{db_field} = ${param_count}")
942
+ # Serialize JSONB fields to JSON
943
+ if field in ["permissions", "parent_roles", "attributes"]:
944
+ params.append(json.dumps(role_data[field]))
945
+ else:
946
+ params.append(role_data[field])
947
+ param_count += 1
948
+
949
+ if not update_fields:
950
+ raise NodeValidationError("No valid fields provided for update")
951
+
952
+ # Add updated_at timestamp
953
+ update_fields.append(f"updated_at = ${param_count}")
954
+ params.append(datetime.now(UTC))
955
+ param_count += 1
956
+
957
+ # Add WHERE conditions
958
+ params.extend([role_id, tenant_id])
959
+
960
+ update_query = f"""
961
+ UPDATE roles
962
+ SET {', '.join(update_fields)}
963
+ WHERE role_id = ${param_count} AND tenant_id = ${param_count + 1}
964
+ RETURNING role_id, name, description, permissions, parent_roles, attributes, is_active, updated_at
965
+ """
966
+
967
+ result = self._db_node.run(
968
+ query=update_query, parameters=params, fetch_mode="one"
969
+ )
970
+ updated_role = result.get("data", [])
971
+ updated_role = updated_role[0] if updated_role else None
972
+
973
+ if not updated_role:
974
+ raise NodeExecutionError(f"Failed to update role: {role_id}")
975
+
976
+ # Update child roles if parent_roles changed
977
+ if "parent_roles" in role_data:
978
+ old_parents = set(existing_role.get("parent_roles", []))
979
+ new_parents = set(role_data["parent_roles"])
980
+
981
+ # Remove from old parents
982
+ removed_parents = old_parents - new_parents
983
+ if removed_parents:
984
+ self._update_child_roles(removed_parents, role_id, tenant_id, "remove")
985
+
986
+ # Add to new parents
987
+ added_parents = new_parents - old_parents
988
+ if added_parents:
989
+ self._update_child_roles(added_parents, role_id, tenant_id, "add")
990
+
991
+ return {
992
+ "result": {
993
+ "role": {
994
+ "role_id": updated_role["role_id"],
995
+ "name": updated_role["name"],
996
+ "description": updated_role["description"],
997
+ "permissions": updated_role["permissions"],
998
+ "parent_roles": updated_role["parent_roles"],
999
+ "attributes": updated_role["attributes"],
1000
+ "is_active": updated_role["is_active"],
1001
+ "updated_at": format_datetime(updated_role["updated_at"]),
1002
+ },
1003
+ "operation": "update_role",
1004
+ "success": True,
1005
+ "timestamp": datetime.now(UTC).isoformat(),
1006
+ }
1007
+ }
784
1008
 
785
1009
  def _delete_role(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
786
1010
  """Delete role with dependency checking."""
787
- raise NotImplementedError("Delete role operation will be implemented")
1011
+ role_id = inputs["role_id"]
1012
+ tenant_id = inputs.get("tenant_id", "default")
1013
+ force = inputs.get("force", False)
1014
+
1015
+ # Validate role exists
1016
+ existing_role = self._get_role_by_id(role_id, tenant_id)
1017
+ if not existing_role:
1018
+ raise NodeValidationError(f"Role not found: {role_id}")
1019
+
1020
+ # Check for dependencies unless force delete
1021
+ if not force:
1022
+ # Check for child roles
1023
+ child_roles_query = """
1024
+ SELECT role_id FROM roles
1025
+ WHERE $1 = ANY(
1026
+ SELECT jsonb_array_elements_text(parent_roles)
1027
+ ) AND tenant_id = $2 AND is_active = true
1028
+ """
1029
+
1030
+ child_result = self._db_node.run(
1031
+ query=child_roles_query,
1032
+ parameters=[role_id, tenant_id],
1033
+ fetch_mode="all",
1034
+ )
1035
+ child_roles = child_result.get("data", [])
1036
+
1037
+ if child_roles:
1038
+ child_role_ids = [r["role_id"] for r in child_roles]
1039
+ raise NodeValidationError(
1040
+ f"Cannot delete role {role_id}: has child roles {child_role_ids}. Use force=True to override."
1041
+ )
1042
+
1043
+ # Check for user assignments
1044
+ user_assignments_query = """
1045
+ SELECT COUNT(*) as user_count FROM user_role_assignments
1046
+ WHERE role_id = $1 AND tenant_id = $2
1047
+ """
1048
+
1049
+ user_result = self._db_node.run(
1050
+ query=user_assignments_query,
1051
+ parameters=[role_id, tenant_id],
1052
+ fetch_mode="one",
1053
+ )
1054
+ user_count = user_result.get("data", [{}])[0].get("user_count", 0)
1055
+
1056
+ if user_count > 0:
1057
+ raise NodeValidationError(
1058
+ f"Cannot delete role {role_id}: assigned to {user_count} users. Use force=True to override."
1059
+ )
1060
+
1061
+ # Store parent roles for cleanup
1062
+ parent_roles = set(existing_role.get("parent_roles", []))
1063
+
1064
+ # Delete user assignments if force
1065
+ if force:
1066
+ delete_assignments_query = """
1067
+ DELETE FROM user_role_assignments WHERE role_id = $1 AND tenant_id = $2
1068
+ """
1069
+
1070
+ self._db_node.run(
1071
+ query=delete_assignments_query, parameters=[role_id, tenant_id]
1072
+ )
1073
+
1074
+ # Remove from child roles of other roles
1075
+ if parent_roles:
1076
+ self._update_child_roles(parent_roles, role_id, tenant_id, "remove")
1077
+
1078
+ # Update child roles to remove this as parent
1079
+ update_children_query = """
1080
+ UPDATE roles
1081
+ SET parent_roles = (
1082
+ SELECT COALESCE(
1083
+ json_agg(value),
1084
+ '[]'::json
1085
+ )
1086
+ FROM jsonb_array_elements_text(parent_roles) AS value
1087
+ WHERE value != $1
1088
+ )::jsonb,
1089
+ updated_at = $2
1090
+ WHERE $1 = ANY(
1091
+ SELECT jsonb_array_elements_text(parent_roles)
1092
+ ) AND tenant_id = $3
1093
+ """
1094
+
1095
+ self._db_node.run(
1096
+ query=update_children_query,
1097
+ parameters=[role_id, datetime.now(UTC), tenant_id],
1098
+ )
1099
+
1100
+ # Delete the role
1101
+ delete_query = """
1102
+ DELETE FROM roles WHERE role_id = $1 AND tenant_id = $2
1103
+ """
1104
+
1105
+ self._db_node.run(query=delete_query, parameters=[role_id, tenant_id])
1106
+
1107
+ return {
1108
+ "result": {
1109
+ "role_id": role_id,
1110
+ "deleted": True,
1111
+ "force_used": force,
1112
+ "operation": "delete_role",
1113
+ "success": True,
1114
+ "timestamp": datetime.now(UTC).isoformat(),
1115
+ }
1116
+ }
788
1117
 
789
1118
  def _list_roles(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
790
1119
  """List roles with filtering and pagination."""
791
- raise NotImplementedError("List roles operation will be implemented")
1120
+ tenant_id = inputs.get("tenant_id", "default")
1121
+ filters = inputs.get("filters", {})
1122
+ search_query = inputs.get("search_query", "")
1123
+ limit = inputs.get("limit", 50)
1124
+ offset = inputs.get("offset", 0)
1125
+ include_inherited = inputs.get("include_inherited", False)
1126
+
1127
+ # Build WHERE conditions
1128
+ where_conditions = ["tenant_id = $1"]
1129
+ params = [tenant_id]
1130
+ param_count = 2
1131
+
1132
+ # Add search filter
1133
+ if search_query:
1134
+ where_conditions.append(
1135
+ f"(name ILIKE ${param_count} OR description ILIKE ${param_count})"
1136
+ )
1137
+ params.append(f"%{search_query}%")
1138
+ param_count += 1
1139
+
1140
+ # Add field filters
1141
+ if "role_type" in filters:
1142
+ where_conditions.append(f"role_type = ${param_count}")
1143
+ params.append(filters["role_type"])
1144
+ param_count += 1
1145
+
1146
+ if "is_active" in filters:
1147
+ where_conditions.append(f"is_active = ${param_count}")
1148
+ params.append(filters["is_active"])
1149
+ param_count += 1
1150
+
1151
+ if "has_permissions" in filters:
1152
+ where_conditions.append("array_length(permissions, 1) > 0")
1153
+
1154
+ # Build query
1155
+ base_query = f"""
1156
+ SELECT role_id, name, description, role_type, permissions, parent_roles,
1157
+ child_roles, attributes, is_active, created_at, updated_at, created_by
1158
+ FROM roles
1159
+ WHERE {' AND '.join(where_conditions)}
1160
+ ORDER BY created_at DESC
1161
+ """
1162
+
1163
+ # Add pagination
1164
+ if limit > 0:
1165
+ base_query += f" LIMIT ${param_count}"
1166
+ params.append(limit)
1167
+ param_count += 1
1168
+
1169
+ if offset > 0:
1170
+ base_query += f" OFFSET ${param_count}"
1171
+ params.append(offset)
1172
+
1173
+ result = self._db_node.run(
1174
+ query=base_query, parameters=params, fetch_mode="all"
1175
+ )
1176
+ roles_data = result.get("data", [])
1177
+
1178
+ # Get total count
1179
+ count_query = f"""
1180
+ SELECT COUNT(*) as total
1181
+ FROM roles
1182
+ WHERE {' AND '.join(where_conditions)}
1183
+ """
1184
+
1185
+ count_result = self._db_node.run(
1186
+ query=count_query,
1187
+ parameters=(
1188
+ params[: param_count - 2] if limit > 0 else params
1189
+ ), # Exclude LIMIT/OFFSET
1190
+ fetch_mode="one",
1191
+ )
1192
+ total_count = count_result.get("data", [{}])[0].get("total", 0)
1193
+
1194
+ # Enhance roles with inherited permissions if requested
1195
+ enhanced_roles = []
1196
+ for role_data in roles_data:
1197
+ enhanced_role = {
1198
+ "role_id": role_data["role_id"],
1199
+ "name": role_data["name"],
1200
+ "description": role_data["description"],
1201
+ "role_type": role_data["role_type"],
1202
+ "permissions": role_data["permissions"],
1203
+ "parent_roles": role_data["parent_roles"],
1204
+ "child_roles": role_data["child_roles"],
1205
+ "attributes": role_data["attributes"],
1206
+ "is_active": role_data["is_active"],
1207
+ "created_at": format_datetime(role_data["created_at"]),
1208
+ "updated_at": format_datetime(role_data["updated_at"]),
1209
+ "created_by": role_data["created_by"],
1210
+ }
1211
+
1212
+ if include_inherited:
1213
+ # Get inherited permissions
1214
+ role_hierarchy = self._build_role_hierarchy(tenant_id)
1215
+ inherited_perms = self._get_inherited_permissions(
1216
+ role_data["role_id"], role_hierarchy
1217
+ )
1218
+ all_perms = set(role_data["permissions"]) | inherited_perms
1219
+
1220
+ enhanced_role["inherited_permissions"] = list(inherited_perms)
1221
+ enhanced_role["all_permissions"] = list(all_perms)
1222
+ enhanced_role["permission_count"] = {
1223
+ "direct": len(role_data["permissions"]),
1224
+ "inherited": len(inherited_perms),
1225
+ "total": len(all_perms),
1226
+ }
1227
+
1228
+ enhanced_roles.append(enhanced_role)
1229
+
1230
+ return {
1231
+ "result": {
1232
+ "roles": enhanced_roles,
1233
+ "pagination": {
1234
+ "total": total_count,
1235
+ "limit": limit,
1236
+ "offset": offset,
1237
+ "returned": len(enhanced_roles),
1238
+ },
1239
+ "filters_applied": {
1240
+ "search_query": search_query,
1241
+ "filters": filters,
1242
+ "include_inherited": include_inherited,
1243
+ },
1244
+ "operation": "list_roles",
1245
+ "timestamp": datetime.now(UTC).isoformat(),
1246
+ }
1247
+ }
792
1248
 
793
1249
  def _get_role(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
794
1250
  """Get detailed role information."""
795
- raise NotImplementedError("Get role operation will be implemented")
1251
+ role_id = inputs["role_id"]
1252
+ tenant_id = inputs.get("tenant_id", "default")
1253
+ include_inherited = inputs.get("include_inherited", True)
1254
+ include_users = inputs.get("include_users", False)
1255
+
1256
+ # Get basic role information
1257
+ role_data = self._get_role_by_id(role_id, tenant_id)
1258
+ if not role_data:
1259
+ raise NodeValidationError(f"Role not found: {role_id}")
1260
+
1261
+ # Build enhanced role info
1262
+ enhanced_role = {
1263
+ "role_id": role_data["role_id"],
1264
+ "name": role_data["name"],
1265
+ "description": role_data["description"],
1266
+ "role_type": role_data["role_type"],
1267
+ "permissions": role_data["permissions"],
1268
+ "parent_roles": role_data["parent_roles"],
1269
+ "attributes": role_data["attributes"],
1270
+ "is_active": role_data["is_active"],
1271
+ "created_at": format_datetime(role_data["created_at"]),
1272
+ "updated_at": format_datetime(role_data["updated_at"]),
1273
+ }
1274
+
1275
+ # Add inherited permissions if requested
1276
+ if include_inherited:
1277
+ role_hierarchy = self._build_role_hierarchy(tenant_id)
1278
+ inherited_perms = self._get_inherited_permissions(role_id, role_hierarchy)
1279
+ all_perms = set(role_data["permissions"]) | inherited_perms
1280
+
1281
+ enhanced_role["inherited_permissions"] = list(inherited_perms)
1282
+ enhanced_role["all_permissions"] = list(all_perms)
1283
+ enhanced_role["permission_count"] = {
1284
+ "direct": len(role_data["permissions"]),
1285
+ "inherited": len(inherited_perms),
1286
+ "total": len(all_perms),
1287
+ }
1288
+
1289
+ # Get child roles from hierarchy
1290
+ child_roles = []
1291
+ for role_info in role_hierarchy.values():
1292
+ if role_id in role_info.get("parent_roles", []):
1293
+ child_roles.append(
1294
+ {
1295
+ "role_id": role_info["role_id"],
1296
+ "name": role_info["name"],
1297
+ "is_active": role_info["is_active"],
1298
+ }
1299
+ )
1300
+ enhanced_role["child_roles_detailed"] = child_roles
1301
+
1302
+ # Add user assignments if requested
1303
+ if include_users:
1304
+ users_query = """
1305
+ SELECT ur.user_id, ur.assigned_at, ur.assigned_by, u.email, u.status
1306
+ FROM user_role_assignments ur
1307
+ LEFT JOIN users u ON ur.user_id = u.user_id AND ur.tenant_id = u.tenant_id
1308
+ WHERE ur.role_id = $1 AND ur.tenant_id = $2
1309
+ ORDER BY ur.assigned_at DESC
1310
+ """
1311
+
1312
+ users_result = self._db_node.run(
1313
+ query=users_query, parameters=[role_id, tenant_id], fetch_mode="all"
1314
+ )
1315
+ users_data = users_result.get("data", [])
1316
+
1317
+ enhanced_role["assigned_users"] = [
1318
+ {
1319
+ "user_id": user["user_id"],
1320
+ "email": user.get("email"),
1321
+ "status": user.get("status"),
1322
+ "assigned_at": (
1323
+ user["assigned_at"].isoformat() if user["assigned_at"] else None
1324
+ ),
1325
+ "assigned_by": user["assigned_by"],
1326
+ }
1327
+ for user in users_data
1328
+ ]
1329
+ enhanced_role["user_count"] = len(users_data)
1330
+
1331
+ return {
1332
+ "result": {
1333
+ "role": enhanced_role,
1334
+ "operation": "get_role",
1335
+ "timestamp": datetime.now(UTC).isoformat(),
1336
+ }
1337
+ }
796
1338
 
797
1339
  def _unassign_user(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
798
1340
  """Unassign role from user."""
799
- raise NotImplementedError("Unassign user operation will be implemented")
1341
+ user_id = inputs["user_id"]
1342
+ role_id = inputs["role_id"]
1343
+ tenant_id = inputs.get("tenant_id", "default")
1344
+
1345
+ # Check if assignment exists
1346
+ check_query = """
1347
+ SELECT user_id, role_id, assigned_at, assigned_by
1348
+ FROM user_role_assignments
1349
+ WHERE user_id = $1 AND role_id = $2 AND tenant_id = $3
1350
+ """
1351
+
1352
+ existing = self._db_node.run(
1353
+ query=check_query,
1354
+ parameters=[user_id, role_id, tenant_id],
1355
+ fetch_mode="one",
1356
+ )
1357
+ assignment = existing.get("result", {}).get("data")
1358
+
1359
+ if not assignment:
1360
+ return {
1361
+ "result": {
1362
+ "unassignment": {
1363
+ "user_id": user_id,
1364
+ "role_id": role_id,
1365
+ "was_assigned": False,
1366
+ "message": "User was not assigned to this role",
1367
+ },
1368
+ "operation": "unassign_user",
1369
+ "success": True,
1370
+ "timestamp": datetime.now(UTC).isoformat(),
1371
+ }
1372
+ }
1373
+
1374
+ # Remove assignment
1375
+ delete_query = """
1376
+ DELETE FROM user_role_assignments
1377
+ WHERE user_id = $1 AND role_id = $2 AND tenant_id = $3
1378
+ """
1379
+
1380
+ self._db_node.run(query=delete_query, parameters=[user_id, role_id, tenant_id])
1381
+
1382
+ return {
1383
+ "result": {
1384
+ "unassignment": {
1385
+ "user_id": user_id,
1386
+ "role_id": role_id,
1387
+ "was_assigned": True,
1388
+ "previously_assigned_at": (
1389
+ assignment["assigned_at"].isoformat()
1390
+ if assignment["assigned_at"]
1391
+ else None
1392
+ ),
1393
+ "previously_assigned_by": assignment["assigned_by"],
1394
+ "unassigned_at": datetime.now(UTC).isoformat(),
1395
+ "unassigned_by": inputs.get("unassigned_by", "system"),
1396
+ },
1397
+ "operation": "unassign_user",
1398
+ "success": True,
1399
+ "timestamp": datetime.now(UTC).isoformat(),
1400
+ }
1401
+ }
800
1402
 
801
1403
  def _add_permission(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
802
1404
  """Add permission to role."""
803
- raise NotImplementedError("Add permission operation will be implemented")
1405
+ role_id = inputs["role_id"]
1406
+ permission = inputs["permission"]
1407
+ tenant_id = inputs.get("tenant_id", "default")
1408
+
1409
+ # Validate role exists
1410
+ role_data = self._get_role_by_id(role_id, tenant_id)
1411
+ if not role_data:
1412
+ raise NodeValidationError(f"Role not found: {role_id}")
1413
+
1414
+ current_permissions = set(role_data.get("permissions", []))
1415
+
1416
+ # Check if permission already exists
1417
+ if permission in current_permissions:
1418
+ return {
1419
+ "result": {
1420
+ "permission_added": False,
1421
+ "role_id": role_id,
1422
+ "permission": permission,
1423
+ "message": "Permission already exists on role",
1424
+ "operation": "add_permission",
1425
+ "timestamp": datetime.now(UTC).isoformat(),
1426
+ }
1427
+ }
1428
+
1429
+ # Add permission
1430
+ new_permissions = list(current_permissions | {permission})
1431
+
1432
+ update_query = """
1433
+ UPDATE roles
1434
+ SET permissions = $1, updated_at = $2
1435
+ WHERE role_id = $3 AND tenant_id = $4
1436
+ RETURNING permissions
1437
+ """
1438
+
1439
+ result = self._db_node.run(
1440
+ query=update_query,
1441
+ parameters=[
1442
+ json.dumps(new_permissions),
1443
+ datetime.now(UTC),
1444
+ role_id,
1445
+ tenant_id,
1446
+ ],
1447
+ fetch_mode="one",
1448
+ )
1449
+ updated_permissions = result.get("data", [{}])[0].get("permissions", [])
1450
+
1451
+ return {
1452
+ "result": {
1453
+ "permission_added": True,
1454
+ "role_id": role_id,
1455
+ "permission": permission,
1456
+ "permissions_count": len(updated_permissions),
1457
+ "all_permissions": updated_permissions,
1458
+ "operation": "add_permission",
1459
+ "timestamp": datetime.now(UTC).isoformat(),
1460
+ }
1461
+ }
804
1462
 
805
1463
  def _remove_permission(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
806
1464
  """Remove permission from role."""
807
- raise NotImplementedError("Remove permission operation will be implemented")
1465
+ role_id = inputs["role_id"]
1466
+ permission = inputs["permission"]
1467
+ tenant_id = inputs.get("tenant_id", "default")
1468
+
1469
+ # Validate role exists
1470
+ role_data = self._get_role_by_id(role_id, tenant_id)
1471
+ if not role_data:
1472
+ raise NodeValidationError(f"Role not found: {role_id}")
1473
+
1474
+ current_permissions = set(role_data.get("permissions", []))
1475
+
1476
+ # Check if permission exists
1477
+ if permission not in current_permissions:
1478
+ return {
1479
+ "result": {
1480
+ "permission_removed": False,
1481
+ "role_id": role_id,
1482
+ "permission": permission,
1483
+ "message": "Permission does not exist on role",
1484
+ "operation": "remove_permission",
1485
+ "timestamp": datetime.now(UTC).isoformat(),
1486
+ }
1487
+ }
1488
+
1489
+ # Remove permission
1490
+ new_permissions = list(current_permissions - {permission})
1491
+
1492
+ update_query = """
1493
+ UPDATE roles
1494
+ SET permissions = $1, updated_at = $2
1495
+ WHERE role_id = $3 AND tenant_id = $4
1496
+ RETURNING permissions
1497
+ """
1498
+
1499
+ result = self._db_node.run(
1500
+ query=update_query,
1501
+ parameters=[
1502
+ json.dumps(new_permissions),
1503
+ datetime.now(UTC),
1504
+ role_id,
1505
+ tenant_id,
1506
+ ],
1507
+ fetch_mode="one",
1508
+ )
1509
+ updated_permissions = result.get("data", [{}])[0].get("permissions", [])
1510
+
1511
+ return {
1512
+ "result": {
1513
+ "permission_removed": True,
1514
+ "role_id": role_id,
1515
+ "permission": permission,
1516
+ "permissions_count": len(updated_permissions),
1517
+ "all_permissions": updated_permissions,
1518
+ "operation": "remove_permission",
1519
+ "timestamp": datetime.now(UTC).isoformat(),
1520
+ }
1521
+ }
808
1522
 
809
1523
  def _bulk_unassign(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
810
1524
  """Bulk unassign role from multiple users."""
811
- raise NotImplementedError("Bulk unassign operation will be implemented")
1525
+ role_id = inputs["role_id"]
1526
+ user_ids = inputs["user_ids"]
1527
+ tenant_id = inputs.get("tenant_id", "default")
1528
+
1529
+ if not isinstance(user_ids, list):
1530
+ raise NodeValidationError("user_ids must be a list for bulk operations")
1531
+
1532
+ results = {
1533
+ "unassigned": [],
1534
+ "not_assigned": [],
1535
+ "failed": [],
1536
+ "stats": {"unassigned": 0, "not_assigned": 0, "failed": 0},
1537
+ }
1538
+
1539
+ for user_id in user_ids:
1540
+ try:
1541
+ unassign_inputs = {
1542
+ "operation": "unassign_user",
1543
+ "user_id": user_id,
1544
+ "role_id": role_id,
1545
+ "tenant_id": tenant_id,
1546
+ "unassigned_by": inputs.get("unassigned_by", "system"),
1547
+ }
1548
+
1549
+ result = self._unassign_user(unassign_inputs)
1550
+ unassignment = result["result"]["unassignment"]
1551
+
1552
+ if unassignment["was_assigned"]:
1553
+ results["unassigned"].append(
1554
+ {
1555
+ "user_id": user_id,
1556
+ "previously_assigned_at": unassignment[
1557
+ "previously_assigned_at"
1558
+ ],
1559
+ }
1560
+ )
1561
+ results["stats"]["unassigned"] += 1
1562
+ else:
1563
+ results["not_assigned"].append({"user_id": user_id})
1564
+ results["stats"]["not_assigned"] += 1
1565
+
1566
+ except Exception as e:
1567
+ results["failed"].append({"user_id": user_id, "error": str(e)})
1568
+ results["stats"]["failed"] += 1
1569
+
1570
+ return {
1571
+ "result": {
1572
+ "operation": "bulk_unassign",
1573
+ "role_id": role_id,
1574
+ "results": results,
1575
+ "timestamp": datetime.now(UTC).isoformat(),
1576
+ }
1577
+ }
812
1578
 
813
1579
  def _get_user_roles(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
814
1580
  """Get all roles for a user."""
815
- raise NotImplementedError("Get user roles operation will be implemented")
1581
+ user_id = inputs["user_id"]
1582
+ tenant_id = inputs.get("tenant_id", "default")
1583
+ include_inherited = inputs.get("include_inherited", True)
1584
+ include_inactive = inputs.get("include_inactive", False)
1585
+
1586
+ # Get user role assignments
1587
+ roles_query = """
1588
+ SELECT ur.role_id, ur.assigned_at, ur.assigned_by,
1589
+ r.name, r.description, r.role_type, r.permissions,
1590
+ r.parent_roles, r.attributes, r.is_active
1591
+ FROM user_role_assignments ur
1592
+ JOIN roles r ON ur.role_id = r.role_id AND ur.tenant_id = r.tenant_id
1593
+ WHERE ur.user_id = $1 AND ur.tenant_id = $2
1594
+ """
1595
+
1596
+ params = [user_id, tenant_id]
1597
+
1598
+ if not include_inactive:
1599
+ roles_query += " AND r.is_active = true"
1600
+
1601
+ roles_query += " ORDER BY ur.assigned_at DESC"
1602
+
1603
+ result = self._db_node.run(
1604
+ query=roles_query, parameters=params, fetch_mode="all"
1605
+ )
1606
+ roles_data = result.get("data", [])
1607
+
1608
+ user_roles = []
1609
+ all_permissions = set()
1610
+ role_hierarchy = (
1611
+ self._build_role_hierarchy(tenant_id) if include_inherited else {}
1612
+ )
1613
+
1614
+ for role_data in roles_data:
1615
+ role_info = {
1616
+ "role_id": role_data["role_id"],
1617
+ "name": role_data["name"],
1618
+ "description": role_data["description"],
1619
+ "role_type": role_data["role_type"],
1620
+ "permissions": role_data["permissions"],
1621
+ "parent_roles": role_data["parent_roles"],
1622
+ "attributes": role_data["attributes"],
1623
+ "is_active": role_data["is_active"],
1624
+ "assigned_at": format_datetime(role_data["assigned_at"]),
1625
+ "assigned_by": role_data["assigned_by"],
1626
+ }
1627
+
1628
+ # Add permissions from this role
1629
+ direct_permissions = set(role_data["permissions"])
1630
+ all_permissions.update(direct_permissions)
1631
+
1632
+ if include_inherited:
1633
+ inherited_perms = self._get_inherited_permissions(
1634
+ role_data["role_id"], role_hierarchy
1635
+ )
1636
+ all_permissions.update(inherited_perms)
1637
+
1638
+ role_info["inherited_permissions"] = list(inherited_perms)
1639
+ role_info["all_permissions"] = list(
1640
+ direct_permissions | inherited_perms
1641
+ )
1642
+ role_info["permission_count"] = {
1643
+ "direct": len(direct_permissions),
1644
+ "inherited": len(inherited_perms),
1645
+ "total": len(direct_permissions | inherited_perms),
1646
+ }
1647
+
1648
+ user_roles.append(role_info)
1649
+
1650
+ return {
1651
+ "result": {
1652
+ "user_id": user_id,
1653
+ "roles": user_roles,
1654
+ "summary": {
1655
+ "role_count": len(user_roles),
1656
+ "active_roles": len([r for r in user_roles if r["is_active"]]),
1657
+ "total_permissions": len(all_permissions),
1658
+ "unique_permissions": list(all_permissions),
1659
+ },
1660
+ "options": {
1661
+ "include_inherited": include_inherited,
1662
+ "include_inactive": include_inactive,
1663
+ },
1664
+ "operation": "get_user_roles",
1665
+ "timestamp": datetime.now(UTC).isoformat(),
1666
+ }
1667
+ }
816
1668
 
817
1669
  def _get_role_users(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
818
1670
  """Get all users assigned to a role."""
819
- raise NotImplementedError("Get role users operation will be implemented")
1671
+ role_id = inputs["role_id"]
1672
+ tenant_id = inputs.get("tenant_id", "default")
1673
+ include_user_details = inputs.get("include_user_details", True)
1674
+ limit = inputs.get("limit", 100)
1675
+ offset = inputs.get("offset", 0)
1676
+
1677
+ # Validate role exists
1678
+ role_data = self._get_role_by_id(role_id, tenant_id)
1679
+ if not role_data:
1680
+ raise NodeValidationError(f"Role not found: {role_id}")
1681
+
1682
+ # Get user assignments
1683
+ if include_user_details:
1684
+ users_query = """
1685
+ SELECT ur.user_id, ur.assigned_at, ur.assigned_by,
1686
+ u.email, u.first_name, u.last_name, u.status, u.created_at as user_created_at
1687
+ FROM user_role_assignments ur
1688
+ LEFT JOIN users u ON ur.user_id = u.user_id AND ur.tenant_id = u.tenant_id
1689
+ WHERE ur.role_id = $1 AND ur.tenant_id = $2
1690
+ ORDER BY ur.assigned_at DESC
1691
+ LIMIT $3 OFFSET $4
1692
+ """
1693
+ else:
1694
+ users_query = """
1695
+ SELECT ur.user_id, ur.assigned_at, ur.assigned_by
1696
+ FROM user_role_assignments ur
1697
+ WHERE ur.role_id = $1 AND ur.tenant_id = $2
1698
+ ORDER BY ur.assigned_at DESC
1699
+ LIMIT $3 OFFSET $4
1700
+ """
1701
+
1702
+ result = self._db_node.run(
1703
+ query=users_query,
1704
+ parameters=[role_id, tenant_id, limit, offset],
1705
+ fetch_mode="all",
1706
+ )
1707
+ users_data = result.get("result", {}).get("data", [])
1708
+
1709
+ # Get total count
1710
+ count_query = """
1711
+ SELECT COUNT(*) as total
1712
+ FROM user_role_assignments
1713
+ WHERE role_id = $1 AND tenant_id = $2
1714
+ """
1715
+
1716
+ count_result = self._db_node.run(
1717
+ query=count_query, parameters=[role_id, tenant_id], fetch_mode="one"
1718
+ )
1719
+ total_count = count_result.get("data", [{}])[0].get("total", 0)
1720
+
1721
+ # Format user data
1722
+ assigned_users = []
1723
+ for user_data in users_data:
1724
+ user_info = {
1725
+ "user_id": user_data["user_id"],
1726
+ "assigned_at": (
1727
+ user_data["assigned_at"].isoformat()
1728
+ if user_data["assigned_at"]
1729
+ else None
1730
+ ),
1731
+ "assigned_by": user_data["assigned_by"],
1732
+ }
1733
+
1734
+ if include_user_details:
1735
+ user_info.update(
1736
+ {
1737
+ "email": user_data.get("email"),
1738
+ "first_name": user_data.get("first_name"),
1739
+ "last_name": user_data.get("last_name"),
1740
+ "status": user_data.get("status"),
1741
+ "user_created_at": (
1742
+ user_data["user_created_at"].isoformat()
1743
+ if user_data.get("user_created_at")
1744
+ else None
1745
+ ),
1746
+ }
1747
+ )
1748
+
1749
+ assigned_users.append(user_info)
1750
+
1751
+ return {
1752
+ "result": {
1753
+ "role": {
1754
+ "role_id": role_id,
1755
+ "name": role_data["name"],
1756
+ "description": role_data["description"],
1757
+ "is_active": role_data["is_active"],
1758
+ },
1759
+ "assigned_users": assigned_users,
1760
+ "pagination": {
1761
+ "total": total_count,
1762
+ "limit": limit,
1763
+ "offset": offset,
1764
+ "returned": len(assigned_users),
1765
+ },
1766
+ "options": {"include_user_details": include_user_details},
1767
+ "operation": "get_role_users",
1768
+ "timestamp": datetime.now(UTC).isoformat(),
1769
+ }
1770
+ }
820
1771
 
821
1772
  def _validate_hierarchy(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
822
1773
  """Validate entire role hierarchy for consistency."""
823
- raise NotImplementedError("Validate hierarchy operation will be implemented")
1774
+ tenant_id = inputs.get("tenant_id", "default")
1775
+ fix_issues = inputs.get("fix_issues", False)
1776
+
1777
+ # Build role hierarchy
1778
+ role_hierarchy = self._build_role_hierarchy(tenant_id)
1779
+
1780
+ validation_results = {
1781
+ "circular_dependencies": [],
1782
+ "missing_parents": [],
1783
+ "orphaned_child_references": [],
1784
+ "inactive_parent_references": [],
1785
+ "inconsistent_child_arrays": [],
1786
+ "total_roles": len(role_hierarchy),
1787
+ "issues_found": 0,
1788
+ }
1789
+
1790
+ # Check each role for issues
1791
+ for role_id, role_data in role_hierarchy.items():
1792
+ # Check for circular dependencies
1793
+ try:
1794
+ visited = set()
1795
+ self._check_circular_dependency_recursive(
1796
+ role_id, role_hierarchy, visited
1797
+ )
1798
+ except ValueError as e:
1799
+ validation_results["circular_dependencies"].append(
1800
+ {"role_id": role_id, "issue": str(e)}
1801
+ )
1802
+
1803
+ # Check for missing parent roles
1804
+ parent_roles = role_data.get("parent_roles", [])
1805
+ for parent_id in parent_roles:
1806
+ if parent_id not in role_hierarchy:
1807
+ validation_results["missing_parents"].append(
1808
+ {"role_id": role_id, "missing_parent": parent_id}
1809
+ )
1810
+ elif not role_hierarchy[parent_id].get("is_active", True):
1811
+ validation_results["inactive_parent_references"].append(
1812
+ {"role_id": role_id, "inactive_parent": parent_id}
1813
+ )
1814
+
1815
+ # Check child role consistency
1816
+ child_roles = role_data.get("child_roles", [])
1817
+ for child_id in child_roles:
1818
+ if child_id not in role_hierarchy:
1819
+ validation_results["orphaned_child_references"].append(
1820
+ {"role_id": role_id, "orphaned_child": child_id}
1821
+ )
1822
+ else:
1823
+ # Check if child actually has this role as parent
1824
+ child_data = role_hierarchy[child_id]
1825
+ child_parents = child_data.get("parent_roles", [])
1826
+ if role_id not in child_parents:
1827
+ validation_results["inconsistent_child_arrays"].append(
1828
+ {
1829
+ "parent_role": role_id,
1830
+ "child_role": child_id,
1831
+ "issue": "Child role does not reference parent",
1832
+ }
1833
+ )
1834
+
1835
+ # Count total issues
1836
+ total_issues = (
1837
+ len(validation_results["circular_dependencies"])
1838
+ + len(validation_results["missing_parents"])
1839
+ + len(validation_results["orphaned_child_references"])
1840
+ + len(validation_results["inactive_parent_references"])
1841
+ + len(validation_results["inconsistent_child_arrays"])
1842
+ )
1843
+
1844
+ validation_results["issues_found"] = total_issues
1845
+ validation_results["is_valid"] = total_issues == 0
1846
+
1847
+ # Fix issues if requested
1848
+ fixes_applied = []
1849
+ if fix_issues and total_issues > 0:
1850
+ # Fix orphaned child references
1851
+ for issue in validation_results["orphaned_child_references"]:
1852
+ try:
1853
+ self._remove_orphaned_child_reference(
1854
+ issue["role_id"], issue["orphaned_child"], tenant_id
1855
+ )
1856
+ fixes_applied.append(
1857
+ f"Removed orphaned child reference {issue['orphaned_child']} from {issue['role_id']}"
1858
+ )
1859
+ except Exception as e:
1860
+ fixes_applied.append(
1861
+ f"Failed to fix orphaned child reference: {str(e)}"
1862
+ )
1863
+
1864
+ # Fix inconsistent child arrays
1865
+ for issue in validation_results["inconsistent_child_arrays"]:
1866
+ try:
1867
+ self._sync_parent_child_relationship(
1868
+ issue["parent_role"], issue["child_role"], tenant_id
1869
+ )
1870
+ fixes_applied.append(
1871
+ f"Synced parent-child relationship: {issue['parent_role']} <-> {issue['child_role']}"
1872
+ )
1873
+ except Exception as e:
1874
+ fixes_applied.append(f"Failed to sync relationship: {str(e)}")
1875
+
1876
+ result = {
1877
+ "result": {
1878
+ "validation": validation_results,
1879
+ "operation": "validate_hierarchy",
1880
+ "timestamp": datetime.now(UTC).isoformat(),
1881
+ }
1882
+ }
1883
+
1884
+ if fix_issues:
1885
+ result["result"]["fixes_applied"] = fixes_applied
1886
+ result["result"]["fix_count"] = len(fixes_applied)
1887
+
1888
+ return result
1889
+
1890
+ def _check_circular_dependency_recursive(
1891
+ self, role_id: str, role_hierarchy: Dict[str, Dict], visited: Set[str]
1892
+ ):
1893
+ """Recursively check for circular dependencies."""
1894
+ if role_id in visited:
1895
+ raise ValueError(f"Circular dependency detected involving role: {role_id}")
1896
+
1897
+ visited.add(role_id)
1898
+
1899
+ if role_id in role_hierarchy:
1900
+ parent_roles = role_hierarchy[role_id].get("parent_roles", [])
1901
+ for parent_id in parent_roles:
1902
+ self._check_circular_dependency_recursive(
1903
+ parent_id, role_hierarchy, visited.copy()
1904
+ )
1905
+
1906
+ def _remove_orphaned_child_reference(
1907
+ self, parent_role_id: str, orphaned_child_id: str, tenant_id: str
1908
+ ):
1909
+ """Remove orphaned child reference from parent role."""
1910
+ update_query = """
1911
+ UPDATE roles
1912
+ SET child_roles = (
1913
+ SELECT COALESCE(
1914
+ json_agg(value),
1915
+ '[]'::json
1916
+ )
1917
+ FROM jsonb_array_elements_text(child_roles) AS value
1918
+ WHERE value != $1
1919
+ )::jsonb,
1920
+ updated_at = $2
1921
+ WHERE role_id = $3 AND tenant_id = $4
1922
+ """
1923
+
1924
+ self._db_node.run(
1925
+ query=update_query,
1926
+ parameters=[
1927
+ orphaned_child_id,
1928
+ datetime.now(UTC),
1929
+ parent_role_id,
1930
+ tenant_id,
1931
+ ],
1932
+ )
1933
+
1934
+ def _sync_parent_child_relationship(
1935
+ self, parent_role_id: str, child_role_id: str, tenant_id: str
1936
+ ):
1937
+ """Ensure parent-child relationship is consistent in both directions."""
1938
+ # Add child to parent's child_roles if not present
1939
+ add_child_query = """
1940
+ UPDATE roles
1941
+ SET child_roles = (
1942
+ CASE
1943
+ WHEN child_roles ? $1 THEN child_roles
1944
+ ELSE child_roles || jsonb_build_array($1)
1945
+ END
1946
+ ),
1947
+ updated_at = $2
1948
+ WHERE role_id = $3 AND tenant_id = $4
1949
+ AND NOT ($1 = ANY(
1950
+ SELECT jsonb_array_elements_text(child_roles)
1951
+ ))
1952
+ """
1953
+
1954
+ self._db_node.run(
1955
+ query=add_child_query,
1956
+ parameters=[child_role_id, datetime.now(UTC), parent_role_id, tenant_id],
1957
+ )