kailash 0.3.1__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 +293 -12
  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.1.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.1.dist-info/RECORD +0 -136
  143. {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/WHEEL +0 -0
  144. {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/entry_points.txt +0 -0
  145. {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/licenses/LICENSE +0 -0
  146. {kailash-0.3.1.dist-info → kailash-0.4.0.dist-info}/top_level.txt +0 -0
@@ -2,11 +2,11 @@
2
2
 
3
3
  This module provides nodes for connecting to SharePoint using Microsoft Graph API.
4
4
  It supports modern authentication with MSAL and provides better compatibility
5
- with Azure AD app registrations.
5
+ with Azure AD app registrations. Supports multiple authentication methods.
6
6
 
7
7
  Design purpose:
8
8
  - Enable seamless integration with SharePoint via Graph API
9
- - Support app-only authentication with client credentials
9
+ - Support multiple authentication methods (client credentials, certificate, username/password, managed identity, device code)
10
10
  - Provide operations for file management and search
11
11
  - Align with database persistence requirements for orchestration
12
12
 
@@ -21,9 +21,11 @@ Downstream consumers:
21
21
  - Long-running workflows with state persistence
22
22
  """
23
23
 
24
+ import base64
24
25
  import os
26
+ from datetime import datetime, timedelta
25
27
  from pathlib import Path
26
- from typing import Any
28
+ from typing import Any, Optional
27
29
 
28
30
  import requests
29
31
 
@@ -44,7 +46,7 @@ class SharePointGraphReader(Node):
44
46
  to the legacy SharePoint REST API.
45
47
 
46
48
  Key features:
47
- 1. Modern authentication with MSAL
49
+ 1. Multiple authentication methods (client credentials, certificate, username/password, managed identity, device code)
48
50
  2. Support for listing, downloading, and searching files
49
51
  3. Folder navigation and library support
50
52
  4. Stateless design for orchestration compatibility
@@ -56,9 +58,10 @@ class SharePointGraphReader(Node):
56
58
  3. Search for files by name
57
59
  4. Navigate folder structures
58
60
 
59
- Example:
61
+ Example (Client Credentials):
60
62
  >>> reader = SharePointGraphReader()
61
63
  >>> result = reader.execute(
64
+ ... auth_method="client_credentials",
62
65
  ... tenant_id="your-tenant-id",
63
66
  ... client_id="your-client-id",
64
67
  ... client_secret="your-secret",
@@ -67,21 +70,41 @@ class SharePointGraphReader(Node):
67
70
  ... library_name="Documents",
68
71
  ... folder_path="Reports/2024"
69
72
  ... )
73
+
74
+ Example (Certificate):
75
+ >>> reader = SharePointGraphReader()
76
+ >>> result = reader.execute(
77
+ ... auth_method="certificate",
78
+ ... tenant_id="your-tenant-id",
79
+ ... client_id="your-client-id",
80
+ ... certificate_path="/path/to/cert.pem",
81
+ ... site_url="https://company.sharepoint.com/sites/project",
82
+ ... operation="list_files"
83
+ ... )
70
84
  """
71
85
 
72
86
  def get_metadata(self) -> NodeMetadata:
73
87
  """Get node metadata for discovery and orchestration."""
74
88
  return NodeMetadata(
75
89
  name="SharePoint Graph Reader",
76
- description="Read files from SharePoint using Microsoft Graph API",
77
- tags={"sharepoint", "graph", "reader", "cloud", "microsoft"},
78
- version="2.0.0",
90
+ description="Read files from SharePoint using Microsoft Graph API with multiple authentication methods",
91
+ tags={"sharepoint", "graph", "reader", "cloud", "microsoft", "multi-auth"},
92
+ version="3.0.0",
79
93
  author="Kailash SDK",
80
94
  )
81
95
 
82
96
  def get_parameters(self) -> dict[str, NodeParameter]:
83
- """Define input parameters for SharePoint Graph operations."""
97
+ """Define input parameters for SharePoint Graph operations with multiple auth methods."""
84
98
  return {
99
+ # Authentication method selection
100
+ "auth_method": NodeParameter(
101
+ name="auth_method",
102
+ type=str,
103
+ required=False,
104
+ default="client_credentials",
105
+ description="Authentication method: client_credentials, certificate, username_password, managed_identity, device_code",
106
+ ),
107
+ # Common auth parameters
85
108
  "tenant_id": NodeParameter(
86
109
  name="tenant_id",
87
110
  type=str,
@@ -98,8 +121,62 @@ class SharePointGraphReader(Node):
98
121
  name="client_secret",
99
122
  type=str,
100
123
  required=False,
101
- description="Azure AD app client secret",
124
+ description="Azure AD app client secret (for client_credentials auth)",
125
+ ),
126
+ # Certificate auth parameters
127
+ "certificate_path": NodeParameter(
128
+ name="certificate_path",
129
+ type=str,
130
+ required=False,
131
+ description="Path to certificate file for certificate authentication",
132
+ ),
133
+ "certificate_password": NodeParameter(
134
+ name="certificate_password",
135
+ type=str,
136
+ required=False,
137
+ description="Password for certificate file (if encrypted)",
138
+ ),
139
+ "certificate_thumbprint": NodeParameter(
140
+ name="certificate_thumbprint",
141
+ type=str,
142
+ required=False,
143
+ description="Certificate thumbprint (alternative to file path)",
102
144
  ),
145
+ # Username/password auth parameters
146
+ "username": NodeParameter(
147
+ name="username",
148
+ type=str,
149
+ required=False,
150
+ description="Username for resource owner password flow",
151
+ ),
152
+ "password": NodeParameter(
153
+ name="password",
154
+ type=str,
155
+ required=False,
156
+ description="Password for resource owner password flow",
157
+ ),
158
+ # Managed identity parameters
159
+ "use_system_identity": NodeParameter(
160
+ name="use_system_identity",
161
+ type=bool,
162
+ required=False,
163
+ default=True,
164
+ description="Use system-assigned managed identity (vs user-assigned)",
165
+ ),
166
+ "managed_identity_client_id": NodeParameter(
167
+ name="managed_identity_client_id",
168
+ type=str,
169
+ required=False,
170
+ description="Client ID for user-assigned managed identity",
171
+ ),
172
+ # Device code parameters
173
+ "device_code_callback": NodeParameter(
174
+ name="device_code_callback",
175
+ type=str,
176
+ required=False,
177
+ description="Callback function name for device code display",
178
+ ),
179
+ # SharePoint operation parameters
103
180
  "site_url": NodeParameter(
104
181
  name="site_url",
105
182
  type=str,
@@ -184,6 +261,235 @@ class SharePointGraphReader(Node):
184
261
  },
185
262
  }
186
263
 
264
+ def _authenticate_certificate(
265
+ self,
266
+ tenant_id: str,
267
+ client_id: str,
268
+ certificate_path: Optional[str] = None,
269
+ certificate_password: Optional[str] = None,
270
+ certificate_thumbprint: Optional[str] = None,
271
+ ) -> dict[str, Any]:
272
+ """Authenticate using certificate-based authentication."""
273
+ try:
274
+ import msal
275
+ except ImportError:
276
+ raise NodeConfigurationError(
277
+ "MSAL library not installed. Install with: pip install msal"
278
+ )
279
+
280
+ # Load certificate
281
+ if certificate_path:
282
+ with open(certificate_path, "rb") as f:
283
+ cert_data = f.read()
284
+
285
+ # Try to load as PEM or DER
286
+ try:
287
+ if certificate_password:
288
+ from cryptography import x509
289
+ from cryptography.hazmat.primitives import hashes, serialization
290
+ from cryptography.hazmat.primitives.serialization import pkcs12
291
+
292
+ private_key, certificate, _ = pkcs12.load_key_and_certificates(
293
+ cert_data,
294
+ certificate_password.encode() if certificate_password else None,
295
+ )
296
+ else:
297
+ from cryptography import x509
298
+ from cryptography.hazmat.primitives import hashes, serialization
299
+
300
+ # Load PEM certificate
301
+ certificate = x509.load_pem_x509_certificate(cert_data)
302
+ private_key = serialization.load_pem_private_key(
303
+ cert_data, password=None
304
+ )
305
+ except Exception as e:
306
+ raise NodeConfigurationError(f"Failed to load certificate: {e}")
307
+
308
+ # Get thumbprint
309
+ thumbprint = (
310
+ base64.urlsafe_b64encode(certificate.fingerprint(hashes.SHA1()))
311
+ .decode("utf-8")
312
+ .rstrip("=")
313
+ )
314
+
315
+ # Create client credential from certificate
316
+ client_credential = {
317
+ "private_key": private_key,
318
+ "thumbprint": thumbprint,
319
+ "public_certificate": certificate.public_bytes(
320
+ serialization.Encoding.PEM
321
+ ).decode(),
322
+ }
323
+ elif certificate_thumbprint:
324
+ # Use provided thumbprint (assumes cert is already registered in Azure AD)
325
+ client_credential = {"thumbprint": certificate_thumbprint}
326
+ else:
327
+ raise NodeConfigurationError(
328
+ "Either certificate_path or certificate_thumbprint must be provided"
329
+ )
330
+
331
+ app = msal.ConfidentialClientApplication(
332
+ client_id=client_id,
333
+ client_credential=client_credential,
334
+ authority=f"https://login.microsoftonline.com/{tenant_id}",
335
+ )
336
+
337
+ result = app.acquire_token_for_client(
338
+ scopes=["https://graph.microsoft.com/.default"]
339
+ )
340
+
341
+ if "access_token" not in result:
342
+ error_msg = result.get("error_description", "Unknown authentication error")
343
+ raise NodeExecutionError(f"Certificate authentication failed: {error_msg}")
344
+
345
+ return {
346
+ "token": result["access_token"],
347
+ "headers": {
348
+ "Authorization": f"Bearer {result['access_token']}",
349
+ "Accept": "application/json",
350
+ "Content-Type": "application/json",
351
+ },
352
+ }
353
+
354
+ def _authenticate_username_password(
355
+ self, tenant_id: str, client_id: str, username: str, password: str
356
+ ) -> dict[str, Any]:
357
+ """Authenticate using username/password (Resource Owner Password Credentials)."""
358
+ try:
359
+ import msal
360
+ except ImportError:
361
+ raise NodeConfigurationError(
362
+ "MSAL library not installed. Install with: pip install msal"
363
+ )
364
+
365
+ app = msal.PublicClientApplication(
366
+ client_id=client_id,
367
+ authority=f"https://login.microsoftonline.com/{tenant_id}",
368
+ )
369
+
370
+ result = app.acquire_token_by_username_password(
371
+ username=username,
372
+ password=password,
373
+ scopes=["https://graph.microsoft.com/.default"],
374
+ )
375
+
376
+ if "access_token" not in result:
377
+ error_msg = result.get("error_description", "Unknown authentication error")
378
+ raise NodeExecutionError(
379
+ f"Username/password authentication failed: {error_msg}"
380
+ )
381
+
382
+ return {
383
+ "token": result["access_token"],
384
+ "headers": {
385
+ "Authorization": f"Bearer {result['access_token']}",
386
+ "Accept": "application/json",
387
+ "Content-Type": "application/json",
388
+ },
389
+ }
390
+
391
+ def _authenticate_managed_identity(
392
+ self,
393
+ use_system_identity: bool = True,
394
+ managed_identity_client_id: Optional[str] = None,
395
+ ) -> dict[str, Any]:
396
+ """Authenticate using Azure Managed Identity."""
397
+ # Managed Identity endpoint
398
+ msi_endpoint = os.environ.get(
399
+ "MSI_ENDPOINT", "http://169.254.169.254/metadata/identity/oauth2/token"
400
+ )
401
+
402
+ params = {
403
+ "api-version": "2019-08-01",
404
+ "resource": "https://graph.microsoft.com",
405
+ }
406
+
407
+ headers = {"Metadata": "true"}
408
+
409
+ # Add secret if using App Service
410
+ msi_secret = os.environ.get("MSI_SECRET")
411
+ if msi_secret:
412
+ headers["X-IDENTITY-HEADER"] = msi_secret
413
+
414
+ # Use user-assigned identity if specified
415
+ if not use_system_identity and managed_identity_client_id:
416
+ params["client_id"] = managed_identity_client_id
417
+
418
+ try:
419
+ response = requests.get(msi_endpoint, params=params, headers=headers)
420
+ response.raise_for_status()
421
+
422
+ token_data = response.json()
423
+ access_token = token_data["access_token"]
424
+
425
+ return {
426
+ "token": access_token,
427
+ "headers": {
428
+ "Authorization": f"Bearer {access_token}",
429
+ "Accept": "application/json",
430
+ "Content-Type": "application/json",
431
+ },
432
+ }
433
+ except Exception as e:
434
+ raise NodeExecutionError(
435
+ f"Managed Identity authentication failed: {e}. "
436
+ "Ensure this code is running in an Azure environment with Managed Identity enabled."
437
+ )
438
+
439
+ def _authenticate_device_code(
440
+ self, tenant_id: str, client_id: str, device_code_callback: Optional[str] = None
441
+ ) -> dict[str, Any]:
442
+ """Authenticate using device code flow."""
443
+ try:
444
+ import msal
445
+ except ImportError:
446
+ raise NodeConfigurationError(
447
+ "MSAL library not installed. Install with: pip install msal"
448
+ )
449
+
450
+ app = msal.PublicClientApplication(
451
+ client_id=client_id,
452
+ authority=f"https://login.microsoftonline.com/{tenant_id}",
453
+ )
454
+
455
+ flow = app.initiate_device_flow(scopes=["https://graph.microsoft.com/.default"])
456
+
457
+ if "user_code" not in flow:
458
+ raise NodeExecutionError("Failed to initiate device flow")
459
+
460
+ # Display the code to user
461
+ print(f"\nTo authenticate, visit: {flow['verification_uri']}")
462
+ print(f"Enter code: {flow['user_code']}\n")
463
+
464
+ # If callback provided, call it with the flow info
465
+ if device_code_callback:
466
+ try:
467
+ # Use importlib to safely import callback function instead of eval
468
+ import importlib
469
+
470
+ module_name, func_name = device_code_callback.rsplit(".", 1)
471
+ module = importlib.import_module(module_name)
472
+ callback_func = getattr(module, func_name)
473
+ callback_func(flow)
474
+ except:
475
+ pass
476
+
477
+ # Wait for user to authenticate
478
+ result = app.acquire_token_by_device_flow(flow)
479
+
480
+ if "access_token" not in result:
481
+ error_msg = result.get("error_description", "Unknown authentication error")
482
+ raise NodeExecutionError(f"Device code authentication failed: {error_msg}")
483
+
484
+ return {
485
+ "token": result["access_token"],
486
+ "headers": {
487
+ "Authorization": f"Bearer {result['access_token']}",
488
+ "Accept": "application/json",
489
+ "Content-Type": "application/json",
490
+ },
491
+ }
492
+
187
493
  def _get_site_data(self, site_url: str, headers: dict[str, str]) -> dict[str, Any]:
188
494
  """Get SharePoint site data from Graph API."""
189
495
  # Convert SharePoint URL to Graph API site ID format
@@ -384,21 +690,16 @@ class SharePointGraphReader(Node):
384
690
  )
385
691
 
386
692
  def run(self, **kwargs) -> dict[str, Any]:
387
- """Execute SharePoint Graph operation.
693
+ """Execute SharePoint Graph operation with selected authentication method.
388
694
 
389
695
  This method is stateless and returns JSON-serializable results
390
696
  suitable for database persistence and orchestration.
391
697
  """
392
- # Validate required parameters
393
- tenant_id = kwargs.get("tenant_id")
394
- client_id = kwargs.get("client_id")
395
- client_secret = kwargs.get("client_secret")
698
+ auth_method = kwargs.get("auth_method", "client_credentials")
396
699
  site_url = kwargs.get("site_url")
397
700
 
398
- if not all([tenant_id, client_id, client_secret, site_url]):
399
- raise NodeValidationError(
400
- "tenant_id, client_id, client_secret, and site_url are required"
401
- )
701
+ if not site_url:
702
+ raise NodeValidationError("site_url is required")
402
703
 
403
704
  # Get operation
404
705
  operation = kwargs.get("operation", "list_files")
@@ -413,8 +714,83 @@ class SharePointGraphReader(Node):
413
714
  f"Invalid operation '{operation}'. Must be one of: {', '.join(valid_operations)}"
414
715
  )
415
716
 
416
- # Authenticate and get site data
417
- auth_data = self._authenticate(tenant_id, client_id, client_secret)
717
+ # Authenticate based on method
718
+ if auth_method == "client_credentials":
719
+ tenant_id = kwargs.get("tenant_id")
720
+ client_id = kwargs.get("client_id")
721
+ client_secret = kwargs.get("client_secret")
722
+
723
+ if not all([tenant_id, client_id, client_secret]):
724
+ raise NodeValidationError(
725
+ "tenant_id, client_id, and client_secret are required for client_credentials auth"
726
+ )
727
+
728
+ auth_data = self._authenticate(tenant_id, client_id, client_secret)
729
+
730
+ elif auth_method == "certificate":
731
+ tenant_id = kwargs.get("tenant_id")
732
+ client_id = kwargs.get("client_id")
733
+
734
+ if not all([tenant_id, client_id]):
735
+ raise NodeValidationError(
736
+ "tenant_id and client_id are required for certificate auth"
737
+ )
738
+
739
+ auth_data = self._authenticate_certificate(
740
+ tenant_id=tenant_id,
741
+ client_id=client_id,
742
+ certificate_path=kwargs.get("certificate_path"),
743
+ certificate_password=kwargs.get("certificate_password"),
744
+ certificate_thumbprint=kwargs.get("certificate_thumbprint"),
745
+ )
746
+
747
+ elif auth_method == "username_password":
748
+ tenant_id = kwargs.get("tenant_id")
749
+ client_id = kwargs.get("client_id")
750
+ username = kwargs.get("username")
751
+ password = kwargs.get("password")
752
+
753
+ if not all([tenant_id, client_id, username, password]):
754
+ raise NodeValidationError(
755
+ "tenant_id, client_id, username, and password are required for username_password auth"
756
+ )
757
+
758
+ auth_data = self._authenticate_username_password(
759
+ tenant_id=tenant_id,
760
+ client_id=client_id,
761
+ username=username,
762
+ password=password,
763
+ )
764
+
765
+ elif auth_method == "managed_identity":
766
+ auth_data = self._authenticate_managed_identity(
767
+ use_system_identity=kwargs.get("use_system_identity", True),
768
+ managed_identity_client_id=kwargs.get("managed_identity_client_id"),
769
+ )
770
+
771
+ elif auth_method == "device_code":
772
+ tenant_id = kwargs.get("tenant_id")
773
+ client_id = kwargs.get("client_id")
774
+
775
+ if not all([tenant_id, client_id]):
776
+ raise NodeValidationError(
777
+ "tenant_id and client_id are required for device_code auth"
778
+ )
779
+
780
+ auth_data = self._authenticate_device_code(
781
+ tenant_id=tenant_id,
782
+ client_id=client_id,
783
+ device_code_callback=kwargs.get("device_code_callback"),
784
+ )
785
+
786
+ else:
787
+ raise NodeValidationError(
788
+ f"Invalid auth_method: {auth_method}. "
789
+ "Must be one of: client_credentials, certificate, username_password, "
790
+ "managed_identity, device_code"
791
+ )
792
+
793
+ # Get site data and execute operation
418
794
  headers = auth_data["headers"]
419
795
  site_data = self._get_site_data(site_url, headers)
420
796
  site_id = site_data["id"]
kailash/nodes/data/sql.py CHANGED
@@ -12,11 +12,14 @@ Design Philosophy:
12
12
  5. Transaction support
13
13
  """
14
14
 
15
+ import base64
15
16
  import os
16
17
  import threading
17
18
  import time
18
- from datetime import datetime
19
+ from datetime import date, datetime, timedelta
20
+ from decimal import Decimal
19
21
  from typing import Any, Optional
22
+ from uuid import UUID
20
23
 
21
24
  import yaml
22
25
  from sqlalchemy import create_engine, text
@@ -249,6 +252,9 @@ class SQLDatabaseNode(Node):
249
252
  # Add connection_string to kwargs for base class validation
250
253
  kwargs["connection_string"] = connection_string
251
254
 
255
+ # Extract access control manager before passing to parent
256
+ self.access_control_manager = kwargs.pop("access_control_manager", None)
257
+
252
258
  # Call parent constructor
253
259
  super().__init__(**kwargs)
254
260
 
@@ -294,6 +300,12 @@ class SQLDatabaseNode(Node):
294
300
  default="dict",
295
301
  description="Result format: 'dict', 'list', or 'raw'",
296
302
  ),
303
+ "user_context": NodeParameter(
304
+ name="user_context",
305
+ type=Any,
306
+ required=False,
307
+ description="User context for access control",
308
+ ),
297
309
  }
298
310
 
299
311
  @staticmethod
@@ -357,11 +369,22 @@ class SQLDatabaseNode(Node):
357
369
  query = kwargs.get("query")
358
370
  parameters = kwargs.get("parameters", [])
359
371
  result_format = kwargs.get("result_format", "dict")
372
+ user_context = kwargs.get("user_context")
360
373
 
361
374
  # Validate required parameters
362
375
  if not query:
363
376
  raise NodeExecutionError("query parameter is required")
364
377
 
378
+ # Check access control if enabled
379
+ if self.access_control_manager and user_context:
380
+ from kailash.access_control import NodePermission
381
+
382
+ decision = self.access_control_manager.check_node_access(
383
+ user_context, self.metadata.name, NodePermission.EXECUTE
384
+ )
385
+ if not decision.allowed:
386
+ raise NodeExecutionError(f"Access denied: {decision.reason}")
387
+
365
388
  # Validate query safety
366
389
  self._validate_query_safety(query)
367
390
 
@@ -450,6 +473,20 @@ class SQLDatabaseNode(Node):
450
473
  f"Query executed successfully in {execution_time:.3f}s, {row_count} rows affected/returned"
451
474
  )
452
475
 
476
+ # Apply data masking if access control is enabled
477
+ if self.access_control_manager and user_context and formatted_data:
478
+ if result_format == "dict" and isinstance(formatted_data, list):
479
+ masked_data = []
480
+ for row in formatted_data:
481
+ if isinstance(row, dict):
482
+ masked_row = self.access_control_manager.apply_data_masking(
483
+ user_context, self.metadata.name, row
484
+ )
485
+ masked_data.append(masked_row)
486
+ else:
487
+ masked_data.append(row)
488
+ formatted_data = masked_data
489
+
453
490
  return {
454
491
  "data": formatted_data,
455
492
  "row_count": row_count,
@@ -789,6 +826,35 @@ class SQLDatabaseNode(Node):
789
826
 
790
827
  return modified_query, param_dict
791
828
 
829
+ def _serialize_value(self, value: Any) -> Any:
830
+ """Convert database-specific types to JSON-serializable types.
831
+
832
+ Args:
833
+ value: Value to serialize
834
+
835
+ Returns:
836
+ JSON-serializable value
837
+ """
838
+ if value is None:
839
+ return None
840
+ elif isinstance(value, Decimal):
841
+ return float(value)
842
+ elif isinstance(value, datetime):
843
+ return value.isoformat()
844
+ elif isinstance(value, date):
845
+ return value.isoformat()
846
+ elif isinstance(value, timedelta):
847
+ return value.total_seconds()
848
+ elif isinstance(value, UUID):
849
+ return str(value)
850
+ elif isinstance(value, bytes):
851
+ return base64.b64encode(value).decode("utf-8")
852
+ elif isinstance(value, (list, tuple)):
853
+ return [self._serialize_value(item) for item in value]
854
+ elif isinstance(value, dict):
855
+ return {k: self._serialize_value(v) for k, v in value.items()}
856
+ return value
857
+
792
858
  def _format_results(
793
859
  self, rows: list, columns: list[str], result_format: str
794
860
  ) -> list[Any]:
@@ -805,19 +871,42 @@ class SQLDatabaseNode(Node):
805
871
  if result_format == "dict":
806
872
  # List of dictionaries with column names as keys
807
873
  # SQLAlchemy rows can be converted to dict using _asdict() or dict()
808
- return [dict(row._mapping) for row in rows]
874
+ result = []
875
+ for row in rows:
876
+ row_dict = dict(row._mapping)
877
+ # Serialize values for JSON compatibility
878
+ serialized_dict = {
879
+ k: self._serialize_value(v) for k, v in row_dict.items()
880
+ }
881
+ result.append(serialized_dict)
882
+ return result
809
883
 
810
884
  elif result_format == "list":
811
885
  # List of lists (raw rows)
812
- return [list(row) for row in rows]
886
+ result = []
887
+ for row in rows:
888
+ serialized_row = [self._serialize_value(value) for value in row]
889
+ result.append(serialized_row)
890
+ return result
813
891
 
814
892
  elif result_format == "raw":
815
893
  # Raw SQLAlchemy row objects (converted to list for JSON serialization)
816
- return [list(row) for row in rows]
894
+ result = []
895
+ for row in rows:
896
+ serialized_row = [self._serialize_value(value) for value in row]
897
+ result.append(serialized_row)
898
+ return result
817
899
 
818
900
  else:
819
901
  # Default to dict format
820
902
  self.logger.warning(
821
903
  f"Unknown result_format '{result_format}', defaulting to 'dict'"
822
904
  )
823
- return [dict(zip(columns, row, strict=False)) for row in rows]
905
+ result = []
906
+ for row in rows:
907
+ row_dict = dict(zip(columns, row, strict=False))
908
+ serialized_dict = {
909
+ k: self._serialize_value(v) for k, v in row_dict.items()
910
+ }
911
+ result.append(serialized_dict)
912
+ return result