kailash 0.3.2__py3-none-any.whl → 0.4.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 +33 -1
- kailash/access_control/__init__.py +129 -0
- kailash/access_control/managers.py +461 -0
- kailash/access_control/rule_evaluators.py +467 -0
- kailash/access_control_abac.py +825 -0
- kailash/config/__init__.py +27 -0
- kailash/config/database_config.py +359 -0
- kailash/database/__init__.py +28 -0
- kailash/database/execution_pipeline.py +499 -0
- kailash/middleware/__init__.py +306 -0
- kailash/middleware/auth/__init__.py +33 -0
- kailash/middleware/auth/access_control.py +436 -0
- kailash/middleware/auth/auth_manager.py +422 -0
- kailash/middleware/auth/jwt_auth.py +477 -0
- kailash/middleware/auth/kailash_jwt_auth.py +616 -0
- kailash/middleware/communication/__init__.py +37 -0
- kailash/middleware/communication/ai_chat.py +989 -0
- kailash/middleware/communication/api_gateway.py +802 -0
- kailash/middleware/communication/events.py +470 -0
- kailash/middleware/communication/realtime.py +710 -0
- kailash/middleware/core/__init__.py +21 -0
- kailash/middleware/core/agent_ui.py +890 -0
- kailash/middleware/core/schema.py +643 -0
- kailash/middleware/core/workflows.py +396 -0
- kailash/middleware/database/__init__.py +63 -0
- kailash/middleware/database/base.py +113 -0
- kailash/middleware/database/base_models.py +525 -0
- kailash/middleware/database/enums.py +106 -0
- kailash/middleware/database/migrations.py +12 -0
- kailash/{api/database.py → middleware/database/models.py} +183 -291
- kailash/middleware/database/repositories.py +685 -0
- kailash/middleware/database/session_manager.py +19 -0
- kailash/middleware/mcp/__init__.py +38 -0
- kailash/middleware/mcp/client_integration.py +585 -0
- kailash/middleware/mcp/enhanced_server.py +576 -0
- kailash/nodes/__init__.py +25 -3
- kailash/nodes/admin/__init__.py +35 -0
- kailash/nodes/admin/audit_log.py +794 -0
- kailash/nodes/admin/permission_check.py +864 -0
- kailash/nodes/admin/role_management.py +823 -0
- kailash/nodes/admin/security_event.py +1519 -0
- kailash/nodes/admin/user_management.py +944 -0
- kailash/nodes/ai/a2a.py +24 -7
- kailash/nodes/ai/ai_providers.py +1 -0
- kailash/nodes/ai/embedding_generator.py +11 -11
- kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
- kailash/nodes/ai/llm_agent.py +407 -2
- kailash/nodes/ai/self_organizing.py +85 -10
- kailash/nodes/api/auth.py +287 -6
- kailash/nodes/api/rest.py +151 -0
- kailash/nodes/auth/__init__.py +17 -0
- kailash/nodes/auth/directory_integration.py +1228 -0
- kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
- kailash/nodes/auth/mfa.py +2338 -0
- kailash/nodes/auth/risk_assessment.py +872 -0
- kailash/nodes/auth/session_management.py +1093 -0
- kailash/nodes/auth/sso.py +1040 -0
- kailash/nodes/base.py +344 -13
- kailash/nodes/base_cycle_aware.py +4 -2
- kailash/nodes/base_with_acl.py +1 -1
- kailash/nodes/code/python.py +283 -10
- kailash/nodes/compliance/__init__.py +9 -0
- kailash/nodes/compliance/data_retention.py +1888 -0
- kailash/nodes/compliance/gdpr.py +2004 -0
- kailash/nodes/data/__init__.py +22 -2
- kailash/nodes/data/async_connection.py +469 -0
- kailash/nodes/data/async_sql.py +757 -0
- kailash/nodes/data/async_vector.py +598 -0
- kailash/nodes/data/readers.py +767 -0
- kailash/nodes/data/retrieval.py +360 -1
- kailash/nodes/data/sharepoint_graph.py +397 -21
- kailash/nodes/data/sql.py +94 -5
- kailash/nodes/data/streaming.py +68 -8
- kailash/nodes/data/vector_db.py +54 -4
- kailash/nodes/enterprise/__init__.py +13 -0
- kailash/nodes/enterprise/batch_processor.py +741 -0
- kailash/nodes/enterprise/data_lineage.py +497 -0
- kailash/nodes/logic/convergence.py +31 -9
- kailash/nodes/logic/operations.py +14 -3
- kailash/nodes/mixins/__init__.py +8 -0
- kailash/nodes/mixins/event_emitter.py +201 -0
- kailash/nodes/mixins/mcp.py +9 -4
- kailash/nodes/mixins/security.py +165 -0
- kailash/nodes/monitoring/__init__.py +7 -0
- kailash/nodes/monitoring/performance_benchmark.py +2497 -0
- kailash/nodes/rag/__init__.py +284 -0
- kailash/nodes/rag/advanced.py +1615 -0
- kailash/nodes/rag/agentic.py +773 -0
- kailash/nodes/rag/conversational.py +999 -0
- kailash/nodes/rag/evaluation.py +875 -0
- kailash/nodes/rag/federated.py +1188 -0
- kailash/nodes/rag/graph.py +721 -0
- kailash/nodes/rag/multimodal.py +671 -0
- kailash/nodes/rag/optimized.py +933 -0
- kailash/nodes/rag/privacy.py +1059 -0
- kailash/nodes/rag/query_processing.py +1335 -0
- kailash/nodes/rag/realtime.py +764 -0
- kailash/nodes/rag/registry.py +547 -0
- kailash/nodes/rag/router.py +837 -0
- kailash/nodes/rag/similarity.py +1854 -0
- kailash/nodes/rag/strategies.py +566 -0
- kailash/nodes/rag/workflows.py +575 -0
- kailash/nodes/security/__init__.py +19 -0
- kailash/nodes/security/abac_evaluator.py +1411 -0
- kailash/nodes/security/audit_log.py +91 -0
- kailash/nodes/security/behavior_analysis.py +1893 -0
- kailash/nodes/security/credential_manager.py +401 -0
- kailash/nodes/security/rotating_credentials.py +760 -0
- kailash/nodes/security/security_event.py +132 -0
- kailash/nodes/security/threat_detection.py +1103 -0
- kailash/nodes/testing/__init__.py +9 -0
- kailash/nodes/testing/credential_testing.py +499 -0
- kailash/nodes/transform/__init__.py +10 -2
- kailash/nodes/transform/chunkers.py +592 -1
- kailash/nodes/transform/processors.py +484 -14
- kailash/nodes/validation.py +321 -0
- kailash/runtime/access_controlled.py +1 -1
- kailash/runtime/async_local.py +41 -7
- kailash/runtime/docker.py +1 -1
- kailash/runtime/local.py +474 -55
- kailash/runtime/parallel.py +1 -1
- kailash/runtime/parallel_cyclic.py +1 -1
- kailash/runtime/testing.py +210 -2
- kailash/utils/migrations/__init__.py +25 -0
- kailash/utils/migrations/generator.py +433 -0
- kailash/utils/migrations/models.py +231 -0
- kailash/utils/migrations/runner.py +489 -0
- kailash/utils/secure_logging.py +342 -0
- kailash/workflow/__init__.py +16 -0
- kailash/workflow/cyclic_runner.py +3 -4
- kailash/workflow/graph.py +70 -2
- kailash/workflow/resilience.py +249 -0
- kailash/workflow/templates.py +726 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
- kailash-0.4.0.dist-info/RECORD +223 -0
- kailash/api/__init__.py +0 -17
- kailash/api/__main__.py +0 -6
- kailash/api/studio_secure.py +0 -893
- kailash/mcp/__main__.py +0 -13
- kailash/mcp/server_new.py +0 -336
- kailash/mcp/servers/__init__.py +0 -12
- kailash-0.3.2.dist-info/RECORD +0 -136
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/top_level.txt +0 -0
kailash/__init__.py
CHANGED
@@ -2,6 +2,10 @@
|
|
2
2
|
|
3
3
|
The Kailash SDK provides a comprehensive framework for creating nodes and workflows
|
4
4
|
that align with container-node architecture while allowing rapid prototyping.
|
5
|
+
|
6
|
+
New in v0.4.0: Enterprise middleware architecture with real-time agent-frontend
|
7
|
+
communication, dynamic workflows, AI chat integration, and production-ready
|
8
|
+
session management. Complete refactor from monolithic to composable middleware.
|
5
9
|
"""
|
6
10
|
|
7
11
|
from kailash.nodes.base import Node, NodeMetadata, NodeParameter
|
@@ -12,12 +16,28 @@ from kailash.workflow.builder import WorkflowBuilder
|
|
12
16
|
from kailash.workflow.graph import Connection, NodeInstance, Workflow
|
13
17
|
from kailash.workflow.visualization import WorkflowVisualizer
|
14
18
|
|
19
|
+
# Import middleware components (enhanced in v0.4.0)
|
20
|
+
try:
|
21
|
+
from kailash.middleware import (
|
22
|
+
AgentUIMiddleware,
|
23
|
+
AIChatMiddleware,
|
24
|
+
APIGateway,
|
25
|
+
RealtimeMiddleware,
|
26
|
+
create_gateway,
|
27
|
+
)
|
28
|
+
|
29
|
+
_MIDDLEWARE_AVAILABLE = True
|
30
|
+
except ImportError:
|
31
|
+
_MIDDLEWARE_AVAILABLE = False
|
32
|
+
# Middleware dependencies not available
|
33
|
+
|
15
34
|
# For backward compatibility
|
16
35
|
WorkflowGraph = Workflow
|
17
36
|
|
18
|
-
__version__ = "0.
|
37
|
+
__version__ = "0.4.0"
|
19
38
|
|
20
39
|
__all__ = [
|
40
|
+
# Core workflow components
|
21
41
|
"Workflow",
|
22
42
|
"WorkflowGraph", # Backward compatibility
|
23
43
|
"NodeInstance",
|
@@ -29,3 +49,15 @@ __all__ = [
|
|
29
49
|
"NodeMetadata",
|
30
50
|
"LocalRuntime",
|
31
51
|
]
|
52
|
+
|
53
|
+
# Add middleware to exports if available
|
54
|
+
if _MIDDLEWARE_AVAILABLE:
|
55
|
+
__all__.extend(
|
56
|
+
[
|
57
|
+
"AgentUIMiddleware",
|
58
|
+
"RealtimeMiddleware",
|
59
|
+
"APIGateway",
|
60
|
+
"AIChatMiddleware",
|
61
|
+
"create_gateway",
|
62
|
+
]
|
63
|
+
)
|
@@ -0,0 +1,129 @@
|
|
1
|
+
"""Access control package with composition-based architecture.
|
2
|
+
|
3
|
+
This package provides clean, testable access control components:
|
4
|
+
- Rule evaluators for RBAC, ABAC, and hybrid strategies
|
5
|
+
- Composable access control managers
|
6
|
+
- Backward compatibility with existing code
|
7
|
+
"""
|
8
|
+
|
9
|
+
import os
|
10
|
+
|
11
|
+
# Import core types first (avoiding circular imports)
|
12
|
+
import sys
|
13
|
+
from typing import Any, Dict, List
|
14
|
+
|
15
|
+
# Add parent directory to path for imports
|
16
|
+
sys.path.insert(0, os.path.dirname(os.path.dirname(__file__)))
|
17
|
+
|
18
|
+
# Import core access control components directly
|
19
|
+
import importlib.util
|
20
|
+
import os
|
21
|
+
|
22
|
+
# Load the original access_control module to avoid import conflicts
|
23
|
+
_spec = importlib.util.spec_from_file_location(
|
24
|
+
"original_access_control",
|
25
|
+
os.path.join(os.path.dirname(os.path.dirname(__file__)), "access_control.py"),
|
26
|
+
)
|
27
|
+
_original_module = importlib.util.module_from_spec(_spec)
|
28
|
+
_spec.loader.exec_module(_original_module)
|
29
|
+
|
30
|
+
# Import core types from original module
|
31
|
+
NodePermission = _original_module.NodePermission
|
32
|
+
WorkflowPermission = _original_module.WorkflowPermission
|
33
|
+
PermissionEffect = _original_module.PermissionEffect
|
34
|
+
PermissionRule = _original_module.PermissionRule
|
35
|
+
UserContext = _original_module.UserContext
|
36
|
+
AccessDecision = _original_module.AccessDecision
|
37
|
+
ConditionEvaluator = _original_module.ConditionEvaluator
|
38
|
+
|
39
|
+
# Import utility functions from original module
|
40
|
+
get_access_control_manager = _original_module.get_access_control_manager
|
41
|
+
set_access_control_manager = _original_module.set_access_control_manager
|
42
|
+
|
43
|
+
# Import new composition-based components
|
44
|
+
from kailash.access_control.managers import AccessControlManager # noqa: E402
|
45
|
+
from kailash.access_control.rule_evaluators import ( # noqa: E402
|
46
|
+
ABACRuleEvaluator,
|
47
|
+
HybridRuleEvaluator,
|
48
|
+
RBACRuleEvaluator,
|
49
|
+
RuleEvaluator,
|
50
|
+
create_rule_evaluator,
|
51
|
+
)
|
52
|
+
|
53
|
+
# ABAC components are available directly from kailash.access_control_abac
|
54
|
+
# Not imported here to avoid circular import issues
|
55
|
+
|
56
|
+
# Export all components
|
57
|
+
__all__ = [
|
58
|
+
# Core types
|
59
|
+
"NodePermission",
|
60
|
+
"WorkflowPermission",
|
61
|
+
"PermissionEffect",
|
62
|
+
"PermissionRule",
|
63
|
+
"UserContext",
|
64
|
+
"AccessDecision",
|
65
|
+
"ConditionEvaluator",
|
66
|
+
# Composition-based components
|
67
|
+
"AccessControlManager",
|
68
|
+
"RuleEvaluator",
|
69
|
+
"RBACRuleEvaluator",
|
70
|
+
"ABACRuleEvaluator",
|
71
|
+
"HybridRuleEvaluator",
|
72
|
+
"create_rule_evaluator",
|
73
|
+
# ABAC components available from kailash.access_control_abac
|
74
|
+
# Utility functions
|
75
|
+
"get_access_control_manager",
|
76
|
+
"set_access_control_manager",
|
77
|
+
# ABAC helper functions
|
78
|
+
"create_attribute_condition",
|
79
|
+
"create_complex_condition",
|
80
|
+
]
|
81
|
+
|
82
|
+
|
83
|
+
# Helper functions for creating ABAC conditions
|
84
|
+
def create_attribute_condition(
|
85
|
+
path: str, operator: str, value: Any, case_sensitive: bool = True
|
86
|
+
) -> Dict[str, Any]:
|
87
|
+
"""Create an attribute condition configuration.
|
88
|
+
|
89
|
+
Helper function to create properly formatted attribute conditions
|
90
|
+
for use in permission rules.
|
91
|
+
|
92
|
+
Args:
|
93
|
+
path: Attribute path (e.g., "user.attributes.department")
|
94
|
+
operator: Comparison operator
|
95
|
+
value: Value to compare against
|
96
|
+
case_sensitive: Whether comparison is case sensitive
|
97
|
+
|
98
|
+
Returns:
|
99
|
+
Condition configuration dict
|
100
|
+
"""
|
101
|
+
return {
|
102
|
+
"type": "attribute_expression",
|
103
|
+
"value": {
|
104
|
+
"attribute_path": path,
|
105
|
+
"operator": operator,
|
106
|
+
"value": value,
|
107
|
+
"case_sensitive": case_sensitive,
|
108
|
+
},
|
109
|
+
}
|
110
|
+
|
111
|
+
|
112
|
+
def create_complex_condition(
|
113
|
+
operator: str, conditions: List[Dict[str, Any]]
|
114
|
+
) -> Dict[str, Any]:
|
115
|
+
"""Create a complex attribute condition with logical operators.
|
116
|
+
|
117
|
+
Helper function to create AND/OR/NOT conditions.
|
118
|
+
|
119
|
+
Args:
|
120
|
+
operator: Logical operator (and/or/not)
|
121
|
+
conditions: List of conditions to combine
|
122
|
+
|
123
|
+
Returns:
|
124
|
+
Complex condition configuration dict
|
125
|
+
"""
|
126
|
+
return {
|
127
|
+
"type": "attribute_expression",
|
128
|
+
"value": {"operator": operator, "conditions": conditions},
|
129
|
+
}
|
@@ -0,0 +1,461 @@
|
|
1
|
+
"""Composition-based access control managers.
|
2
|
+
|
3
|
+
This module provides clean, testable access control managers using composition
|
4
|
+
instead of inheritance, solving the architectural issues with the previous design.
|
5
|
+
"""
|
6
|
+
|
7
|
+
import logging
|
8
|
+
import threading
|
9
|
+
from typing import Any, Dict, List, Optional, Union
|
10
|
+
|
11
|
+
# Import base access control components
|
12
|
+
try:
|
13
|
+
from kailash.access_control import (
|
14
|
+
AccessDecision,
|
15
|
+
NodePermission,
|
16
|
+
PermissionRule,
|
17
|
+
UserContext,
|
18
|
+
WorkflowPermission,
|
19
|
+
)
|
20
|
+
except ImportError:
|
21
|
+
# Local definitions to handle circular import during initial setup
|
22
|
+
from dataclasses import dataclass
|
23
|
+
from datetime import datetime
|
24
|
+
from enum import Enum
|
25
|
+
|
26
|
+
class NodePermission(Enum):
|
27
|
+
EXECUTE = "execute"
|
28
|
+
READ_OUTPUT = "read_output"
|
29
|
+
WRITE_INPUT = "write_input"
|
30
|
+
|
31
|
+
class WorkflowPermission(Enum):
|
32
|
+
VIEW = "view"
|
33
|
+
EXECUTE = "execute"
|
34
|
+
MODIFY = "modify"
|
35
|
+
|
36
|
+
class PermissionEffect(Enum):
|
37
|
+
ALLOW = "allow"
|
38
|
+
DENY = "deny"
|
39
|
+
CONDITIONAL = "conditional"
|
40
|
+
|
41
|
+
@dataclass
|
42
|
+
class UserContext:
|
43
|
+
user_id: str
|
44
|
+
tenant_id: str
|
45
|
+
email: str
|
46
|
+
roles: List[str]
|
47
|
+
attributes: Dict[str, Any]
|
48
|
+
|
49
|
+
@dataclass
|
50
|
+
class AccessDecision:
|
51
|
+
allowed: bool
|
52
|
+
reason: str
|
53
|
+
applied_rules: List[str]
|
54
|
+
conditions_met: Optional[Dict[str, bool]] = None
|
55
|
+
masked_fields: Optional[List[str]] = None
|
56
|
+
|
57
|
+
@dataclass
|
58
|
+
class PermissionRule:
|
59
|
+
id: str
|
60
|
+
resource_type: str
|
61
|
+
resource_id: str
|
62
|
+
permission: Union[NodePermission, WorkflowPermission]
|
63
|
+
effect: PermissionEffect
|
64
|
+
user_id: Optional[str] = None
|
65
|
+
role: Optional[str] = None
|
66
|
+
tenant_id: Optional[str] = None
|
67
|
+
conditions: Optional[Dict[str, Any]] = None
|
68
|
+
priority: int = 0
|
69
|
+
expires_at: Optional[datetime] = None
|
70
|
+
|
71
|
+
|
72
|
+
from kailash.access_control.rule_evaluators import RuleEvaluator, create_rule_evaluator
|
73
|
+
|
74
|
+
logger = logging.getLogger(__name__)
|
75
|
+
|
76
|
+
|
77
|
+
class AccessControlManager:
|
78
|
+
"""Access control manager using composition pattern.
|
79
|
+
|
80
|
+
This manager separates rule storage from rule evaluation, allowing:
|
81
|
+
- Easy testing with mock evaluators
|
82
|
+
- Flexible evaluation strategies (RBAC, ABAC, Hybrid)
|
83
|
+
- Clear separation of concerns
|
84
|
+
- No inheritance-related bugs
|
85
|
+
|
86
|
+
Example:
|
87
|
+
>>> # Create with hybrid evaluation (RBAC + ABAC)
|
88
|
+
>>> manager = AccessControlManager()
|
89
|
+
|
90
|
+
>>> # Or specify evaluation strategy
|
91
|
+
>>> rbac_manager = AccessControlManager(strategy="rbac")
|
92
|
+
>>> abac_manager = AccessControlManager(strategy="abac")
|
93
|
+
|
94
|
+
>>> # Add rules
|
95
|
+
>>> manager.add_rule(PermissionRule(...))
|
96
|
+
|
97
|
+
>>> # Check access
|
98
|
+
>>> decision = manager.check_node_access(user, "node_id", NodePermission.EXECUTE)
|
99
|
+
"""
|
100
|
+
|
101
|
+
def __init__(
|
102
|
+
self,
|
103
|
+
rule_evaluator: Optional[RuleEvaluator] = None,
|
104
|
+
strategy: str = "hybrid",
|
105
|
+
enabled: bool = True,
|
106
|
+
):
|
107
|
+
"""Initialize access control manager.
|
108
|
+
|
109
|
+
Args:
|
110
|
+
rule_evaluator: Custom rule evaluator (overrides strategy)
|
111
|
+
strategy: Evaluation strategy ('rbac', 'abac', 'hybrid')
|
112
|
+
enabled: Whether access control is enabled
|
113
|
+
"""
|
114
|
+
self.enabled = enabled
|
115
|
+
self.rules: List[PermissionRule] = []
|
116
|
+
|
117
|
+
# Use provided evaluator or create one based on strategy
|
118
|
+
if rule_evaluator:
|
119
|
+
self.rule_evaluator = rule_evaluator
|
120
|
+
else:
|
121
|
+
self.rule_evaluator = create_rule_evaluator(strategy)
|
122
|
+
|
123
|
+
# Cache for performance
|
124
|
+
self._cache: Dict[str, AccessDecision] = {}
|
125
|
+
self._cache_lock = threading.Lock()
|
126
|
+
|
127
|
+
# Audit logging
|
128
|
+
self.audit_logger = logging.getLogger("kailash.access_control.audit")
|
129
|
+
|
130
|
+
# Data masking for ABAC (only needed for abac/hybrid strategies)
|
131
|
+
self._masking_rules: Dict[str, List[Any]] = {}
|
132
|
+
if strategy in ["abac", "hybrid"]:
|
133
|
+
self._init_abac_components()
|
134
|
+
|
135
|
+
logger.info(
|
136
|
+
f"Initialized AccessControlManager with {type(self.rule_evaluator).__name__}"
|
137
|
+
)
|
138
|
+
|
139
|
+
def _init_abac_components(self) -> None:
|
140
|
+
"""Initialize ABAC-specific components."""
|
141
|
+
try:
|
142
|
+
from kailash.access_control_abac import AttributeEvaluator, DataMasker
|
143
|
+
|
144
|
+
self.attribute_evaluator = AttributeEvaluator()
|
145
|
+
self.data_masker = DataMasker(self.attribute_evaluator)
|
146
|
+
except ImportError:
|
147
|
+
logger.warning("ABAC components not available, data masking disabled")
|
148
|
+
self.attribute_evaluator = None
|
149
|
+
self.data_masker = None
|
150
|
+
|
151
|
+
def add_rule(self, rule: PermissionRule) -> None:
|
152
|
+
"""Add a permission rule.
|
153
|
+
|
154
|
+
Args:
|
155
|
+
rule: Permission rule to add
|
156
|
+
"""
|
157
|
+
self.rules.append(rule)
|
158
|
+
self._clear_cache()
|
159
|
+
logger.debug(
|
160
|
+
f"Added rule {rule.id} for {rule.resource_type}:{rule.resource_id}"
|
161
|
+
)
|
162
|
+
|
163
|
+
def remove_rule(self, rule_id: str) -> bool:
|
164
|
+
"""Remove a permission rule.
|
165
|
+
|
166
|
+
Args:
|
167
|
+
rule_id: ID of rule to remove
|
168
|
+
|
169
|
+
Returns:
|
170
|
+
True if rule was found and removed
|
171
|
+
"""
|
172
|
+
initial_count = len(self.rules)
|
173
|
+
self.rules = [r for r in self.rules if r.id != rule_id]
|
174
|
+
removed = len(self.rules) < initial_count
|
175
|
+
|
176
|
+
if removed:
|
177
|
+
self._clear_cache()
|
178
|
+
logger.debug(f"Removed rule {rule_id}")
|
179
|
+
|
180
|
+
return removed
|
181
|
+
|
182
|
+
def check_workflow_access(
|
183
|
+
self,
|
184
|
+
user: UserContext,
|
185
|
+
workflow_id: str,
|
186
|
+
permission: WorkflowPermission,
|
187
|
+
runtime_context: Optional[Dict[str, Any]] = None,
|
188
|
+
) -> AccessDecision:
|
189
|
+
"""Check if user has permission on workflow.
|
190
|
+
|
191
|
+
Args:
|
192
|
+
user: User requesting access
|
193
|
+
workflow_id: Workflow to access
|
194
|
+
permission: Permission being requested
|
195
|
+
runtime_context: Additional runtime context
|
196
|
+
|
197
|
+
Returns:
|
198
|
+
AccessDecision with allow/deny and reasoning
|
199
|
+
"""
|
200
|
+
if not self.enabled:
|
201
|
+
return AccessDecision(
|
202
|
+
allowed=True,
|
203
|
+
reason="Access control disabled",
|
204
|
+
applied_rules=[],
|
205
|
+
)
|
206
|
+
|
207
|
+
cache_key = f"workflow:{workflow_id}:{user.user_id}:{permission.value}"
|
208
|
+
|
209
|
+
# Check cache (if no runtime context)
|
210
|
+
if not runtime_context:
|
211
|
+
with self._cache_lock:
|
212
|
+
if cache_key in self._cache:
|
213
|
+
cached_decision = self._cache[cache_key]
|
214
|
+
logger.debug(f"Cache hit for {cache_key}")
|
215
|
+
return cached_decision
|
216
|
+
|
217
|
+
# Get applicable rules
|
218
|
+
applicable_rules = self._get_applicable_rules(
|
219
|
+
"workflow", workflow_id, permission
|
220
|
+
)
|
221
|
+
|
222
|
+
# Evaluate using configured strategy
|
223
|
+
decision = self.rule_evaluator.evaluate_rules(
|
224
|
+
applicable_rules,
|
225
|
+
user,
|
226
|
+
"workflow",
|
227
|
+
workflow_id,
|
228
|
+
permission,
|
229
|
+
runtime_context or {},
|
230
|
+
)
|
231
|
+
|
232
|
+
# Cache decision (if no runtime context)
|
233
|
+
if not runtime_context:
|
234
|
+
with self._cache_lock:
|
235
|
+
self._cache[cache_key] = decision
|
236
|
+
|
237
|
+
# Audit log
|
238
|
+
self._audit_log(user, "workflow", workflow_id, permission, decision)
|
239
|
+
|
240
|
+
return decision
|
241
|
+
|
242
|
+
def check_node_access(
|
243
|
+
self,
|
244
|
+
user: UserContext,
|
245
|
+
node_id: str,
|
246
|
+
permission: NodePermission,
|
247
|
+
runtime_context: Optional[Dict[str, Any]] = None,
|
248
|
+
) -> AccessDecision:
|
249
|
+
"""Check if user has permission on node.
|
250
|
+
|
251
|
+
Args:
|
252
|
+
user: User requesting access
|
253
|
+
node_id: Node to access
|
254
|
+
permission: Permission being requested
|
255
|
+
runtime_context: Additional runtime context
|
256
|
+
|
257
|
+
Returns:
|
258
|
+
AccessDecision with allow/deny and reasoning
|
259
|
+
"""
|
260
|
+
if not self.enabled:
|
261
|
+
return AccessDecision(
|
262
|
+
allowed=True,
|
263
|
+
reason="Access control disabled",
|
264
|
+
applied_rules=[],
|
265
|
+
)
|
266
|
+
|
267
|
+
cache_key = f"node:{node_id}:{user.user_id}:{permission.value}"
|
268
|
+
|
269
|
+
# Check cache (if no runtime context)
|
270
|
+
if not runtime_context:
|
271
|
+
with self._cache_lock:
|
272
|
+
if cache_key in self._cache:
|
273
|
+
cached_decision = self._cache[cache_key]
|
274
|
+
logger.debug(f"Cache hit for {cache_key}")
|
275
|
+
return cached_decision
|
276
|
+
|
277
|
+
# Get applicable rules
|
278
|
+
applicable_rules = self._get_applicable_rules("node", node_id, permission)
|
279
|
+
|
280
|
+
# Evaluate using configured strategy
|
281
|
+
decision = self.rule_evaluator.evaluate_rules(
|
282
|
+
applicable_rules,
|
283
|
+
user,
|
284
|
+
"node",
|
285
|
+
node_id,
|
286
|
+
permission,
|
287
|
+
runtime_context or {},
|
288
|
+
)
|
289
|
+
|
290
|
+
# Cache decision (if no runtime context)
|
291
|
+
if not runtime_context:
|
292
|
+
with self._cache_lock:
|
293
|
+
self._cache[cache_key] = decision
|
294
|
+
|
295
|
+
# Audit log
|
296
|
+
self._audit_log(user, "node", node_id, permission, decision)
|
297
|
+
|
298
|
+
return decision
|
299
|
+
|
300
|
+
def get_accessible_nodes(
|
301
|
+
self, user: UserContext, workflow_id: str, permission: NodePermission
|
302
|
+
) -> set[str]:
|
303
|
+
"""Get all nodes user can access in a workflow.
|
304
|
+
|
305
|
+
Args:
|
306
|
+
user: User to check access for
|
307
|
+
workflow_id: Workflow containing nodes
|
308
|
+
permission: Permission type to check
|
309
|
+
|
310
|
+
Returns:
|
311
|
+
Set of accessible node IDs
|
312
|
+
"""
|
313
|
+
# Get all node rules for this workflow
|
314
|
+
node_rules = [
|
315
|
+
rule
|
316
|
+
for rule in self.rules
|
317
|
+
if rule.resource_type == "node" and rule.permission == permission
|
318
|
+
]
|
319
|
+
|
320
|
+
accessible = set()
|
321
|
+
|
322
|
+
for rule in node_rules:
|
323
|
+
decision = self.check_node_access(user, rule.resource_id, permission)
|
324
|
+
if decision.allowed:
|
325
|
+
accessible.add(rule.resource_id)
|
326
|
+
|
327
|
+
return accessible
|
328
|
+
|
329
|
+
def add_masking_rule(self, node_id: str, rule: Any) -> None:
|
330
|
+
"""Add attribute-based masking rule for a node."""
|
331
|
+
if not hasattr(self, "data_masker") or self.data_masker is None:
|
332
|
+
logger.warning("Data masking not available - use ABAC or hybrid strategy")
|
333
|
+
return
|
334
|
+
|
335
|
+
if node_id not in self._masking_rules:
|
336
|
+
self._masking_rules[node_id] = []
|
337
|
+
|
338
|
+
self._masking_rules[node_id].append(rule)
|
339
|
+
logger.info(f"Added masking rule for node {node_id}")
|
340
|
+
|
341
|
+
def apply_data_masking(
|
342
|
+
self, user: UserContext, node_id: str, data: Dict[str, Any]
|
343
|
+
) -> Dict[str, Any]:
|
344
|
+
"""Apply attribute-based data masking to node output."""
|
345
|
+
# Check if ABAC components are available
|
346
|
+
if not hasattr(self, "data_masker") or self.data_masker is None:
|
347
|
+
logger.warning("Data masking not available - returning original data")
|
348
|
+
return data
|
349
|
+
|
350
|
+
# Get masking rules for node
|
351
|
+
rules = self._masking_rules.get(node_id, [])
|
352
|
+
if not rules:
|
353
|
+
return data
|
354
|
+
|
355
|
+
# Build context for evaluation
|
356
|
+
context = {"user": user, "node_id": node_id, "data": data}
|
357
|
+
|
358
|
+
# Apply masking
|
359
|
+
return self.data_masker.apply_masking(data, rules, context)
|
360
|
+
|
361
|
+
def supports_conditions(self) -> bool:
|
362
|
+
"""Check if current evaluator supports conditional rules.
|
363
|
+
|
364
|
+
Returns:
|
365
|
+
True if complex conditions are supported
|
366
|
+
"""
|
367
|
+
return self.rule_evaluator.supports_conditions()
|
368
|
+
|
369
|
+
def get_strategy_info(self) -> Dict[str, Any]:
|
370
|
+
"""Get information about the current evaluation strategy.
|
371
|
+
|
372
|
+
Returns:
|
373
|
+
Dictionary with strategy details
|
374
|
+
"""
|
375
|
+
return {
|
376
|
+
"evaluator_type": type(self.rule_evaluator).__name__,
|
377
|
+
"supports_conditions": self.supports_conditions(),
|
378
|
+
"enabled": self.enabled,
|
379
|
+
"rule_count": len(self.rules),
|
380
|
+
}
|
381
|
+
|
382
|
+
def _get_applicable_rules(
|
383
|
+
self,
|
384
|
+
resource_type: str,
|
385
|
+
resource_id: str,
|
386
|
+
permission: Union[NodePermission, WorkflowPermission],
|
387
|
+
) -> List[PermissionRule]:
|
388
|
+
"""Get rules that apply to a specific resource and permission.
|
389
|
+
|
390
|
+
Args:
|
391
|
+
resource_type: Type of resource (node/workflow)
|
392
|
+
resource_id: Specific resource ID
|
393
|
+
permission: Permission being checked
|
394
|
+
|
395
|
+
Returns:
|
396
|
+
List of applicable rules
|
397
|
+
"""
|
398
|
+
applicable_rules = []
|
399
|
+
|
400
|
+
for rule in self.rules:
|
401
|
+
# Check resource type, ID, and permission match
|
402
|
+
if (
|
403
|
+
rule.resource_type == resource_type
|
404
|
+
and rule.resource_id == resource_id
|
405
|
+
and rule.permission == permission
|
406
|
+
):
|
407
|
+
|
408
|
+
# Check expiration
|
409
|
+
if rule.expires_at:
|
410
|
+
from datetime import UTC, datetime
|
411
|
+
|
412
|
+
if rule.expires_at < datetime.now(UTC):
|
413
|
+
continue
|
414
|
+
|
415
|
+
applicable_rules.append(rule)
|
416
|
+
|
417
|
+
return applicable_rules
|
418
|
+
|
419
|
+
def _clear_cache(self) -> None:
|
420
|
+
"""Clear the access decision cache."""
|
421
|
+
with self._cache_lock:
|
422
|
+
self._cache.clear()
|
423
|
+
logger.debug("Cleared access control cache")
|
424
|
+
|
425
|
+
def _audit_log(
|
426
|
+
self,
|
427
|
+
user: UserContext,
|
428
|
+
resource_type: str,
|
429
|
+
resource_id: str,
|
430
|
+
permission: Union[NodePermission, WorkflowPermission],
|
431
|
+
decision: AccessDecision,
|
432
|
+
) -> None:
|
433
|
+
"""Log access control decision for auditing.
|
434
|
+
|
435
|
+
Args:
|
436
|
+
user: User who made the request
|
437
|
+
resource_type: Type of resource accessed
|
438
|
+
resource_id: ID of resource accessed
|
439
|
+
permission: Permission that was checked
|
440
|
+
decision: Access control decision
|
441
|
+
"""
|
442
|
+
self.audit_logger.info(
|
443
|
+
"Access decision",
|
444
|
+
extra={
|
445
|
+
"user_id": user.user_id,
|
446
|
+
"tenant_id": user.tenant_id,
|
447
|
+
"resource_type": resource_type,
|
448
|
+
"resource_id": resource_id,
|
449
|
+
"permission": permission.value,
|
450
|
+
"allowed": decision.allowed,
|
451
|
+
"reason": decision.reason,
|
452
|
+
"applied_rules": decision.applied_rules,
|
453
|
+
"evaluator": type(self.rule_evaluator).__name__,
|
454
|
+
},
|
455
|
+
)
|
456
|
+
|
457
|
+
|
458
|
+
# Export components
|
459
|
+
__all__ = [
|
460
|
+
"AccessControlManager",
|
461
|
+
]
|