kailash 0.3.2__py3-none-any.whl → 0.4.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 (151) hide show
  1. kailash/__init__.py +33 -1
  2. kailash/access_control/__init__.py +129 -0
  3. kailash/access_control/managers.py +461 -0
  4. kailash/access_control/rule_evaluators.py +467 -0
  5. kailash/access_control_abac.py +825 -0
  6. kailash/config/__init__.py +27 -0
  7. kailash/config/database_config.py +359 -0
  8. kailash/database/__init__.py +28 -0
  9. kailash/database/execution_pipeline.py +499 -0
  10. kailash/middleware/__init__.py +306 -0
  11. kailash/middleware/auth/__init__.py +33 -0
  12. kailash/middleware/auth/access_control.py +436 -0
  13. kailash/middleware/auth/auth_manager.py +422 -0
  14. kailash/middleware/auth/jwt_auth.py +477 -0
  15. kailash/middleware/auth/kailash_jwt_auth.py +616 -0
  16. kailash/middleware/communication/__init__.py +37 -0
  17. kailash/middleware/communication/ai_chat.py +989 -0
  18. kailash/middleware/communication/api_gateway.py +802 -0
  19. kailash/middleware/communication/events.py +470 -0
  20. kailash/middleware/communication/realtime.py +710 -0
  21. kailash/middleware/core/__init__.py +21 -0
  22. kailash/middleware/core/agent_ui.py +890 -0
  23. kailash/middleware/core/schema.py +643 -0
  24. kailash/middleware/core/workflows.py +396 -0
  25. kailash/middleware/database/__init__.py +63 -0
  26. kailash/middleware/database/base.py +113 -0
  27. kailash/middleware/database/base_models.py +525 -0
  28. kailash/middleware/database/enums.py +106 -0
  29. kailash/middleware/database/migrations.py +12 -0
  30. kailash/{api/database.py → middleware/database/models.py} +183 -291
  31. kailash/middleware/database/repositories.py +685 -0
  32. kailash/middleware/database/session_manager.py +19 -0
  33. kailash/middleware/mcp/__init__.py +38 -0
  34. kailash/middleware/mcp/client_integration.py +585 -0
  35. kailash/middleware/mcp/enhanced_server.py +576 -0
  36. kailash/nodes/__init__.py +27 -3
  37. kailash/nodes/admin/__init__.py +42 -0
  38. kailash/nodes/admin/audit_log.py +794 -0
  39. kailash/nodes/admin/permission_check.py +864 -0
  40. kailash/nodes/admin/role_management.py +823 -0
  41. kailash/nodes/admin/security_event.py +1523 -0
  42. kailash/nodes/admin/user_management.py +944 -0
  43. kailash/nodes/ai/a2a.py +24 -7
  44. kailash/nodes/ai/ai_providers.py +248 -40
  45. kailash/nodes/ai/embedding_generator.py +11 -11
  46. kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
  47. kailash/nodes/ai/llm_agent.py +436 -5
  48. kailash/nodes/ai/self_organizing.py +85 -10
  49. kailash/nodes/ai/vision_utils.py +148 -0
  50. kailash/nodes/alerts/__init__.py +26 -0
  51. kailash/nodes/alerts/base.py +234 -0
  52. kailash/nodes/alerts/discord.py +499 -0
  53. kailash/nodes/api/auth.py +287 -6
  54. kailash/nodes/api/rest.py +151 -0
  55. kailash/nodes/auth/__init__.py +17 -0
  56. kailash/nodes/auth/directory_integration.py +1228 -0
  57. kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
  58. kailash/nodes/auth/mfa.py +2338 -0
  59. kailash/nodes/auth/risk_assessment.py +872 -0
  60. kailash/nodes/auth/session_management.py +1093 -0
  61. kailash/nodes/auth/sso.py +1040 -0
  62. kailash/nodes/base.py +344 -13
  63. kailash/nodes/base_cycle_aware.py +4 -2
  64. kailash/nodes/base_with_acl.py +1 -1
  65. kailash/nodes/code/python.py +283 -10
  66. kailash/nodes/compliance/__init__.py +9 -0
  67. kailash/nodes/compliance/data_retention.py +1888 -0
  68. kailash/nodes/compliance/gdpr.py +2004 -0
  69. kailash/nodes/data/__init__.py +22 -2
  70. kailash/nodes/data/async_connection.py +469 -0
  71. kailash/nodes/data/async_sql.py +757 -0
  72. kailash/nodes/data/async_vector.py +598 -0
  73. kailash/nodes/data/readers.py +767 -0
  74. kailash/nodes/data/retrieval.py +360 -1
  75. kailash/nodes/data/sharepoint_graph.py +397 -21
  76. kailash/nodes/data/sql.py +94 -5
  77. kailash/nodes/data/streaming.py +68 -8
  78. kailash/nodes/data/vector_db.py +54 -4
  79. kailash/nodes/enterprise/__init__.py +13 -0
  80. kailash/nodes/enterprise/batch_processor.py +741 -0
  81. kailash/nodes/enterprise/data_lineage.py +497 -0
  82. kailash/nodes/logic/convergence.py +31 -9
  83. kailash/nodes/logic/operations.py +14 -3
  84. kailash/nodes/mixins/__init__.py +8 -0
  85. kailash/nodes/mixins/event_emitter.py +201 -0
  86. kailash/nodes/mixins/mcp.py +9 -4
  87. kailash/nodes/mixins/security.py +165 -0
  88. kailash/nodes/monitoring/__init__.py +7 -0
  89. kailash/nodes/monitoring/performance_benchmark.py +2497 -0
  90. kailash/nodes/rag/__init__.py +284 -0
  91. kailash/nodes/rag/advanced.py +1615 -0
  92. kailash/nodes/rag/agentic.py +773 -0
  93. kailash/nodes/rag/conversational.py +999 -0
  94. kailash/nodes/rag/evaluation.py +875 -0
  95. kailash/nodes/rag/federated.py +1188 -0
  96. kailash/nodes/rag/graph.py +721 -0
  97. kailash/nodes/rag/multimodal.py +671 -0
  98. kailash/nodes/rag/optimized.py +933 -0
  99. kailash/nodes/rag/privacy.py +1059 -0
  100. kailash/nodes/rag/query_processing.py +1335 -0
  101. kailash/nodes/rag/realtime.py +764 -0
  102. kailash/nodes/rag/registry.py +547 -0
  103. kailash/nodes/rag/router.py +837 -0
  104. kailash/nodes/rag/similarity.py +1854 -0
  105. kailash/nodes/rag/strategies.py +566 -0
  106. kailash/nodes/rag/workflows.py +575 -0
  107. kailash/nodes/security/__init__.py +19 -0
  108. kailash/nodes/security/abac_evaluator.py +1411 -0
  109. kailash/nodes/security/audit_log.py +103 -0
  110. kailash/nodes/security/behavior_analysis.py +1893 -0
  111. kailash/nodes/security/credential_manager.py +401 -0
  112. kailash/nodes/security/rotating_credentials.py +760 -0
  113. kailash/nodes/security/security_event.py +133 -0
  114. kailash/nodes/security/threat_detection.py +1103 -0
  115. kailash/nodes/testing/__init__.py +9 -0
  116. kailash/nodes/testing/credential_testing.py +499 -0
  117. kailash/nodes/transform/__init__.py +10 -2
  118. kailash/nodes/transform/chunkers.py +592 -1
  119. kailash/nodes/transform/processors.py +484 -14
  120. kailash/nodes/validation.py +321 -0
  121. kailash/runtime/access_controlled.py +1 -1
  122. kailash/runtime/async_local.py +41 -7
  123. kailash/runtime/docker.py +1 -1
  124. kailash/runtime/local.py +474 -55
  125. kailash/runtime/parallel.py +1 -1
  126. kailash/runtime/parallel_cyclic.py +1 -1
  127. kailash/runtime/testing.py +210 -2
  128. kailash/security.py +1 -1
  129. kailash/utils/migrations/__init__.py +25 -0
  130. kailash/utils/migrations/generator.py +433 -0
  131. kailash/utils/migrations/models.py +231 -0
  132. kailash/utils/migrations/runner.py +489 -0
  133. kailash/utils/secure_logging.py +342 -0
  134. kailash/workflow/__init__.py +16 -0
  135. kailash/workflow/cyclic_runner.py +3 -4
  136. kailash/workflow/graph.py +70 -2
  137. kailash/workflow/resilience.py +249 -0
  138. kailash/workflow/templates.py +726 -0
  139. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/METADATA +256 -20
  140. kailash-0.4.1.dist-info/RECORD +227 -0
  141. kailash/api/__init__.py +0 -17
  142. kailash/api/__main__.py +0 -6
  143. kailash/api/studio_secure.py +0 -893
  144. kailash/mcp/__main__.py +0 -13
  145. kailash/mcp/server_new.py +0 -336
  146. kailash/mcp/servers/__init__.py +0 -12
  147. kailash-0.3.2.dist-info/RECORD +0 -136
  148. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/WHEEL +0 -0
  149. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/entry_points.txt +0 -0
  150. {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/licenses/LICENSE +0 -0
  151. {kailash-0.3.2.dist-info → kailash-0.4.1.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.1: Production-ready Alert Nodes with Discord integration and
7
+ AI Provider Vision Support. Rich Discord alerts with embeds, rate limiting,
8
+ and universal vision capabilities across OpenAI, Anthropic, and Ollama providers.
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.3.0"
37
+ __version__ = "0.4.1"
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
+ ]