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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (74) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control/__init__.py +1 -1
  3. kailash/client/__init__.py +12 -0
  4. kailash/client/enhanced_client.py +306 -0
  5. kailash/core/actors/__init__.py +16 -0
  6. kailash/core/actors/adaptive_pool_controller.py +630 -0
  7. kailash/core/actors/connection_actor.py +566 -0
  8. kailash/core/actors/supervisor.py +364 -0
  9. kailash/core/ml/__init__.py +1 -0
  10. kailash/core/ml/query_patterns.py +544 -0
  11. kailash/core/monitoring/__init__.py +19 -0
  12. kailash/core/monitoring/connection_metrics.py +488 -0
  13. kailash/core/optimization/__init__.py +1 -0
  14. kailash/core/resilience/__init__.py +17 -0
  15. kailash/core/resilience/circuit_breaker.py +382 -0
  16. kailash/edge/__init__.py +16 -0
  17. kailash/edge/compliance.py +834 -0
  18. kailash/edge/discovery.py +659 -0
  19. kailash/edge/location.py +582 -0
  20. kailash/gateway/__init__.py +33 -0
  21. kailash/gateway/api.py +289 -0
  22. kailash/gateway/enhanced_gateway.py +357 -0
  23. kailash/gateway/resource_resolver.py +217 -0
  24. kailash/gateway/security.py +227 -0
  25. kailash/middleware/auth/access_control.py +6 -6
  26. kailash/middleware/auth/models.py +2 -2
  27. kailash/middleware/communication/ai_chat.py +7 -7
  28. kailash/middleware/communication/api_gateway.py +5 -15
  29. kailash/middleware/database/base_models.py +1 -7
  30. kailash/middleware/gateway/__init__.py +22 -0
  31. kailash/middleware/gateway/checkpoint_manager.py +398 -0
  32. kailash/middleware/gateway/deduplicator.py +382 -0
  33. kailash/middleware/gateway/durable_gateway.py +417 -0
  34. kailash/middleware/gateway/durable_request.py +498 -0
  35. kailash/middleware/gateway/event_store.py +499 -0
  36. kailash/middleware/mcp/enhanced_server.py +2 -2
  37. kailash/nodes/admin/permission_check.py +817 -33
  38. kailash/nodes/admin/role_management.py +1242 -108
  39. kailash/nodes/admin/schema_manager.py +438 -0
  40. kailash/nodes/admin/user_management.py +1124 -1582
  41. kailash/nodes/code/__init__.py +8 -1
  42. kailash/nodes/code/async_python.py +1035 -0
  43. kailash/nodes/code/python.py +1 -0
  44. kailash/nodes/data/async_sql.py +9 -3
  45. kailash/nodes/data/query_pipeline.py +641 -0
  46. kailash/nodes/data/query_router.py +895 -0
  47. kailash/nodes/data/sql.py +20 -11
  48. kailash/nodes/data/workflow_connection_pool.py +1071 -0
  49. kailash/nodes/monitoring/__init__.py +3 -5
  50. kailash/nodes/monitoring/connection_dashboard.py +822 -0
  51. kailash/nodes/rag/__init__.py +2 -7
  52. kailash/resources/__init__.py +40 -0
  53. kailash/resources/factory.py +533 -0
  54. kailash/resources/health.py +319 -0
  55. kailash/resources/reference.py +288 -0
  56. kailash/resources/registry.py +392 -0
  57. kailash/runtime/async_local.py +711 -302
  58. kailash/testing/__init__.py +34 -0
  59. kailash/testing/async_test_case.py +353 -0
  60. kailash/testing/async_utils.py +345 -0
  61. kailash/testing/fixtures.py +458 -0
  62. kailash/testing/mock_registry.py +495 -0
  63. kailash/workflow/__init__.py +8 -0
  64. kailash/workflow/async_builder.py +621 -0
  65. kailash/workflow/async_patterns.py +766 -0
  66. kailash/workflow/cyclic_runner.py +107 -16
  67. kailash/workflow/graph.py +7 -2
  68. kailash/workflow/resilience.py +11 -1
  69. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/METADATA +19 -4
  70. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/RECORD +74 -28
  71. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/WHEEL +0 -0
  72. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/entry_points.txt +0 -0
  73. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/licenses/LICENSE +0 -0
  74. {kailash-0.5.0.dist-info → kailash-0.6.1.dist-info}/top_level.txt +0 -0
@@ -18,6 +18,7 @@ Features:
18
18
 
19
19
  import hashlib
20
20
  import json
21
+ import logging
21
22
  from dataclasses import dataclass
22
23
  from datetime import UTC, datetime, timedelta
23
24
  from enum import Enum
@@ -40,9 +41,11 @@ from kailash.access_control_abac import (
40
41
  LogicalOperator,
41
42
  )
42
43
  from kailash.nodes.base import Node, NodeParameter, register_node
43
- from kailash.nodes.data import AsyncSQLDatabaseNode
44
+ from kailash.nodes.data import SQLDatabaseNode
44
45
  from kailash.sdk_exceptions import NodeExecutionError, NodeValidationError
45
46
 
47
+ from .schema_manager import AdminSchemaManager
48
+
46
49
 
47
50
  class PermissionCheckOperation(Enum):
48
51
  """Supported permission check operations."""
@@ -195,6 +198,8 @@ class PermissionCheckNode(Node):
195
198
  self._attribute_evaluator = None
196
199
  self._permission_cache = {}
197
200
  self._cache_timestamps = {}
201
+ self._schema_manager = None
202
+ self.logger = logging.getLogger(__name__)
198
203
 
199
204
  def get_parameters(self) -> Dict[str, NodeParameter]:
200
205
  """Define parameters for permission checking operations."""
@@ -286,6 +291,13 @@ class PermissionCheckNode(Node):
286
291
  default=True,
287
292
  description="Include timing information in results",
288
293
  ),
294
+ NodeParameter(
295
+ name="audit",
296
+ type=bool,
297
+ required=False,
298
+ default=False,
299
+ description="Enable audit logging for this operation",
300
+ ),
289
301
  # Multi-tenancy
290
302
  NodeParameter(
291
303
  name="tenant_id",
@@ -308,6 +320,56 @@ class PermissionCheckNode(Node):
308
320
  default=True,
309
321
  description="Enable strict validation of inputs",
310
322
  ),
323
+ # Additional validation options
324
+ NodeParameter(
325
+ name="conditions",
326
+ type=list,
327
+ required=False,
328
+ description="List of ABAC conditions to validate",
329
+ ),
330
+ NodeParameter(
331
+ name="validate_syntax",
332
+ type=bool,
333
+ required=False,
334
+ default=True,
335
+ description="Validate condition syntax",
336
+ ),
337
+ NodeParameter(
338
+ name="test_evaluation",
339
+ type=bool,
340
+ required=False,
341
+ default=True,
342
+ description="Test condition evaluation with provided context",
343
+ ),
344
+ NodeParameter(
345
+ name="permission_type",
346
+ type=str,
347
+ required=False,
348
+ default="all",
349
+ choices=["all", "direct", "inherited"],
350
+ description="Type of permissions to return",
351
+ ),
352
+ NodeParameter(
353
+ name="check_inheritance",
354
+ type=bool,
355
+ required=False,
356
+ default=True,
357
+ description="Check hierarchical resource inheritance",
358
+ ),
359
+ NodeParameter(
360
+ name="include_rules",
361
+ type=bool,
362
+ required=False,
363
+ default=True,
364
+ description="Include rule details in explanation",
365
+ ),
366
+ NodeParameter(
367
+ name="include_hierarchy",
368
+ type=bool,
369
+ required=False,
370
+ default=True,
371
+ description="Include role hierarchy breakdown",
372
+ ),
311
373
  ]
312
374
  }
313
375
 
@@ -348,6 +410,10 @@ class PermissionCheckNode(Node):
348
410
 
349
411
  def _init_dependencies(self, inputs: Dict[str, Any]):
350
412
  """Initialize database and access manager dependencies."""
413
+ # Skip initialization if already initialized (for testing)
414
+ if hasattr(self, "_db_node") and self._db_node is not None:
415
+ return
416
+
351
417
  # Get database config
352
418
  db_config = inputs.get(
353
419
  "database_config",
@@ -361,8 +427,22 @@ class PermissionCheckNode(Node):
361
427
  },
362
428
  )
363
429
 
364
- # Initialize async database node
365
- self._db_node = AsyncSQLDatabaseNode(name="permission_check_db", **db_config)
430
+ # Initialize database node
431
+ self._db_node = SQLDatabaseNode(name="permission_check_db", **db_config)
432
+
433
+ # Initialize schema manager and ensure schema exists
434
+ if not self._schema_manager:
435
+ self._schema_manager = AdminSchemaManager(db_config)
436
+
437
+ # Validate schema exists, create if needed
438
+ try:
439
+ validation = self._schema_manager.validate_schema()
440
+ if not validation["is_valid"]:
441
+ self.logger.info("Creating unified admin schema...")
442
+ self._schema_manager.create_full_schema(drop_existing=False)
443
+ self.logger.info("Unified admin schema created successfully")
444
+ except Exception as e:
445
+ self.logger.warning(f"Schema validation/creation failed: {e}")
366
446
 
367
447
  # Initialize enhanced access manager
368
448
  self._access_manager = AccessControlManager(strategy="abac")
@@ -379,6 +459,7 @@ class PermissionCheckNode(Node):
379
459
  cache_ttl = inputs.get("cache_ttl", 300)
380
460
  explain = inputs.get("explain", False)
381
461
  include_timing = inputs.get("include_timing", True)
462
+ audit = inputs.get("audit", False)
382
463
 
383
464
  start_time = datetime.now(UTC) if include_timing else None
384
465
 
@@ -458,6 +539,24 @@ class PermissionCheckNode(Node):
458
539
  cache_data["explanation"] = explanation.to_dict()
459
540
  self._set_cache(cache_key, cache_data, cache_ttl)
460
541
 
542
+ # Log to audit trail if enabled
543
+ if audit:
544
+ self._create_audit_log(
545
+ user_id=user_id,
546
+ action="permission_check",
547
+ resource_type="resource",
548
+ resource_id=resource_id,
549
+ details={
550
+ "permission": permission,
551
+ "allowed": allowed,
552
+ "context": context,
553
+ "rbac_result": rbac_result,
554
+ "abac_result": abac_result,
555
+ },
556
+ success=allowed,
557
+ tenant_id=tenant_id,
558
+ )
559
+
461
560
  result = {
462
561
  "result": {
463
562
  "check": check_result.to_dict(),
@@ -623,30 +722,36 @@ class PermissionCheckNode(Node):
623
722
 
624
723
  def _get_user_context(self, user_id: str, tenant_id: str) -> Optional[UserContext]:
625
724
  """Get user context for permission evaluation."""
626
- # Query user data from database
725
+ # Query user data from unified admin schema
627
726
  query = """
628
727
  SELECT user_id, email, roles, attributes, status, tenant_id
629
728
  FROM users
630
729
  WHERE user_id = $1 AND tenant_id = $2 AND status = 'active'
631
730
  """
632
731
 
633
- self._db_node.config.update(
634
- {"query": query, "params": [user_id, tenant_id], "fetch_mode": "one"}
635
- )
732
+ try:
733
+ result = self._db_node.run(
734
+ query=query, parameters=[user_id, tenant_id], result_format="dict"
735
+ )
636
736
 
637
- result = self._db_node.run()
638
- user_data = result.get("result", {}).get("data")
737
+ # Handle the corrected result structure
738
+ user_rows = result.get("data", [])
739
+ if not user_rows:
740
+ return None
639
741
 
640
- if not user_data:
641
- return None
742
+ user_data = user_rows[0]
642
743
 
643
- return UserContext(
644
- user_id=user_data["user_id"],
645
- tenant_id=user_data["tenant_id"],
646
- email=user_data["email"],
647
- roles=user_data.get("roles", []),
648
- attributes=user_data.get("attributes", {}),
649
- )
744
+ return UserContext(
745
+ user_id=user_data["user_id"],
746
+ tenant_id=user_data["tenant_id"],
747
+ email=user_data["email"],
748
+ roles=user_data.get("roles", []),
749
+ attributes=user_data.get("attributes", {}),
750
+ )
751
+ except Exception as e:
752
+ # Log the error and return None to indicate user not found
753
+ self.logger.warning(f"Failed to get user context for {user_id}: {e}")
754
+ return None
650
755
 
651
756
  def _check_rbac_permission(
652
757
  self, user_context: UserContext, resource_id: str, permission: str
@@ -687,9 +792,9 @@ class PermissionCheckNode(Node):
687
792
  # Convert permission string to NodePermission if possible
688
793
  node_permission = NodePermission.EXECUTE # Default
689
794
  if permission.lower() in ["read", "view"]:
690
- node_permission = NodePermission.VIEW
795
+ node_permission = NodePermission.READ_OUTPUT # Map view to read_output
691
796
  elif permission.lower() in ["write", "edit", "update"]:
692
- node_permission = NodePermission.EDIT
797
+ node_permission = NodePermission.WRITE_INPUT # Map edit to write_input
693
798
  elif permission.lower() in ["execute", "run"]:
694
799
  node_permission = NodePermission.EXECUTE
695
800
  elif permission.lower() in ["delete", "remove"]:
@@ -733,19 +838,25 @@ class PermissionCheckNode(Node):
733
838
 
734
839
  SELECT r.role_id, r.permissions, r.parent_roles
735
840
  FROM roles r
736
- JOIN role_hierarchy rh ON r.role_id = ANY(rh.parent_roles)
841
+ JOIN role_hierarchy rh ON r.role_id = ANY(
842
+ SELECT jsonb_array_elements_text(rh.parent_roles)
843
+ )
737
844
  WHERE r.tenant_id = $2 AND r.is_active = true
738
845
  )
739
- SELECT DISTINCT unnest(permissions) as permission
846
+ SELECT DISTINCT unnest(
847
+ CASE
848
+ WHEN jsonb_typeof(permissions) = 'array'
849
+ THEN ARRAY(SELECT jsonb_array_elements_text(permissions))
850
+ ELSE ARRAY[]::text[]
851
+ END
852
+ ) as permission
740
853
  FROM role_hierarchy
741
854
  """
742
855
 
743
- self._db_node.config.update(
744
- {"query": query, "params": [role_id, tenant_id], "fetch_mode": "all"}
856
+ result = self._db_node.run(
857
+ query=query, parameters=[role_id, tenant_id], result_format="dict"
745
858
  )
746
-
747
- result = self._db_node.run()
748
- permission_rows = result.get("result", {}).get("data", [])
859
+ permission_rows = result.get("data", [])
749
860
 
750
861
  return {row["permission"] for row in permission_rows}
751
862
 
@@ -841,24 +952,697 @@ class PermissionCheckNode(Node):
841
952
  # Additional operations would follow similar patterns
842
953
  def _check_node_access(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
843
954
  """Check access to a specific node type."""
844
- raise NotImplementedError("Check node access operation will be implemented")
955
+ user_id = inputs["user_id"]
956
+ node_type = inputs["resource_id"] # node_type is passed as resource_id
957
+ permission = inputs.get("permission", "execute") # Default to execute
958
+ tenant_id = inputs.get("tenant_id", "default")
959
+ context = inputs.get("context", {})
960
+
961
+ # Get user context
962
+ user_context = self._get_user_context(user_id, tenant_id)
963
+ if not user_context:
964
+ raise NodeValidationError(f"User not found: {user_id}")
965
+
966
+ # Map permission string to NodePermission enum
967
+ permission_mapping = {
968
+ "view": NodePermission.READ_OUTPUT, # Map view to read_output
969
+ "edit": NodePermission.WRITE_INPUT, # Map edit to write_input
970
+ "execute": NodePermission.EXECUTE,
971
+ "delete": NodePermission.SKIP, # Map delete to skip (no direct delete permission)
972
+ }
973
+
974
+ node_permission = permission_mapping.get(
975
+ permission.lower(), NodePermission.EXECUTE
976
+ )
977
+
978
+ start_time = datetime.now(UTC)
979
+
980
+ # Use the access control manager for node access check
981
+ try:
982
+ decision = self._access_manager.check_node_access(
983
+ user=user_context,
984
+ resource_id=node_type,
985
+ permission=node_permission,
986
+ context=context,
987
+ )
988
+
989
+ evaluation_time_ms = (datetime.now(UTC) - start_time).total_seconds() * 1000
990
+
991
+ return {
992
+ "result": {
993
+ "access_check": {
994
+ "allowed": decision.allowed,
995
+ "reason": decision.reason,
996
+ "user_id": user_id,
997
+ "node_type": node_type,
998
+ "permission": permission,
999
+ "evaluation_time_ms": evaluation_time_ms,
1000
+ "decision_id": decision.decision_id,
1001
+ },
1002
+ "operation": "check_node_access",
1003
+ "timestamp": datetime.now(UTC).isoformat(),
1004
+ }
1005
+ }
1006
+
1007
+ except Exception as e:
1008
+ return {
1009
+ "result": {
1010
+ "access_check": {
1011
+ "allowed": False,
1012
+ "reason": f"Access check failed: {str(e)}",
1013
+ "user_id": user_id,
1014
+ "node_type": node_type,
1015
+ "permission": permission,
1016
+ "evaluation_time_ms": (
1017
+ datetime.now(UTC) - start_time
1018
+ ).total_seconds()
1019
+ * 1000,
1020
+ "error": True,
1021
+ },
1022
+ "operation": "check_node_access",
1023
+ "timestamp": datetime.now(UTC).isoformat(),
1024
+ }
1025
+ }
845
1026
 
846
1027
  def _check_workflow_access(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
847
1028
  """Check access to workflow operations."""
848
- raise NotImplementedError("Check workflow access operation will be implemented")
1029
+ user_id = inputs["user_id"]
1030
+ workflow_id = inputs["resource_id"] # workflow_id is passed as resource_id
1031
+ permission = inputs.get("permission", "execute") # execute, view, edit, delete
1032
+ tenant_id = inputs.get("tenant_id", "default")
1033
+ context = inputs.get("context", {})
1034
+
1035
+ # Get user context
1036
+ user_context = self._get_user_context(user_id, tenant_id)
1037
+ if not user_context:
1038
+ raise NodeValidationError(f"User not found: {user_id}")
1039
+
1040
+ # Map permission string to WorkflowPermission enum
1041
+ permission_mapping = {
1042
+ "view": WorkflowPermission.VIEW,
1043
+ "execute": WorkflowPermission.EXECUTE,
1044
+ "edit": WorkflowPermission.MODIFY, # EDIT mapped to MODIFY
1045
+ "delete": WorkflowPermission.DELETE,
1046
+ "deploy": WorkflowPermission.DEPLOY,
1047
+ "share": WorkflowPermission.SHARE,
1048
+ }
1049
+
1050
+ workflow_permission = permission_mapping.get(
1051
+ permission.lower(), WorkflowPermission.EXECUTE
1052
+ )
1053
+
1054
+ start_time = datetime.now(UTC)
1055
+
1056
+ # Use the access control manager for workflow access check
1057
+ try:
1058
+ decision = self._access_manager.check_workflow_access(
1059
+ user=user_context,
1060
+ workflow_id=workflow_id,
1061
+ permission=workflow_permission,
1062
+ context=context,
1063
+ )
1064
+
1065
+ evaluation_time_ms = (datetime.now(UTC) - start_time).total_seconds() * 1000
1066
+
1067
+ return {
1068
+ "result": {
1069
+ "access_check": {
1070
+ "allowed": decision.allowed,
1071
+ "reason": decision.reason,
1072
+ "user_id": user_id,
1073
+ "workflow_id": workflow_id,
1074
+ "permission": permission,
1075
+ "evaluation_time_ms": evaluation_time_ms,
1076
+ "decision_id": decision.decision_id,
1077
+ "applied_rules": getattr(decision, "applied_rules", []),
1078
+ },
1079
+ "operation": "check_workflow_access",
1080
+ "timestamp": datetime.now(UTC).isoformat(),
1081
+ }
1082
+ }
1083
+
1084
+ except Exception as e:
1085
+ return {
1086
+ "result": {
1087
+ "access_check": {
1088
+ "allowed": False,
1089
+ "reason": f"Workflow access check failed: {str(e)}",
1090
+ "user_id": user_id,
1091
+ "workflow_id": workflow_id,
1092
+ "permission": permission,
1093
+ "evaluation_time_ms": (
1094
+ datetime.now(UTC) - start_time
1095
+ ).total_seconds()
1096
+ * 1000,
1097
+ "error": True,
1098
+ },
1099
+ "operation": "check_workflow_access",
1100
+ "timestamp": datetime.now(UTC).isoformat(),
1101
+ }
1102
+ }
849
1103
 
850
1104
  def _get_user_permissions(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
851
1105
  """Get all permissions for a user."""
852
- raise NotImplementedError("Get user permissions operation will be implemented")
1106
+ user_id = inputs["user_id"]
1107
+ tenant_id = inputs.get("tenant_id", "default")
1108
+ include_inherited = inputs.get("include_inherited", True)
1109
+ permission_type = inputs.get("permission_type", "all") # all, direct, inherited
1110
+
1111
+ # Get user context
1112
+ user_context = self._get_user_context(user_id, tenant_id)
1113
+ if not user_context:
1114
+ raise NodeValidationError(f"User not found: {user_id}")
1115
+
1116
+ start_time = datetime.now(UTC)
1117
+
1118
+ # Get all effective permissions
1119
+ all_permissions = self._get_user_effective_permissions(user_context)
1120
+
1121
+ # Get direct permissions from roles
1122
+ direct_permissions = set()
1123
+ role_permissions_breakdown = {}
1124
+
1125
+ for role in user_context.roles:
1126
+ role_perms = self._get_role_direct_permissions(role, tenant_id)
1127
+ direct_permissions.update(role_perms)
1128
+ role_permissions_breakdown[role] = list(role_perms)
1129
+
1130
+ # Calculate inherited permissions
1131
+ inherited_permissions = (
1132
+ all_permissions - direct_permissions if include_inherited else set()
1133
+ )
1134
+
1135
+ # Filter based on permission_type
1136
+ result_permissions = set()
1137
+ if permission_type == "all":
1138
+ result_permissions = all_permissions
1139
+ elif permission_type == "direct":
1140
+ result_permissions = direct_permissions
1141
+ elif permission_type == "inherited":
1142
+ result_permissions = inherited_permissions
1143
+
1144
+ # Categorize permissions by resource type
1145
+ categorized_permissions = {
1146
+ "workflow": [],
1147
+ "node": [],
1148
+ "resource": [],
1149
+ "admin": [],
1150
+ "other": [],
1151
+ }
1152
+
1153
+ for perm in result_permissions:
1154
+ if perm.startswith("workflow:"):
1155
+ categorized_permissions["workflow"].append(perm)
1156
+ elif perm.startswith("node:"):
1157
+ categorized_permissions["node"].append(perm)
1158
+ elif perm.startswith("resource:"):
1159
+ categorized_permissions["resource"].append(perm)
1160
+ elif perm.startswith("admin:"):
1161
+ categorized_permissions["admin"].append(perm)
1162
+ else:
1163
+ categorized_permissions["other"].append(perm)
1164
+
1165
+ evaluation_time_ms = (datetime.now(UTC) - start_time).total_seconds() * 1000
1166
+
1167
+ return {
1168
+ "result": {
1169
+ "user_permissions": {
1170
+ "user_id": user_id,
1171
+ "permissions": list(result_permissions),
1172
+ "categorized_permissions": categorized_permissions,
1173
+ "role_breakdown": role_permissions_breakdown,
1174
+ "permission_counts": {
1175
+ "total": len(all_permissions),
1176
+ "direct": len(direct_permissions),
1177
+ "inherited": len(inherited_permissions),
1178
+ "returned": len(result_permissions),
1179
+ },
1180
+ "evaluation_time_ms": evaluation_time_ms,
1181
+ },
1182
+ "options": {
1183
+ "include_inherited": include_inherited,
1184
+ "permission_type": permission_type,
1185
+ },
1186
+ "operation": "get_user_permissions",
1187
+ "timestamp": datetime.now(UTC).isoformat(),
1188
+ }
1189
+ }
1190
+
1191
+ def _get_role_direct_permissions(self, role_id: str, tenant_id: str) -> Set[str]:
1192
+ """Get direct permissions for a role (no inheritance)."""
1193
+ query = """
1194
+ SELECT permissions
1195
+ FROM roles
1196
+ WHERE role_id = $1 AND tenant_id = $2 AND is_active = true
1197
+ """
1198
+
1199
+ result = self._db_node.run(
1200
+ query=query, parameters=[role_id, tenant_id], result_format="dict"
1201
+ )
1202
+ role_rows = result.get("data", [])
1203
+ role_data = role_rows[0] if role_rows else None
1204
+
1205
+ if not role_data:
1206
+ return set()
1207
+
1208
+ return set(role_data.get("permissions", []))
853
1209
 
854
1210
  def _explain_permission(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
855
1211
  """Provide detailed explanation of permission logic."""
856
- raise NotImplementedError("Explain permission operation will be implemented")
1212
+ user_id = inputs["user_id"]
1213
+ resource_id = inputs["resource_id"]
1214
+ permission = inputs["permission"]
1215
+ tenant_id = inputs.get("tenant_id", "default")
1216
+ context = inputs.get("context", {})
1217
+ include_rules = inputs.get("include_rules", True)
1218
+ include_hierarchy = inputs.get("include_hierarchy", True)
1219
+
1220
+ # Get user context
1221
+ user_context = self._get_user_context(user_id, tenant_id)
1222
+ if not user_context:
1223
+ raise NodeValidationError(f"User not found: {user_id}")
1224
+
1225
+ start_time = datetime.now(UTC)
1226
+
1227
+ # Perform detailed permission check with explanation
1228
+ rbac_result = self._check_rbac_permission(user_context, resource_id, permission)
1229
+ abac_result = self._check_abac_permission(
1230
+ user_context, resource_id, permission, context
1231
+ )
1232
+ final_result = rbac_result and abac_result
1233
+
1234
+ # Build comprehensive explanation
1235
+ explanation = {
1236
+ "permission_granted": final_result,
1237
+ "evaluation_steps": [],
1238
+ "rbac_analysis": {},
1239
+ "abac_analysis": {},
1240
+ "decision_factors": [],
1241
+ }
1242
+
1243
+ # RBAC Analysis
1244
+ user_permissions = self._get_user_effective_permissions(user_context)
1245
+ required_permission = f"{resource_id}:{permission}"
1246
+ wildcard_resource = f"{resource_id}:*"
1247
+ wildcard_permission = f"*:{permission}"
1248
+ global_wildcard = "*:*"
1249
+
1250
+ rbac_matches = []
1251
+ if required_permission in user_permissions:
1252
+ rbac_matches.append({"type": "exact", "permission": required_permission})
1253
+ if wildcard_resource in user_permissions:
1254
+ rbac_matches.append(
1255
+ {"type": "resource_wildcard", "permission": wildcard_resource}
1256
+ )
1257
+ if wildcard_permission in user_permissions:
1258
+ rbac_matches.append(
1259
+ {"type": "permission_wildcard", "permission": wildcard_permission}
1260
+ )
1261
+ if global_wildcard in user_permissions:
1262
+ rbac_matches.append(
1263
+ {"type": "global_wildcard", "permission": global_wildcard}
1264
+ )
1265
+
1266
+ explanation["rbac_analysis"] = {
1267
+ "result": rbac_result,
1268
+ "required_permission": required_permission,
1269
+ "matching_permissions": rbac_matches,
1270
+ "user_roles": user_context.roles,
1271
+ "total_permissions": len(user_permissions),
1272
+ }
1273
+
1274
+ # ABAC Analysis
1275
+ explanation["abac_analysis"] = {
1276
+ "result": abac_result,
1277
+ "context_provided": context,
1278
+ "user_attributes": user_context.attributes,
1279
+ "evaluation_method": "access_control_manager",
1280
+ }
1281
+
1282
+ # Role hierarchy breakdown if requested
1283
+ if include_hierarchy:
1284
+ role_hierarchy_breakdown = {}
1285
+ for role in user_context.roles:
1286
+ direct_perms = self._get_role_direct_permissions(role, tenant_id)
1287
+ inherited_perms = (
1288
+ self._get_role_permissions(role, tenant_id) - direct_perms
1289
+ )
1290
+
1291
+ role_hierarchy_breakdown[role] = {
1292
+ "direct_permissions": list(direct_perms),
1293
+ "inherited_permissions": list(inherited_perms),
1294
+ "has_required_permission": required_permission
1295
+ in (direct_perms | inherited_perms),
1296
+ }
1297
+
1298
+ explanation["role_hierarchy"] = role_hierarchy_breakdown
1299
+
1300
+ # Decision factors
1301
+ if rbac_result:
1302
+ explanation["decision_factors"].append(
1303
+ "RBAC: User has required permission through role assignment"
1304
+ )
1305
+ else:
1306
+ explanation["decision_factors"].append(
1307
+ "RBAC: User lacks required permission"
1308
+ )
1309
+
1310
+ if context:
1311
+ if abac_result:
1312
+ explanation["decision_factors"].append(
1313
+ "ABAC: Context attributes satisfy policy conditions"
1314
+ )
1315
+ else:
1316
+ explanation["decision_factors"].append(
1317
+ "ABAC: Context attributes do not satisfy policy conditions"
1318
+ )
1319
+ else:
1320
+ explanation["decision_factors"].append(
1321
+ "ABAC: No context provided, defaulting to allow"
1322
+ )
1323
+
1324
+ explanation["decision_factors"].append(
1325
+ f"Final Decision: {'ALLOW' if final_result else 'DENY'}"
1326
+ )
1327
+
1328
+ # Evaluation steps
1329
+ explanation["evaluation_steps"] = [
1330
+ f"1. Retrieved user context for {user_id}",
1331
+ f"2. Evaluated RBAC permissions: {'PASS' if rbac_result else 'FAIL'}",
1332
+ f"3. Evaluated ABAC conditions: {'PASS' if abac_result else 'FAIL'}",
1333
+ f"4. Combined results: {'ALLOW' if final_result else 'DENY'}",
1334
+ ]
1335
+
1336
+ evaluation_time_ms = (datetime.now(UTC) - start_time).total_seconds() * 1000
1337
+
1338
+ return {
1339
+ "result": {
1340
+ "explanation": explanation,
1341
+ "summary": {
1342
+ "permission_granted": final_result,
1343
+ "user_id": user_id,
1344
+ "resource_id": resource_id,
1345
+ "permission": permission,
1346
+ "evaluation_time_ms": evaluation_time_ms,
1347
+ },
1348
+ "operation": "explain_permission",
1349
+ "timestamp": datetime.now(UTC).isoformat(),
1350
+ }
1351
+ }
857
1352
 
858
1353
  def _validate_conditions(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
859
1354
  """Validate ABAC conditions and rules."""
860
- raise NotImplementedError("Validate conditions operation will be implemented")
1355
+ conditions = inputs.get("conditions", [])
1356
+ context = inputs.get("context", {})
1357
+ user_id = inputs.get("user_id")
1358
+ tenant_id = inputs.get("tenant_id", "default")
1359
+ validate_syntax = inputs.get("validate_syntax", True)
1360
+ test_evaluation = inputs.get("test_evaluation", True)
1361
+
1362
+ start_time = datetime.now(UTC)
1363
+ validation_results = {
1364
+ "valid_conditions": [],
1365
+ "invalid_conditions": [],
1366
+ "syntax_errors": [],
1367
+ "evaluation_errors": [],
1368
+ "total_conditions": len(conditions),
1369
+ "valid_count": 0,
1370
+ "invalid_count": 0,
1371
+ }
1372
+
1373
+ # Get user context for testing if user_id provided
1374
+ user_context = None
1375
+ if user_id:
1376
+ user_context = self._get_user_context(user_id, tenant_id)
1377
+
1378
+ for i, condition in enumerate(conditions):
1379
+ condition_result = {
1380
+ "index": i,
1381
+ "condition": condition,
1382
+ "valid": True,
1383
+ "errors": [],
1384
+ }
1385
+
1386
+ # Validate syntax if requested
1387
+ if validate_syntax:
1388
+ try:
1389
+ # Basic structure validation
1390
+ if not isinstance(condition, dict):
1391
+ condition_result["errors"].append(
1392
+ "Condition must be a dictionary"
1393
+ )
1394
+ condition_result["valid"] = False
1395
+ else:
1396
+ required_fields = ["attribute", "operator", "value"]
1397
+ for field in required_fields:
1398
+ if field not in condition:
1399
+ condition_result["errors"].append(
1400
+ f"Missing required field: {field}"
1401
+ )
1402
+ condition_result["valid"] = False
1403
+
1404
+ # Validate operator
1405
+ valid_operators = [
1406
+ "eq",
1407
+ "ne",
1408
+ "lt",
1409
+ "le",
1410
+ "gt",
1411
+ "ge",
1412
+ "in",
1413
+ "not_in",
1414
+ "contains",
1415
+ "regex",
1416
+ ]
1417
+ if (
1418
+ "operator" in condition
1419
+ and condition["operator"] not in valid_operators
1420
+ ):
1421
+ condition_result["errors"].append(
1422
+ f"Invalid operator: {condition['operator']}. Valid operators: {valid_operators}"
1423
+ )
1424
+ condition_result["valid"] = False
1425
+
1426
+ except Exception as e:
1427
+ condition_result["errors"].append(
1428
+ f"Syntax validation error: {str(e)}"
1429
+ )
1430
+ condition_result["valid"] = False
1431
+
1432
+ # Test evaluation if requested and syntax is valid
1433
+ if test_evaluation and condition_result["valid"] and context:
1434
+ try:
1435
+ # Create an AttributeCondition for testing
1436
+ attr_condition = AttributeCondition(
1437
+ attribute=condition["attribute"],
1438
+ operator=AttributeOperator(condition["operator"]),
1439
+ value=condition["value"],
1440
+ )
1441
+
1442
+ # Test evaluation with provided context
1443
+ test_context = context.copy()
1444
+ if user_context:
1445
+ test_context.update(user_context.attributes)
1446
+
1447
+ # Use the attribute evaluator to test
1448
+ result = self._attribute_evaluator.evaluate_condition(
1449
+ attr_condition, test_context
1450
+ )
1451
+ condition_result["evaluation_result"] = result
1452
+ condition_result["test_context"] = test_context
1453
+
1454
+ except Exception as e:
1455
+ condition_result["errors"].append(f"Evaluation error: {str(e)}")
1456
+ condition_result["valid"] = False
1457
+ validation_results["evaluation_errors"].append(
1458
+ {"condition_index": i, "error": str(e)}
1459
+ )
1460
+
1461
+ # Categorize result
1462
+ if condition_result["valid"]:
1463
+ validation_results["valid_conditions"].append(condition_result)
1464
+ validation_results["valid_count"] += 1
1465
+ else:
1466
+ validation_results["invalid_conditions"].append(condition_result)
1467
+ validation_results["invalid_count"] += 1
1468
+
1469
+ if condition_result["errors"]:
1470
+ validation_results["syntax_errors"].extend(
1471
+ [
1472
+ {"condition_index": i, "error": error}
1473
+ for error in condition_result["errors"]
1474
+ ]
1475
+ )
1476
+
1477
+ evaluation_time_ms = (datetime.now(UTC) - start_time).total_seconds() * 1000
1478
+
1479
+ return {
1480
+ "result": {
1481
+ "validation": validation_results,
1482
+ "summary": {
1483
+ "all_valid": validation_results["invalid_count"] == 0,
1484
+ "success_rate": validation_results["valid_count"]
1485
+ / max(validation_results["total_conditions"], 1)
1486
+ * 100,
1487
+ "evaluation_time_ms": evaluation_time_ms,
1488
+ },
1489
+ "options": {
1490
+ "validate_syntax": validate_syntax,
1491
+ "test_evaluation": test_evaluation,
1492
+ "context_provided": bool(context),
1493
+ "user_context_used": user_context is not None,
1494
+ },
1495
+ "operation": "validate_conditions",
1496
+ "timestamp": datetime.now(UTC).isoformat(),
1497
+ }
1498
+ }
861
1499
 
862
1500
  def _check_hierarchical(self, inputs: Dict[str, Any]) -> Dict[str, Any]:
863
1501
  """Check permissions with hierarchical resource access."""
864
- raise NotImplementedError("Check hierarchical operation will be implemented")
1502
+ user_id = inputs["user_id"]
1503
+ resource_path = inputs["resource_id"] # e.g., "org/team/project/workflow"
1504
+ permission = inputs["permission"]
1505
+ tenant_id = inputs.get("tenant_id", "default")
1506
+ context = inputs.get("context", {})
1507
+ check_inheritance = inputs.get("check_inheritance", True)
1508
+
1509
+ # Get user context
1510
+ user_context = self._get_user_context(user_id, tenant_id)
1511
+ if not user_context:
1512
+ raise NodeValidationError(f"User not found: {user_id}")
1513
+
1514
+ start_time = datetime.now(UTC)
1515
+
1516
+ # Parse resource hierarchy
1517
+ resource_parts = resource_path.split("/")
1518
+ hierarchical_checks = []
1519
+
1520
+ # Check permission at each level if inheritance is enabled
1521
+ if check_inheritance:
1522
+ # Check from most specific to most general
1523
+ for i in range(len(resource_parts), 0, -1):
1524
+ partial_path = "/".join(resource_parts[:i])
1525
+
1526
+ # Check exact permission
1527
+ exact_check = self._check_rbac_permission(
1528
+ user_context, partial_path, permission
1529
+ )
1530
+
1531
+ # Check wildcard permission at this level
1532
+ wildcard_check = self._check_rbac_permission(
1533
+ user_context, partial_path, "*"
1534
+ )
1535
+
1536
+ hierarchical_checks.append(
1537
+ {
1538
+ "resource_level": partial_path,
1539
+ "depth": i,
1540
+ "exact_permission": exact_check,
1541
+ "wildcard_permission": wildcard_check,
1542
+ "grants_access": exact_check or wildcard_check,
1543
+ }
1544
+ )
1545
+
1546
+ # If we found access at this level, we can stop (inheritance works)
1547
+ if exact_check or wildcard_check:
1548
+ break
1549
+ else:
1550
+ # Only check the exact resource path
1551
+ exact_check = self._check_rbac_permission(
1552
+ user_context, resource_path, permission
1553
+ )
1554
+ wildcard_check = self._check_rbac_permission(
1555
+ user_context, resource_path, "*"
1556
+ )
1557
+
1558
+ hierarchical_checks.append(
1559
+ {
1560
+ "resource_level": resource_path,
1561
+ "depth": len(resource_parts),
1562
+ "exact_permission": exact_check,
1563
+ "wildcard_permission": wildcard_check,
1564
+ "grants_access": exact_check or wildcard_check,
1565
+ }
1566
+ )
1567
+
1568
+ # Determine if access is granted
1569
+ access_granted = any(check["grants_access"] for check in hierarchical_checks)
1570
+
1571
+ # Find the granting level
1572
+ granting_level = None
1573
+ for check in hierarchical_checks:
1574
+ if check["grants_access"]:
1575
+ granting_level = check["resource_level"]
1576
+ break
1577
+
1578
+ # Perform ABAC check if context provided
1579
+ abac_result = True
1580
+ if context:
1581
+ abac_result = self._check_abac_permission(
1582
+ user_context, resource_path, permission, context
1583
+ )
1584
+
1585
+ final_result = access_granted and abac_result
1586
+
1587
+ evaluation_time_ms = (datetime.now(UTC) - start_time).total_seconds() * 1000
1588
+
1589
+ return {
1590
+ "result": {
1591
+ "hierarchical_check": {
1592
+ "allowed": final_result,
1593
+ "rbac_result": access_granted,
1594
+ "abac_result": abac_result,
1595
+ "user_id": user_id,
1596
+ "resource_path": resource_path,
1597
+ "permission": permission,
1598
+ "granting_level": granting_level,
1599
+ "inheritance_used": check_inheritance
1600
+ and granting_level != resource_path,
1601
+ "hierarchy_checks": hierarchical_checks,
1602
+ "evaluation_time_ms": evaluation_time_ms,
1603
+ },
1604
+ "options": {
1605
+ "check_inheritance": check_inheritance,
1606
+ "context_provided": bool(context),
1607
+ },
1608
+ "operation": "check_hierarchical",
1609
+ "timestamp": datetime.now(UTC).isoformat(),
1610
+ }
1611
+ }
1612
+
1613
+ def _create_audit_log(
1614
+ self,
1615
+ user_id: str,
1616
+ action: str,
1617
+ resource_type: str,
1618
+ resource_id: str,
1619
+ details: Dict[str, Any],
1620
+ success: bool,
1621
+ tenant_id: str,
1622
+ ):
1623
+ """Create an audit log entry for the permission check."""
1624
+ try:
1625
+ audit_query = """
1626
+ INSERT INTO admin_audit_log (
1627
+ user_id, action, resource_type, resource_id,
1628
+ operation, details, success, tenant_id, created_at
1629
+ ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9)
1630
+ """
1631
+
1632
+ self._db_node.run(
1633
+ query=audit_query,
1634
+ parameters=[
1635
+ user_id,
1636
+ action,
1637
+ resource_type,
1638
+ resource_id,
1639
+ "permission_check",
1640
+ json.dumps(details),
1641
+ success,
1642
+ tenant_id,
1643
+ datetime.now(UTC),
1644
+ ],
1645
+ )
1646
+ except Exception as e:
1647
+ self.logger.warning(f"Failed to create audit log: {e}")
1648
+ # Don't fail the permission check if audit logging fails