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
@@ -0,0 +1,825 @@
1
+ """Enhanced ABAC (Attribute-Based Access Control) extensions for Kailash SDK.
2
+
3
+ This module extends the existing access control system with sophisticated
4
+ attribute-based access control capabilities, enabling fine-grained permissions
5
+ based on user attributes, resource attributes, and environmental context.
6
+
7
+ Key Features:
8
+ - Hierarchical attribute matching (department trees, security levels)
9
+ - Complex attribute expressions with AND/OR logic
10
+ - Attribute-based data masking and transformation
11
+ - Dynamic permission evaluation based on runtime attributes
12
+ - Backward compatible with existing RBAC rules
13
+ """
14
+
15
+ # Import ConditionEvaluator directly from the original module to avoid fallback
16
+ import importlib.util
17
+ import os
18
+ import re
19
+ from abc import ABC, abstractmethod
20
+ from dataclasses import dataclass, field
21
+ from datetime import UTC, datetime
22
+ from enum import Enum
23
+ from typing import Any, Callable, Dict, List, Optional, Set, Union
24
+
25
+ from kailash.access_control import (
26
+ AccessDecision,
27
+ NodePermission,
28
+ PermissionEffect,
29
+ PermissionRule,
30
+ UserContext,
31
+ WorkflowPermission,
32
+ )
33
+
34
+ # Load the real access_control module directly from file to avoid import conflicts
35
+ _ac_spec = importlib.util.spec_from_file_location(
36
+ "original_access_control",
37
+ os.path.join(os.path.dirname(__file__), "access_control.py"),
38
+ )
39
+ _ac_module = importlib.util.module_from_spec(_ac_spec)
40
+ _ac_spec.loader.exec_module(_ac_module)
41
+
42
+ # Get the real ConditionEvaluator class
43
+ ConditionEvaluator = _ac_module.ConditionEvaluator
44
+
45
+
46
+ class AttributeOperator(Enum):
47
+ """Operators for attribute comparisons."""
48
+
49
+ EQUALS = "equals"
50
+ NOT_EQUALS = "not_equals"
51
+ CONTAINS = "contains"
52
+ NOT_CONTAINS = "not_contains"
53
+ CONTAINS_ANY = "contains_any"
54
+ IN = "in"
55
+ NOT_IN = "not_in"
56
+ GREATER_THAN = "greater_than"
57
+ LESS_THAN = "less_than"
58
+ GREATER_OR_EQUAL = "greater_or_equal"
59
+ LESS_OR_EQUAL = "less_or_equal"
60
+ MATCHES = "matches" # Regex match
61
+ HIERARCHICAL_MATCH = "hierarchical_match" # For department trees
62
+ SECURITY_LEVEL_MEETS = "security_level_meets" # Security clearance levels
63
+ SECURITY_LEVEL_BELOW = "security_level_below" # Security clearance levels
64
+ MATCHES_DATA_REGION = "matches_data_region" # Region matching
65
+ BETWEEN = "between" # For ranges
66
+
67
+
68
+ class LogicalOperator(Enum):
69
+ """Logical operators for combining conditions."""
70
+
71
+ AND = "and"
72
+ OR = "or"
73
+ NOT = "not"
74
+
75
+
76
+ @dataclass
77
+ class AttributeCondition:
78
+ """Single attribute condition for ABAC evaluation."""
79
+
80
+ attribute_path: str # Dot notation path e.g., "user.department.level"
81
+ operator: AttributeOperator
82
+ value: Any
83
+ case_sensitive: bool = True
84
+
85
+ def __post_init__(self):
86
+ """Validate condition."""
87
+ if not self.attribute_path:
88
+ raise ValueError("attribute_path cannot be empty")
89
+
90
+
91
+ @dataclass
92
+ class AttributeExpression:
93
+ """Complex attribute expression with logical operators."""
94
+
95
+ operator: LogicalOperator
96
+ conditions: List[Union[AttributeCondition, "AttributeExpression"]]
97
+
98
+ def __post_init__(self):
99
+ """Validate expression."""
100
+ if not self.conditions:
101
+ raise ValueError("conditions cannot be empty")
102
+ if self.operator == LogicalOperator.NOT and len(self.conditions) != 1:
103
+ raise ValueError("NOT operator requires exactly one condition")
104
+
105
+
106
+ @dataclass
107
+ class AttributeMaskingRule:
108
+ """Rule for attribute-based data masking."""
109
+
110
+ field_path: str # Field to mask in output
111
+ mask_type: str # "redact", "partial", "hash", "replace"
112
+ mask_value: Optional[Any] = None # Replacement value for "replace" type
113
+ condition: Optional[AttributeExpression] = None # When to apply masking
114
+
115
+
116
+ class AttributeEvaluator:
117
+ """Enhanced attribute evaluator for ABAC."""
118
+
119
+ def __init__(self):
120
+ """Initialize evaluator with operator handlers."""
121
+ self.operators = {
122
+ AttributeOperator.EQUALS: self._eval_equals,
123
+ AttributeOperator.NOT_EQUALS: self._eval_not_equals,
124
+ AttributeOperator.CONTAINS: self._eval_contains,
125
+ AttributeOperator.NOT_CONTAINS: self._eval_not_contains,
126
+ AttributeOperator.CONTAINS_ANY: self._eval_contains_any,
127
+ AttributeOperator.IN: self._eval_in,
128
+ AttributeOperator.NOT_IN: self._eval_not_in,
129
+ AttributeOperator.GREATER_THAN: self._eval_greater_than,
130
+ AttributeOperator.LESS_THAN: self._eval_less_than,
131
+ AttributeOperator.GREATER_OR_EQUAL: self._eval_greater_or_equal,
132
+ AttributeOperator.LESS_OR_EQUAL: self._eval_less_or_equal,
133
+ AttributeOperator.MATCHES: self._eval_matches,
134
+ AttributeOperator.HIERARCHICAL_MATCH: self._eval_hierarchical_match,
135
+ AttributeOperator.SECURITY_LEVEL_MEETS: self._eval_security_level_meets,
136
+ AttributeOperator.SECURITY_LEVEL_BELOW: self._eval_security_level_below,
137
+ AttributeOperator.MATCHES_DATA_REGION: self._eval_matches_data_region,
138
+ AttributeOperator.BETWEEN: self._eval_between,
139
+ }
140
+
141
+ def evaluate_expression(
142
+ self,
143
+ expression: Union[AttributeCondition, AttributeExpression],
144
+ context: Dict[str, Any],
145
+ ) -> bool:
146
+ """Evaluate an attribute expression."""
147
+ if isinstance(expression, AttributeCondition):
148
+ return self._evaluate_condition(expression, context)
149
+ elif isinstance(expression, AttributeExpression):
150
+ return self._evaluate_logical_expression(expression, context)
151
+ else:
152
+ raise ValueError(f"Unknown expression type: {type(expression)}")
153
+
154
+ def _evaluate_condition(
155
+ self, condition: AttributeCondition, context: Dict[str, Any]
156
+ ) -> bool:
157
+ """Evaluate a single attribute condition."""
158
+ # Extract value from context using attribute path
159
+ value = self._extract_value(condition.attribute_path, context)
160
+
161
+ # Get operator handler
162
+ operator_func = self.operators.get(condition.operator)
163
+ if not operator_func:
164
+ raise ValueError(f"Unknown operator: {condition.operator}")
165
+
166
+ # Evaluate condition
167
+ return operator_func(value, condition.value, condition.case_sensitive)
168
+
169
+ def _evaluate_logical_expression(
170
+ self, expression: AttributeExpression, context: Dict[str, Any]
171
+ ) -> bool:
172
+ """Evaluate a logical expression."""
173
+ if expression.operator == LogicalOperator.AND:
174
+ return all(
175
+ self.evaluate_expression(cond, context)
176
+ for cond in expression.conditions
177
+ )
178
+ elif expression.operator == LogicalOperator.OR:
179
+ return any(
180
+ self.evaluate_expression(cond, context)
181
+ for cond in expression.conditions
182
+ )
183
+ elif expression.operator == LogicalOperator.NOT:
184
+ return not self.evaluate_expression(expression.conditions[0], context)
185
+ else:
186
+ raise ValueError(f"Unknown logical operator: {expression.operator}")
187
+
188
+ def _extract_value(self, path: str, context: Dict[str, Any]) -> Any:
189
+ """Extract value from context using dot notation path."""
190
+ parts = path.split(".")
191
+ value = context
192
+
193
+ for part in parts:
194
+ if isinstance(value, dict):
195
+ value = value.get(part)
196
+ elif hasattr(value, part):
197
+ value = getattr(value, part)
198
+ else:
199
+ return None
200
+
201
+ if value is None:
202
+ return None
203
+
204
+ return value
205
+
206
+ # Operator implementations
207
+ def _eval_equals(self, value: Any, expected: Any, case_sensitive: bool) -> bool:
208
+ """Evaluate equals operator."""
209
+ if not case_sensitive and isinstance(value, str) and isinstance(expected, str):
210
+ return value.lower() == expected.lower()
211
+ return value == expected
212
+
213
+ def _eval_not_equals(self, value: Any, expected: Any, case_sensitive: bool) -> bool:
214
+ """Evaluate not equals operator."""
215
+ return not self._eval_equals(value, expected, case_sensitive)
216
+
217
+ def _eval_contains(self, value: Any, expected: Any, case_sensitive: bool) -> bool:
218
+ """Evaluate contains operator."""
219
+ if value is None:
220
+ return False
221
+
222
+ if isinstance(value, (list, set, tuple)):
223
+ return expected in value
224
+ elif isinstance(value, str) and isinstance(expected, str):
225
+ if not case_sensitive:
226
+ return expected.lower() in value.lower()
227
+ return expected in value
228
+ elif isinstance(value, dict):
229
+ return expected in value
230
+
231
+ return False
232
+
233
+ def _eval_not_contains(
234
+ self, value: Any, expected: Any, case_sensitive: bool
235
+ ) -> bool:
236
+ """Evaluate not contains operator."""
237
+ return not self._eval_contains(value, expected, case_sensitive)
238
+
239
+ def _eval_in(self, value: Any, expected: Any, case_sensitive: bool) -> bool:
240
+ """Evaluate in operator."""
241
+ if not isinstance(expected, (list, set, tuple)):
242
+ return False
243
+
244
+ if not case_sensitive and isinstance(value, str):
245
+ expected_lower = [e.lower() if isinstance(e, str) else e for e in expected]
246
+ return value.lower() in expected_lower
247
+
248
+ return value in expected
249
+
250
+ def _eval_not_in(self, value: Any, expected: Any, case_sensitive: bool) -> bool:
251
+ """Evaluate not in operator."""
252
+ return not self._eval_in(value, expected, case_sensitive)
253
+
254
+ def _eval_greater_than(
255
+ self, value: Any, expected: Any, case_sensitive: bool
256
+ ) -> bool:
257
+ """Evaluate greater than operator."""
258
+ try:
259
+ return value > expected
260
+ except TypeError:
261
+ return False
262
+
263
+ def _eval_less_than(self, value: Any, expected: Any, case_sensitive: bool) -> bool:
264
+ """Evaluate less than operator."""
265
+ try:
266
+ return value < expected
267
+ except TypeError:
268
+ return False
269
+
270
+ def _eval_greater_or_equal(
271
+ self, value: Any, expected: Any, case_sensitive: bool
272
+ ) -> bool:
273
+ """Evaluate greater or equal operator."""
274
+ try:
275
+ return value >= expected
276
+ except TypeError:
277
+ return False
278
+
279
+ def _eval_less_or_equal(
280
+ self, value: Any, expected: Any, case_sensitive: bool
281
+ ) -> bool:
282
+ """Evaluate less or equal operator."""
283
+ try:
284
+ return value <= expected
285
+ except TypeError:
286
+ return False
287
+
288
+ def _eval_matches(self, value: Any, expected: Any, case_sensitive: bool) -> bool:
289
+ """Evaluate regex match operator."""
290
+ if not isinstance(value, str) or not isinstance(expected, str):
291
+ return False
292
+
293
+ flags = 0 if case_sensitive else re.IGNORECASE
294
+ try:
295
+ return bool(re.match(expected, value, flags))
296
+ except re.error:
297
+ return False
298
+
299
+ def _eval_hierarchical_match(
300
+ self, value: Any, expected: Any, case_sensitive: bool
301
+ ) -> bool:
302
+ """Evaluate hierarchical match (e.g., department trees)."""
303
+ if not isinstance(value, str) or not isinstance(expected, str):
304
+ return False
305
+
306
+ # Support paths like "engineering.backend.api"
307
+ # Match if value is equal to or child of expected
308
+ if not case_sensitive:
309
+ value = value.lower()
310
+ expected = expected.lower()
311
+
312
+ return value == expected or value.startswith(expected + ".")
313
+
314
+ def _eval_contains_any(
315
+ self, value: Any, expected: Any, case_sensitive: bool
316
+ ) -> bool:
317
+ """Evaluate if value contains any of the expected items."""
318
+ if not isinstance(expected, (list, set, tuple)):
319
+ return False
320
+
321
+ if isinstance(value, (list, set, tuple)):
322
+ # Check if any expected item is in value
323
+ for item in expected:
324
+ if item in value:
325
+ return True
326
+ return False
327
+ elif isinstance(value, str):
328
+ # Check if value contains any of the expected strings
329
+ if not case_sensitive:
330
+ value = value.lower()
331
+ expected = [e.lower() if isinstance(e, str) else e for e in expected]
332
+
333
+ for item in expected:
334
+ if isinstance(item, str) and item in value:
335
+ return True
336
+ return False
337
+
338
+ return False
339
+
340
+ def _eval_security_level_meets(
341
+ self, value: Any, expected: Any, case_sensitive: bool
342
+ ) -> bool:
343
+ """Evaluate if security clearance meets minimum level."""
344
+ # Define clearance hierarchy
345
+ clearance_levels = {
346
+ "public": 0,
347
+ "internal": 1,
348
+ "confidential": 2,
349
+ "secret": 3,
350
+ "top_secret": 4,
351
+ }
352
+
353
+ if not isinstance(value, str) or not isinstance(expected, str):
354
+ return False
355
+
356
+ value_level = clearance_levels.get(value.lower(), 0)
357
+ required_level = clearance_levels.get(expected.lower(), 0)
358
+
359
+ return value_level >= required_level
360
+
361
+ def _eval_security_level_below(
362
+ self, value: Any, expected: Any, case_sensitive: bool
363
+ ) -> bool:
364
+ """Evaluate if security clearance is below a certain level."""
365
+ # Define clearance hierarchy
366
+ clearance_levels = {
367
+ "public": 0,
368
+ "internal": 1,
369
+ "confidential": 2,
370
+ "secret": 3,
371
+ "top_secret": 4,
372
+ }
373
+
374
+ if not isinstance(value, str) or not isinstance(expected, str):
375
+ return False
376
+
377
+ value_level = clearance_levels.get(value.lower(), 0)
378
+ threshold_level = clearance_levels.get(expected.lower(), 0)
379
+
380
+ return value_level < threshold_level
381
+
382
+ def _eval_matches_data_region(
383
+ self, value: Any, expected: Any, case_sensitive: bool
384
+ ) -> bool:
385
+ """Evaluate if user region matches data region requirements."""
386
+ # Simple region matching - can be extended for complex regional rules
387
+ if not isinstance(value, str) or not isinstance(expected, str):
388
+ return False
389
+
390
+ # Special handling for global access
391
+ if value.lower() == "global":
392
+ return True
393
+
394
+ # Check exact match or region family (e.g., us_east matches us_*)
395
+ if not case_sensitive:
396
+ value = value.lower()
397
+ expected = expected.lower()
398
+
399
+ if value == expected:
400
+ return True
401
+
402
+ # Check region family (e.g., us_east and us_west both match "us")
403
+ value_family = value.split("_")[0] if "_" in value else value
404
+ expected_family = expected.split("_")[0] if "_" in expected else expected
405
+
406
+ return value_family == expected_family
407
+
408
+ def _eval_between(self, value: Any, expected: Any, case_sensitive: bool) -> bool:
409
+ """Evaluate if value is between two bounds (inclusive)."""
410
+ if not isinstance(expected, (list, tuple)) or len(expected) != 2:
411
+ return False
412
+
413
+ try:
414
+ lower_bound, upper_bound = expected
415
+ return lower_bound <= value <= upper_bound
416
+ except (TypeError, ValueError):
417
+ return False
418
+
419
+
420
+ class DataMasker:
421
+ """Handles attribute-based data masking."""
422
+
423
+ def __init__(self, attribute_evaluator: AttributeEvaluator):
424
+ """Initialize with attribute evaluator."""
425
+ self.attribute_evaluator = attribute_evaluator
426
+ self.mask_functions = {
427
+ "redact": self._mask_redact,
428
+ "partial": self._mask_partial,
429
+ "hash": self._mask_hash,
430
+ "replace": self._mask_replace,
431
+ }
432
+
433
+ def apply_masking(
434
+ self,
435
+ data: Dict[str, Any],
436
+ rules: List[AttributeMaskingRule],
437
+ context: Dict[str, Any],
438
+ ) -> Dict[str, Any]:
439
+ """Apply masking rules to data."""
440
+ masked_data = data.copy()
441
+
442
+ for rule in rules:
443
+ # Check if rule applies
444
+ if rule.condition:
445
+ if not self.attribute_evaluator.evaluate_expression(
446
+ rule.condition, context
447
+ ):
448
+ continue
449
+
450
+ # Apply masking
451
+ masked_data = self._apply_mask_to_field(
452
+ masked_data, rule.field_path, rule.mask_type, rule.mask_value
453
+ )
454
+
455
+ return masked_data
456
+
457
+ def _apply_mask_to_field(
458
+ self, data: Dict[str, Any], field_path: str, mask_type: str, mask_value: Any
459
+ ) -> Dict[str, Any]:
460
+ """Apply mask to specific field."""
461
+ parts = field_path.split(".")
462
+
463
+ # Navigate to parent of target field
464
+ current = data
465
+ for part in parts[:-1]:
466
+ if part not in current:
467
+ return data # Field doesn't exist
468
+ current = current[part]
469
+
470
+ # Apply mask
471
+ field_name = parts[-1]
472
+ if field_name in current:
473
+ mask_func = self.mask_functions.get(mask_type, self._mask_redact)
474
+ current[field_name] = mask_func(current[field_name], mask_value)
475
+
476
+ return data
477
+
478
+ def _mask_redact(self, value: Any, mask_value: Any) -> str:
479
+ """Completely redact value."""
480
+ return "[REDACTED]"
481
+
482
+ def _mask_partial(self, value: Any, mask_value: Any) -> str:
483
+ """Partially mask value (show first/last few characters)."""
484
+ if not isinstance(value, str):
485
+ value = str(value)
486
+
487
+ if len(value) <= 4:
488
+ return "*" * len(value)
489
+
490
+ # Show first and last 2 characters
491
+ return value[:2] + "*" * (len(value) - 4) + value[-2:]
492
+
493
+ def _mask_hash(self, value: Any, mask_value: Any) -> str:
494
+ """Replace with hash of value."""
495
+ import hashlib
496
+
497
+ value_str = str(value).encode("utf-8")
498
+ return hashlib.sha256(value_str).hexdigest()[:16]
499
+
500
+ def _mask_replace(self, value: Any, mask_value: Any) -> Any:
501
+ """Replace with specified value."""
502
+ return mask_value if mask_value is not None else "[MASKED]"
503
+
504
+
505
+ class EnhancedConditionEvaluator(ConditionEvaluator):
506
+ """Enhanced condition evaluator with ABAC support."""
507
+
508
+ def __init__(self):
509
+ """Initialize with enhanced evaluators."""
510
+ super().__init__()
511
+
512
+ # Ensure evaluators attribute exists (should be set by parent)
513
+ if not hasattr(self, "evaluators") or self.evaluators is None:
514
+ self.evaluators = {}
515
+ self.attribute_evaluator = AttributeEvaluator()
516
+
517
+ # Add ABAC-specific evaluators
518
+ self.evaluators.update(
519
+ {
520
+ "attribute_expression": self._eval_attribute_expression,
521
+ "department_hierarchy": self._eval_department_hierarchy,
522
+ "security_clearance": self._eval_security_clearance,
523
+ "geographic_region": self._eval_geographic_region,
524
+ "time_of_day": self._eval_time_of_day,
525
+ "day_of_week": self._eval_day_of_week,
526
+ }
527
+ )
528
+
529
+ def _eval_attribute_expression(
530
+ self, value: Dict[str, Any], context: Dict[str, Any]
531
+ ) -> bool:
532
+ """Evaluate complex attribute expression."""
533
+ # Build expression from dict representation
534
+ expression = self._build_expression(value)
535
+ result = self.attribute_evaluator.evaluate_expression(expression, context)
536
+ return result
537
+
538
+ def _build_expression(
539
+ self, config: Dict[str, Any]
540
+ ) -> Union[AttributeCondition, AttributeExpression]:
541
+ """Build expression from configuration."""
542
+ if "operator" in config and config["operator"] in ["and", "or", "not"]:
543
+ # Logical expression
544
+ conditions = []
545
+ for cond_config in config.get("conditions", []):
546
+ conditions.append(self._build_expression(cond_config))
547
+
548
+ return AttributeExpression(
549
+ operator=LogicalOperator(config["operator"]), conditions=conditions
550
+ )
551
+ else:
552
+ # Attribute condition
553
+ return AttributeCondition(
554
+ attribute_path=config.get("attribute_path", ""),
555
+ operator=AttributeOperator(config.get("operator", "equals")),
556
+ value=config.get("value"),
557
+ case_sensitive=config.get("case_sensitive", True),
558
+ )
559
+
560
+ def _eval_department_hierarchy(
561
+ self, value: Dict[str, Any], context: Dict[str, Any]
562
+ ) -> bool:
563
+ """Evaluate department hierarchy condition."""
564
+ user = context.get("user")
565
+ if not user or not hasattr(user, "attributes"):
566
+ return False
567
+
568
+ user_dept = user.attributes.get("department", "")
569
+ allowed_dept = value.get("department", "")
570
+ include_children = value.get("include_children", True)
571
+
572
+ if include_children:
573
+ # Use hierarchical matching
574
+ return user_dept == allowed_dept or user_dept.startswith(allowed_dept + ".")
575
+ else:
576
+ return user_dept == allowed_dept
577
+
578
+ def _eval_security_clearance(
579
+ self, value: Dict[str, Any], context: Dict[str, Any]
580
+ ) -> bool:
581
+ """Evaluate security clearance level."""
582
+ user = context.get("user")
583
+ if not user or not hasattr(user, "attributes"):
584
+ return False
585
+
586
+ # Define clearance hierarchy
587
+ clearance_levels = {
588
+ "public": 0,
589
+ "internal": 1,
590
+ "confidential": 2,
591
+ "secret": 3,
592
+ "top_secret": 4,
593
+ }
594
+
595
+ user_clearance = user.attributes.get("security_clearance", "public")
596
+ required_clearance = value.get("minimum_clearance", "public")
597
+
598
+ user_level = clearance_levels.get(user_clearance, 0)
599
+ required_level = clearance_levels.get(required_clearance, 0)
600
+
601
+ return user_level >= required_level
602
+
603
+ def _eval_geographic_region(
604
+ self, value: Dict[str, Any], context: Dict[str, Any]
605
+ ) -> bool:
606
+ """Evaluate geographic region condition."""
607
+ user = context.get("user")
608
+ if not user or not hasattr(user, "attributes"):
609
+ return False
610
+
611
+ user_region = user.attributes.get("region", "")
612
+ allowed_regions = value.get("allowed_regions", [])
613
+
614
+ if isinstance(allowed_regions, str):
615
+ allowed_regions = [allowed_regions]
616
+
617
+ return user_region in allowed_regions
618
+
619
+ def _eval_time_of_day(self, value: Dict[str, Any], context: Dict[str, Any]) -> bool:
620
+ """Evaluate time of day condition."""
621
+ from datetime import time
622
+
623
+ now = datetime.now().time()
624
+ start_time = time.fromisoformat(value.get("start", "00:00"))
625
+ end_time = time.fromisoformat(value.get("end", "23:59"))
626
+
627
+ # Handle overnight ranges
628
+ if start_time <= end_time:
629
+ return start_time <= now <= end_time
630
+ else:
631
+ return now >= start_time or now <= end_time
632
+
633
+ def _eval_day_of_week(self, value: Dict[str, Any], context: Dict[str, Any]) -> bool:
634
+ """Evaluate day of week condition."""
635
+
636
+ allowed_days = value.get("allowed_days", [])
637
+ if isinstance(allowed_days, str):
638
+ allowed_days = [allowed_days]
639
+
640
+ # Convert to lowercase for comparison
641
+ allowed_days = [day.lower() for day in allowed_days]
642
+
643
+ current_day = datetime.now().strftime("%A").lower()
644
+ return current_day in allowed_days
645
+
646
+
647
+ class EnhancedAccessControlManager:
648
+ """Enhanced Access Control Manager with ABAC capabilities."""
649
+
650
+ def __init__(self):
651
+ """Initialize with enhanced evaluators."""
652
+ self.condition_evaluator = EnhancedConditionEvaluator()
653
+ self.attribute_evaluator = AttributeEvaluator()
654
+ self.data_masker = DataMasker(self.attribute_evaluator)
655
+ self.rules: List[PermissionRule] = []
656
+
657
+ def add_rule(self, rule: PermissionRule):
658
+ """Add a permission rule."""
659
+ self.rules.append(rule)
660
+
661
+ def check_node_access(
662
+ self,
663
+ user: UserContext,
664
+ resource_id: str,
665
+ permission: NodePermission,
666
+ context: Optional[Dict[str, Any]] = None,
667
+ ) -> AccessDecision:
668
+ """Check if user has access to a node resource."""
669
+ if context is None:
670
+ context = self._build_context(user)
671
+
672
+ # Check all applicable rules
673
+ applicable_rules = [
674
+ rule
675
+ for rule in self.rules
676
+ if (rule.resource_type == "node" or rule.resource_type == "database_query")
677
+ and rule.resource_id == resource_id
678
+ and rule.permission == permission
679
+ ]
680
+
681
+ if not applicable_rules:
682
+ return AccessDecision(
683
+ allowed=False, reason="No applicable rules found", applied_rules=[]
684
+ )
685
+
686
+ # Evaluate rules
687
+ for rule in applicable_rules:
688
+ if rule.conditions:
689
+ try:
690
+ result = self.condition_evaluator.evaluate(
691
+ rule.conditions["type"],
692
+ rule.conditions.get("value", {}),
693
+ context,
694
+ )
695
+ if result and rule.effect == PermissionEffect.ALLOW:
696
+ return AccessDecision(
697
+ allowed=True,
698
+ reason=f"Rule {rule.id} granted access",
699
+ applied_rules=[rule.id],
700
+ )
701
+ elif result and rule.effect == PermissionEffect.DENY:
702
+ return AccessDecision(
703
+ allowed=False,
704
+ reason=f"Rule {rule.id} denied access",
705
+ applied_rules=[rule.id],
706
+ )
707
+ except Exception as e:
708
+ # Rule evaluation failed - deny access
709
+ return AccessDecision(
710
+ allowed=False,
711
+ reason=f"Rule evaluation failed: {e}",
712
+ applied_rules=[rule.id],
713
+ )
714
+
715
+ # Default deny
716
+ return AccessDecision(
717
+ allowed=False,
718
+ reason="No matching allow rules",
719
+ applied_rules=[rule.id for rule in applicable_rules],
720
+ )
721
+
722
+ def mask_data(
723
+ self,
724
+ data: Dict[str, Any],
725
+ masking_rules: Dict[str, Dict[str, Any]],
726
+ user: UserContext,
727
+ ) -> Dict[str, Any]:
728
+ """Apply data masking based on user attributes."""
729
+ context = self._build_context(user)
730
+ masked_data = data.copy()
731
+
732
+ for field_name, mask_config in masking_rules.items():
733
+ if field_name in masked_data:
734
+ # Check if masking condition applies
735
+ condition = mask_config.get("condition", {})
736
+ attr_condition = AttributeCondition(
737
+ attribute_path=condition["attribute_path"],
738
+ operator=AttributeOperator(condition["operator"]),
739
+ value=condition["value"],
740
+ )
741
+ if self.attribute_evaluator._evaluate_condition(
742
+ attr_condition, context
743
+ ):
744
+ # Apply masking
745
+ original_value = masked_data[field_name]
746
+ mask_type = mask_config.get("mask_type", "redact")
747
+
748
+ if mask_type == "partial":
749
+ visible_chars = mask_config.get("visible_chars", 4)
750
+ mask_char = mask_config.get("mask_char", "*")
751
+ if (
752
+ isinstance(original_value, str)
753
+ and len(original_value) > visible_chars
754
+ ):
755
+ masked_data[field_name] = (
756
+ original_value[:2]
757
+ + mask_char * (len(original_value) - 4)
758
+ + original_value[-2:]
759
+ )
760
+ elif mask_type == "range":
761
+ ranges = mask_config.get("ranges", [])
762
+ if isinstance(original_value, (int, float)):
763
+ if original_value < 1000000:
764
+ masked_data[field_name] = (
765
+ ranges[0] if ranges else "< $1M"
766
+ )
767
+ elif original_value < 10000000:
768
+ masked_data[field_name] = (
769
+ ranges[1] if len(ranges) > 1 else "$1M-$10M"
770
+ )
771
+ elif original_value < 50000000:
772
+ masked_data[field_name] = (
773
+ ranges[2] if len(ranges) > 2 else "$10M-$50M"
774
+ )
775
+ else:
776
+ masked_data[field_name] = (
777
+ ranges[3] if len(ranges) > 3 else "> $50M"
778
+ )
779
+ elif mask_type == "hash":
780
+ import hashlib
781
+
782
+ value_str = str(original_value).encode("utf-8")
783
+ masked_data[field_name] = hashlib.sha256(value_str).hexdigest()[
784
+ :16
785
+ ]
786
+ else: # redact
787
+ masked_data[field_name] = "[REDACTED]"
788
+
789
+ return masked_data
790
+
791
+ def _build_context(self, user: UserContext) -> Dict[str, Any]:
792
+ """Build evaluation context from user and environment."""
793
+
794
+ now = datetime.now()
795
+ return {
796
+ "user": {
797
+ "user_id": user.user_id,
798
+ "tenant_id": user.tenant_id,
799
+ "email": user.email,
800
+ "roles": user.roles,
801
+ "attributes": user.attributes,
802
+ },
803
+ "context": {
804
+ "time": {
805
+ "hour": now.hour,
806
+ "minute": now.minute,
807
+ "weekday": now.weekday() + 1, # 1-7 for Monday-Sunday
808
+ "timestamp": now.isoformat(),
809
+ }
810
+ },
811
+ }
812
+
813
+
814
+ # Export enhanced components
815
+ __all__ = [
816
+ "AttributeOperator",
817
+ "LogicalOperator",
818
+ "AttributeCondition",
819
+ "AttributeExpression",
820
+ "AttributeMaskingRule",
821
+ "AttributeEvaluator",
822
+ "DataMasker",
823
+ "EnhancedConditionEvaluator",
824
+ "EnhancedAccessControlManager",
825
+ ]