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