kailash 0.3.2__py3-none-any.whl → 0.4.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- kailash/__init__.py +33 -1
- kailash/access_control/__init__.py +129 -0
- kailash/access_control/managers.py +461 -0
- kailash/access_control/rule_evaluators.py +467 -0
- kailash/access_control_abac.py +825 -0
- kailash/config/__init__.py +27 -0
- kailash/config/database_config.py +359 -0
- kailash/database/__init__.py +28 -0
- kailash/database/execution_pipeline.py +499 -0
- kailash/middleware/__init__.py +306 -0
- kailash/middleware/auth/__init__.py +33 -0
- kailash/middleware/auth/access_control.py +436 -0
- kailash/middleware/auth/auth_manager.py +422 -0
- kailash/middleware/auth/jwt_auth.py +477 -0
- kailash/middleware/auth/kailash_jwt_auth.py +616 -0
- kailash/middleware/communication/__init__.py +37 -0
- kailash/middleware/communication/ai_chat.py +989 -0
- kailash/middleware/communication/api_gateway.py +802 -0
- kailash/middleware/communication/events.py +470 -0
- kailash/middleware/communication/realtime.py +710 -0
- kailash/middleware/core/__init__.py +21 -0
- kailash/middleware/core/agent_ui.py +890 -0
- kailash/middleware/core/schema.py +643 -0
- kailash/middleware/core/workflows.py +396 -0
- kailash/middleware/database/__init__.py +63 -0
- kailash/middleware/database/base.py +113 -0
- kailash/middleware/database/base_models.py +525 -0
- kailash/middleware/database/enums.py +106 -0
- kailash/middleware/database/migrations.py +12 -0
- kailash/{api/database.py → middleware/database/models.py} +183 -291
- kailash/middleware/database/repositories.py +685 -0
- kailash/middleware/database/session_manager.py +19 -0
- kailash/middleware/mcp/__init__.py +38 -0
- kailash/middleware/mcp/client_integration.py +585 -0
- kailash/middleware/mcp/enhanced_server.py +576 -0
- kailash/nodes/__init__.py +25 -3
- kailash/nodes/admin/__init__.py +35 -0
- kailash/nodes/admin/audit_log.py +794 -0
- kailash/nodes/admin/permission_check.py +864 -0
- kailash/nodes/admin/role_management.py +823 -0
- kailash/nodes/admin/security_event.py +1519 -0
- kailash/nodes/admin/user_management.py +944 -0
- kailash/nodes/ai/a2a.py +24 -7
- kailash/nodes/ai/ai_providers.py +1 -0
- kailash/nodes/ai/embedding_generator.py +11 -11
- kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
- kailash/nodes/ai/llm_agent.py +407 -2
- kailash/nodes/ai/self_organizing.py +85 -10
- kailash/nodes/api/auth.py +287 -6
- kailash/nodes/api/rest.py +151 -0
- kailash/nodes/auth/__init__.py +17 -0
- kailash/nodes/auth/directory_integration.py +1228 -0
- kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
- kailash/nodes/auth/mfa.py +2338 -0
- kailash/nodes/auth/risk_assessment.py +872 -0
- kailash/nodes/auth/session_management.py +1093 -0
- kailash/nodes/auth/sso.py +1040 -0
- kailash/nodes/base.py +344 -13
- kailash/nodes/base_cycle_aware.py +4 -2
- kailash/nodes/base_with_acl.py +1 -1
- kailash/nodes/code/python.py +283 -10
- kailash/nodes/compliance/__init__.py +9 -0
- kailash/nodes/compliance/data_retention.py +1888 -0
- kailash/nodes/compliance/gdpr.py +2004 -0
- kailash/nodes/data/__init__.py +22 -2
- kailash/nodes/data/async_connection.py +469 -0
- kailash/nodes/data/async_sql.py +757 -0
- kailash/nodes/data/async_vector.py +598 -0
- kailash/nodes/data/readers.py +767 -0
- kailash/nodes/data/retrieval.py +360 -1
- kailash/nodes/data/sharepoint_graph.py +397 -21
- kailash/nodes/data/sql.py +94 -5
- kailash/nodes/data/streaming.py +68 -8
- kailash/nodes/data/vector_db.py +54 -4
- kailash/nodes/enterprise/__init__.py +13 -0
- kailash/nodes/enterprise/batch_processor.py +741 -0
- kailash/nodes/enterprise/data_lineage.py +497 -0
- kailash/nodes/logic/convergence.py +31 -9
- kailash/nodes/logic/operations.py +14 -3
- kailash/nodes/mixins/__init__.py +8 -0
- kailash/nodes/mixins/event_emitter.py +201 -0
- kailash/nodes/mixins/mcp.py +9 -4
- kailash/nodes/mixins/security.py +165 -0
- kailash/nodes/monitoring/__init__.py +7 -0
- kailash/nodes/monitoring/performance_benchmark.py +2497 -0
- kailash/nodes/rag/__init__.py +284 -0
- kailash/nodes/rag/advanced.py +1615 -0
- kailash/nodes/rag/agentic.py +773 -0
- kailash/nodes/rag/conversational.py +999 -0
- kailash/nodes/rag/evaluation.py +875 -0
- kailash/nodes/rag/federated.py +1188 -0
- kailash/nodes/rag/graph.py +721 -0
- kailash/nodes/rag/multimodal.py +671 -0
- kailash/nodes/rag/optimized.py +933 -0
- kailash/nodes/rag/privacy.py +1059 -0
- kailash/nodes/rag/query_processing.py +1335 -0
- kailash/nodes/rag/realtime.py +764 -0
- kailash/nodes/rag/registry.py +547 -0
- kailash/nodes/rag/router.py +837 -0
- kailash/nodes/rag/similarity.py +1854 -0
- kailash/nodes/rag/strategies.py +566 -0
- kailash/nodes/rag/workflows.py +575 -0
- kailash/nodes/security/__init__.py +19 -0
- kailash/nodes/security/abac_evaluator.py +1411 -0
- kailash/nodes/security/audit_log.py +91 -0
- kailash/nodes/security/behavior_analysis.py +1893 -0
- kailash/nodes/security/credential_manager.py +401 -0
- kailash/nodes/security/rotating_credentials.py +760 -0
- kailash/nodes/security/security_event.py +132 -0
- kailash/nodes/security/threat_detection.py +1103 -0
- kailash/nodes/testing/__init__.py +9 -0
- kailash/nodes/testing/credential_testing.py +499 -0
- kailash/nodes/transform/__init__.py +10 -2
- kailash/nodes/transform/chunkers.py +592 -1
- kailash/nodes/transform/processors.py +484 -14
- kailash/nodes/validation.py +321 -0
- kailash/runtime/access_controlled.py +1 -1
- kailash/runtime/async_local.py +41 -7
- kailash/runtime/docker.py +1 -1
- kailash/runtime/local.py +474 -55
- kailash/runtime/parallel.py +1 -1
- kailash/runtime/parallel_cyclic.py +1 -1
- kailash/runtime/testing.py +210 -2
- kailash/utils/migrations/__init__.py +25 -0
- kailash/utils/migrations/generator.py +433 -0
- kailash/utils/migrations/models.py +231 -0
- kailash/utils/migrations/runner.py +489 -0
- kailash/utils/secure_logging.py +342 -0
- kailash/workflow/__init__.py +16 -0
- kailash/workflow/cyclic_runner.py +3 -4
- kailash/workflow/graph.py +70 -2
- kailash/workflow/resilience.py +249 -0
- kailash/workflow/templates.py +726 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
- kailash-0.4.0.dist-info/RECORD +223 -0
- kailash/api/__init__.py +0 -17
- kailash/api/__main__.py +0 -6
- kailash/api/studio_secure.py +0 -893
- kailash/mcp/__main__.py +0 -13
- kailash/mcp/server_new.py +0 -336
- kailash/mcp/servers/__init__.py +0 -12
- kailash-0.3.2.dist-info/RECORD +0 -136
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/top_level.txt +0 -0
@@ -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
|
+
]
|