Skip to content

Commit 9c0cff1

Browse files
committed
feat(auth): added Azure authentication
1 parent 3e379d4 commit 9c0cff1

File tree

8 files changed

+1501
-1
lines changed

8 files changed

+1501
-1
lines changed
Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
"""
2+
Azure client module for the application-sdk framework.
3+
4+
This module provides a unified interface for connecting to and interacting
5+
with Azure Storage services including Blob Storage and Data Lake Storage Gen2.
6+
7+
The module includes:
8+
- AzureClient: Main client for unified Azure service access
9+
- AzureAuthProvider: Authentication provider for different Azure auth methods
10+
- AzureStorageClient: Storage service client for Blob and Data Lake Storage
11+
- Utilities: Common Azure helper functions
12+
"""
13+
14+
from .azure import AzureClient
15+
from .azure_auth import AzureAuthProvider
16+
from .azure_services import AzureStorageClient
17+
from .azure_utils import (
18+
parse_azure_url,
19+
validate_azure_credentials,
20+
build_azure_connection_string,
21+
extract_azure_resource_info,
22+
validate_azure_permissions,
23+
get_azure_service_endpoint,
24+
format_azure_error_message,
25+
)
26+
27+
__all__ = [
28+
# Main client
29+
"AzureClient",
30+
31+
# Authentication
32+
"AzureAuthProvider",
33+
34+
# Service-specific clients
35+
"AzureStorageClient",
36+
37+
# Utilities
38+
"parse_azure_url",
39+
"validate_azure_credentials",
40+
"build_azure_connection_string",
41+
"extract_azure_resource_info",
42+
"validate_azure_permissions",
43+
"get_azure_service_endpoint",
44+
"format_azure_error_message",
45+
]
46+
47+
__version__ = "0.1.0"
Lines changed: 252 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,252 @@
1+
"""
2+
Azure client implementation for the application-sdk framework.
3+
4+
This module provides the main AzureClient class that serves as a unified interface
5+
for connecting to and interacting with Azure Storage services. It supports Service Principal
6+
authentication and provides service-specific subclients.
7+
"""
8+
9+
import asyncio
10+
from concurrent.futures import ThreadPoolExecutor
11+
from typing import Any, Dict, Optional, Type
12+
13+
from azure.core.exceptions import AzureError, ClientAuthenticationError
14+
from azure.identity import DefaultAzureCredential
15+
16+
from application_sdk.clients import ClientInterface
17+
from application_sdk.clients.azure.azure_auth import AzureAuthProvider
18+
from application_sdk.clients.azure.azure_services import AzureStorageClient
19+
from application_sdk.common.error_codes import ClientError, CommonError
20+
from application_sdk.common.credential_utils import resolve_credentials
21+
from application_sdk.observability.logger_adaptor import get_logger
22+
23+
logger = get_logger(__name__)
24+
25+
26+
class AzureClient(ClientInterface):
27+
"""
28+
Main Azure client for the application-sdk framework.
29+
30+
This client provides a unified interface for connecting to and interacting
31+
with Azure Storage services. It supports Service Principal authentication
32+
and provides service-specific subclients for different Azure services.
33+
34+
Attributes:
35+
credentials (Dict[str, Any]): Azure connection credentials
36+
resolved_credentials (Dict[str, Any]): Resolved credentials after processing
37+
credential (DefaultAzureCredential): Azure credential instance
38+
auth_provider (AzureAuthProvider): Authentication provider instance
39+
_services (Dict[str, Any]): Cache of service clients
40+
_executor (ThreadPoolExecutor): Thread pool for async operations
41+
_connection_health (bool): Connection health status
42+
"""
43+
44+
def __init__(
45+
self,
46+
credentials: Optional[Dict[str, Any]] = None,
47+
max_workers: int = 10,
48+
**kwargs: Any,
49+
):
50+
"""
51+
Initialize the Azure client.
52+
53+
Args:
54+
credentials (Optional[Dict[str, Any]]): Azure Service Principal credentials.
55+
Must include tenant_id, client_id, and client_secret.
56+
max_workers (int): Maximum number of worker threads for async operations.
57+
**kwargs: Additional keyword arguments passed to service clients.
58+
"""
59+
self.credentials = credentials or {}
60+
self.resolved_credentials: Dict[str, Any] = {}
61+
self.credential: Optional[DefaultAzureCredential] = None
62+
self.auth_provider = AzureAuthProvider()
63+
self._services: Dict[str, Any] = {}
64+
self._executor = ThreadPoolExecutor(max_workers=max_workers)
65+
self._connection_health = False
66+
self._kwargs = kwargs
67+
68+
async def load(self, credentials: Optional[Dict[str, Any]] = None) -> None:
69+
"""
70+
Load and establish Azure connection using Service Principal authentication.
71+
72+
Args:
73+
credentials (Optional[Dict[str, Any]]): Azure Service Principal credentials.
74+
If provided, will override the credentials passed to __init__.
75+
Must include tenant_id, client_id, and client_secret.
76+
77+
Raises:
78+
ClientError: If connection fails due to authentication or connection issues
79+
"""
80+
if credentials:
81+
self.credentials = credentials
82+
83+
try:
84+
logger.info("Loading Azure client...")
85+
86+
# Resolve credentials using framework's credential resolution
87+
self.resolved_credentials = await resolve_credentials(self.credentials)
88+
89+
# Create Azure credential using Service Principal authentication
90+
self.credential = await self.auth_provider.create_credential(
91+
auth_type="service_principal",
92+
credentials=self.resolved_credentials
93+
)
94+
95+
# Test the connection
96+
await self._test_connection()
97+
98+
self._connection_health = True
99+
logger.info("Azure client loaded successfully")
100+
101+
except ClientAuthenticationError as e:
102+
logger.error(f"Azure authentication failed: {str(e)}")
103+
raise ClientError(f"{ClientError.CLIENT_AUTH_ERROR}: {str(e)}")
104+
except AzureError as e:
105+
logger.error(f"Azure connection error: {str(e)}")
106+
raise ClientError(f"{ClientError.CLIENT_AUTH_ERROR}: {str(e)}")
107+
except Exception as e:
108+
logger.error(f"Unexpected error loading Azure client: {str(e)}")
109+
raise ClientError(f"{ClientError.CLIENT_AUTH_ERROR}: {str(e)}")
110+
111+
async def close(self) -> None:
112+
"""Close Azure connections and clean up resources."""
113+
try:
114+
logger.info("Closing Azure client...")
115+
116+
# Close all service clients
117+
for service_name, service_client in self._services.items():
118+
try:
119+
if hasattr(service_client, 'close'):
120+
await service_client.close()
121+
elif hasattr(service_client, 'disconnect'):
122+
await service_client.disconnect()
123+
except Exception as e:
124+
logger.warning(f"Error closing {service_name} client: {str(e)}")
125+
126+
# Clear service cache
127+
self._services.clear()
128+
129+
# Shutdown executor
130+
self._executor.shutdown(wait=True)
131+
132+
# Reset connection health
133+
self._connection_health = False
134+
135+
logger.info("Azure client closed successfully")
136+
137+
except Exception as e:
138+
logger.error(f"Error closing Azure client: {str(e)}")
139+
140+
async def get_storage_client(self) -> AzureStorageClient:
141+
"""
142+
Get Azure Storage service client.
143+
144+
Returns:
145+
AzureStorageClient: Configured Azure Storage client.
146+
147+
Raises:
148+
ClientError: If client is not loaded or storage client creation fails.
149+
"""
150+
if not self._connection_health:
151+
raise ClientError(f"{ClientError.CLIENT_AUTH_ERROR}: Client not loaded")
152+
153+
if "storage" not in self._services:
154+
try:
155+
self._services["storage"] = AzureStorageClient(
156+
credential=self.credential,
157+
**self._kwargs
158+
)
159+
await self._services["storage"].load(self.resolved_credentials)
160+
except Exception as e:
161+
logger.error(f"Failed to create storage client: {str(e)}")
162+
raise ClientError(f"{ClientError.CLIENT_AUTH_ERROR}: {str(e)}")
163+
164+
return self._services["storage"]
165+
166+
async def get_service_client(self, service_type: str) -> Any:
167+
"""
168+
Get a service client by type.
169+
170+
Args:
171+
service_type (str): Type of service client to retrieve.
172+
Supported values: 'storage'.
173+
174+
Returns:
175+
Any: The requested service client.
176+
177+
Raises:
178+
ValueError: If service_type is not supported.
179+
ClientError: If client creation fails.
180+
"""
181+
service_mapping = {
182+
"storage": self.get_storage_client,
183+
}
184+
185+
if service_type not in service_mapping:
186+
raise ValueError(f"Unsupported service type: {service_type}")
187+
188+
return await service_mapping[service_type]()
189+
190+
async def health_check(self) -> Dict[str, Any]:
191+
"""
192+
Perform health check on Azure connection and services.
193+
194+
Returns:
195+
Dict[str, Any]: Health status information.
196+
"""
197+
health_status = {
198+
"connection_health": self._connection_health,
199+
"services": {},
200+
"overall_health": False
201+
}
202+
203+
if not self._connection_health:
204+
return health_status
205+
206+
# Check each service
207+
for service_name, service_client in self._services.items():
208+
try:
209+
if hasattr(service_client, 'health_check'):
210+
service_health = await service_client.health_check()
211+
else:
212+
service_health = {"status": "unknown"}
213+
214+
health_status["services"][service_name] = service_health
215+
except Exception as e:
216+
health_status["services"][service_name] = {
217+
"status": "error",
218+
"error": str(e)
219+
}
220+
221+
# Overall health is True if connection is healthy and at least one service is available
222+
health_status["overall_health"] = (
223+
self._connection_health and
224+
len(health_status["services"]) > 0
225+
)
226+
227+
return health_status
228+
229+
async def _test_connection(self) -> None:
230+
"""
231+
Test the Azure connection by attempting to get a token.
232+
233+
Raises:
234+
ClientAuthenticationError: If connection test fails.
235+
"""
236+
try:
237+
# Test the credential by getting a token
238+
await asyncio.get_event_loop().run_in_executor(
239+
self._executor,
240+
self.credential.get_token,
241+
"https://management.azure.com/.default"
242+
)
243+
except Exception as e:
244+
raise ClientAuthenticationError(f"Connection test failed: {str(e)}")
245+
246+
def __enter__(self):
247+
"""Context manager entry."""
248+
return self
249+
250+
def __exit__(self, exc_type, exc_val, exc_tb):
251+
"""Context manager exit."""
252+
asyncio.create_task(self.close())

0 commit comments

Comments
 (0)