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.
Files changed (83) hide show
  1. kailash/__init__.py +1 -1
  2. kailash/access_control.py +740 -0
  3. kailash/api/__main__.py +6 -0
  4. kailash/api/auth.py +668 -0
  5. kailash/api/custom_nodes.py +285 -0
  6. kailash/api/custom_nodes_secure.py +377 -0
  7. kailash/api/database.py +620 -0
  8. kailash/api/studio.py +915 -0
  9. kailash/api/studio_secure.py +893 -0
  10. kailash/mcp/__init__.py +53 -0
  11. kailash/mcp/__main__.py +13 -0
  12. kailash/mcp/ai_registry_server.py +712 -0
  13. kailash/mcp/client.py +447 -0
  14. kailash/mcp/client_new.py +334 -0
  15. kailash/mcp/server.py +293 -0
  16. kailash/mcp/server_new.py +336 -0
  17. kailash/mcp/servers/__init__.py +12 -0
  18. kailash/mcp/servers/ai_registry.py +289 -0
  19. kailash/nodes/__init__.py +4 -2
  20. kailash/nodes/ai/__init__.py +38 -0
  21. kailash/nodes/ai/a2a.py +1790 -0
  22. kailash/nodes/ai/agents.py +116 -2
  23. kailash/nodes/ai/ai_providers.py +206 -8
  24. kailash/nodes/ai/intelligent_agent_orchestrator.py +2108 -0
  25. kailash/nodes/ai/iterative_llm_agent.py +1280 -0
  26. kailash/nodes/ai/llm_agent.py +324 -1
  27. kailash/nodes/ai/self_organizing.py +1623 -0
  28. kailash/nodes/api/http.py +106 -25
  29. kailash/nodes/api/rest.py +116 -21
  30. kailash/nodes/base.py +15 -2
  31. kailash/nodes/base_async.py +45 -0
  32. kailash/nodes/base_cycle_aware.py +374 -0
  33. kailash/nodes/base_with_acl.py +338 -0
  34. kailash/nodes/code/python.py +135 -27
  35. kailash/nodes/data/readers.py +116 -53
  36. kailash/nodes/data/writers.py +16 -6
  37. kailash/nodes/logic/__init__.py +8 -0
  38. kailash/nodes/logic/async_operations.py +48 -9
  39. kailash/nodes/logic/convergence.py +642 -0
  40. kailash/nodes/logic/loop.py +153 -0
  41. kailash/nodes/logic/operations.py +212 -27
  42. kailash/nodes/logic/workflow.py +26 -18
  43. kailash/nodes/mixins/__init__.py +11 -0
  44. kailash/nodes/mixins/mcp.py +228 -0
  45. kailash/nodes/mixins.py +387 -0
  46. kailash/nodes/transform/__init__.py +8 -1
  47. kailash/nodes/transform/processors.py +119 -4
  48. kailash/runtime/__init__.py +2 -1
  49. kailash/runtime/access_controlled.py +458 -0
  50. kailash/runtime/local.py +106 -33
  51. kailash/runtime/parallel_cyclic.py +529 -0
  52. kailash/sdk_exceptions.py +90 -5
  53. kailash/security.py +845 -0
  54. kailash/tracking/manager.py +38 -15
  55. kailash/tracking/models.py +1 -1
  56. kailash/tracking/storage/filesystem.py +30 -2
  57. kailash/utils/__init__.py +8 -0
  58. kailash/workflow/__init__.py +18 -0
  59. kailash/workflow/convergence.py +270 -0
  60. kailash/workflow/cycle_analyzer.py +768 -0
  61. kailash/workflow/cycle_builder.py +573 -0
  62. kailash/workflow/cycle_config.py +709 -0
  63. kailash/workflow/cycle_debugger.py +760 -0
  64. kailash/workflow/cycle_exceptions.py +601 -0
  65. kailash/workflow/cycle_profiler.py +671 -0
  66. kailash/workflow/cycle_state.py +338 -0
  67. kailash/workflow/cyclic_runner.py +985 -0
  68. kailash/workflow/graph.py +500 -39
  69. kailash/workflow/migration.py +768 -0
  70. kailash/workflow/safety.py +365 -0
  71. kailash/workflow/templates.py +744 -0
  72. kailash/workflow/validation.py +693 -0
  73. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/METADATA +446 -13
  74. kailash-0.2.0.dist-info/RECORD +125 -0
  75. kailash/nodes/mcp/__init__.py +0 -11
  76. kailash/nodes/mcp/client.py +0 -554
  77. kailash/nodes/mcp/resource.py +0 -682
  78. kailash/nodes/mcp/server.py +0 -577
  79. kailash-0.1.4.dist-info/RECORD +0 -85
  80. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/WHEEL +0 -0
  81. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/entry_points.txt +0 -0
  82. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/licenses/LICENSE +0 -0
  83. {kailash-0.1.4.dist-info → kailash-0.2.0.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,740 @@
1
+ """
2
+ Access Control System for Kailash SDK
3
+
4
+ This module provides fine-grained access control at the workflow and node level,
5
+ enabling permission-based execution paths and data access restrictions.
6
+
7
+ Key Features:
8
+ - Node-level permissions (execute, read_output, write_input)
9
+ - Workflow-level permissions (view, execute, modify)
10
+ - Permission-based conditional routing
11
+ - Data masking for restricted nodes
12
+ - Audit logging of access attempts
13
+ - Integration with JWT authentication
14
+
15
+ Design Philosophy:
16
+ - Fail-safe defaults (deny by default)
17
+ - Minimal performance overhead
18
+ - Transparent to existing workflows
19
+ - Flexible permission models
20
+
21
+ Usage:
22
+ >>> from kailash.access_control import AccessControlManager, NodePermission, UserContext
23
+ >>> acm = AccessControlManager()
24
+ >>> user_context = UserContext(user_id="test", tenant_id="test", email="test@test.com")
25
+ >>> node_id = "test_node"
26
+ >>> decision = acm.check_node_access(user_context, node_id, NodePermission.EXECUTE)
27
+ >>> decision.allowed
28
+ False
29
+
30
+ Implementation:
31
+ Access control is enforced at multiple levels:
32
+ 1. Workflow level - Can user execute/view the workflow?
33
+ 2. Node level - Can user execute specific nodes?
34
+ 3. Data level - Can user see outputs from specific nodes?
35
+ 4. Routing level - Which path should user take based on permissions?
36
+
37
+ Security Considerations:
38
+ - Permissions are cached per execution for performance
39
+ - Access denied by default for unknown users/nodes
40
+ - All access attempts are logged for audit
41
+ - Sensitive data is masked, not just hidden
42
+
43
+ Testing:
44
+ See tests/test_access_control.py for comprehensive tests
45
+
46
+ Future Enhancements:
47
+ - Dynamic permission evaluation based on data
48
+ - Time-based access restrictions
49
+ - Delegation and impersonation support
50
+ """
51
+
52
+ import logging
53
+ import threading
54
+ from dataclasses import dataclass, field
55
+ from datetime import datetime, timezone
56
+ from enum import Enum
57
+ from typing import Any, Callable, Dict, List, Optional, Set, Union
58
+
59
+ logger = logging.getLogger(__name__)
60
+
61
+
62
+ class WorkflowPermission(Enum):
63
+ """Workflow-level permissions"""
64
+
65
+ VIEW = "view" # Can see workflow exists
66
+ EXECUTE = "execute" # Can run workflow
67
+ MODIFY = "modify" # Can edit workflow
68
+ DELETE = "delete" # Can delete workflow
69
+ SHARE = "share" # Can share with others
70
+ ADMIN = "admin" # Full control
71
+
72
+
73
+ class NodePermission(Enum):
74
+ """Node-level permissions"""
75
+
76
+ EXECUTE = "execute" # Can execute node
77
+ READ_OUTPUT = "read_output" # Can see node outputs
78
+ WRITE_INPUT = "write_input" # Can provide inputs
79
+ SKIP = "skip" # Node is skipped for user
80
+ MASK_OUTPUT = "mask_output" # Output is masked/redacted
81
+
82
+
83
+ class PermissionEffect(Enum):
84
+ """Effect of a permission rule"""
85
+
86
+ ALLOW = "allow"
87
+ DENY = "deny"
88
+ CONDITIONAL = "conditional" # Depends on runtime evaluation
89
+
90
+
91
+ @dataclass
92
+ class UserContext:
93
+ """
94
+ User context for access control decisions.
95
+
96
+ Contains all information needed to make access control decisions for a user,
97
+ including identity, tenant membership, roles, and session information.
98
+
99
+ Design Purpose:
100
+ Provides a standardized way to represent user identity and permissions
101
+ across the entire access control system. Enables fine-grained access
102
+ control based on user attributes, roles, and context.
103
+
104
+ Upstream Dependencies:
105
+ - Authentication systems (JWT, API keys)
106
+ - User management systems
107
+ - Tenant management systems
108
+
109
+ Downstream Consumers:
110
+ - AccessControlManager for permission checking
111
+ - AccessControlledRuntime for workflow execution
112
+ - Audit logging systems for tracking access
113
+
114
+ Usage Patterns:
115
+ - Created during authentication/login process
116
+ - Passed to all access control functions
117
+ - Used in workflow and node execution contexts
118
+ - Logged for audit and compliance purposes
119
+
120
+ Implementation Details:
121
+ Uses dataclass for efficient attribute access and comparison.
122
+ Immutable once created to prevent privilege escalation.
123
+ Supports custom attributes for extensible authorization.
124
+
125
+ Example:
126
+ >>> user = UserContext(
127
+ ... user_id="user123",
128
+ ... tenant_id="tenant001",
129
+ ... email="user@example.com",
130
+ ... roles=["analyst", "viewer"]
131
+ ... )
132
+ >>> print(user.user_id)
133
+ user123
134
+ """
135
+
136
+ user_id: str
137
+ tenant_id: str
138
+ email: str
139
+ roles: List[str] = field(default_factory=list)
140
+ permissions: List[str] = field(default_factory=list)
141
+ attributes: Dict[str, Any] = field(default_factory=dict) # Custom attributes
142
+ session_id: Optional[str] = None
143
+ ip_address: Optional[str] = None
144
+
145
+
146
+ @dataclass
147
+ class PermissionRule:
148
+ """
149
+ A single permission rule defining access control policies.
150
+
151
+ Represents a single access control rule that grants or denies permissions
152
+ to users for specific resources (workflows or nodes) based on their
153
+ identity, roles, and contextual conditions.
154
+
155
+ Design Purpose:
156
+ Provides a flexible, declarative way to define access control policies.
157
+ Supports role-based access control (RBAC), attribute-based access control (ABAC),
158
+ and conditional permissions based on runtime context.
159
+
160
+ Upstream Dependencies:
161
+ - Administrative interfaces for rule creation
162
+ - Policy management systems
163
+ - Configuration files or databases
164
+
165
+ Downstream Consumers:
166
+ - AccessControlManager for rule evaluation
167
+ - Audit systems for logging policy decisions
168
+ - Policy analysis tools for rule validation
169
+
170
+ Usage Patterns:
171
+ - Created by administrators to define access policies
172
+ - Evaluated during workflow and node execution
173
+ - Cached for performance optimization
174
+ - Updated when policies change
175
+
176
+ Implementation Details:
177
+ Uses dataclass for efficient serialization and comparison.
178
+ Supports priority-based rule ordering for conflict resolution.
179
+ Includes expiration for time-limited permissions.
180
+ Conditions enable complex policy logic.
181
+
182
+ Example:
183
+ >>> rule = PermissionRule(
184
+ ... id="allow_analysts_read",
185
+ ... resource_type="node",
186
+ ... resource_id="sensitive_data",
187
+ ... permission=NodePermission.READ_OUTPUT,
188
+ ... effect=PermissionEffect.ALLOW,
189
+ ... role="analyst"
190
+ ... )
191
+ >>> print(rule.id)
192
+ allow_analysts_read
193
+ """
194
+
195
+ id: str
196
+ resource_type: str # "workflow" or "node"
197
+ resource_id: str # workflow_id or node_id
198
+ permission: Union[WorkflowPermission, NodePermission]
199
+ effect: PermissionEffect
200
+
201
+ # Who does this apply to?
202
+ user_id: Optional[str] = None # Specific user
203
+ role: Optional[str] = None # Any user with role
204
+ tenant_id: Optional[str] = None # All users in tenant
205
+
206
+ # Conditions
207
+ conditions: Dict[str, Any] = field(default_factory=dict)
208
+
209
+ # Metadata
210
+ created_at: datetime = field(default_factory=lambda: datetime.now(timezone.utc))
211
+ created_by: Optional[str] = None
212
+ expires_at: Optional[datetime] = None
213
+ priority: int = 0 # Higher priority rules evaluated first
214
+
215
+
216
+ @dataclass
217
+ class AccessDecision:
218
+ """
219
+ Result of an access control decision.
220
+
221
+ Contains the outcome of evaluating access control rules for a specific
222
+ user and resource, including whether access is allowed, the reasoning,
223
+ and any additional actions required (like data masking).
224
+
225
+ Design Purpose:
226
+ Provides a comprehensive result object that captures not just the
227
+ allow/deny decision, but also the context and reasoning behind it.
228
+ Enables audit logging, debugging, and conditional execution.
229
+
230
+ Upstream Dependencies:
231
+ - AccessControlManager rule evaluation
232
+ - Permission rule matching logic
233
+ - Conditional evaluation systems
234
+
235
+ Downstream Consumers:
236
+ - AccessControlledRuntime for execution decisions
237
+ - Audit logging systems for compliance
238
+ - Error handling for access denial messages
239
+ - Data masking systems for output filtering
240
+
241
+ Usage Patterns:
242
+ - Returned by all access control check methods
243
+ - Logged for audit and debugging purposes
244
+ - Used to determine execution flow
245
+ - Provides user-friendly error messages
246
+
247
+ Implementation Details:
248
+ Immutable after creation to ensure decision integrity.
249
+ Includes applied rules for transparency and debugging.
250
+ Supports conditional decisions for complex scenarios.
251
+ Contains masking information for data protection.
252
+
253
+ Example:
254
+ >>> decision = AccessDecision(
255
+ ... allowed=True,
256
+ ... reason="User has analyst role",
257
+ ... masked_fields=["ssn", "phone"]
258
+ ... )
259
+ >>> print(decision.allowed)
260
+ True
261
+ """
262
+
263
+ allowed: bool
264
+ reason: str
265
+ applied_rules: List[PermissionRule] = field(default_factory=list)
266
+ conditions_met: Dict[str, bool] = field(default_factory=dict)
267
+ masked_fields: List[str] = field(default_factory=list) # Fields to mask in output
268
+ redirect_node: Optional[str] = None # Alternative node to execute
269
+
270
+
271
+ class ConditionEvaluator:
272
+ """Evaluates conditions for conditional permissions"""
273
+
274
+ def __init__(self):
275
+ self.evaluators: Dict[str, Callable] = {
276
+ "time_range": self._eval_time_range,
277
+ "data_contains": self._eval_data_contains,
278
+ "user_attribute": self._eval_user_attribute,
279
+ "ip_range": self._eval_ip_range,
280
+ "custom": self._eval_custom,
281
+ }
282
+
283
+ def evaluate(
284
+ self, condition_type: str, condition_value: Any, context: Dict[str, Any]
285
+ ) -> bool:
286
+ """Evaluate a condition"""
287
+ evaluator = self.evaluators.get(condition_type)
288
+ if not evaluator:
289
+ logger.warning(f"Unknown condition type: {condition_type}")
290
+ return False
291
+
292
+ try:
293
+ return evaluator(condition_value, context)
294
+ except Exception as e:
295
+ logger.error(f"Error evaluating condition {condition_type}: {e}")
296
+ return False
297
+
298
+ def _eval_time_range(self, value: Dict[str, str], context: Dict[str, Any]) -> bool:
299
+ """Check if current time is within range"""
300
+ from datetime import datetime, time
301
+
302
+ now = datetime.now().time()
303
+ start = time.fromisoformat(value.get("start", "00:00"))
304
+ end = time.fromisoformat(value.get("end", "23:59"))
305
+ return start <= now <= end
306
+
307
+ def _eval_data_contains(
308
+ self, value: Dict[str, Any], context: Dict[str, Any]
309
+ ) -> bool:
310
+ """Check if data contains specific values"""
311
+ data = context.get("data", {})
312
+ field = value.get("field")
313
+ expected = value.get("value")
314
+
315
+ if field and field in data:
316
+ return data[field] == expected
317
+ return False
318
+
319
+ def _eval_user_attribute(
320
+ self, value: Dict[str, Any], context: Dict[str, Any]
321
+ ) -> bool:
322
+ """Check user attributes"""
323
+ user = context.get("user")
324
+ if not user:
325
+ return False
326
+
327
+ attr_name = value.get("attribute")
328
+ expected = value.get("value")
329
+
330
+ return user.attributes.get(attr_name) == expected
331
+
332
+ def _eval_ip_range(self, value: Dict[str, Any], context: Dict[str, Any]) -> bool:
333
+ """Check if IP is in allowed range"""
334
+ # Simplified IP check - in production use ipaddress module
335
+ allowed_ips = value.get("allowed", [])
336
+ user_ip = context.get("user", {}).get("ip_address")
337
+
338
+ return user_ip in allowed_ips
339
+
340
+ def _eval_custom(self, value: Dict[str, Any], context: Dict[str, Any]) -> bool:
341
+ """Evaluate custom condition"""
342
+ # This would call a custom function registered by the user
343
+ return True
344
+
345
+
346
+ class AccessControlManager:
347
+ """
348
+ Main access control manager for the Kailash SDK.
349
+
350
+ Centralized manager for evaluating access control policies, managing permission
351
+ rules, and making authorization decisions for workflows and nodes. Provides
352
+ caching, audit logging, and conditional permission evaluation.
353
+
354
+ Design Purpose:
355
+ Serves as the single source of truth for access control decisions.
356
+ Separates policy definition from policy enforcement, enabling
357
+ flexible and maintainable security policies.
358
+
359
+ Upstream Dependencies:
360
+ - Administrative interfaces for rule management
361
+ - Configuration systems for policy definition
362
+ - Authentication systems for user context
363
+
364
+ Downstream Consumers:
365
+ - AccessControlledRuntime for workflow execution
366
+ - Node execution systems for permission checks
367
+ - Audit and monitoring systems for compliance
368
+ - Administrative tools for policy analysis
369
+
370
+ Usage Patterns:
371
+ - Created once per application/service instance
372
+ - Rules added during startup or configuration
373
+ - Permission checks made during execution
374
+ - Cache managed automatically for performance
375
+
376
+ Implementation Details:
377
+ Thread-safe with locking for cache management.
378
+ Rules evaluated in priority order with fail-safe defaults.
379
+ Conditional evaluation supports complex policies.
380
+ Audit logging for all access decisions.
381
+
382
+ Error Handling:
383
+ - Invalid rules are logged and ignored
384
+ - Missing permissions default to deny
385
+ - Evaluation errors are logged and treated as deny
386
+ - Cache errors fall back to direct evaluation
387
+
388
+ Side Effects:
389
+ - Logs audit events for all access decisions
390
+ - Caches results for performance optimization
391
+ - May trigger security alerts on repeated denials
392
+
393
+ Example:
394
+ >>> acm = AccessControlManager(enabled=True)
395
+ >>> rule = PermissionRule(
396
+ ... id="allow_admin",
397
+ ... resource_type="workflow",
398
+ ... resource_id="sensitive_workflow",
399
+ ... permission=WorkflowPermission.EXECUTE,
400
+ ... effect=PermissionEffect.ALLOW,
401
+ ... role="admin"
402
+ ... )
403
+ >>> acm.add_rule(rule)
404
+ >>> user = UserContext(user_id="admin1", tenant_id="test", email="admin@test.com", roles=["admin"])
405
+ >>> decision = acm.check_workflow_access(user, "sensitive_workflow", WorkflowPermission.EXECUTE)
406
+ >>> decision.allowed
407
+ True
408
+ """
409
+
410
+ def __init__(self, enabled: bool = False):
411
+ self.enabled = enabled # Disabled by default
412
+ self.rules: List[PermissionRule] = []
413
+ self.condition_evaluator = ConditionEvaluator()
414
+ self._cache = {} # Cache access decisions
415
+ self._cache_lock = threading.Lock()
416
+
417
+ # Audit logger
418
+ self.audit_logger = logging.getLogger("kailash.access_control.audit")
419
+
420
+ def add_rule(self, rule: PermissionRule):
421
+ """Add a permission rule"""
422
+ self.rules.append(rule)
423
+ # Clear cache when rules change
424
+ with self._cache_lock:
425
+ self._cache.clear()
426
+
427
+ def remove_rule(self, rule_id: str):
428
+ """Remove a permission rule"""
429
+ self.rules = [r for r in self.rules if r.id != rule_id]
430
+ with self._cache_lock:
431
+ self._cache.clear()
432
+
433
+ def check_workflow_access(
434
+ self, user: UserContext, workflow_id: str, permission: WorkflowPermission
435
+ ) -> AccessDecision:
436
+ """Check if user has permission on workflow"""
437
+ cache_key = f"workflow:{workflow_id}:{user.user_id}:{permission.value}"
438
+
439
+ # Check cache
440
+ with self._cache_lock:
441
+ if cache_key in self._cache:
442
+ return self._cache[cache_key]
443
+
444
+ # Evaluate rules
445
+ decision = self._evaluate_rules(user, "workflow", workflow_id, permission, {})
446
+
447
+ # Cache decision
448
+ with self._cache_lock:
449
+ self._cache[cache_key] = decision
450
+
451
+ # Audit log
452
+ self._audit_log(user, "workflow", workflow_id, permission, decision)
453
+
454
+ return decision
455
+
456
+ def check_node_access(
457
+ self,
458
+ user: UserContext,
459
+ node_id: str,
460
+ permission: NodePermission,
461
+ runtime_context: Dict[str, Any] = None,
462
+ ) -> AccessDecision:
463
+ """Check if user has permission on node"""
464
+ cache_key = f"node:{node_id}:{user.user_id}:{permission.value}"
465
+
466
+ # For runtime-dependent permissions, don't use cache
467
+ if runtime_context and any(
468
+ r.effect == PermissionEffect.CONDITIONAL
469
+ for r in self.rules
470
+ if r.resource_id == node_id
471
+ ):
472
+ return self._evaluate_rules(
473
+ user, "node", node_id, permission, runtime_context or {}
474
+ )
475
+
476
+ # Check cache
477
+ with self._cache_lock:
478
+ if cache_key in self._cache:
479
+ return self._cache[cache_key]
480
+
481
+ # Evaluate rules
482
+ decision = self._evaluate_rules(
483
+ user, "node", node_id, permission, runtime_context or {}
484
+ )
485
+
486
+ # Cache decision
487
+ with self._cache_lock:
488
+ self._cache[cache_key] = decision
489
+
490
+ # Audit log
491
+ self._audit_log(user, "node", node_id, permission, decision)
492
+
493
+ return decision
494
+
495
+ def get_accessible_nodes(
496
+ self, user: UserContext, workflow_id: str, permission: NodePermission
497
+ ) -> Set[str]:
498
+ """Get all nodes user can access in a workflow"""
499
+ accessible = set()
500
+
501
+ # Get all node rules for this workflow
502
+ node_rules = [
503
+ r
504
+ for r in self.rules
505
+ if r.resource_type == "node" and r.permission == permission
506
+ ]
507
+
508
+ for rule in node_rules:
509
+ if self._rule_applies_to_user(rule, user):
510
+ if rule.effect == PermissionEffect.ALLOW:
511
+ accessible.add(rule.resource_id)
512
+ elif rule.effect == PermissionEffect.DENY:
513
+ accessible.discard(rule.resource_id)
514
+
515
+ return accessible
516
+
517
+ def get_permission_based_route(
518
+ self,
519
+ user: UserContext,
520
+ conditional_node_id: str,
521
+ true_path_nodes: List[str],
522
+ false_path_nodes: List[str],
523
+ ) -> List[str]:
524
+ """Determine which path user should take based on permissions"""
525
+ # Check if user has access to nodes in true path
526
+ true_path_accessible = all(
527
+ self.check_node_access(user, node_id, NodePermission.EXECUTE).allowed
528
+ for node_id in true_path_nodes
529
+ )
530
+
531
+ if true_path_accessible:
532
+ return true_path_nodes
533
+ else:
534
+ # Check false path
535
+ false_path_accessible = all(
536
+ self.check_node_access(user, node_id, NodePermission.EXECUTE).allowed
537
+ for node_id in false_path_nodes
538
+ )
539
+
540
+ if false_path_accessible:
541
+ return false_path_nodes
542
+ else:
543
+ # User can't access either path
544
+ return []
545
+
546
+ def mask_node_output(
547
+ self, user: UserContext, node_id: str, output: Dict[str, Any]
548
+ ) -> Dict[str, Any]:
549
+ """Mask sensitive fields in node output"""
550
+ decision = self.check_node_access(user, node_id, NodePermission.READ_OUTPUT)
551
+
552
+ if not decision.allowed:
553
+ # Completely mask output
554
+ return {"_masked": True, "reason": "Access denied"}
555
+
556
+ if decision.masked_fields:
557
+ # Mask specific fields
558
+ masked_output = output.copy()
559
+ for field in decision.masked_fields:
560
+ if field in masked_output:
561
+ masked_output[field] = "***MASKED***"
562
+ return masked_output
563
+
564
+ return output
565
+
566
+ def _evaluate_rules(
567
+ self,
568
+ user: UserContext,
569
+ resource_type: str,
570
+ resource_id: str,
571
+ permission: Union[WorkflowPermission, NodePermission],
572
+ runtime_context: Dict[str, Any],
573
+ ) -> AccessDecision:
574
+ """Evaluate all applicable rules"""
575
+ applicable_rules = []
576
+
577
+ # Find applicable rules
578
+ for rule in sorted(self.rules, key=lambda r: r.priority, reverse=True):
579
+ if (
580
+ rule.resource_type == resource_type
581
+ and rule.resource_id == resource_id
582
+ and rule.permission == permission
583
+ and self._rule_applies_to_user(rule, user)
584
+ ):
585
+
586
+ # Check expiration
587
+ if rule.expires_at and rule.expires_at < datetime.now(timezone.utc):
588
+ continue
589
+
590
+ applicable_rules.append(rule)
591
+
592
+ # Evaluate rules
593
+ context = {
594
+ "user": user,
595
+ "runtime": runtime_context,
596
+ "timestamp": datetime.now(timezone.utc),
597
+ }
598
+
599
+ final_effect = PermissionEffect.DENY # Default deny
600
+ conditions_met = {}
601
+ masked_fields = []
602
+
603
+ for rule in applicable_rules:
604
+ if rule.effect == PermissionEffect.CONDITIONAL:
605
+ # Evaluate conditions
606
+ all_conditions_met = True
607
+ for cond_type, cond_value in rule.conditions.items():
608
+ met = self.condition_evaluator.evaluate(
609
+ cond_type, cond_value, context
610
+ )
611
+ conditions_met[f"{rule.id}:{cond_type}"] = met
612
+ if not met:
613
+ all_conditions_met = False
614
+ break
615
+
616
+ if all_conditions_met:
617
+ final_effect = PermissionEffect.ALLOW
618
+ if "masked_fields" in rule.conditions:
619
+ masked_fields.extend(rule.conditions["masked_fields"])
620
+ else:
621
+ final_effect = rule.effect
622
+
623
+ # Explicit deny takes precedence
624
+ if final_effect == PermissionEffect.DENY:
625
+ break
626
+
627
+ allowed = final_effect == PermissionEffect.ALLOW
628
+ reason = f"Permission {permission.value} {'granted' if allowed else 'denied'} for {resource_type} {resource_id}"
629
+
630
+ return AccessDecision(
631
+ allowed=allowed,
632
+ reason=reason,
633
+ applied_rules=applicable_rules,
634
+ conditions_met=conditions_met,
635
+ masked_fields=masked_fields,
636
+ )
637
+
638
+ def _rule_applies_to_user(self, rule: PermissionRule, user: UserContext) -> bool:
639
+ """Check if a rule applies to a user"""
640
+ # Specific user
641
+ if rule.user_id and rule.user_id == user.user_id:
642
+ return True
643
+
644
+ # Role-based
645
+ if rule.role and rule.role in user.roles:
646
+ return True
647
+
648
+ # Tenant-based
649
+ if rule.tenant_id and rule.tenant_id == user.tenant_id:
650
+ return True
651
+
652
+ # No restrictions means it applies to all
653
+ if not rule.user_id and not rule.role and not rule.tenant_id:
654
+ return True
655
+
656
+ return False
657
+
658
+ def _audit_log(
659
+ self,
660
+ user: UserContext,
661
+ resource_type: str,
662
+ resource_id: str,
663
+ permission: Union[WorkflowPermission, NodePermission],
664
+ decision: AccessDecision,
665
+ ):
666
+ """Log access attempt for audit"""
667
+ self.audit_logger.info(
668
+ f"Access {'granted' if decision.allowed else 'denied'}: "
669
+ f"user={user.user_id}, resource={resource_type}:{resource_id}, "
670
+ f"permission={permission.value}, reason={decision.reason}"
671
+ )
672
+
673
+
674
+ # Global access control manager
675
+ _access_control_manager = AccessControlManager()
676
+
677
+
678
+ def get_access_control_manager() -> AccessControlManager:
679
+ """Get the global access control manager"""
680
+ return _access_control_manager
681
+
682
+
683
+ def set_access_control_manager(manager: AccessControlManager):
684
+ """Set a custom access control manager"""
685
+ global _access_control_manager
686
+ _access_control_manager = manager
687
+
688
+
689
+ # Decorators for easy integration
690
+ def require_workflow_permission(permission: WorkflowPermission):
691
+ """Decorator to require workflow permission"""
692
+
693
+ def decorator(func):
694
+ def wrapper(self, *args, **kwargs):
695
+ # Extract user context and workflow ID from self
696
+ user = getattr(self, "user_context", None)
697
+ workflow_id = getattr(self, "workflow_id", None)
698
+
699
+ if user and workflow_id:
700
+ acm = get_access_control_manager()
701
+ decision = acm.check_workflow_access(user, workflow_id, permission)
702
+
703
+ if not decision.allowed:
704
+ raise PermissionError(f"Access denied: {decision.reason}")
705
+
706
+ return func(self, *args, **kwargs)
707
+
708
+ return wrapper
709
+
710
+ return decorator
711
+
712
+
713
+ def require_node_permission(permission: NodePermission):
714
+ """Decorator to require node permission"""
715
+
716
+ def decorator(func):
717
+ def wrapper(self, *args, **kwargs):
718
+ # Extract user context and node ID from self
719
+ user = getattr(self, "user_context", None)
720
+ node_id = getattr(self, "node_id", self.__class__.__name__)
721
+
722
+ if user:
723
+ acm = get_access_control_manager()
724
+ runtime_context = kwargs.get("_runtime_context", {})
725
+ decision = acm.check_node_access(
726
+ user, node_id, permission, runtime_context
727
+ )
728
+
729
+ if not decision.allowed:
730
+ if permission == NodePermission.EXECUTE and decision.redirect_node:
731
+ # Redirect to alternative node
732
+ kwargs["_redirect_to"] = decision.redirect_node
733
+ else:
734
+ raise PermissionError(f"Access denied: {decision.reason}")
735
+
736
+ return func(self, *args, **kwargs)
737
+
738
+ return wrapper
739
+
740
+ return decorator