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.
- 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 +27 -3
- kailash/nodes/admin/__init__.py +42 -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 +1523 -0
- kailash/nodes/admin/user_management.py +944 -0
- kailash/nodes/ai/a2a.py +24 -7
- kailash/nodes/ai/ai_providers.py +248 -40
- kailash/nodes/ai/embedding_generator.py +11 -11
- kailash/nodes/ai/intelligent_agent_orchestrator.py +99 -11
- kailash/nodes/ai/llm_agent.py +436 -5
- kailash/nodes/ai/self_organizing.py +85 -10
- kailash/nodes/ai/vision_utils.py +148 -0
- kailash/nodes/alerts/__init__.py +26 -0
- kailash/nodes/alerts/base.py +234 -0
- kailash/nodes/alerts/discord.py +499 -0
- 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 +103 -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 +133 -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/security.py +1 -1
- 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.1.dist-info}/METADATA +256 -20
- kailash-0.4.1.dist-info/RECORD +227 -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.1.dist-info}/WHEEL +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/entry_points.txt +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/licenses/LICENSE +0 -0
- {kailash-0.3.2.dist-info → kailash-0.4.1.dist-info}/top_level.txt +0 -0
@@ -0,0 +1,760 @@
|
|
1
|
+
"""Rotating credential node for automatic credential refresh and zero-downtime rotation.
|
2
|
+
|
3
|
+
This module provides automatic credential rotation capabilities with expiration
|
4
|
+
detection, refresh from multiple sources, zero-downtime rotation, and notification
|
5
|
+
systems for enterprise security requirements.
|
6
|
+
|
7
|
+
Key Features:
|
8
|
+
- Automatic expiration detection
|
9
|
+
- Multi-source credential refresh
|
10
|
+
- Zero-downtime rotation
|
11
|
+
- Notification system for rotation events
|
12
|
+
- Configurable rotation policies
|
13
|
+
- Audit trail for credential operations
|
14
|
+
"""
|
15
|
+
|
16
|
+
import json
|
17
|
+
import threading
|
18
|
+
import time
|
19
|
+
from datetime import datetime, timedelta
|
20
|
+
from typing import Any, Callable, Dict, List, Optional
|
21
|
+
|
22
|
+
from kailash.nodes.base import Node, NodeMetadata, NodeParameter, register_node
|
23
|
+
from kailash.nodes.security.credential_manager import CredentialManagerNode
|
24
|
+
from kailash.sdk_exceptions import NodeConfigurationError, NodeExecutionError
|
25
|
+
|
26
|
+
|
27
|
+
@register_node()
|
28
|
+
class RotatingCredentialNode(Node):
|
29
|
+
"""Node for automatic credential rotation with expiration detection and refresh.
|
30
|
+
|
31
|
+
This node automatically manages credential lifecycles, detecting expiration,
|
32
|
+
refreshing from configured sources, and providing zero-downtime rotation
|
33
|
+
for enterprise applications.
|
34
|
+
|
35
|
+
Key capabilities:
|
36
|
+
1. Automatic expiration detection
|
37
|
+
2. Multi-source credential refresh
|
38
|
+
3. Zero-downtime rotation
|
39
|
+
4. Notification system
|
40
|
+
5. Configurable rotation policies
|
41
|
+
6. Audit trail maintenance
|
42
|
+
|
43
|
+
Example:
|
44
|
+
>>> rotator = RotatingCredentialNode()
|
45
|
+
>>> result = rotator.execute(
|
46
|
+
... operation="start_rotation",
|
47
|
+
... credential_name="api_token",
|
48
|
+
... check_interval=3600, # Check every hour
|
49
|
+
... expiration_threshold=86400, # Rotate 24h before expiry
|
50
|
+
... refresh_sources=["vault", "aws_secrets"],
|
51
|
+
... notification_webhooks=["https://alerts.company.com/webhook"]
|
52
|
+
... )
|
53
|
+
"""
|
54
|
+
|
55
|
+
def get_metadata(self) -> NodeMetadata:
|
56
|
+
"""Get node metadata for discovery and orchestration."""
|
57
|
+
return NodeMetadata(
|
58
|
+
name="Rotating Credential Node",
|
59
|
+
description="Automatic credential rotation with expiration detection and refresh",
|
60
|
+
tags={"security", "credentials", "rotation", "automation", "enterprise"},
|
61
|
+
version="1.0.0",
|
62
|
+
author="Kailash SDK",
|
63
|
+
)
|
64
|
+
|
65
|
+
def get_parameters(self) -> Dict[str, NodeParameter]:
|
66
|
+
"""Define input parameters for credential rotation operations."""
|
67
|
+
return {
|
68
|
+
"operation": NodeParameter(
|
69
|
+
name="operation",
|
70
|
+
type=str,
|
71
|
+
required=False,
|
72
|
+
default="start_rotation",
|
73
|
+
description="Operation: start_rotation, stop_rotation, check_status, rotate_now, get_audit_log",
|
74
|
+
),
|
75
|
+
"credential_name": NodeParameter(
|
76
|
+
name="credential_name",
|
77
|
+
type=str,
|
78
|
+
required=False,
|
79
|
+
description="Name of the credential to manage rotation for",
|
80
|
+
),
|
81
|
+
"check_interval": NodeParameter(
|
82
|
+
name="check_interval",
|
83
|
+
type=int,
|
84
|
+
required=False,
|
85
|
+
default=3600,
|
86
|
+
description="Interval in seconds between expiration checks",
|
87
|
+
),
|
88
|
+
"expiration_threshold": NodeParameter(
|
89
|
+
name="expiration_threshold",
|
90
|
+
type=int,
|
91
|
+
required=False,
|
92
|
+
default=86400,
|
93
|
+
description="Seconds before expiration to trigger rotation",
|
94
|
+
),
|
95
|
+
"refresh_sources": NodeParameter(
|
96
|
+
name="refresh_sources",
|
97
|
+
type=list,
|
98
|
+
required=False,
|
99
|
+
default=["env", "file"],
|
100
|
+
description="Sources to refresh credentials from (env, file, vault, aws_secrets, etc.)",
|
101
|
+
),
|
102
|
+
"refresh_config": NodeParameter(
|
103
|
+
name="refresh_config",
|
104
|
+
type=dict,
|
105
|
+
required=False,
|
106
|
+
default={},
|
107
|
+
description="Configuration for refresh sources",
|
108
|
+
),
|
109
|
+
"notification_webhooks": NodeParameter(
|
110
|
+
name="notification_webhooks",
|
111
|
+
type=list,
|
112
|
+
required=False,
|
113
|
+
default=[],
|
114
|
+
description="Webhook URLs to notify on rotation events",
|
115
|
+
),
|
116
|
+
"notification_emails": NodeParameter(
|
117
|
+
name="notification_emails",
|
118
|
+
type=list,
|
119
|
+
required=False,
|
120
|
+
default=[],
|
121
|
+
description="Email addresses to notify on rotation events",
|
122
|
+
),
|
123
|
+
"rotation_policy": NodeParameter(
|
124
|
+
name="rotation_policy",
|
125
|
+
type=str,
|
126
|
+
required=False,
|
127
|
+
default="proactive",
|
128
|
+
description="Rotation policy: proactive, reactive, scheduled",
|
129
|
+
),
|
130
|
+
"schedule_cron": NodeParameter(
|
131
|
+
name="schedule_cron",
|
132
|
+
type=str,
|
133
|
+
required=False,
|
134
|
+
description="Cron expression for scheduled rotation (if policy is scheduled)",
|
135
|
+
),
|
136
|
+
"zero_downtime": NodeParameter(
|
137
|
+
name="zero_downtime",
|
138
|
+
type=bool,
|
139
|
+
required=False,
|
140
|
+
default=True,
|
141
|
+
description="Whether to use zero-downtime rotation strategy",
|
142
|
+
),
|
143
|
+
"rollback_on_failure": NodeParameter(
|
144
|
+
name="rollback_on_failure",
|
145
|
+
type=bool,
|
146
|
+
required=False,
|
147
|
+
default=True,
|
148
|
+
description="Whether to rollback to previous credential on rotation failure",
|
149
|
+
),
|
150
|
+
"audit_log_enabled": NodeParameter(
|
151
|
+
name="audit_log_enabled",
|
152
|
+
type=bool,
|
153
|
+
required=False,
|
154
|
+
default=True,
|
155
|
+
description="Whether to maintain audit log of rotation activities",
|
156
|
+
),
|
157
|
+
}
|
158
|
+
|
159
|
+
def __init__(self, **kwargs):
|
160
|
+
"""Initialize the RotatingCredentialNode."""
|
161
|
+
super().__init__(**kwargs)
|
162
|
+
self._rotation_threads = {}
|
163
|
+
self._credential_cache = {}
|
164
|
+
self._audit_log = []
|
165
|
+
self._credential_manager = CredentialManagerNode(
|
166
|
+
credential_name="rotating_credentials",
|
167
|
+
credential_type="custom",
|
168
|
+
name="rotation_credential_manager",
|
169
|
+
)
|
170
|
+
self._rotation_status = {}
|
171
|
+
|
172
|
+
def _log_audit_event(
|
173
|
+
self,
|
174
|
+
credential_name: str,
|
175
|
+
event_type: str,
|
176
|
+
details: Dict[str, Any],
|
177
|
+
success: bool = True,
|
178
|
+
):
|
179
|
+
"""Log an audit event for credential rotation."""
|
180
|
+
audit_entry = {
|
181
|
+
"timestamp": datetime.now().isoformat(),
|
182
|
+
"credential_name": credential_name,
|
183
|
+
"event_type": event_type,
|
184
|
+
"success": success,
|
185
|
+
"details": details,
|
186
|
+
}
|
187
|
+
self._audit_log.append(audit_entry)
|
188
|
+
|
189
|
+
# Keep only last 1000 entries to prevent memory growth
|
190
|
+
if len(self._audit_log) > 1000:
|
191
|
+
self._audit_log = self._audit_log[-1000:]
|
192
|
+
|
193
|
+
def _send_notification(
|
194
|
+
self,
|
195
|
+
credential_name: str,
|
196
|
+
event_type: str,
|
197
|
+
message: str,
|
198
|
+
webhook_urls: List[str],
|
199
|
+
email_addresses: List[str],
|
200
|
+
):
|
201
|
+
"""Send notifications about rotation events."""
|
202
|
+
notification_data = {
|
203
|
+
"timestamp": datetime.now().isoformat(),
|
204
|
+
"credential_name": credential_name,
|
205
|
+
"event_type": event_type,
|
206
|
+
"message": message,
|
207
|
+
}
|
208
|
+
|
209
|
+
# Send webhook notifications
|
210
|
+
for webhook_url in webhook_urls:
|
211
|
+
try:
|
212
|
+
import requests
|
213
|
+
|
214
|
+
response = requests.post(
|
215
|
+
webhook_url,
|
216
|
+
json=notification_data,
|
217
|
+
timeout=10,
|
218
|
+
headers={"Content-Type": "application/json"},
|
219
|
+
)
|
220
|
+
if response.status_code == 200:
|
221
|
+
self._log_audit_event(
|
222
|
+
credential_name,
|
223
|
+
"notification_sent",
|
224
|
+
{"webhook": webhook_url, "status": "success"},
|
225
|
+
)
|
226
|
+
else:
|
227
|
+
self._log_audit_event(
|
228
|
+
credential_name,
|
229
|
+
"notification_failed",
|
230
|
+
{"webhook": webhook_url, "status_code": response.status_code},
|
231
|
+
success=False,
|
232
|
+
)
|
233
|
+
except Exception as e:
|
234
|
+
self._log_audit_event(
|
235
|
+
credential_name,
|
236
|
+
"notification_error",
|
237
|
+
{"webhook": webhook_url, "error": str(e)},
|
238
|
+
success=False,
|
239
|
+
)
|
240
|
+
|
241
|
+
# Email notifications would be implemented here
|
242
|
+
# For this example, we'll just log them
|
243
|
+
for email in email_addresses:
|
244
|
+
self._log_audit_event(
|
245
|
+
credential_name,
|
246
|
+
"email_notification",
|
247
|
+
{"email": email, "message": message},
|
248
|
+
)
|
249
|
+
|
250
|
+
def _check_credential_expiration(
|
251
|
+
self,
|
252
|
+
credential_name: str,
|
253
|
+
expiration_threshold: int,
|
254
|
+
) -> Dict[str, Any]:
|
255
|
+
"""Check if a credential is approaching expiration."""
|
256
|
+
try:
|
257
|
+
# Get current credential
|
258
|
+
credential_result = self._credential_manager.run(
|
259
|
+
operation="get_credential", credential_name=credential_name
|
260
|
+
)
|
261
|
+
|
262
|
+
if not credential_result.get("success"):
|
263
|
+
return {
|
264
|
+
"needs_rotation": False,
|
265
|
+
"error": "Failed to retrieve credential",
|
266
|
+
}
|
267
|
+
|
268
|
+
credential_data = credential_result.get("credential", {})
|
269
|
+
expires_at = credential_data.get("expires_at")
|
270
|
+
|
271
|
+
if not expires_at:
|
272
|
+
return {
|
273
|
+
"needs_rotation": False,
|
274
|
+
"reason": "No expiration date set",
|
275
|
+
}
|
276
|
+
|
277
|
+
# Parse expiration time
|
278
|
+
if isinstance(expires_at, str):
|
279
|
+
expiry_time = datetime.fromisoformat(expires_at.replace("Z", "+00:00"))
|
280
|
+
else:
|
281
|
+
expiry_time = expires_at
|
282
|
+
|
283
|
+
current_time = (
|
284
|
+
datetime.now(expiry_time.tzinfo)
|
285
|
+
if expiry_time.tzinfo
|
286
|
+
else datetime.now()
|
287
|
+
)
|
288
|
+
time_until_expiry = (expiry_time - current_time).total_seconds()
|
289
|
+
|
290
|
+
needs_rotation = time_until_expiry <= expiration_threshold
|
291
|
+
|
292
|
+
return {
|
293
|
+
"needs_rotation": needs_rotation,
|
294
|
+
"expires_at": expires_at,
|
295
|
+
"time_until_expiry": time_until_expiry,
|
296
|
+
"threshold": expiration_threshold,
|
297
|
+
"current_time": current_time.isoformat(),
|
298
|
+
}
|
299
|
+
|
300
|
+
except Exception as e:
|
301
|
+
return {
|
302
|
+
"needs_rotation": False,
|
303
|
+
"error": str(e),
|
304
|
+
}
|
305
|
+
|
306
|
+
def _refresh_credential(
|
307
|
+
self,
|
308
|
+
credential_name: str,
|
309
|
+
refresh_sources: List[str],
|
310
|
+
refresh_config: Dict[str, Any],
|
311
|
+
) -> Dict[str, Any]:
|
312
|
+
"""Refresh a credential from configured sources."""
|
313
|
+
try:
|
314
|
+
# Try each refresh source in order
|
315
|
+
for source in refresh_sources:
|
316
|
+
try:
|
317
|
+
refresh_result = self._credential_manager.run(
|
318
|
+
operation="get_credential",
|
319
|
+
credential_name=credential_name,
|
320
|
+
credential_sources=[source],
|
321
|
+
**refresh_config.get(source, {}),
|
322
|
+
)
|
323
|
+
|
324
|
+
if refresh_result.get("success"):
|
325
|
+
return {
|
326
|
+
"success": True,
|
327
|
+
"source": source,
|
328
|
+
"credential": refresh_result["credential"],
|
329
|
+
}
|
330
|
+
|
331
|
+
except Exception as e:
|
332
|
+
self._log_audit_event(
|
333
|
+
credential_name,
|
334
|
+
"refresh_source_failed",
|
335
|
+
{"source": source, "error": str(e)},
|
336
|
+
success=False,
|
337
|
+
)
|
338
|
+
continue
|
339
|
+
|
340
|
+
return {
|
341
|
+
"success": False,
|
342
|
+
"error": "All refresh sources failed",
|
343
|
+
}
|
344
|
+
|
345
|
+
except Exception as e:
|
346
|
+
return {
|
347
|
+
"success": False,
|
348
|
+
"error": str(e),
|
349
|
+
}
|
350
|
+
|
351
|
+
def _perform_rotation(
|
352
|
+
self,
|
353
|
+
credential_name: str,
|
354
|
+
refresh_sources: List[str],
|
355
|
+
refresh_config: Dict[str, Any],
|
356
|
+
zero_downtime: bool = True,
|
357
|
+
rollback_on_failure: bool = True,
|
358
|
+
) -> Dict[str, Any]:
|
359
|
+
"""Perform credential rotation with optional zero-downtime strategy."""
|
360
|
+
rotation_start = datetime.now()
|
361
|
+
|
362
|
+
try:
|
363
|
+
# Step 1: Get current credential (for rollback if needed)
|
364
|
+
current_credential = None
|
365
|
+
if rollback_on_failure:
|
366
|
+
current_result = self._credential_manager.run(
|
367
|
+
operation="get_credential", credential_name=credential_name
|
368
|
+
)
|
369
|
+
if current_result.get("success"):
|
370
|
+
current_credential = current_result["credential"]
|
371
|
+
|
372
|
+
# Step 2: Refresh credential from sources
|
373
|
+
refresh_result = self._refresh_credential(
|
374
|
+
credential_name, refresh_sources, refresh_config
|
375
|
+
)
|
376
|
+
|
377
|
+
if not refresh_result.get("success"):
|
378
|
+
self._log_audit_event(
|
379
|
+
credential_name,
|
380
|
+
"rotation_failed",
|
381
|
+
{"stage": "refresh", "error": refresh_result.get("error")},
|
382
|
+
success=False,
|
383
|
+
)
|
384
|
+
return {
|
385
|
+
"success": False,
|
386
|
+
"error": f"Failed to refresh credential: {refresh_result.get('error')}",
|
387
|
+
"stage": "refresh",
|
388
|
+
}
|
389
|
+
|
390
|
+
new_credential = refresh_result["credential"]
|
391
|
+
|
392
|
+
# Step 3: Validate new credential
|
393
|
+
validation_result = self._credential_manager.run(
|
394
|
+
operation="validate_credential",
|
395
|
+
credential_name=credential_name,
|
396
|
+
credential_data=new_credential,
|
397
|
+
)
|
398
|
+
|
399
|
+
if not validation_result.get("valid", True):
|
400
|
+
self._log_audit_event(
|
401
|
+
credential_name,
|
402
|
+
"rotation_failed",
|
403
|
+
{
|
404
|
+
"stage": "validation",
|
405
|
+
"error": "New credential validation failed",
|
406
|
+
},
|
407
|
+
success=False,
|
408
|
+
)
|
409
|
+
return {
|
410
|
+
"success": False,
|
411
|
+
"error": "New credential validation failed",
|
412
|
+
"stage": "validation",
|
413
|
+
}
|
414
|
+
|
415
|
+
# Step 4: Store new credential
|
416
|
+
if zero_downtime:
|
417
|
+
# In zero-downtime mode, we would typically:
|
418
|
+
# 1. Store new credential with a temporary name
|
419
|
+
# 2. Test it in parallel with current credential
|
420
|
+
# 3. Atomically switch to new credential
|
421
|
+
# 4. Remove old credential
|
422
|
+
|
423
|
+
temp_credential_name = f"{credential_name}_rotating_{int(time.time())}"
|
424
|
+
|
425
|
+
store_result = self._credential_manager.run(
|
426
|
+
operation="store_credential",
|
427
|
+
credential_name=temp_credential_name,
|
428
|
+
credential_data=new_credential,
|
429
|
+
)
|
430
|
+
|
431
|
+
if not store_result.get("success"):
|
432
|
+
self._log_audit_event(
|
433
|
+
credential_name,
|
434
|
+
"rotation_failed",
|
435
|
+
{"stage": "temp_store", "error": store_result.get("error")},
|
436
|
+
success=False,
|
437
|
+
)
|
438
|
+
return {
|
439
|
+
"success": False,
|
440
|
+
"error": f"Failed to store temporary credential: {store_result.get('error')}",
|
441
|
+
"stage": "temp_store",
|
442
|
+
}
|
443
|
+
|
444
|
+
# Test new credential (this would be application-specific)
|
445
|
+
# For this example, we'll assume it passes
|
446
|
+
|
447
|
+
# Atomic switch
|
448
|
+
final_store_result = self._credential_manager.run(
|
449
|
+
operation="store_credential",
|
450
|
+
credential_name=credential_name,
|
451
|
+
credential_data=new_credential,
|
452
|
+
)
|
453
|
+
|
454
|
+
if not final_store_result.get("success"):
|
455
|
+
# Rollback if requested
|
456
|
+
if rollback_on_failure and current_credential:
|
457
|
+
self._credential_manager.run(
|
458
|
+
operation="store_credential",
|
459
|
+
credential_name=credential_name,
|
460
|
+
credential_data=current_credential,
|
461
|
+
)
|
462
|
+
|
463
|
+
self._log_audit_event(
|
464
|
+
credential_name,
|
465
|
+
"rotation_failed",
|
466
|
+
{
|
467
|
+
"stage": "final_store",
|
468
|
+
"error": final_store_result.get("error"),
|
469
|
+
},
|
470
|
+
success=False,
|
471
|
+
)
|
472
|
+
return {
|
473
|
+
"success": False,
|
474
|
+
"error": f"Failed to store final credential: {final_store_result.get('error')}",
|
475
|
+
"stage": "final_store",
|
476
|
+
}
|
477
|
+
|
478
|
+
# Clean up temporary credential
|
479
|
+
self._credential_manager.run(
|
480
|
+
operation="delete_credential", credential_name=temp_credential_name
|
481
|
+
)
|
482
|
+
|
483
|
+
else:
|
484
|
+
# Direct replacement
|
485
|
+
store_result = self._credential_manager.run(
|
486
|
+
operation="store_credential",
|
487
|
+
credential_name=credential_name,
|
488
|
+
credential_data=new_credential,
|
489
|
+
)
|
490
|
+
|
491
|
+
if not store_result.get("success"):
|
492
|
+
# Rollback if requested
|
493
|
+
if rollback_on_failure and current_credential:
|
494
|
+
self._credential_manager.run(
|
495
|
+
operation="store_credential",
|
496
|
+
credential_name=credential_name,
|
497
|
+
credential_data=current_credential,
|
498
|
+
)
|
499
|
+
|
500
|
+
self._log_audit_event(
|
501
|
+
credential_name,
|
502
|
+
"rotation_failed",
|
503
|
+
{"stage": "store", "error": store_result.get("error")},
|
504
|
+
success=False,
|
505
|
+
)
|
506
|
+
return {
|
507
|
+
"success": False,
|
508
|
+
"error": f"Failed to store credential: {store_result.get('error')}",
|
509
|
+
"stage": "store",
|
510
|
+
}
|
511
|
+
|
512
|
+
rotation_end = datetime.now()
|
513
|
+
rotation_duration = (rotation_end - rotation_start).total_seconds()
|
514
|
+
|
515
|
+
# Log successful rotation
|
516
|
+
self._log_audit_event(
|
517
|
+
credential_name,
|
518
|
+
"rotation_completed",
|
519
|
+
{
|
520
|
+
"source": refresh_result["source"],
|
521
|
+
"duration_seconds": rotation_duration,
|
522
|
+
"zero_downtime": zero_downtime,
|
523
|
+
},
|
524
|
+
)
|
525
|
+
|
526
|
+
return {
|
527
|
+
"success": True,
|
528
|
+
"source": refresh_result["source"],
|
529
|
+
"rotation_duration": rotation_duration,
|
530
|
+
"rotated_at": rotation_end.isoformat(),
|
531
|
+
}
|
532
|
+
|
533
|
+
except Exception as e:
|
534
|
+
self._log_audit_event(
|
535
|
+
credential_name, "rotation_error", {"error": str(e)}, success=False
|
536
|
+
)
|
537
|
+
return {
|
538
|
+
"success": False,
|
539
|
+
"error": str(e),
|
540
|
+
"stage": "exception",
|
541
|
+
}
|
542
|
+
|
543
|
+
def _rotation_worker(
|
544
|
+
self,
|
545
|
+
credential_name: str,
|
546
|
+
check_interval: int,
|
547
|
+
expiration_threshold: int,
|
548
|
+
refresh_sources: List[str],
|
549
|
+
refresh_config: Dict[str, Any],
|
550
|
+
notification_webhooks: List[str],
|
551
|
+
notification_emails: List[str],
|
552
|
+
zero_downtime: bool,
|
553
|
+
rollback_on_failure: bool,
|
554
|
+
):
|
555
|
+
"""Background worker for automatic credential rotation."""
|
556
|
+
self._rotation_status[credential_name] = {
|
557
|
+
"active": True,
|
558
|
+
"last_check": None,
|
559
|
+
"last_rotation": None,
|
560
|
+
"next_check": datetime.now() + timedelta(seconds=check_interval),
|
561
|
+
}
|
562
|
+
|
563
|
+
while self._rotation_status[credential_name]["active"]:
|
564
|
+
try:
|
565
|
+
# Check if credential needs rotation
|
566
|
+
check_result = self._check_credential_expiration(
|
567
|
+
credential_name, expiration_threshold
|
568
|
+
)
|
569
|
+
|
570
|
+
self._rotation_status[credential_name][
|
571
|
+
"last_check"
|
572
|
+
] = datetime.now().isoformat()
|
573
|
+
|
574
|
+
if check_result.get("needs_rotation"):
|
575
|
+
self._log_audit_event(
|
576
|
+
credential_name,
|
577
|
+
"rotation_triggered",
|
578
|
+
{"reason": "expiration_threshold", "details": check_result},
|
579
|
+
)
|
580
|
+
|
581
|
+
# Send notification about rotation start
|
582
|
+
self._send_notification(
|
583
|
+
credential_name,
|
584
|
+
"rotation_started",
|
585
|
+
f"Credential rotation started for {credential_name}",
|
586
|
+
notification_webhooks,
|
587
|
+
notification_emails,
|
588
|
+
)
|
589
|
+
|
590
|
+
# Perform rotation
|
591
|
+
rotation_result = self._perform_rotation(
|
592
|
+
credential_name,
|
593
|
+
refresh_sources,
|
594
|
+
refresh_config,
|
595
|
+
zero_downtime,
|
596
|
+
rollback_on_failure,
|
597
|
+
)
|
598
|
+
|
599
|
+
if rotation_result["success"]:
|
600
|
+
self._rotation_status[credential_name][
|
601
|
+
"last_rotation"
|
602
|
+
] = datetime.now().isoformat()
|
603
|
+
|
604
|
+
# Send success notification
|
605
|
+
self._send_notification(
|
606
|
+
credential_name,
|
607
|
+
"rotation_completed",
|
608
|
+
f"Credential rotation completed successfully for {credential_name}",
|
609
|
+
notification_webhooks,
|
610
|
+
notification_emails,
|
611
|
+
)
|
612
|
+
else:
|
613
|
+
# Send failure notification
|
614
|
+
self._send_notification(
|
615
|
+
credential_name,
|
616
|
+
"rotation_failed",
|
617
|
+
f"Credential rotation failed for {credential_name}: {rotation_result.get('error')}",
|
618
|
+
notification_webhooks,
|
619
|
+
notification_emails,
|
620
|
+
)
|
621
|
+
|
622
|
+
# Update next check time
|
623
|
+
self._rotation_status[credential_name]["next_check"] = (
|
624
|
+
datetime.now() + timedelta(seconds=check_interval)
|
625
|
+
).isoformat()
|
626
|
+
|
627
|
+
# Sleep until next check
|
628
|
+
time.sleep(check_interval)
|
629
|
+
|
630
|
+
except Exception as e:
|
631
|
+
self._log_audit_event(
|
632
|
+
credential_name,
|
633
|
+
"rotation_worker_error",
|
634
|
+
{"error": str(e)},
|
635
|
+
success=False,
|
636
|
+
)
|
637
|
+
time.sleep(min(check_interval, 300)) # Sleep at most 5 minutes on error
|
638
|
+
|
639
|
+
def run(self, **kwargs) -> Dict[str, Any]:
|
640
|
+
"""Execute credential rotation operation."""
|
641
|
+
operation = kwargs.get("operation", "start_rotation")
|
642
|
+
|
643
|
+
if operation == "start_rotation":
|
644
|
+
credential_name = kwargs.get("credential_name")
|
645
|
+
if not credential_name:
|
646
|
+
raise NodeConfigurationError(
|
647
|
+
"credential_name is required for start_rotation"
|
648
|
+
)
|
649
|
+
|
650
|
+
# Stop existing rotation if running
|
651
|
+
if credential_name in self._rotation_threads:
|
652
|
+
self._rotation_status[credential_name]["active"] = False
|
653
|
+
self._rotation_threads[credential_name].join(timeout=5)
|
654
|
+
|
655
|
+
# Start new rotation worker
|
656
|
+
rotation_thread = threading.Thread(
|
657
|
+
target=self._rotation_worker,
|
658
|
+
args=(
|
659
|
+
credential_name,
|
660
|
+
kwargs.get("check_interval", 3600),
|
661
|
+
kwargs.get("expiration_threshold", 86400),
|
662
|
+
kwargs.get("refresh_sources", ["env", "file"]),
|
663
|
+
kwargs.get("refresh_config", {}),
|
664
|
+
kwargs.get("notification_webhooks", []),
|
665
|
+
kwargs.get("notification_emails", []),
|
666
|
+
kwargs.get("zero_downtime", True),
|
667
|
+
kwargs.get("rollback_on_failure", True),
|
668
|
+
),
|
669
|
+
daemon=True,
|
670
|
+
)
|
671
|
+
|
672
|
+
rotation_thread.start()
|
673
|
+
self._rotation_threads[credential_name] = rotation_thread
|
674
|
+
|
675
|
+
return {
|
676
|
+
"success": True,
|
677
|
+
"message": f"Rotation started for credential: {credential_name}",
|
678
|
+
"credential_name": credential_name,
|
679
|
+
"check_interval": kwargs.get("check_interval", 3600),
|
680
|
+
"expiration_threshold": kwargs.get("expiration_threshold", 86400),
|
681
|
+
}
|
682
|
+
|
683
|
+
elif operation == "stop_rotation":
|
684
|
+
credential_name = kwargs.get("credential_name")
|
685
|
+
if not credential_name:
|
686
|
+
raise NodeConfigurationError(
|
687
|
+
"credential_name is required for stop_rotation"
|
688
|
+
)
|
689
|
+
|
690
|
+
if credential_name in self._rotation_status:
|
691
|
+
self._rotation_status[credential_name]["active"] = False
|
692
|
+
|
693
|
+
if credential_name in self._rotation_threads:
|
694
|
+
self._rotation_threads[credential_name].join(timeout=5)
|
695
|
+
del self._rotation_threads[credential_name]
|
696
|
+
|
697
|
+
return {
|
698
|
+
"success": True,
|
699
|
+
"message": f"Rotation stopped for credential: {credential_name}",
|
700
|
+
"credential_name": credential_name,
|
701
|
+
}
|
702
|
+
|
703
|
+
elif operation == "check_status":
|
704
|
+
credential_name = kwargs.get("credential_name")
|
705
|
+
|
706
|
+
if credential_name:
|
707
|
+
status = self._rotation_status.get(credential_name, {})
|
708
|
+
return {
|
709
|
+
"credential_name": credential_name,
|
710
|
+
"status": status,
|
711
|
+
"thread_active": credential_name in self._rotation_threads,
|
712
|
+
}
|
713
|
+
else:
|
714
|
+
return {
|
715
|
+
"all_credentials": self._rotation_status,
|
716
|
+
"active_threads": list(self._rotation_threads.keys()),
|
717
|
+
}
|
718
|
+
|
719
|
+
elif operation == "rotate_now":
|
720
|
+
credential_name = kwargs.get("credential_name")
|
721
|
+
if not credential_name:
|
722
|
+
raise NodeConfigurationError(
|
723
|
+
"credential_name is required for rotate_now"
|
724
|
+
)
|
725
|
+
|
726
|
+
return self._perform_rotation(
|
727
|
+
credential_name,
|
728
|
+
kwargs.get("refresh_sources", ["env", "file"]),
|
729
|
+
kwargs.get("refresh_config", {}),
|
730
|
+
kwargs.get("zero_downtime", True),
|
731
|
+
kwargs.get("rollback_on_failure", True),
|
732
|
+
)
|
733
|
+
|
734
|
+
elif operation == "get_audit_log":
|
735
|
+
credential_name = kwargs.get("credential_name")
|
736
|
+
|
737
|
+
if credential_name:
|
738
|
+
# Filter audit log for specific credential
|
739
|
+
filtered_log = [
|
740
|
+
entry
|
741
|
+
for entry in self._audit_log
|
742
|
+
if entry["credential_name"] == credential_name
|
743
|
+
]
|
744
|
+
return {
|
745
|
+
"credential_name": credential_name,
|
746
|
+
"audit_log": filtered_log,
|
747
|
+
"total_entries": len(filtered_log),
|
748
|
+
}
|
749
|
+
else:
|
750
|
+
return {
|
751
|
+
"audit_log": self._audit_log,
|
752
|
+
"total_entries": len(self._audit_log),
|
753
|
+
}
|
754
|
+
|
755
|
+
else:
|
756
|
+
raise NodeConfigurationError(f"Invalid operation: {operation}")
|
757
|
+
|
758
|
+
async def async_run(self, **kwargs) -> Dict[str, Any]:
|
759
|
+
"""Async execution method for enterprise integration."""
|
760
|
+
return self.run(**kwargs)
|