atlan-application-sdk 2.1.1__py3-none-any.whl → 2.3.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.
- application_sdk/application/__init__.py +68 -1
- application_sdk/application/metadata_extraction/sql.py +44 -0
- application_sdk/clients/azure/__init__.py +6 -0
- application_sdk/clients/azure/auth.py +288 -0
- application_sdk/clients/azure/client.py +336 -0
- application_sdk/clients/temporal.py +6 -1
- application_sdk/common/error_codes.py +9 -0
- application_sdk/constants.py +25 -0
- application_sdk/server/fastapi/__init__.py +30 -1
- application_sdk/server/fastapi/models.py +62 -1
- application_sdk/transformers/query/__init__.py +26 -0
- application_sdk/version.py +1 -1
- application_sdk/worker.py +104 -6
- {atlan_application_sdk-2.1.1.dist-info → atlan_application_sdk-2.3.0.dist-info}/METADATA +5 -1
- {atlan_application_sdk-2.1.1.dist-info → atlan_application_sdk-2.3.0.dist-info}/RECORD +18 -15
- {atlan_application_sdk-2.1.1.dist-info → atlan_application_sdk-2.3.0.dist-info}/WHEEL +0 -0
- {atlan_application_sdk-2.1.1.dist-info → atlan_application_sdk-2.3.0.dist-info}/licenses/LICENSE +0 -0
- {atlan_application_sdk-2.1.1.dist-info → atlan_application_sdk-2.3.0.dist-info}/licenses/NOTICE +0 -0
|
@@ -1,13 +1,20 @@
|
|
|
1
1
|
from concurrent.futures import ThreadPoolExecutor
|
|
2
2
|
from typing import Any, Dict, List, Optional, Tuple, Type
|
|
3
3
|
|
|
4
|
+
from typing_extensions import deprecated
|
|
5
|
+
|
|
4
6
|
from application_sdk.activities import ActivitiesInterface
|
|
5
7
|
from application_sdk.clients.base import BaseClient
|
|
6
8
|
from application_sdk.clients.utils import get_workflow_client
|
|
7
|
-
from application_sdk.constants import ENABLE_MCP
|
|
9
|
+
from application_sdk.constants import APPLICATION_MODE, ENABLE_MCP, ApplicationMode
|
|
8
10
|
from application_sdk.handlers.base import BaseHandler
|
|
9
11
|
from application_sdk.interceptors.models import EventRegistration
|
|
12
|
+
from application_sdk.observability.decorators.observability_decorator import (
|
|
13
|
+
observability,
|
|
14
|
+
)
|
|
10
15
|
from application_sdk.observability.logger_adaptor import get_logger
|
|
16
|
+
from application_sdk.observability.metrics_adaptor import get_metrics
|
|
17
|
+
from application_sdk.observability.traces_adaptor import get_traces
|
|
11
18
|
from application_sdk.server import ServerInterface
|
|
12
19
|
from application_sdk.server.fastapi import APIServer, HttpWorkflowTrigger
|
|
13
20
|
from application_sdk.server.fastapi.models import EventWorkflowTrigger
|
|
@@ -15,6 +22,8 @@ from application_sdk.worker import Worker
|
|
|
15
22
|
from application_sdk.workflows import WorkflowInterface
|
|
16
23
|
|
|
17
24
|
logger = get_logger(__name__)
|
|
25
|
+
metrics = get_metrics()
|
|
26
|
+
traces = get_traces()
|
|
18
27
|
|
|
19
28
|
|
|
20
29
|
class BaseApplication:
|
|
@@ -110,6 +119,40 @@ class BaseApplication:
|
|
|
110
119
|
|
|
111
120
|
self.event_subscriptions[event_id].workflow_class = workflow_class
|
|
112
121
|
|
|
122
|
+
async def start(
|
|
123
|
+
self,
|
|
124
|
+
workflow_class: Type[WorkflowInterface],
|
|
125
|
+
ui_enabled: bool = True,
|
|
126
|
+
has_configmap: bool = False,
|
|
127
|
+
):
|
|
128
|
+
"""Start the application based on the configured APPLICATION_MODE.
|
|
129
|
+
|
|
130
|
+
Args:
|
|
131
|
+
workflow_class: The workflow class to register with the server.
|
|
132
|
+
ui_enabled: Whether to enable the UI. Defaults to True.
|
|
133
|
+
has_configmap: Whether the application has a configmap. Defaults to False.
|
|
134
|
+
|
|
135
|
+
Behavior based on APPLICATION_MODE:
|
|
136
|
+
- LOCAL: Starts worker in daemon mode and server (for local development)
|
|
137
|
+
- WORKER: Starts only the worker in non-daemon mode (for production worker pods)
|
|
138
|
+
- SERVER: Starts only the server (for production API server pods)
|
|
139
|
+
|
|
140
|
+
Raises:
|
|
141
|
+
ValueError: If APPLICATION_MODE is not a valid ApplicationMode value.
|
|
142
|
+
"""
|
|
143
|
+
if APPLICATION_MODE in (ApplicationMode.LOCAL, ApplicationMode.WORKER):
|
|
144
|
+
await self._start_worker(
|
|
145
|
+
daemon=APPLICATION_MODE == ApplicationMode.LOCAL,
|
|
146
|
+
)
|
|
147
|
+
|
|
148
|
+
if APPLICATION_MODE in (ApplicationMode.LOCAL, ApplicationMode.SERVER):
|
|
149
|
+
await self._setup_server(
|
|
150
|
+
workflow_class=workflow_class,
|
|
151
|
+
ui_enabled=ui_enabled,
|
|
152
|
+
has_configmap=has_configmap,
|
|
153
|
+
)
|
|
154
|
+
await self._start_server()
|
|
155
|
+
|
|
113
156
|
async def setup_workflow(
|
|
114
157
|
self,
|
|
115
158
|
workflow_and_activities_classes: List[
|
|
@@ -167,7 +210,12 @@ class BaseApplication:
|
|
|
167
210
|
raise ValueError("Workflow client not initialized")
|
|
168
211
|
return await self.workflow_client.start_workflow(workflow_args, workflow_class) # type: ignore
|
|
169
212
|
|
|
213
|
+
@deprecated("Use application.start(). Deprecated since v2.3.0.")
|
|
214
|
+
@observability(logger=logger, metrics=metrics, traces=traces)
|
|
170
215
|
async def start_worker(self, daemon: bool = True):
|
|
216
|
+
return await self._start_worker(daemon=daemon)
|
|
217
|
+
|
|
218
|
+
async def _start_worker(self, daemon: bool = True):
|
|
171
219
|
"""
|
|
172
220
|
Start the worker for the application.
|
|
173
221
|
|
|
@@ -178,11 +226,25 @@ class BaseApplication:
|
|
|
178
226
|
raise ValueError("Worker not initialized")
|
|
179
227
|
await self.worker.start(daemon=daemon)
|
|
180
228
|
|
|
229
|
+
@deprecated("Use application.start(). Deprecated since v2.3.0.")
|
|
230
|
+
@observability(logger=logger, metrics=metrics, traces=traces)
|
|
181
231
|
async def setup_server(
|
|
182
232
|
self,
|
|
183
233
|
workflow_class: Type[WorkflowInterface],
|
|
184
234
|
ui_enabled: bool = True,
|
|
185
235
|
has_configmap: bool = False,
|
|
236
|
+
):
|
|
237
|
+
return await self._setup_server(
|
|
238
|
+
workflow_class=workflow_class,
|
|
239
|
+
ui_enabled=ui_enabled,
|
|
240
|
+
has_configmap=has_configmap,
|
|
241
|
+
)
|
|
242
|
+
|
|
243
|
+
async def _setup_server(
|
|
244
|
+
self,
|
|
245
|
+
workflow_class: Type[WorkflowInterface],
|
|
246
|
+
ui_enabled: bool = True,
|
|
247
|
+
has_configmap: bool = False,
|
|
186
248
|
):
|
|
187
249
|
"""
|
|
188
250
|
Set up FastAPI server and automatically mount MCP if enabled.
|
|
@@ -239,7 +301,12 @@ class BaseApplication:
|
|
|
239
301
|
triggers=[HttpWorkflowTrigger()],
|
|
240
302
|
)
|
|
241
303
|
|
|
304
|
+
@deprecated("Use application.start(). Deprecated since v2.3.0.")
|
|
305
|
+
@observability(logger=logger, metrics=metrics, traces=traces)
|
|
242
306
|
async def start_server(self):
|
|
307
|
+
return await self._start_server()
|
|
308
|
+
|
|
309
|
+
async def _start_server(self):
|
|
243
310
|
"""
|
|
244
311
|
Start the FastAPI server for the application.
|
|
245
312
|
|
|
@@ -1,6 +1,8 @@
|
|
|
1
1
|
from concurrent.futures import ThreadPoolExecutor
|
|
2
2
|
from typing import Any, Dict, List, Optional, Tuple, Type
|
|
3
3
|
|
|
4
|
+
from typing_extensions import deprecated
|
|
5
|
+
|
|
4
6
|
from application_sdk.application import BaseApplication
|
|
5
7
|
from application_sdk.clients.sql import BaseSQLClient
|
|
6
8
|
from application_sdk.clients.utils import get_workflow_client
|
|
@@ -146,8 +148,33 @@ class BaseSQLMetadataExtractionApplication(BaseApplication):
|
|
|
146
148
|
)
|
|
147
149
|
return workflow_response
|
|
148
150
|
|
|
151
|
+
@observability(logger=logger, metrics=metrics, traces=traces)
|
|
152
|
+
async def start(
|
|
153
|
+
self,
|
|
154
|
+
workflow_class: Type = BaseSQLMetadataExtractionWorkflow,
|
|
155
|
+
ui_enabled: bool = True,
|
|
156
|
+
has_configmap: bool = False,
|
|
157
|
+
):
|
|
158
|
+
"""Start the SQL metadata extraction application.
|
|
159
|
+
|
|
160
|
+
Args:
|
|
161
|
+
workflow_class: The workflow class to register. Defaults to BaseSQLMetadataExtractionWorkflow.
|
|
162
|
+
ui_enabled: Whether to enable the UI. Defaults to True.
|
|
163
|
+
has_configmap: Whether the application has a configmap. Defaults to False.
|
|
164
|
+
"""
|
|
165
|
+
await super().start(
|
|
166
|
+
workflow_class=workflow_class,
|
|
167
|
+
ui_enabled=ui_enabled,
|
|
168
|
+
has_configmap=has_configmap,
|
|
169
|
+
)
|
|
170
|
+
|
|
171
|
+
@deprecated("Use application.start(). Deprecated since v2.3.0.")
|
|
149
172
|
@observability(logger=logger, metrics=metrics, traces=traces)
|
|
150
173
|
async def start_worker(self, daemon: bool = True):
|
|
174
|
+
return await self._start_worker(daemon=daemon)
|
|
175
|
+
|
|
176
|
+
@observability(logger=logger, metrics=metrics, traces=traces)
|
|
177
|
+
async def _start_worker(self, daemon: bool = True):
|
|
151
178
|
"""
|
|
152
179
|
Start the worker for the SQL metadata extraction application.
|
|
153
180
|
"""
|
|
@@ -155,12 +182,27 @@ class BaseSQLMetadataExtractionApplication(BaseApplication):
|
|
|
155
182
|
raise ValueError("Worker not initialized")
|
|
156
183
|
await self.worker.start(daemon=daemon)
|
|
157
184
|
|
|
185
|
+
@deprecated("Use application.start(). Deprecated since v2.3.0.")
|
|
158
186
|
@observability(logger=logger, metrics=metrics, traces=traces)
|
|
159
187
|
async def setup_server(
|
|
188
|
+
self,
|
|
189
|
+
workflow_class: Type = BaseSQLMetadataExtractionWorkflow,
|
|
190
|
+
ui_enabled: bool = True,
|
|
191
|
+
has_configmap: bool = False,
|
|
192
|
+
):
|
|
193
|
+
return await self._setup_server(
|
|
194
|
+
workflow_class=workflow_class,
|
|
195
|
+
ui_enabled=ui_enabled,
|
|
196
|
+
has_configmap=has_configmap,
|
|
197
|
+
)
|
|
198
|
+
|
|
199
|
+
@observability(logger=logger, metrics=metrics, traces=traces)
|
|
200
|
+
async def _setup_server(
|
|
160
201
|
self,
|
|
161
202
|
workflow_class: Type[
|
|
162
203
|
BaseSQLMetadataExtractionWorkflow
|
|
163
204
|
] = BaseSQLMetadataExtractionWorkflow,
|
|
205
|
+
ui_enabled: bool = True,
|
|
164
206
|
has_configmap: bool = False,
|
|
165
207
|
) -> Any:
|
|
166
208
|
"""
|
|
@@ -168,6 +210,7 @@ class BaseSQLMetadataExtractionApplication(BaseApplication):
|
|
|
168
210
|
|
|
169
211
|
Args:
|
|
170
212
|
workflow_class (Type): Workflow class to register with the server. Defaults to BaseSQLMetadataExtractionWorkflow.
|
|
213
|
+
ui_enabled (bool): Whether to enable the UI. Defaults to True.
|
|
171
214
|
has_configmap (bool): Whether the application has a configmap. Defaults to False.
|
|
172
215
|
|
|
173
216
|
Returns:
|
|
@@ -180,6 +223,7 @@ class BaseSQLMetadataExtractionApplication(BaseApplication):
|
|
|
180
223
|
self.server = APIServer(
|
|
181
224
|
handler=self.handler_class(sql_client=self.client_class()),
|
|
182
225
|
workflow_client=self.workflow_client,
|
|
226
|
+
ui_enabled=ui_enabled,
|
|
183
227
|
has_configmap=has_configmap,
|
|
184
228
|
)
|
|
185
229
|
|
|
@@ -0,0 +1,288 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Azure authentication provider for the application-sdk framework.
|
|
3
|
+
|
|
4
|
+
This module provides the AzureAuthProvider class that handles Azure
|
|
5
|
+
Service Principal authentication for the application-sdk framework.
|
|
6
|
+
|
|
7
|
+
Example:
|
|
8
|
+
>>> from application_sdk.clients.azure.auth import AzureAuthProvider
|
|
9
|
+
>>> import asyncio
|
|
10
|
+
>>>
|
|
11
|
+
>>> # Create authentication provider
|
|
12
|
+
>>> auth_provider = AzureAuthProvider()
|
|
13
|
+
>>>
|
|
14
|
+
>>> # Authenticate with Service Principal credentials
|
|
15
|
+
>>> credentials = {
|
|
16
|
+
... "tenant_id": "your-tenant-id",
|
|
17
|
+
... "client_id": "your-client-id",
|
|
18
|
+
... "client_secret": "your-client-secret"
|
|
19
|
+
... }
|
|
20
|
+
>>>
|
|
21
|
+
>>> # Create credential
|
|
22
|
+
>>> credential = await auth_provider.create_credential(
|
|
23
|
+
... auth_type="service_principal",
|
|
24
|
+
... credentials=credentials
|
|
25
|
+
... )
|
|
26
|
+
>>>
|
|
27
|
+
>>> # Alternative credential key formats are also supported
|
|
28
|
+
>>> alt_credentials = {
|
|
29
|
+
... "tenantId": "your-tenant-id", # camelCase
|
|
30
|
+
... "clientId": "your-client-id", # camelCase
|
|
31
|
+
... "clientSecret": "your-client-secret" # camelCase
|
|
32
|
+
... }
|
|
33
|
+
>>>
|
|
34
|
+
>>> credential = await auth_provider.create_credential(
|
|
35
|
+
... auth_type="service_principal",
|
|
36
|
+
... credentials=alt_credentials
|
|
37
|
+
... )
|
|
38
|
+
>>>
|
|
39
|
+
>>> # Error handling for missing credentials
|
|
40
|
+
>>> try:
|
|
41
|
+
... await auth_provider.create_credential(
|
|
42
|
+
... auth_type="service_principal",
|
|
43
|
+
... credentials={"tenant_id": "only-tenant"} # Missing client_id and client_secret
|
|
44
|
+
... )
|
|
45
|
+
... except CommonError as e:
|
|
46
|
+
... print(f"Authentication failed: {e}")
|
|
47
|
+
... # Output: Authentication failed: Missing required credential keys: client_id, client_secret
|
|
48
|
+
>>>
|
|
49
|
+
>>> # Unsupported authentication type
|
|
50
|
+
>>> try:
|
|
51
|
+
... await auth_provider.create_credential(
|
|
52
|
+
... auth_type="unsupported_type",
|
|
53
|
+
... credentials=credentials
|
|
54
|
+
... )
|
|
55
|
+
... except CommonError as e:
|
|
56
|
+
... print(f"Authentication failed: {e}")
|
|
57
|
+
... # Output: Authentication failed: Only 'service_principal' authentication is supported. Received: unsupported_type
|
|
58
|
+
"""
|
|
59
|
+
|
|
60
|
+
from typing import Any, Dict, Optional
|
|
61
|
+
|
|
62
|
+
from azure.core.credentials import TokenCredential
|
|
63
|
+
from azure.core.exceptions import ClientAuthenticationError
|
|
64
|
+
from azure.identity import ClientSecretCredential
|
|
65
|
+
from pydantic import BaseModel, ConfigDict, Field, ValidationError
|
|
66
|
+
|
|
67
|
+
from application_sdk.clients.azure import AZURE_MANAGEMENT_API_ENDPOINT
|
|
68
|
+
from application_sdk.common.error_codes import CommonError
|
|
69
|
+
from application_sdk.common.utils import run_sync
|
|
70
|
+
from application_sdk.observability.logger_adaptor import get_logger
|
|
71
|
+
|
|
72
|
+
logger = get_logger(__name__)
|
|
73
|
+
|
|
74
|
+
|
|
75
|
+
class ServicePrincipalCredentials(BaseModel):
|
|
76
|
+
"""
|
|
77
|
+
Pydantic model for Azure Service Principal credentials.
|
|
78
|
+
|
|
79
|
+
Supports both snake_case and camelCase field names through field aliases.
|
|
80
|
+
All fields are required for service principal authentication.
|
|
81
|
+
|
|
82
|
+
Attributes:
|
|
83
|
+
tenant_id: Azure tenant ID (also accepts 'tenantId').
|
|
84
|
+
client_id: Azure client ID (also accepts 'clientId').
|
|
85
|
+
client_secret: Azure client secret (also accepts 'clientSecret').
|
|
86
|
+
"""
|
|
87
|
+
|
|
88
|
+
tenant_id: str = Field(
|
|
89
|
+
...,
|
|
90
|
+
alias="tenantId",
|
|
91
|
+
description="Azure tenant ID for service principal authentication",
|
|
92
|
+
)
|
|
93
|
+
client_id: str = Field(
|
|
94
|
+
...,
|
|
95
|
+
alias="clientId",
|
|
96
|
+
description="Azure client ID for service principal authentication",
|
|
97
|
+
)
|
|
98
|
+
client_secret: str = Field(
|
|
99
|
+
...,
|
|
100
|
+
alias="clientSecret",
|
|
101
|
+
description="Azure client secret for service principal authentication",
|
|
102
|
+
)
|
|
103
|
+
|
|
104
|
+
model_config = ConfigDict(
|
|
105
|
+
populate_by_name=True, # Allow both field name and alias
|
|
106
|
+
extra="ignore", # Ignore additional fields (Azure client may need extra fields like storage_account_name, network_config, etc.)
|
|
107
|
+
validate_assignment=True, # Validate on assignment
|
|
108
|
+
)
|
|
109
|
+
|
|
110
|
+
|
|
111
|
+
class AzureAuthProvider:
|
|
112
|
+
"""
|
|
113
|
+
Azure authentication provider for handling Service Principal authentication.
|
|
114
|
+
|
|
115
|
+
This class provides a unified interface for creating Azure credentials
|
|
116
|
+
using Service Principal authentication with Azure SDK.
|
|
117
|
+
|
|
118
|
+
Supported authentication method:
|
|
119
|
+
- service_principal: Using client ID, client secret, and tenant ID
|
|
120
|
+
"""
|
|
121
|
+
|
|
122
|
+
def __init__(self):
|
|
123
|
+
"""Initialize the Azure authentication provider."""
|
|
124
|
+
pass
|
|
125
|
+
|
|
126
|
+
async def create_credential(
|
|
127
|
+
self,
|
|
128
|
+
auth_type: str = "service_principal",
|
|
129
|
+
credentials: Optional[Dict[str, Any]] = None,
|
|
130
|
+
) -> TokenCredential:
|
|
131
|
+
"""
|
|
132
|
+
Create Azure credential using Service Principal authentication.
|
|
133
|
+
|
|
134
|
+
Args:
|
|
135
|
+
auth_type (str): Type of authentication to use.
|
|
136
|
+
Currently only supports 'service_principal'.
|
|
137
|
+
credentials (Optional[Dict[str, Any]]): Service Principal credentials.
|
|
138
|
+
Required fields: tenant_id, client_id, client_secret.
|
|
139
|
+
|
|
140
|
+
Returns:
|
|
141
|
+
TokenCredential: Azure credential instance.
|
|
142
|
+
|
|
143
|
+
Raises:
|
|
144
|
+
CommonError: If authentication type is not supported or credentials are invalid.
|
|
145
|
+
ClientAuthenticationError: If credential creation fails.
|
|
146
|
+
"""
|
|
147
|
+
try:
|
|
148
|
+
logger.debug(f"Creating Azure credential with auth type: {auth_type}")
|
|
149
|
+
|
|
150
|
+
if auth_type.lower() != "service_principal":
|
|
151
|
+
raise CommonError(
|
|
152
|
+
f"{CommonError.CREDENTIALS_PARSE_ERROR}: "
|
|
153
|
+
f"Only 'service_principal' authentication is supported. "
|
|
154
|
+
f"Received: {auth_type}"
|
|
155
|
+
)
|
|
156
|
+
|
|
157
|
+
if not credentials:
|
|
158
|
+
raise CommonError(
|
|
159
|
+
f"{CommonError.CREDENTIALS_PARSE_ERROR}: "
|
|
160
|
+
"Credentials required for service principal authentication"
|
|
161
|
+
)
|
|
162
|
+
|
|
163
|
+
return await self._create_service_principal_credential(credentials)
|
|
164
|
+
|
|
165
|
+
except ClientAuthenticationError as e:
|
|
166
|
+
logger.error(f"Azure authentication failed: {str(e)}")
|
|
167
|
+
raise CommonError(f"{CommonError.AZURE_CREDENTIAL_ERROR}: {str(e)}")
|
|
168
|
+
except ValueError as e:
|
|
169
|
+
logger.error(f"Invalid Azure credential parameters: {str(e)}")
|
|
170
|
+
raise CommonError(
|
|
171
|
+
f"{CommonError.CREDENTIALS_PARSE_ERROR}: Invalid credential parameters - {str(e)}"
|
|
172
|
+
)
|
|
173
|
+
except TypeError as e:
|
|
174
|
+
logger.error(f"Wrong Azure credential parameter types: {str(e)}")
|
|
175
|
+
raise CommonError(
|
|
176
|
+
f"{CommonError.CREDENTIALS_PARSE_ERROR}: Invalid credential parameter types - {str(e)}"
|
|
177
|
+
)
|
|
178
|
+
except Exception as e:
|
|
179
|
+
logger.error(f"Unexpected error creating Azure credential: {str(e)}")
|
|
180
|
+
raise CommonError(
|
|
181
|
+
f"{CommonError.CREDENTIALS_PARSE_ERROR}: Unexpected error - {str(e)}"
|
|
182
|
+
)
|
|
183
|
+
|
|
184
|
+
async def _create_service_principal_credential(
|
|
185
|
+
self, credentials: Dict[str, Any]
|
|
186
|
+
) -> ClientSecretCredential:
|
|
187
|
+
"""
|
|
188
|
+
Create service principal credential.
|
|
189
|
+
|
|
190
|
+
Args:
|
|
191
|
+
credentials (Dict[str, Any]): Service principal credentials.
|
|
192
|
+
Must include tenant_id, client_id, and client_secret.
|
|
193
|
+
|
|
194
|
+
Returns:
|
|
195
|
+
ClientSecretCredential: Service principal credential.
|
|
196
|
+
|
|
197
|
+
Raises:
|
|
198
|
+
CommonError: If required credentials are missing or invalid.
|
|
199
|
+
"""
|
|
200
|
+
if not credentials:
|
|
201
|
+
raise CommonError(
|
|
202
|
+
f"{CommonError.CREDENTIALS_PARSE_ERROR}: "
|
|
203
|
+
"Credentials required for service principal authentication"
|
|
204
|
+
)
|
|
205
|
+
|
|
206
|
+
try:
|
|
207
|
+
# Validate credentials using Pydantic model
|
|
208
|
+
validated_credentials = ServicePrincipalCredentials(**credentials)
|
|
209
|
+
except ValidationError as e:
|
|
210
|
+
# Pydantic provides detailed error messages for all validation errors
|
|
211
|
+
# Format errors into a user-friendly message
|
|
212
|
+
error_details = "; ".join(
|
|
213
|
+
[
|
|
214
|
+
f"{'.'.join(str(loc) for loc in err['loc'])}: {err['msg']}"
|
|
215
|
+
for err in e.errors()
|
|
216
|
+
]
|
|
217
|
+
)
|
|
218
|
+
error_message = f"Invalid credential parameters: {error_details}"
|
|
219
|
+
logger.error(f"Azure credential validation failed: {error_message}")
|
|
220
|
+
raise CommonError(f"{CommonError.CREDENTIALS_PARSE_ERROR}: {error_message}")
|
|
221
|
+
|
|
222
|
+
logger.debug(
|
|
223
|
+
f"Creating service principal credential for tenant: {validated_credentials.tenant_id}"
|
|
224
|
+
)
|
|
225
|
+
|
|
226
|
+
try:
|
|
227
|
+
return await run_sync(ClientSecretCredential)(
|
|
228
|
+
validated_credentials.tenant_id,
|
|
229
|
+
validated_credentials.client_id,
|
|
230
|
+
validated_credentials.client_secret,
|
|
231
|
+
)
|
|
232
|
+
except ValueError as e:
|
|
233
|
+
logger.error(f"Invalid Azure credential parameters: {str(e)}")
|
|
234
|
+
raise CommonError(
|
|
235
|
+
f"{CommonError.CREDENTIALS_PARSE_ERROR}: Invalid credential parameters - {str(e)}"
|
|
236
|
+
)
|
|
237
|
+
except TypeError as e:
|
|
238
|
+
logger.error(f"Wrong Azure credential parameter types: {str(e)}")
|
|
239
|
+
raise CommonError(
|
|
240
|
+
f"{CommonError.CREDENTIALS_PARSE_ERROR}: Invalid credential parameter types - {str(e)}"
|
|
241
|
+
)
|
|
242
|
+
except ClientAuthenticationError as e:
|
|
243
|
+
logger.error(f"Azure authentication failed: {str(e)}")
|
|
244
|
+
raise CommonError(f"{CommonError.AZURE_CREDENTIAL_ERROR}: {str(e)}")
|
|
245
|
+
except Exception as e:
|
|
246
|
+
logger.error(f"Unexpected error creating Azure credential: {str(e)}")
|
|
247
|
+
raise CommonError(
|
|
248
|
+
f"{CommonError.CREDENTIALS_PARSE_ERROR}: Unexpected error - {str(e)}"
|
|
249
|
+
)
|
|
250
|
+
|
|
251
|
+
async def validate_credential(self, credential: TokenCredential) -> bool:
|
|
252
|
+
"""
|
|
253
|
+
Validate Azure credential by attempting to get a token.
|
|
254
|
+
|
|
255
|
+
Args:
|
|
256
|
+
credential (TokenCredential): Azure credential to validate.
|
|
257
|
+
|
|
258
|
+
Returns:
|
|
259
|
+
bool: True if credential is valid, False otherwise.
|
|
260
|
+
"""
|
|
261
|
+
try:
|
|
262
|
+
logger.debug("Validating Azure credential")
|
|
263
|
+
|
|
264
|
+
# Try to get a token for Azure Management API
|
|
265
|
+
token = await run_sync(credential.get_token)(AZURE_MANAGEMENT_API_ENDPOINT)
|
|
266
|
+
|
|
267
|
+
if token and hasattr(token, "token"):
|
|
268
|
+
logger.debug("Azure credential validation successful")
|
|
269
|
+
return True
|
|
270
|
+
else:
|
|
271
|
+
logger.warning("Azure credential validation failed: No token received")
|
|
272
|
+
return False
|
|
273
|
+
|
|
274
|
+
except ClientAuthenticationError as e:
|
|
275
|
+
logger.error(
|
|
276
|
+
f"Azure credential validation failed - authentication error: {str(e)}"
|
|
277
|
+
)
|
|
278
|
+
return False
|
|
279
|
+
except ValueError as e:
|
|
280
|
+
logger.error(
|
|
281
|
+
f"Azure credential validation failed - invalid parameters: {str(e)}"
|
|
282
|
+
)
|
|
283
|
+
return False
|
|
284
|
+
except Exception as e:
|
|
285
|
+
logger.error(
|
|
286
|
+
f"Azure credential validation failed - unexpected error: {str(e)}"
|
|
287
|
+
)
|
|
288
|
+
return False
|