kailash 0.5.0__py3-none-any.whl → 0.6.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kailash/__init__.py +1 -1
- kailash/client/__init__.py +12 -0
- kailash/client/enhanced_client.py +306 -0
- kailash/core/actors/__init__.py +16 -0
- kailash/core/actors/connection_actor.py +566 -0
- kailash/core/actors/supervisor.py +364 -0
- kailash/edge/__init__.py +16 -0
- kailash/edge/compliance.py +834 -0
- kailash/edge/discovery.py +659 -0
- kailash/edge/location.py +582 -0
- kailash/gateway/__init__.py +33 -0
- kailash/gateway/api.py +289 -0
- kailash/gateway/enhanced_gateway.py +357 -0
- kailash/gateway/resource_resolver.py +217 -0
- kailash/gateway/security.py +227 -0
- kailash/middleware/auth/models.py +2 -2
- kailash/middleware/database/base_models.py +1 -7
- kailash/middleware/gateway/__init__.py +22 -0
- kailash/middleware/gateway/checkpoint_manager.py +398 -0
- kailash/middleware/gateway/deduplicator.py +382 -0
- kailash/middleware/gateway/durable_gateway.py +417 -0
- kailash/middleware/gateway/durable_request.py +498 -0
- kailash/middleware/gateway/event_store.py +459 -0
- kailash/nodes/admin/permission_check.py +817 -33
- kailash/nodes/admin/role_management.py +1242 -108
- kailash/nodes/admin/schema_manager.py +438 -0
- kailash/nodes/admin/user_management.py +1124 -1582
- kailash/nodes/code/__init__.py +8 -1
- kailash/nodes/code/async_python.py +1035 -0
- kailash/nodes/code/python.py +1 -0
- kailash/nodes/data/async_sql.py +9 -3
- kailash/nodes/data/sql.py +20 -11
- kailash/nodes/data/workflow_connection_pool.py +643 -0
- kailash/nodes/rag/__init__.py +1 -4
- kailash/resources/__init__.py +40 -0
- kailash/resources/factory.py +533 -0
- kailash/resources/health.py +319 -0
- kailash/resources/reference.py +288 -0
- kailash/resources/registry.py +392 -0
- kailash/runtime/async_local.py +711 -302
- kailash/testing/__init__.py +34 -0
- kailash/testing/async_test_case.py +353 -0
- kailash/testing/async_utils.py +345 -0
- kailash/testing/fixtures.py +458 -0
- kailash/testing/mock_registry.py +495 -0
- kailash/workflow/__init__.py +8 -0
- kailash/workflow/async_builder.py +621 -0
- kailash/workflow/async_patterns.py +766 -0
- kailash/workflow/cyclic_runner.py +107 -16
- kailash/workflow/graph.py +7 -2
- kailash/workflow/resilience.py +11 -1
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/METADATA +7 -4
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/RECORD +57 -22
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/WHEEL +0 -0
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.5.0.dist-info → kailash-0.6.0.dist-info}/top_level.txt +0 -0
@@ -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
|
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
|
365
|
-
self._db_node =
|
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
|
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
|
-
|
634
|
-
|
635
|
-
|
732
|
+
try:
|
733
|
+
result = self._db_node.run(
|
734
|
+
query=query, parameters=[user_id, tenant_id], result_format="dict"
|
735
|
+
)
|
636
736
|
|
637
|
-
|
638
|
-
|
737
|
+
# Handle the corrected result structure
|
738
|
+
user_rows = result.get("data", [])
|
739
|
+
if not user_rows:
|
740
|
+
return None
|
639
741
|
|
640
|
-
|
641
|
-
return None
|
742
|
+
user_data = user_rows[0]
|
642
743
|
|
643
|
-
|
644
|
-
|
645
|
-
|
646
|
-
|
647
|
-
|
648
|
-
|
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.
|
795
|
+
node_permission = NodePermission.READ_OUTPUT # Map view to read_output
|
691
796
|
elif permission.lower() in ["write", "edit", "update"]:
|
692
|
-
node_permission = NodePermission.
|
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(
|
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(
|
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.
|
744
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|