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.
Files changed (146) 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 +25 -3
  37. kailash/nodes/admin/__init__.py +35 -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 +1519 -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 +1 -0
  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 +407 -2
  48. kailash/nodes/ai/self_organizing.py +85 -10
  49. kailash/nodes/api/auth.py +287 -6
  50. kailash/nodes/api/rest.py +151 -0
  51. kailash/nodes/auth/__init__.py +17 -0
  52. kailash/nodes/auth/directory_integration.py +1228 -0
  53. kailash/nodes/auth/enterprise_auth_provider.py +1328 -0
  54. kailash/nodes/auth/mfa.py +2338 -0
  55. kailash/nodes/auth/risk_assessment.py +872 -0
  56. kailash/nodes/auth/session_management.py +1093 -0
  57. kailash/nodes/auth/sso.py +1040 -0
  58. kailash/nodes/base.py +344 -13
  59. kailash/nodes/base_cycle_aware.py +4 -2
  60. kailash/nodes/base_with_acl.py +1 -1
  61. kailash/nodes/code/python.py +283 -10
  62. kailash/nodes/compliance/__init__.py +9 -0
  63. kailash/nodes/compliance/data_retention.py +1888 -0
  64. kailash/nodes/compliance/gdpr.py +2004 -0
  65. kailash/nodes/data/__init__.py +22 -2
  66. kailash/nodes/data/async_connection.py +469 -0
  67. kailash/nodes/data/async_sql.py +757 -0
  68. kailash/nodes/data/async_vector.py +598 -0
  69. kailash/nodes/data/readers.py +767 -0
  70. kailash/nodes/data/retrieval.py +360 -1
  71. kailash/nodes/data/sharepoint_graph.py +397 -21
  72. kailash/nodes/data/sql.py +94 -5
  73. kailash/nodes/data/streaming.py +68 -8
  74. kailash/nodes/data/vector_db.py +54 -4
  75. kailash/nodes/enterprise/__init__.py +13 -0
  76. kailash/nodes/enterprise/batch_processor.py +741 -0
  77. kailash/nodes/enterprise/data_lineage.py +497 -0
  78. kailash/nodes/logic/convergence.py +31 -9
  79. kailash/nodes/logic/operations.py +14 -3
  80. kailash/nodes/mixins/__init__.py +8 -0
  81. kailash/nodes/mixins/event_emitter.py +201 -0
  82. kailash/nodes/mixins/mcp.py +9 -4
  83. kailash/nodes/mixins/security.py +165 -0
  84. kailash/nodes/monitoring/__init__.py +7 -0
  85. kailash/nodes/monitoring/performance_benchmark.py +2497 -0
  86. kailash/nodes/rag/__init__.py +284 -0
  87. kailash/nodes/rag/advanced.py +1615 -0
  88. kailash/nodes/rag/agentic.py +773 -0
  89. kailash/nodes/rag/conversational.py +999 -0
  90. kailash/nodes/rag/evaluation.py +875 -0
  91. kailash/nodes/rag/federated.py +1188 -0
  92. kailash/nodes/rag/graph.py +721 -0
  93. kailash/nodes/rag/multimodal.py +671 -0
  94. kailash/nodes/rag/optimized.py +933 -0
  95. kailash/nodes/rag/privacy.py +1059 -0
  96. kailash/nodes/rag/query_processing.py +1335 -0
  97. kailash/nodes/rag/realtime.py +764 -0
  98. kailash/nodes/rag/registry.py +547 -0
  99. kailash/nodes/rag/router.py +837 -0
  100. kailash/nodes/rag/similarity.py +1854 -0
  101. kailash/nodes/rag/strategies.py +566 -0
  102. kailash/nodes/rag/workflows.py +575 -0
  103. kailash/nodes/security/__init__.py +19 -0
  104. kailash/nodes/security/abac_evaluator.py +1411 -0
  105. kailash/nodes/security/audit_log.py +91 -0
  106. kailash/nodes/security/behavior_analysis.py +1893 -0
  107. kailash/nodes/security/credential_manager.py +401 -0
  108. kailash/nodes/security/rotating_credentials.py +760 -0
  109. kailash/nodes/security/security_event.py +132 -0
  110. kailash/nodes/security/threat_detection.py +1103 -0
  111. kailash/nodes/testing/__init__.py +9 -0
  112. kailash/nodes/testing/credential_testing.py +499 -0
  113. kailash/nodes/transform/__init__.py +10 -2
  114. kailash/nodes/transform/chunkers.py +592 -1
  115. kailash/nodes/transform/processors.py +484 -14
  116. kailash/nodes/validation.py +321 -0
  117. kailash/runtime/access_controlled.py +1 -1
  118. kailash/runtime/async_local.py +41 -7
  119. kailash/runtime/docker.py +1 -1
  120. kailash/runtime/local.py +474 -55
  121. kailash/runtime/parallel.py +1 -1
  122. kailash/runtime/parallel_cyclic.py +1 -1
  123. kailash/runtime/testing.py +210 -2
  124. kailash/utils/migrations/__init__.py +25 -0
  125. kailash/utils/migrations/generator.py +433 -0
  126. kailash/utils/migrations/models.py +231 -0
  127. kailash/utils/migrations/runner.py +489 -0
  128. kailash/utils/secure_logging.py +342 -0
  129. kailash/workflow/__init__.py +16 -0
  130. kailash/workflow/cyclic_runner.py +3 -4
  131. kailash/workflow/graph.py +70 -2
  132. kailash/workflow/resilience.py +249 -0
  133. kailash/workflow/templates.py +726 -0
  134. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/METADATA +253 -20
  135. kailash-0.4.0.dist-info/RECORD +223 -0
  136. kailash/api/__init__.py +0 -17
  137. kailash/api/__main__.py +0 -6
  138. kailash/api/studio_secure.py +0 -893
  139. kailash/mcp/__main__.py +0 -13
  140. kailash/mcp/server_new.py +0 -336
  141. kailash/mcp/servers/__init__.py +0 -12
  142. kailash-0.3.2.dist-info/RECORD +0 -136
  143. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
  144. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
  145. {kailash-0.3.2.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
  146. {kailash-0.3.2.dist-info → kailash-0.4.0.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)