kailash 0.1.4__py3-none-any.whl → 0.2.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/access_control.py +740 -0
- kailash/api/__main__.py +6 -0
- kailash/api/auth.py +668 -0
- kailash/api/custom_nodes.py +285 -0
- kailash/api/custom_nodes_secure.py +377 -0
- kailash/api/database.py +620 -0
- kailash/api/studio.py +915 -0
- kailash/api/studio_secure.py +893 -0
- kailash/mcp/__init__.py +53 -0
- kailash/mcp/__main__.py +13 -0
- kailash/mcp/ai_registry_server.py +712 -0
- kailash/mcp/client.py +447 -0
- kailash/mcp/client_new.py +334 -0
- kailash/mcp/server.py +293 -0
- kailash/mcp/server_new.py +336 -0
- kailash/mcp/servers/__init__.py +12 -0
- kailash/mcp/servers/ai_registry.py +289 -0
- kailash/nodes/__init__.py +4 -2
- kailash/nodes/ai/__init__.py +38 -0
- kailash/nodes/ai/a2a.py +1790 -0
- kailash/nodes/ai/agents.py +116 -2
- kailash/nodes/ai/ai_providers.py +206 -8
- kailash/nodes/ai/intelligent_agent_orchestrator.py +2108 -0
- kailash/nodes/ai/iterative_llm_agent.py +1280 -0
- kailash/nodes/ai/llm_agent.py +324 -1
- kailash/nodes/ai/self_organizing.py +1623 -0
- kailash/nodes/api/http.py +106 -25
- kailash/nodes/api/rest.py +116 -21
- kailash/nodes/base.py +15 -2
- kailash/nodes/base_async.py +45 -0
- kailash/nodes/base_cycle_aware.py +374 -0
- kailash/nodes/base_with_acl.py +338 -0
- kailash/nodes/code/python.py +135 -27
- kailash/nodes/data/readers.py +116 -53
- kailash/nodes/data/writers.py +16 -6
- kailash/nodes/logic/__init__.py +8 -0
- kailash/nodes/logic/async_operations.py +48 -9
- kailash/nodes/logic/convergence.py +642 -0
- kailash/nodes/logic/loop.py +153 -0
- kailash/nodes/logic/operations.py +212 -27
- kailash/nodes/logic/workflow.py +26 -18
- kailash/nodes/mixins/__init__.py +11 -0
- kailash/nodes/mixins/mcp.py +228 -0
- kailash/nodes/mixins.py +387 -0
- kailash/nodes/transform/__init__.py +8 -1
- kailash/nodes/transform/processors.py +119 -4
- kailash/runtime/__init__.py +2 -1
- kailash/runtime/access_controlled.py +458 -0
- kailash/runtime/local.py +106 -33
- kailash/runtime/parallel_cyclic.py +529 -0
- kailash/sdk_exceptions.py +90 -5
- kailash/security.py +845 -0
- kailash/tracking/manager.py +38 -15
- kailash/tracking/models.py +1 -1
- kailash/tracking/storage/filesystem.py +30 -2
- kailash/utils/__init__.py +8 -0
- kailash/workflow/__init__.py +18 -0
- kailash/workflow/convergence.py +270 -0
- kailash/workflow/cycle_analyzer.py +768 -0
- kailash/workflow/cycle_builder.py +573 -0
- kailash/workflow/cycle_config.py +709 -0
- kailash/workflow/cycle_debugger.py +760 -0
- kailash/workflow/cycle_exceptions.py +601 -0
- kailash/workflow/cycle_profiler.py +671 -0
- kailash/workflow/cycle_state.py +338 -0
- kailash/workflow/cyclic_runner.py +985 -0
- kailash/workflow/graph.py +500 -39
- kailash/workflow/migration.py +768 -0
- kailash/workflow/safety.py +365 -0
- kailash/workflow/templates.py +744 -0
- kailash/workflow/validation.py +693 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/METADATA +446 -13
- kailash-0.2.0.dist-info/RECORD +125 -0
- kailash/nodes/mcp/__init__.py +0 -11
- kailash/nodes/mcp/client.py +0 -554
- kailash/nodes/mcp/resource.py +0 -682
- kailash/nodes/mcp/server.py +0 -577
- kailash-0.1.4.dist-info/RECORD +0 -85
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
@@ -7,8 +7,117 @@ from kailash.nodes.base import Node, NodeParameter, register_node
|
|
7
7
|
|
8
8
|
|
9
9
|
@register_node()
|
10
|
-
class
|
11
|
-
"""
|
10
|
+
class FilterNode(Node):
|
11
|
+
"""
|
12
|
+
Filters data based on configurable conditions and operators.
|
13
|
+
|
14
|
+
This node provides flexible data filtering capabilities for lists and collections,
|
15
|
+
supporting various comparison operators and field-based filtering for structured
|
16
|
+
data. It's designed to work seamlessly in data processing pipelines, reducing
|
17
|
+
datasets to items that match specific criteria.
|
18
|
+
|
19
|
+
Design Philosophy:
|
20
|
+
The FilterNode embodies the principle of "declarative data selection." Rather
|
21
|
+
than writing custom filtering code, users declare their filtering criteria
|
22
|
+
through simple configuration. The design supports both simple value filtering
|
23
|
+
and complex field-based filtering for dictionaries, making it versatile for
|
24
|
+
various data structures.
|
25
|
+
|
26
|
+
Upstream Dependencies:
|
27
|
+
- Data source nodes providing lists to filter
|
28
|
+
- Transform nodes producing structured data
|
29
|
+
- Aggregation nodes generating collections
|
30
|
+
- API nodes returning result sets
|
31
|
+
- File readers loading datasets
|
32
|
+
|
33
|
+
Downstream Consumers:
|
34
|
+
- Processing nodes working with filtered subsets
|
35
|
+
- Aggregation nodes summarizing filtered data
|
36
|
+
- Writer nodes exporting filtered results
|
37
|
+
- Visualization nodes displaying subsets
|
38
|
+
- Decision nodes based on filter results
|
39
|
+
|
40
|
+
Configuration:
|
41
|
+
The node supports flexible filtering options:
|
42
|
+
- Field selection for dictionary filtering
|
43
|
+
- Multiple comparison operators
|
44
|
+
- Type-aware comparisons
|
45
|
+
- Null value handling
|
46
|
+
- String contains operations
|
47
|
+
|
48
|
+
Implementation Details:
|
49
|
+
- Handles lists of any type (dicts, primitives, objects)
|
50
|
+
- Type coercion for numeric comparisons
|
51
|
+
- Null-safe operations
|
52
|
+
- String conversion for contains operator
|
53
|
+
- Preserves original data structure
|
54
|
+
- Zero-copy filtering (returns references)
|
55
|
+
|
56
|
+
Error Handling:
|
57
|
+
- Graceful handling of type mismatches
|
58
|
+
- Null value comparison logic
|
59
|
+
- Empty data returns empty result
|
60
|
+
- Invalid field names return no matches
|
61
|
+
- Operator errors fail safely
|
62
|
+
|
63
|
+
Side Effects:
|
64
|
+
- No side effects (pure function)
|
65
|
+
- Does not modify input data
|
66
|
+
- Returns new filtered list
|
67
|
+
|
68
|
+
Examples:
|
69
|
+
>>> # Filter list of numbers
|
70
|
+
>>> filter_node = FilterNode()
|
71
|
+
>>> result = filter_node.run(
|
72
|
+
... data=[1, 2, 3, 4, 5],
|
73
|
+
... operator=">",
|
74
|
+
... value=3
|
75
|
+
... )
|
76
|
+
>>> assert result["filtered_data"] == [4, 5]
|
77
|
+
>>>
|
78
|
+
>>> # Filter list of dictionaries by field
|
79
|
+
>>> users = [
|
80
|
+
... {"name": "Alice", "age": 30},
|
81
|
+
... {"name": "Bob", "age": 25},
|
82
|
+
... {"name": "Charlie", "age": 35}
|
83
|
+
... ]
|
84
|
+
>>> result = filter_node.run(
|
85
|
+
... data=users,
|
86
|
+
... field="age",
|
87
|
+
... operator=">=",
|
88
|
+
... value=30
|
89
|
+
... )
|
90
|
+
>>> assert len(result["filtered_data"]) == 2
|
91
|
+
>>> assert result["filtered_data"][0]["name"] == "Alice"
|
92
|
+
>>>
|
93
|
+
>>> # String contains filtering
|
94
|
+
>>> items = [
|
95
|
+
... {"title": "Python Programming"},
|
96
|
+
... {"title": "Java Development"},
|
97
|
+
... {"title": "Python for Data Science"}
|
98
|
+
... ]
|
99
|
+
>>> result = filter_node.run(
|
100
|
+
... data=items,
|
101
|
+
... field="title",
|
102
|
+
... operator="contains",
|
103
|
+
... value="Python"
|
104
|
+
... )
|
105
|
+
>>> assert len(result["filtered_data"]) == 2
|
106
|
+
>>>
|
107
|
+
>>> # Null value handling
|
108
|
+
>>> data_with_nulls = [
|
109
|
+
... {"value": 10},
|
110
|
+
... {"value": None},
|
111
|
+
... {"value": 20}
|
112
|
+
... ]
|
113
|
+
>>> result = filter_node.run(
|
114
|
+
... data=data_with_nulls,
|
115
|
+
... field="value",
|
116
|
+
... operator="!=",
|
117
|
+
... value=None
|
118
|
+
... )
|
119
|
+
>>> assert len(result["filtered_data"]) == 2
|
120
|
+
"""
|
12
121
|
|
13
122
|
def get_parameters(self) -> Dict[str, NodeParameter]:
|
14
123
|
return {
|
@@ -67,8 +176,10 @@ class Filter(Node):
|
|
67
176
|
try:
|
68
177
|
# Handle None values - they fail most comparisons
|
69
178
|
if item_value is None:
|
70
|
-
if operator
|
71
|
-
return
|
179
|
+
if operator == "==":
|
180
|
+
return compare_value is None
|
181
|
+
elif operator == "!=":
|
182
|
+
return compare_value is not None
|
72
183
|
else:
|
73
184
|
return False # None fails all other comparisons
|
74
185
|
|
@@ -379,3 +490,7 @@ class Sort(Node):
|
|
379
490
|
sorted_data = sorted(data, reverse=reverse)
|
380
491
|
|
381
492
|
return {"sorted_data": sorted_data}
|
493
|
+
|
494
|
+
|
495
|
+
# Backward compatibility aliases
|
496
|
+
Filter = FilterNode
|
kailash/runtime/__init__.py
CHANGED
@@ -1,6 +1,7 @@
|
|
1
1
|
"""Runtime engines for the Kailash SDK."""
|
2
2
|
|
3
3
|
from kailash.runtime.local import LocalRuntime
|
4
|
+
from kailash.runtime.parallel_cyclic import ParallelCyclicRuntime
|
4
5
|
from kailash.runtime.runner import WorkflowRunner
|
5
6
|
|
6
|
-
__all__ = ["LocalRuntime", "WorkflowRunner"]
|
7
|
+
__all__ = ["LocalRuntime", "ParallelCyclicRuntime", "WorkflowRunner"]
|
@@ -0,0 +1,458 @@
|
|
1
|
+
"""
|
2
|
+
Access-Controlled Runtime for Kailash SDK
|
3
|
+
|
4
|
+
This module provides an access-controlled runtime that wraps the standard runtime
|
5
|
+
to add permission checks. The standard runtime remains unchanged, ensuring complete
|
6
|
+
backward compatibility.
|
7
|
+
|
8
|
+
Users who don't need access control continue using LocalRuntime as normal.
|
9
|
+
Users who need access control use AccessControlledRuntime instead.
|
10
|
+
|
11
|
+
Example without access control (existing code):
|
12
|
+
>>> from kailash.runtime.local import LocalRuntime
|
13
|
+
>>> from kailash.workflow import Workflow
|
14
|
+
>>> runtime = LocalRuntime()
|
15
|
+
>>> workflow = Workflow(workflow_id="test", name="Test")
|
16
|
+
>>> result, run_id = runtime.execute(workflow) # Works exactly as before
|
17
|
+
|
18
|
+
Example with access control (opt-in):
|
19
|
+
>>> from kailash.runtime.access_controlled import AccessControlledRuntime
|
20
|
+
>>> from kailash.access_control import UserContext, get_access_control_manager
|
21
|
+
>>> user = UserContext(user_id="123", tenant_id="abc", email="user@test.com", roles=["analyst"])
|
22
|
+
>>> runtime = AccessControlledRuntime(user_context=user)
|
23
|
+
>>> # Access control manager is disabled by default for compatibility
|
24
|
+
>>> acm = get_access_control_manager()
|
25
|
+
>>> acm.enabled # Should be False by default
|
26
|
+
False
|
27
|
+
"""
|
28
|
+
|
29
|
+
import logging
|
30
|
+
from typing import Any, Dict, List, Optional, Tuple
|
31
|
+
|
32
|
+
from kailash.access_control import (
|
33
|
+
AccessControlManager,
|
34
|
+
NodePermission,
|
35
|
+
PermissionEffect,
|
36
|
+
PermissionRule,
|
37
|
+
UserContext,
|
38
|
+
WorkflowPermission,
|
39
|
+
get_access_control_manager,
|
40
|
+
)
|
41
|
+
from kailash.nodes.base import Node
|
42
|
+
from kailash.runtime.local import LocalRuntime
|
43
|
+
from kailash.workflow import Workflow
|
44
|
+
|
45
|
+
logger = logging.getLogger(__name__)
|
46
|
+
|
47
|
+
|
48
|
+
class AccessControlledRuntime:
|
49
|
+
"""
|
50
|
+
Runtime with transparent access control layer.
|
51
|
+
|
52
|
+
This runtime wraps the standard LocalRuntime and adds access control
|
53
|
+
checks without modifying the original runtime or requiring any changes
|
54
|
+
to existing nodes or workflows.
|
55
|
+
|
56
|
+
Design Purpose:
|
57
|
+
Provides a drop-in replacement for LocalRuntime that adds security
|
58
|
+
without breaking existing workflows. Enables role-based access control,
|
59
|
+
data masking, and conditional execution based on user permissions.
|
60
|
+
|
61
|
+
Upstream Dependencies:
|
62
|
+
- AccessControlManager for permission evaluation
|
63
|
+
- UserContext from authentication systems
|
64
|
+
- LocalRuntime for actual workflow execution
|
65
|
+
- PermissionRule definitions from configuration
|
66
|
+
|
67
|
+
Downstream Consumers:
|
68
|
+
- Applications requiring secure workflow execution
|
69
|
+
- Multi-tenant systems with user isolation
|
70
|
+
- Audit systems for compliance logging
|
71
|
+
- Data governance systems for access tracking
|
72
|
+
|
73
|
+
Usage Patterns:
|
74
|
+
- Used as direct replacement for LocalRuntime
|
75
|
+
- Configured with user context during initialization
|
76
|
+
- Integrates with JWT authentication systems
|
77
|
+
- Supports both workflow and node-level permissions
|
78
|
+
|
79
|
+
Implementation Details:
|
80
|
+
Wraps LocalRuntime and intercepts workflow execution to add
|
81
|
+
permission checks. Creates access-controlled node wrappers that
|
82
|
+
evaluate permissions before execution. Supports data masking,
|
83
|
+
conditional routing, and fallback execution.
|
84
|
+
|
85
|
+
Error Handling:
|
86
|
+
- Access denied raises PermissionError with clear messages
|
87
|
+
- Missing permissions default to deny for security
|
88
|
+
- Configuration errors are logged and treated as disabled
|
89
|
+
- Evaluation errors fall back to base runtime behavior
|
90
|
+
|
91
|
+
Side Effects:
|
92
|
+
- Logs all access decisions for audit purposes
|
93
|
+
- May redirect execution to alternative nodes
|
94
|
+
- Applies data masking to sensitive outputs
|
95
|
+
- Caches permission decisions for performance
|
96
|
+
|
97
|
+
Example:
|
98
|
+
>>> from kailash.runtime.access_controlled import AccessControlledRuntime
|
99
|
+
>>> from kailash.access_control import UserContext
|
100
|
+
>>> from kailash.workflow import Workflow
|
101
|
+
>>>
|
102
|
+
>>> user = UserContext(user_id="123", tenant_id="abc", email="user@test.com", roles=["analyst"])
|
103
|
+
>>> runtime = AccessControlledRuntime(user_context=user)
|
104
|
+
>>> # By default, access control is disabled for backward compatibility
|
105
|
+
>>> workflow = Workflow(workflow_id="test", name="Test Workflow")
|
106
|
+
>>> isinstance(runtime, AccessControlledRuntime)
|
107
|
+
True
|
108
|
+
"""
|
109
|
+
|
110
|
+
def __init__(
|
111
|
+
self, user_context: UserContext, base_runtime: Optional[LocalRuntime] = None
|
112
|
+
):
|
113
|
+
"""
|
114
|
+
Initialize access-controlled runtime.
|
115
|
+
|
116
|
+
Args:
|
117
|
+
user_context: The user context for access control decisions
|
118
|
+
base_runtime: The underlying runtime to use (defaults to LocalRuntime)
|
119
|
+
"""
|
120
|
+
self.user_context = user_context
|
121
|
+
self.base_runtime = base_runtime or LocalRuntime()
|
122
|
+
self.acm = get_access_control_manager()
|
123
|
+
|
124
|
+
# Track skipped nodes for alternative routing
|
125
|
+
self._skipped_nodes: set[str] = set()
|
126
|
+
self._node_outputs: Dict[str, Any] = {}
|
127
|
+
|
128
|
+
def execute(
|
129
|
+
self, workflow: Workflow, parameters: Optional[Dict[str, Any]] = None
|
130
|
+
) -> Tuple[Any, str]:
|
131
|
+
"""
|
132
|
+
Execute workflow with access control.
|
133
|
+
|
134
|
+
This method has the exact same signature as the standard runtime,
|
135
|
+
ensuring complete compatibility.
|
136
|
+
"""
|
137
|
+
# Check workflow-level access
|
138
|
+
workflow_decision = self.acm.check_workflow_access(
|
139
|
+
self.user_context, workflow.workflow_id, WorkflowPermission.EXECUTE
|
140
|
+
)
|
141
|
+
|
142
|
+
if not workflow_decision.allowed:
|
143
|
+
raise PermissionError(f"Access denied: {workflow_decision.reason}")
|
144
|
+
|
145
|
+
# For simplicity, directly execute with the base runtime
|
146
|
+
# In a full implementation, we would wrap nodes or intercept execution
|
147
|
+
# But for this example, we'll rely on the nodes having access control attributes
|
148
|
+
return self.base_runtime.execute(workflow, parameters)
|
149
|
+
|
150
|
+
def _create_controlled_workflow(self, workflow: Workflow) -> Workflow:
|
151
|
+
"""
|
152
|
+
Create a workflow wrapper that enforces access control.
|
153
|
+
|
154
|
+
This wrapper intercepts node execution to add permission checks
|
155
|
+
without modifying the original workflow.
|
156
|
+
"""
|
157
|
+
# Create a new workflow instance
|
158
|
+
controlled = Workflow(
|
159
|
+
workflow_id=workflow.workflow_id,
|
160
|
+
name=workflow.name,
|
161
|
+
description=workflow.description,
|
162
|
+
version=workflow.version,
|
163
|
+
)
|
164
|
+
|
165
|
+
# Copy graph structure
|
166
|
+
controlled.graph = workflow.graph.copy()
|
167
|
+
|
168
|
+
# Wrap each node with access control
|
169
|
+
for node_id in workflow.graph.nodes:
|
170
|
+
node_data = workflow.graph.nodes[node_id]
|
171
|
+
original_node = node_data.get("node")
|
172
|
+
|
173
|
+
if original_node:
|
174
|
+
# Create access-controlled wrapper for the node
|
175
|
+
wrapped_node = self._create_controlled_node(node_id, original_node)
|
176
|
+
controlled.graph.nodes[node_id]["node"] = wrapped_node
|
177
|
+
|
178
|
+
return controlled
|
179
|
+
|
180
|
+
def _create_controlled_node(self, node_id: str, original_node: Node) -> Node:
|
181
|
+
"""
|
182
|
+
Create an access-controlled wrapper for a node.
|
183
|
+
|
184
|
+
This wrapper intercepts the node's run() method to add permission
|
185
|
+
checks without modifying the original node.
|
186
|
+
"""
|
187
|
+
runtime = self # Capture runtime reference
|
188
|
+
|
189
|
+
class AccessControlledNodeWrapper(Node):
|
190
|
+
"""Dynamic wrapper that adds access control to any node"""
|
191
|
+
|
192
|
+
def __init__(self):
|
193
|
+
# Don't initialize Node base class, just store reference
|
194
|
+
self._original_node = original_node
|
195
|
+
self._node_id = node_id
|
196
|
+
# Copy all attributes from original node
|
197
|
+
for attr, value in original_node.__dict__.items():
|
198
|
+
if not attr.startswith("_"):
|
199
|
+
setattr(self, attr, value)
|
200
|
+
|
201
|
+
def get_parameters(self):
|
202
|
+
"""Delegate to original node"""
|
203
|
+
return self._original_node.get_parameters()
|
204
|
+
|
205
|
+
def validate_config(self):
|
206
|
+
"""Delegate to original node if it has the method"""
|
207
|
+
if hasattr(self._original_node, "validate_config"):
|
208
|
+
return self._original_node.validate_config()
|
209
|
+
return True
|
210
|
+
|
211
|
+
def get_output_schema(self):
|
212
|
+
"""Delegate to original node"""
|
213
|
+
if hasattr(self._original_node, "get_output_schema"):
|
214
|
+
return self._original_node.get_output_schema()
|
215
|
+
return None
|
216
|
+
|
217
|
+
def run(self, **inputs) -> Any:
|
218
|
+
"""Execute with access control checks"""
|
219
|
+
# Check execute permission
|
220
|
+
execute_decision = runtime.acm.check_node_access(
|
221
|
+
runtime.user_context,
|
222
|
+
self._node_id,
|
223
|
+
NodePermission.EXECUTE,
|
224
|
+
runtime_context={"inputs": inputs},
|
225
|
+
)
|
226
|
+
|
227
|
+
if not execute_decision.allowed:
|
228
|
+
# Node execution denied
|
229
|
+
logger.info(
|
230
|
+
f"Node {self._node_id} skipped for user {runtime.user_context.user_id}"
|
231
|
+
)
|
232
|
+
runtime._skipped_nodes.add(self._node_id)
|
233
|
+
|
234
|
+
# Check if there's an alternative path
|
235
|
+
if execute_decision.redirect_node:
|
236
|
+
return {"_redirect_to": execute_decision.redirect_node}
|
237
|
+
|
238
|
+
# Return empty result
|
239
|
+
return {}
|
240
|
+
|
241
|
+
# Execute the original node
|
242
|
+
result = self._original_node.run(**inputs)
|
243
|
+
|
244
|
+
# Check output read permission
|
245
|
+
output_decision = runtime.acm.check_node_access(
|
246
|
+
runtime.user_context,
|
247
|
+
self._node_id,
|
248
|
+
NodePermission.READ_OUTPUT,
|
249
|
+
runtime_context={"output": result},
|
250
|
+
)
|
251
|
+
|
252
|
+
if not output_decision.allowed:
|
253
|
+
# Mask entire output
|
254
|
+
return {"_access_denied": True}
|
255
|
+
|
256
|
+
# Apply field masking if needed
|
257
|
+
if output_decision.masked_fields and isinstance(result, dict):
|
258
|
+
result = runtime._mask_fields(result, output_decision.masked_fields)
|
259
|
+
|
260
|
+
# Store output for conditional routing
|
261
|
+
runtime._node_outputs[self._node_id] = result
|
262
|
+
|
263
|
+
return result
|
264
|
+
|
265
|
+
# Create instance of wrapper
|
266
|
+
wrapper = AccessControlledNodeWrapper()
|
267
|
+
|
268
|
+
# Preserve node metadata
|
269
|
+
wrapper.__class__.__name__ = f"Controlled{original_node.__class__.__name__}"
|
270
|
+
wrapper.__class__.__module__ = original_node.__class__.__module__
|
271
|
+
|
272
|
+
return wrapper
|
273
|
+
|
274
|
+
@staticmethod
|
275
|
+
def _mask_fields(data: Dict[str, Any], fields: List[str]) -> Dict[str, Any]:
|
276
|
+
"""Mask sensitive fields in data"""
|
277
|
+
masked = data.copy()
|
278
|
+
for field in fields:
|
279
|
+
if field in masked:
|
280
|
+
masked[field] = "***MASKED***"
|
281
|
+
return masked
|
282
|
+
|
283
|
+
def _handle_conditional_routing(
|
284
|
+
self, node_id: str, true_path: List[str], false_path: List[str]
|
285
|
+
) -> List[str]:
|
286
|
+
"""
|
287
|
+
Determine which path to take based on permissions.
|
288
|
+
|
289
|
+
This is used for conditional nodes where the path depends on
|
290
|
+
user permissions rather than data conditions.
|
291
|
+
"""
|
292
|
+
# Check which path the user has access to
|
293
|
+
return self.acm.get_permission_based_route(
|
294
|
+
self.user_context, node_id, true_path, false_path
|
295
|
+
)
|
296
|
+
|
297
|
+
|
298
|
+
class AccessControlConfig:
|
299
|
+
"""
|
300
|
+
Configuration for access control in workflows.
|
301
|
+
|
302
|
+
Provides a declarative way to define access rules without modifying
|
303
|
+
workflow code. Enables administrators to configure permissions
|
304
|
+
externally from workflow definitions.
|
305
|
+
|
306
|
+
Design Purpose:
|
307
|
+
Separates access control policy from workflow implementation,
|
308
|
+
enabling dynamic permission changes without code modifications.
|
309
|
+
Supports both workflow-level and node-level permission rules.
|
310
|
+
|
311
|
+
Upstream Dependencies:
|
312
|
+
- Administrative interfaces for rule creation
|
313
|
+
- Configuration management systems
|
314
|
+
- Policy definition templates
|
315
|
+
|
316
|
+
Downstream Consumers:
|
317
|
+
- AccessControlManager for rule application
|
318
|
+
- AccessControlledRuntime for secure execution
|
319
|
+
- Policy management tools for validation
|
320
|
+
|
321
|
+
Usage Patterns:
|
322
|
+
- Created by administrators or configuration systems
|
323
|
+
- Applied to workflows before execution
|
324
|
+
- Used for testing different access scenarios
|
325
|
+
- Integrated with external policy management
|
326
|
+
|
327
|
+
Implementation Details:
|
328
|
+
Maintains list of PermissionRule objects with helper methods
|
329
|
+
for adding common rule types. Rules are applied to manager
|
330
|
+
in batch for consistency.
|
331
|
+
|
332
|
+
Example:
|
333
|
+
>>> config = AccessControlConfig()
|
334
|
+
>>> config.add_workflow_permission(
|
335
|
+
... workflow_id="analytics",
|
336
|
+
... permission=WorkflowPermission.EXECUTE,
|
337
|
+
... role="analyst"
|
338
|
+
... )
|
339
|
+
>>> config.add_node_permission(
|
340
|
+
... workflow_id="analytics",
|
341
|
+
... node_id="sensitive_data",
|
342
|
+
... permission=NodePermission.READ_OUTPUT,
|
343
|
+
... role="admin"
|
344
|
+
... )
|
345
|
+
"""
|
346
|
+
|
347
|
+
def __init__(self):
|
348
|
+
self.rules: List[PermissionRule] = []
|
349
|
+
|
350
|
+
def add_workflow_permission(
|
351
|
+
self,
|
352
|
+
workflow_id: str,
|
353
|
+
permission: WorkflowPermission,
|
354
|
+
user_id: Optional[str] = None,
|
355
|
+
role: Optional[str] = None,
|
356
|
+
effect: PermissionEffect = PermissionEffect.ALLOW,
|
357
|
+
):
|
358
|
+
"""Add a workflow-level permission rule"""
|
359
|
+
rule = PermissionRule(
|
360
|
+
id=f"workflow_{workflow_id}_{permission.value}_{len(self.rules)}",
|
361
|
+
resource_type="workflow",
|
362
|
+
resource_id=workflow_id,
|
363
|
+
permission=permission,
|
364
|
+
effect=effect,
|
365
|
+
user_id=user_id,
|
366
|
+
role=role,
|
367
|
+
)
|
368
|
+
self.rules.append(rule)
|
369
|
+
|
370
|
+
def add_node_permission(
|
371
|
+
self,
|
372
|
+
workflow_id: str,
|
373
|
+
node_id: str,
|
374
|
+
permission: NodePermission,
|
375
|
+
user_id: Optional[str] = None,
|
376
|
+
role: Optional[str] = None,
|
377
|
+
effect: PermissionEffect = PermissionEffect.ALLOW,
|
378
|
+
masked_fields: Optional[List[str]] = None,
|
379
|
+
redirect_node: Optional[str] = None,
|
380
|
+
):
|
381
|
+
"""Add a node-level permission rule"""
|
382
|
+
rule = PermissionRule(
|
383
|
+
id=f"node_{workflow_id}_{node_id}_{permission.value}_{len(self.rules)}",
|
384
|
+
resource_type="node",
|
385
|
+
resource_id=node_id,
|
386
|
+
permission=permission,
|
387
|
+
effect=effect,
|
388
|
+
user_id=user_id,
|
389
|
+
role=role,
|
390
|
+
)
|
391
|
+
|
392
|
+
if masked_fields:
|
393
|
+
rule.conditions["masked_fields"] = masked_fields
|
394
|
+
|
395
|
+
if redirect_node:
|
396
|
+
rule.conditions["redirect_node"] = redirect_node
|
397
|
+
|
398
|
+
self.rules.append(rule)
|
399
|
+
|
400
|
+
def apply_to_manager(self, manager: AccessControlManager):
|
401
|
+
"""Apply all rules to an access control manager"""
|
402
|
+
for rule in self.rules:
|
403
|
+
manager.add_rule(rule)
|
404
|
+
|
405
|
+
|
406
|
+
def execute_with_access_control(
|
407
|
+
workflow: Workflow,
|
408
|
+
user_context: UserContext,
|
409
|
+
parameters: Optional[Dict[str, Any]] = None,
|
410
|
+
access_config: Optional[AccessControlConfig] = None,
|
411
|
+
) -> Tuple[Any, str]:
|
412
|
+
"""
|
413
|
+
Convenience function to execute a workflow with access control.
|
414
|
+
|
415
|
+
Provides a simple way to execute workflows with access control without
|
416
|
+
manually creating runtime instances. Automatically applies access
|
417
|
+
configuration and manages the runtime lifecycle.
|
418
|
+
|
419
|
+
Args:
|
420
|
+
workflow: The workflow to execute
|
421
|
+
user_context: User context for access control decisions
|
422
|
+
parameters: Optional runtime parameters for workflow execution
|
423
|
+
access_config: Optional access control configuration to apply
|
424
|
+
|
425
|
+
Returns:
|
426
|
+
Tuple containing:
|
427
|
+
- result: The workflow execution result
|
428
|
+
- run_id: Unique identifier for this execution run
|
429
|
+
|
430
|
+
Raises:
|
431
|
+
PermissionError: If user lacks permission to execute workflow
|
432
|
+
ValueError: If workflow or user_context is invalid
|
433
|
+
|
434
|
+
Side Effects:
|
435
|
+
- Applies access control rules to global manager if config provided
|
436
|
+
- Logs audit events for access decisions
|
437
|
+
- Enables access control globally during execution
|
438
|
+
|
439
|
+
Example:
|
440
|
+
>>> from kailash.runtime.access_controlled import execute_with_access_control
|
441
|
+
>>> from kailash.access_control import UserContext
|
442
|
+
>>> from kailash.workflow import Workflow
|
443
|
+
>>>
|
444
|
+
>>> user = UserContext(user_id="123", tenant_id="abc", email="user@test.com", roles=["viewer"])
|
445
|
+
>>> workflow = Workflow(workflow_id="test", name="Test")
|
446
|
+
>>> # Function exists and can be called
|
447
|
+
>>> callable(execute_with_access_control)
|
448
|
+
True
|
449
|
+
"""
|
450
|
+
# Set up access control if config provided
|
451
|
+
if access_config:
|
452
|
+
acm = get_access_control_manager()
|
453
|
+
access_config.apply_to_manager(acm)
|
454
|
+
acm.enabled = True # Enable access control
|
455
|
+
|
456
|
+
# Create runtime and execute
|
457
|
+
runtime = AccessControlledRuntime(user_context)
|
458
|
+
return runtime.execute(workflow, parameters)
|