azure-postgresql-auth 1.0.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.
- azure_postgresql_auth/__init__.py +20 -0
- azure_postgresql_auth/core.py +244 -0
- azure_postgresql_auth/errors.py +36 -0
- azure_postgresql_auth/psycopg2/__init__.py +25 -0
- azure_postgresql_auth/psycopg2/entra_connection.py +78 -0
- azure_postgresql_auth/psycopg3/__init__.py +24 -0
- azure_postgresql_auth/psycopg3/async_entra_connection.py +66 -0
- azure_postgresql_auth/psycopg3/entra_connection.py +66 -0
- azure_postgresql_auth/py.typed +0 -0
- azure_postgresql_auth/sqlalchemy/__init__.py +28 -0
- azure_postgresql_auth/sqlalchemy/async_entra_connection.py +73 -0
- azure_postgresql_auth/sqlalchemy/entra_connection.py +70 -0
- azure_postgresql_auth-1.0.0.dist-info/METADATA +360 -0
- azure_postgresql_auth-1.0.0.dist-info/RECORD +16 -0
- azure_postgresql_auth-1.0.0.dist-info/WHEEL +5 -0
- azure_postgresql_auth-1.0.0.dist-info/top_level.txt +1 -0
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Azure PostgreSQL Entra ID Integration Library
|
|
5
|
+
|
|
6
|
+
This library provides connection classes for using Azure Entra ID authentication
|
|
7
|
+
with Azure Database for PostgreSQL across different PostgreSQL drivers.
|
|
8
|
+
|
|
9
|
+
Available modules (with optional dependencies):
|
|
10
|
+
- psycopg2: Support for psycopg2 driver (install with: pip install azurepg-entra[psycopg2])
|
|
11
|
+
- psycopg3: Support for psycopg (v3) driver (install with: pip install azurepg-entra[psycopg3])
|
|
12
|
+
- sqlalchemy: Support for SQLAlchemy ORM (install with: pip install azurepg-entra[sqlalchemy])
|
|
13
|
+
|
|
14
|
+
Core dependencies (always available):
|
|
15
|
+
- azure-identity: For Azure Entra ID authentication
|
|
16
|
+
- azure-core: Core Azure SDK functionality
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
__version__ = "0.1.0"
|
|
20
|
+
__author__ = "Microsoft Corporation"
|
|
@@ -0,0 +1,244 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
import base64
|
|
4
|
+
import json
|
|
5
|
+
from typing import Any, cast
|
|
6
|
+
|
|
7
|
+
from azure.core.credentials import TokenCredential
|
|
8
|
+
from azure.core.credentials_async import AsyncTokenCredential
|
|
9
|
+
from azure.core.exceptions import ClientAuthenticationError
|
|
10
|
+
from azure.identity import CredentialUnavailableError
|
|
11
|
+
from azure.identity import DefaultAzureCredential as DefaultAzureCredential
|
|
12
|
+
from azure.identity.aio import DefaultAzureCredential as AsyncDefaultAzureCredential
|
|
13
|
+
|
|
14
|
+
from azure_postgresql_auth.errors import (
|
|
15
|
+
ScopePermissionError,
|
|
16
|
+
TokenDecodeError,
|
|
17
|
+
UsernameExtractionError,
|
|
18
|
+
)
|
|
19
|
+
|
|
20
|
+
AZURE_DB_FOR_POSTGRES_SCOPE = "https://ossrdbms-aad.database.windows.net/.default"
|
|
21
|
+
AZURE_MANAGEMENT_SCOPE = "https://management.azure.com/.default"
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def get_entra_token(credential: TokenCredential | None, scope: str) -> str:
|
|
25
|
+
"""Acquires an Entra authentication token for Azure PostgreSQL synchronously.
|
|
26
|
+
|
|
27
|
+
Parameters:
|
|
28
|
+
credential (TokenCredential or None): Credential object used to obtain the token.
|
|
29
|
+
If None, the default Azure credentials are used.
|
|
30
|
+
scope (str): The scope for the token request.
|
|
31
|
+
|
|
32
|
+
Returns:
|
|
33
|
+
str: The acquired authentication token to be used as the database password.
|
|
34
|
+
"""
|
|
35
|
+
credential = credential or DefaultAzureCredential()
|
|
36
|
+
cred = credential.get_token(scope)
|
|
37
|
+
return cred.token
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
async def get_entra_token_async(
|
|
41
|
+
credential: AsyncTokenCredential | None, scope: str
|
|
42
|
+
) -> str:
|
|
43
|
+
"""Asynchronously acquires an Entra authentication token for Azure PostgreSQL.
|
|
44
|
+
|
|
45
|
+
Parameters:
|
|
46
|
+
credential (AsyncTokenCredential or None): Asynchronous credential used to obtain the token.
|
|
47
|
+
If None, the default Azure credentials are used.
|
|
48
|
+
scope (str): The scope for the token request.
|
|
49
|
+
|
|
50
|
+
Returns:
|
|
51
|
+
str: The acquired authentication token to be used as the database password.
|
|
52
|
+
"""
|
|
53
|
+
credential = credential or AsyncDefaultAzureCredential()
|
|
54
|
+
async with credential:
|
|
55
|
+
cred = await credential.get_token(scope)
|
|
56
|
+
return cred.token
|
|
57
|
+
|
|
58
|
+
|
|
59
|
+
def decode_jwt(token: str) -> dict[str, Any]:
|
|
60
|
+
"""Decodes a JWT token to extract its payload claims.
|
|
61
|
+
|
|
62
|
+
Parameters:
|
|
63
|
+
token (str): The JWT token string in the standard three-part format.
|
|
64
|
+
|
|
65
|
+
Returns:
|
|
66
|
+
dict[str, Any]: A dictionary containing the claims extracted from the token payload.
|
|
67
|
+
|
|
68
|
+
Raises:
|
|
69
|
+
TokenValueError: If the token format is invalid or cannot be decoded.
|
|
70
|
+
"""
|
|
71
|
+
try:
|
|
72
|
+
payload = token.split(".")[1]
|
|
73
|
+
padding = "=" * (4 - len(payload) % 4)
|
|
74
|
+
decoded_payload = base64.urlsafe_b64decode(payload + padding)
|
|
75
|
+
return cast(dict[str, Any], json.loads(decoded_payload))
|
|
76
|
+
except Exception as e:
|
|
77
|
+
raise TokenDecodeError("Invalid JWT token format") from e
|
|
78
|
+
|
|
79
|
+
|
|
80
|
+
def parse_principal_name(xms_mirid: str) -> str | None:
|
|
81
|
+
"""Parses the principal name from an Azure resource path.
|
|
82
|
+
|
|
83
|
+
Parameters:
|
|
84
|
+
xms_mirid (str): The xms_mirid claim value containing the Azure resource path.
|
|
85
|
+
|
|
86
|
+
Returns:
|
|
87
|
+
str | None: The extracted principal name, or None if parsing fails.
|
|
88
|
+
"""
|
|
89
|
+
if not xms_mirid:
|
|
90
|
+
return None
|
|
91
|
+
|
|
92
|
+
# Parse the xms_mirid claim which looks like
|
|
93
|
+
# /subscriptions/{subId}/resourcegroups/{resourceGroup}/providers/Microsoft.ManagedIdentity/userAssignedIdentities/{principalName}
|
|
94
|
+
last_slash_index = xms_mirid.rfind("/")
|
|
95
|
+
if last_slash_index == -1:
|
|
96
|
+
return None
|
|
97
|
+
|
|
98
|
+
beginning = xms_mirid[:last_slash_index]
|
|
99
|
+
principal_name = xms_mirid[last_slash_index + 1 :]
|
|
100
|
+
|
|
101
|
+
if not principal_name or not beginning.lower().endswith(
|
|
102
|
+
"providers/microsoft.managedidentity/userassignedidentities"
|
|
103
|
+
):
|
|
104
|
+
return None
|
|
105
|
+
|
|
106
|
+
return principal_name
|
|
107
|
+
|
|
108
|
+
|
|
109
|
+
def get_entra_conninfo(credential: TokenCredential | None) -> dict[str, str]:
|
|
110
|
+
"""Synchronously obtains connection information from Entra authentication for Azure PostgreSQL.
|
|
111
|
+
|
|
112
|
+
This function acquires an access token from Azure Entra ID and extracts the username
|
|
113
|
+
from the token claims. It tries multiple claim sources to determine the username.
|
|
114
|
+
|
|
115
|
+
Parameters:
|
|
116
|
+
credential (TokenCredential or None): The credential used for token acquisition.
|
|
117
|
+
If None, DefaultAzureCredential() is used to automatically discover credentials.
|
|
118
|
+
|
|
119
|
+
Returns:
|
|
120
|
+
dict[str, str]: A dictionary with 'user' and 'password' keys, where:
|
|
121
|
+
- 'user': The extracted username from token claims
|
|
122
|
+
- 'password': The Entra ID access token for database authentication
|
|
123
|
+
|
|
124
|
+
Raises:
|
|
125
|
+
TokenDecodeError: If the JWT token cannot be decoded or is malformed.
|
|
126
|
+
UsernameExtractionError: If the username cannot be extracted from token claims.
|
|
127
|
+
ScopePermissionError: The token could not be acquired from the management scope, possibly due to insufficient permissions.
|
|
128
|
+
"""
|
|
129
|
+
credential = credential or DefaultAzureCredential()
|
|
130
|
+
|
|
131
|
+
# Always get the DB-scope token for password
|
|
132
|
+
db_token = get_entra_token(credential, AZURE_DB_FOR_POSTGRES_SCOPE)
|
|
133
|
+
try:
|
|
134
|
+
db_claims = decode_jwt(db_token)
|
|
135
|
+
except TokenDecodeError:
|
|
136
|
+
raise
|
|
137
|
+
xms_mirid = db_claims.get("xms_mirid")
|
|
138
|
+
username = (
|
|
139
|
+
parse_principal_name(xms_mirid)
|
|
140
|
+
if isinstance(xms_mirid, str)
|
|
141
|
+
else None
|
|
142
|
+
or db_claims.get("upn")
|
|
143
|
+
or db_claims.get("preferred_username")
|
|
144
|
+
or db_claims.get("unique_name")
|
|
145
|
+
)
|
|
146
|
+
|
|
147
|
+
if not username:
|
|
148
|
+
# Fall back to management scope ONLY to discover username
|
|
149
|
+
try:
|
|
150
|
+
mgmt_token = get_entra_token(credential, AZURE_MANAGEMENT_SCOPE)
|
|
151
|
+
except (CredentialUnavailableError, ClientAuthenticationError) as e:
|
|
152
|
+
raise ScopePermissionError(
|
|
153
|
+
"Failed to acquire token from management scope"
|
|
154
|
+
) from e
|
|
155
|
+
try:
|
|
156
|
+
mgmt_claims = decode_jwt(mgmt_token)
|
|
157
|
+
except TokenDecodeError:
|
|
158
|
+
raise
|
|
159
|
+
xms_mirid = mgmt_claims.get("xms_mirid")
|
|
160
|
+
username = (
|
|
161
|
+
parse_principal_name(xms_mirid)
|
|
162
|
+
if isinstance(xms_mirid, str)
|
|
163
|
+
else None
|
|
164
|
+
or mgmt_claims.get("upn")
|
|
165
|
+
or mgmt_claims.get("preferred_username")
|
|
166
|
+
or mgmt_claims.get("unique_name")
|
|
167
|
+
)
|
|
168
|
+
|
|
169
|
+
if not username:
|
|
170
|
+
raise UsernameExtractionError(
|
|
171
|
+
"Could not determine username from token claims. "
|
|
172
|
+
"Ensure the identity has the proper Azure AD attributes."
|
|
173
|
+
)
|
|
174
|
+
|
|
175
|
+
return {"user": username, "password": db_token}
|
|
176
|
+
|
|
177
|
+
|
|
178
|
+
async def get_entra_conninfo_async(
|
|
179
|
+
credential: AsyncTokenCredential | None,
|
|
180
|
+
) -> dict[str, str]:
|
|
181
|
+
"""Asynchronously obtains connection information from Entra authentication for Azure PostgreSQL.
|
|
182
|
+
|
|
183
|
+
This function acquires an access token from Azure Entra ID and extracts the username
|
|
184
|
+
from the token claims. It tries multiple claim sources to determine the username.
|
|
185
|
+
|
|
186
|
+
Parameters:
|
|
187
|
+
credential (AsyncTokenCredential or None): The async credential used for token acquisition.
|
|
188
|
+
If None, AsyncDefaultAzureCredential() is used to automatically discover credentials.
|
|
189
|
+
|
|
190
|
+
Returns:
|
|
191
|
+
dict[str, str]: A dictionary with 'user' and 'password' keys, where:
|
|
192
|
+
- 'user': The extracted username from token claims
|
|
193
|
+
- 'password': The Entra ID access token for database authentication
|
|
194
|
+
|
|
195
|
+
Raises:
|
|
196
|
+
TokenDecodeError: If the JWT token cannot be decoded or is malformed.
|
|
197
|
+
UsernameExtractionError: If the username cannot be extracted from token claims.
|
|
198
|
+
ScopePermissionError: The token could not be acquired from the management scope, possibly due to insufficient permissions.
|
|
199
|
+
"""
|
|
200
|
+
credential = credential or AsyncDefaultAzureCredential()
|
|
201
|
+
|
|
202
|
+
db_token = await get_entra_token_async(credential, AZURE_DB_FOR_POSTGRES_SCOPE)
|
|
203
|
+
try:
|
|
204
|
+
db_claims = decode_jwt(db_token)
|
|
205
|
+
except TokenDecodeError:
|
|
206
|
+
raise
|
|
207
|
+
xms_mirid = db_claims.get("xms_mirid")
|
|
208
|
+
username = (
|
|
209
|
+
parse_principal_name(xms_mirid)
|
|
210
|
+
if isinstance(xms_mirid, str)
|
|
211
|
+
else None
|
|
212
|
+
or db_claims.get("upn")
|
|
213
|
+
or db_claims.get("preferred_username")
|
|
214
|
+
or db_claims.get("unique_name")
|
|
215
|
+
)
|
|
216
|
+
|
|
217
|
+
if not username:
|
|
218
|
+
try:
|
|
219
|
+
mgmt_token = await get_entra_token_async(credential, AZURE_MANAGEMENT_SCOPE)
|
|
220
|
+
except (CredentialUnavailableError, ClientAuthenticationError) as e:
|
|
221
|
+
raise ScopePermissionError(
|
|
222
|
+
"Failed to acquire token from management scope"
|
|
223
|
+
) from e
|
|
224
|
+
try:
|
|
225
|
+
mgmt_claims = decode_jwt(mgmt_token)
|
|
226
|
+
except TokenDecodeError:
|
|
227
|
+
raise
|
|
228
|
+
xms_mirid = mgmt_claims.get("xms_mirid")
|
|
229
|
+
username = (
|
|
230
|
+
parse_principal_name(xms_mirid)
|
|
231
|
+
if isinstance(xms_mirid, str)
|
|
232
|
+
else None
|
|
233
|
+
or mgmt_claims.get("upn")
|
|
234
|
+
or mgmt_claims.get("preferred_username")
|
|
235
|
+
or mgmt_claims.get("unique_name")
|
|
236
|
+
)
|
|
237
|
+
|
|
238
|
+
if not username:
|
|
239
|
+
raise UsernameExtractionError(
|
|
240
|
+
"Could not determine username from token claims. "
|
|
241
|
+
"Ensure the identity has the proper Azure AD attributes."
|
|
242
|
+
)
|
|
243
|
+
|
|
244
|
+
return {"user": username, "password": db_token}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
class AzurePgEntraError(Exception):
|
|
4
|
+
"""Base class for all custom exceptions in the project."""
|
|
5
|
+
|
|
6
|
+
pass
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
class TokenDecodeError(AzurePgEntraError):
|
|
10
|
+
"""Raised when a token value is invalid."""
|
|
11
|
+
|
|
12
|
+
pass
|
|
13
|
+
|
|
14
|
+
|
|
15
|
+
class UsernameExtractionError(AzurePgEntraError):
|
|
16
|
+
"""Raised when username cannot be extracted from token."""
|
|
17
|
+
|
|
18
|
+
pass
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
class CredentialValueError(AzurePgEntraError):
|
|
22
|
+
"""Raised when token credential is invalid."""
|
|
23
|
+
|
|
24
|
+
pass
|
|
25
|
+
|
|
26
|
+
|
|
27
|
+
class EntraConnectionValueError(AzurePgEntraError):
|
|
28
|
+
"""Raised when Entra connection credentials are invalid."""
|
|
29
|
+
|
|
30
|
+
pass
|
|
31
|
+
|
|
32
|
+
|
|
33
|
+
class ScopePermissionError(AzurePgEntraError):
|
|
34
|
+
"""Raised when the provided scope does not have sufficient permissions."""
|
|
35
|
+
|
|
36
|
+
pass
|
|
@@ -0,0 +1,25 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Psycopg2 support for Azure Entra ID authentication with Azure Database for PostgreSQL.
|
|
5
|
+
|
|
6
|
+
This module provides a connection class that handles Azure Entra ID token acquisition
|
|
7
|
+
and authentication for synchronous PostgreSQL connections.
|
|
8
|
+
|
|
9
|
+
Requirements:
|
|
10
|
+
Install with: pip install azurepg-entra[psycopg2]
|
|
11
|
+
|
|
12
|
+
This will install:
|
|
13
|
+
- psycopg2-binary>=2.9.0
|
|
14
|
+
|
|
15
|
+
Classes:
|
|
16
|
+
EntraConnection: Synchronous connection class with Entra ID authentication
|
|
17
|
+
"""
|
|
18
|
+
|
|
19
|
+
from .entra_connection import (
|
|
20
|
+
EntraConnection,
|
|
21
|
+
)
|
|
22
|
+
|
|
23
|
+
__all__ = [
|
|
24
|
+
"EntraConnection",
|
|
25
|
+
]
|
|
@@ -0,0 +1,78 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from azure.core.credentials import TokenCredential
|
|
6
|
+
|
|
7
|
+
from azure_postgresql_auth.core import get_entra_conninfo
|
|
8
|
+
from azure_postgresql_auth.errors import (
|
|
9
|
+
CredentialValueError,
|
|
10
|
+
EntraConnectionValueError,
|
|
11
|
+
)
|
|
12
|
+
|
|
13
|
+
try:
|
|
14
|
+
from psycopg2.extensions import connection, make_dsn, parse_dsn
|
|
15
|
+
except ImportError as e:
|
|
16
|
+
# Provide a helpful error message if psycopg2 dependencies are missing
|
|
17
|
+
raise ImportError(
|
|
18
|
+
"psycopg2 dependencies are not installed. "
|
|
19
|
+
"Install them with: pip install azurepg-entra[psycopg2]"
|
|
20
|
+
) from e
|
|
21
|
+
|
|
22
|
+
class EntraConnection(connection):
|
|
23
|
+
"""Establishes a synchronous PostgreSQL connection using Entra authentication.
|
|
24
|
+
|
|
25
|
+
This connection class automatically acquires Azure Entra ID credentials when user
|
|
26
|
+
or password are not provided in the DSN or connection parameters. Authentication
|
|
27
|
+
errors are printed to console for debugging purposes.
|
|
28
|
+
|
|
29
|
+
Parameters:
|
|
30
|
+
dsn (str): PostgreSQL connection string (Data Source Name).
|
|
31
|
+
**kwargs: Additional keyword arguments including:
|
|
32
|
+
- credential (TokenCredential, optional): Azure credential for token acquisition.
|
|
33
|
+
If None, DefaultAzureCredential() is used.
|
|
34
|
+
- user (str, optional): Database username. If not provided, extracted from Entra token.
|
|
35
|
+
- password (str, optional): Database password. If not provided, uses Entra access token.
|
|
36
|
+
|
|
37
|
+
Raises:
|
|
38
|
+
CredentialValueError: If the provided credential is not a valid TokenCredential.
|
|
39
|
+
EntraConnectionValueError: If Entra connection credentials cannot be retrieved
|
|
40
|
+
"""
|
|
41
|
+
|
|
42
|
+
def __init__(self, dsn: str, **kwargs: Any) -> None:
|
|
43
|
+
# Extract current DSN params
|
|
44
|
+
dsn_params = parse_dsn(dsn) if dsn else {}
|
|
45
|
+
|
|
46
|
+
credential = kwargs.pop("credential", None)
|
|
47
|
+
if credential and not isinstance(credential, (TokenCredential)):
|
|
48
|
+
raise CredentialValueError(
|
|
49
|
+
"credential must be a TokenCredential for sync connections"
|
|
50
|
+
)
|
|
51
|
+
|
|
52
|
+
# Check if user and password are already provided
|
|
53
|
+
has_user = "user" in dsn_params or "user" in kwargs
|
|
54
|
+
has_password = "password" in dsn_params or "password" in kwargs
|
|
55
|
+
|
|
56
|
+
# Only get Entra credentials if user or password is missing
|
|
57
|
+
if not has_user or not has_password:
|
|
58
|
+
try:
|
|
59
|
+
entra_creds = get_entra_conninfo(credential)
|
|
60
|
+
except (Exception) as e:
|
|
61
|
+
raise EntraConnectionValueError(
|
|
62
|
+
"Could not retrieve Entra credentials"
|
|
63
|
+
) from e
|
|
64
|
+
|
|
65
|
+
# Only update missing credentials
|
|
66
|
+
if not has_user and "user" in entra_creds:
|
|
67
|
+
dsn_params["user"] = entra_creds["user"]
|
|
68
|
+
if not has_password and "password" in entra_creds:
|
|
69
|
+
dsn_params["password"] = entra_creds["password"]
|
|
70
|
+
|
|
71
|
+
# Update DSN params with any kwargs (kwargs take precedence)
|
|
72
|
+
dsn_params.update(kwargs)
|
|
73
|
+
|
|
74
|
+
# Create new DSN with updated credentials
|
|
75
|
+
new_dsn = make_dsn(**dsn_params)
|
|
76
|
+
|
|
77
|
+
# Call parent constructor with updated DSN only
|
|
78
|
+
super().__init__(new_dsn)
|
|
@@ -0,0 +1,24 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
Psycopg3 support for Azure Entra ID authentication with Azure Database for PostgreSQL.
|
|
5
|
+
|
|
6
|
+
This module provides connection classes that extend psycopg's Connection and AsyncConnection
|
|
7
|
+
to automatically handle Azure Entra ID token acquisition and authentication.
|
|
8
|
+
|
|
9
|
+
Requirements:
|
|
10
|
+
Install with: pip install azurepg-entra[psycopg3]
|
|
11
|
+
|
|
12
|
+
This will install:
|
|
13
|
+
- psycopg[binary]>=3.1.0
|
|
14
|
+
- aiohttp>=3.8.0
|
|
15
|
+
|
|
16
|
+
Classes:
|
|
17
|
+
EntraConnection: Synchronous connection class with Entra ID authentication
|
|
18
|
+
AsyncEntraConnection: Asynchronous connection class with Entra ID authentication
|
|
19
|
+
"""
|
|
20
|
+
|
|
21
|
+
from .async_entra_connection import AsyncEntraConnection
|
|
22
|
+
from .entra_connection import EntraConnection
|
|
23
|
+
|
|
24
|
+
__all__ = ["EntraConnection", "AsyncEntraConnection"]
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from azure.core.credentials_async import AsyncTokenCredential
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from psycopg import AsyncConnection
|
|
9
|
+
except ImportError as e:
|
|
10
|
+
raise ImportError(
|
|
11
|
+
"psycopg3 dependencies are not installed. "
|
|
12
|
+
"Install them with: pip install azurepg-entra[psycopg3]"
|
|
13
|
+
) from e
|
|
14
|
+
|
|
15
|
+
from azure_postgresql_auth.core import get_entra_conninfo_async
|
|
16
|
+
from azure_postgresql_auth.errors import (
|
|
17
|
+
CredentialValueError,
|
|
18
|
+
EntraConnectionValueError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AsyncEntraConnection(AsyncConnection):
|
|
23
|
+
"""Asynchronous connection class for using Entra authentication with Azure PostgreSQL."""
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
async def connect(cls, *args: Any, **kwargs: Any) -> "AsyncEntraConnection":
|
|
27
|
+
"""Establishes an asynchronous PostgreSQL connection using Entra authentication.
|
|
28
|
+
|
|
29
|
+
This method automatically acquires Azure Entra ID credentials when user or password
|
|
30
|
+
are not provided in the connection parameters. Authentication errors are printed to
|
|
31
|
+
console for debugging purposes.
|
|
32
|
+
|
|
33
|
+
Parameters:
|
|
34
|
+
*args: Positional arguments to be forwarded to the parent connection method.
|
|
35
|
+
**kwargs: Keyword arguments including:
|
|
36
|
+
- credential (AsyncTokenCredential, optional): Async Azure credential for token acquisition.
|
|
37
|
+
- user (str, optional): Database username. If not provided, extracted from Entra token.
|
|
38
|
+
- password (str, optional): Database password. If not provided, uses Entra access token.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
AsyncEntraConnection: An open asynchronous connection to the PostgreSQL database.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
CredentialValueError: If the provided credential is not a valid AsyncTokenCredential.
|
|
45
|
+
EntraConnectionValueError: If Entra connection credentials are invalid.
|
|
46
|
+
"""
|
|
47
|
+
credential = kwargs.pop("credential", None)
|
|
48
|
+
if credential and not isinstance(credential, (AsyncTokenCredential)):
|
|
49
|
+
raise CredentialValueError(
|
|
50
|
+
"credential must be an AsyncTokenCredential for async connections"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Check if we need to acquire Entra authentication info
|
|
54
|
+
if not kwargs.get("user") or not kwargs.get("password"):
|
|
55
|
+
try:
|
|
56
|
+
entra_conninfo = await get_entra_conninfo_async(credential)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
raise EntraConnectionValueError(
|
|
59
|
+
"Could not retrieve Entra credentials"
|
|
60
|
+
) from e
|
|
61
|
+
# Always use the token password when Entra authentication is needed
|
|
62
|
+
kwargs["password"] = entra_conninfo["password"]
|
|
63
|
+
if not kwargs.get("user"):
|
|
64
|
+
# If user isn't already set, use the username from the token
|
|
65
|
+
kwargs["user"] = entra_conninfo["user"]
|
|
66
|
+
return await super().connect(*args, **kwargs)
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from azure.core.credentials import TokenCredential
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from psycopg import Connection
|
|
9
|
+
except ImportError as e:
|
|
10
|
+
raise ImportError(
|
|
11
|
+
"psycopg3 dependencies are not installed. "
|
|
12
|
+
"Install them with: pip install azurepg-entra[psycopg3]"
|
|
13
|
+
) from e
|
|
14
|
+
|
|
15
|
+
from azure_postgresql_auth.core import get_entra_conninfo
|
|
16
|
+
from azure_postgresql_auth.errors import (
|
|
17
|
+
CredentialValueError,
|
|
18
|
+
EntraConnectionValueError,
|
|
19
|
+
)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class EntraConnection(Connection):
|
|
23
|
+
"""Synchronous connection class for using Entra authentication with Azure PostgreSQL."""
|
|
24
|
+
|
|
25
|
+
@classmethod
|
|
26
|
+
def connect(cls, *args: Any, **kwargs: Any) -> "EntraConnection":
|
|
27
|
+
"""Establishes a synchronous PostgreSQL connection using Entra authentication.
|
|
28
|
+
|
|
29
|
+
This method automatically acquires Azure Entra ID credentials when user or password
|
|
30
|
+
are not provided in the connection parameters. If authentication fails, the original
|
|
31
|
+
exception is re-raised to the caller.
|
|
32
|
+
|
|
33
|
+
Parameters:
|
|
34
|
+
*args: Positional arguments to be forwarded to the parent connection method.
|
|
35
|
+
**kwargs: Keyword arguments including:
|
|
36
|
+
- credential (TokenCredential, optional): Azure credential for token acquisition.
|
|
37
|
+
- user (str, optional): Database username. If not provided, extracted from Entra token.
|
|
38
|
+
- password (str, optional): Database password. If not provided, uses Entra access token.
|
|
39
|
+
|
|
40
|
+
Returns:
|
|
41
|
+
EntraConnection: An open synchronous connection to the PostgreSQL database.
|
|
42
|
+
|
|
43
|
+
Raises:
|
|
44
|
+
CredentialValueError: If the provided credential is not a valid TokenCredential.
|
|
45
|
+
EntraConnectionValueError: If Entra connection credentials cannot be retrieved
|
|
46
|
+
"""
|
|
47
|
+
credential = kwargs.pop("credential", None)
|
|
48
|
+
if credential and not isinstance(credential, (TokenCredential)):
|
|
49
|
+
raise CredentialValueError(
|
|
50
|
+
"credential must be a TokenCredential for sync connections"
|
|
51
|
+
)
|
|
52
|
+
|
|
53
|
+
# Check if we need to acquire Entra authentication info
|
|
54
|
+
if not kwargs.get("user") or not kwargs.get("password"):
|
|
55
|
+
try:
|
|
56
|
+
entra_conninfo = get_entra_conninfo(credential)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
raise EntraConnectionValueError(
|
|
59
|
+
"Could not retrieve Entra credentials"
|
|
60
|
+
) from e
|
|
61
|
+
# Always use the token password when Entra authentication is needed
|
|
62
|
+
kwargs["password"] = entra_conninfo["password"]
|
|
63
|
+
if not kwargs.get("user"):
|
|
64
|
+
# If user isn't already set, use the username from the token
|
|
65
|
+
kwargs["user"] = entra_conninfo["user"]
|
|
66
|
+
return super().connect(*args, **kwargs)
|
|
File without changes
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
"""
|
|
4
|
+
SQLAlchemy integration for Azure PostgreSQL with Entra ID authentication.
|
|
5
|
+
|
|
6
|
+
This module provides integration between SQLAlchemy and Azure Entra ID
|
|
7
|
+
authentication for PostgreSQL connections. It automatically handles token acquisition
|
|
8
|
+
and credential injection through SQLAlchemy's event system.
|
|
9
|
+
|
|
10
|
+
Requirements:
|
|
11
|
+
Install with: pip install azurepg-entra[sqlalchemy]
|
|
12
|
+
|
|
13
|
+
This will install:
|
|
14
|
+
- sqlalchemy>=2.0.0
|
|
15
|
+
- aiohttp>=3.8.0
|
|
16
|
+
|
|
17
|
+
Functions:
|
|
18
|
+
enable_entra_authentication: Enable Entra ID authentication for synchronous SQLAlchemy engines
|
|
19
|
+
enable_entra_authentication_async: Enable Entra ID authentication for asynchronous SQLAlchemy engines
|
|
20
|
+
"""
|
|
21
|
+
|
|
22
|
+
from .async_entra_connection import enable_entra_authentication_async
|
|
23
|
+
from .entra_connection import enable_entra_authentication
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"enable_entra_authentication",
|
|
27
|
+
"enable_entra_authentication_async",
|
|
28
|
+
]
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from azure.core.credentials import TokenCredential
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from sqlalchemy import event
|
|
9
|
+
from sqlalchemy.engine import Dialect
|
|
10
|
+
from sqlalchemy.ext.asyncio import AsyncEngine
|
|
11
|
+
except ImportError as e:
|
|
12
|
+
# Provide a helpful error message if SQLAlchemy dependencies are missing
|
|
13
|
+
raise ImportError(
|
|
14
|
+
"SQLAlchemy dependencies are not installed. "
|
|
15
|
+
"Install them with: pip install azurepg-entra[sqlalchemy]"
|
|
16
|
+
) from e
|
|
17
|
+
|
|
18
|
+
from azure_postgresql_auth.core import get_entra_conninfo
|
|
19
|
+
from azure_postgresql_auth.errors import (
|
|
20
|
+
CredentialValueError,
|
|
21
|
+
EntraConnectionValueError,
|
|
22
|
+
)
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
def enable_entra_authentication_async(engine: AsyncEngine) -> None:
|
|
26
|
+
"""
|
|
27
|
+
Enable Azure Entra ID authentication for an async SQLAlchemy engine.
|
|
28
|
+
|
|
29
|
+
This function registers an event listener that automatically provides
|
|
30
|
+
Entra ID credentials for each database connection if they are not already set.
|
|
31
|
+
Event handlers do not support async behavior so the token fetching will still
|
|
32
|
+
be synchronous.
|
|
33
|
+
|
|
34
|
+
Args:
|
|
35
|
+
engine: The async SQLAlchemy Engine to enable Entra authentication for
|
|
36
|
+
"""
|
|
37
|
+
|
|
38
|
+
@event.listens_for(engine.sync_engine, "do_connect")
|
|
39
|
+
def provide_token(
|
|
40
|
+
dialect: Dialect, conn_rec: Any, cargs: Any, cparams: dict[str, Any]
|
|
41
|
+
) -> None:
|
|
42
|
+
"""Event handler that provides Entra credentials for each sync connection.
|
|
43
|
+
|
|
44
|
+
Raises:
|
|
45
|
+
CredentialValueError: If the provided credential is not a valid TokenCredential.
|
|
46
|
+
EntraConnectionValueError: If Entra connection credentials cannot be retrieved
|
|
47
|
+
"""
|
|
48
|
+
credential = cparams.get("credential", None)
|
|
49
|
+
if credential and not isinstance(credential, (TokenCredential)):
|
|
50
|
+
raise CredentialValueError(
|
|
51
|
+
"credential must be a TokenCredential for async connections"
|
|
52
|
+
)
|
|
53
|
+
# Check if credentials are already present
|
|
54
|
+
has_user = "user" in cparams
|
|
55
|
+
has_password = "password" in cparams
|
|
56
|
+
|
|
57
|
+
# Only get Entra credentials if user or password is missing
|
|
58
|
+
if not has_user or not has_password:
|
|
59
|
+
try:
|
|
60
|
+
entra_creds = get_entra_conninfo(credential)
|
|
61
|
+
except Exception as e:
|
|
62
|
+
raise EntraConnectionValueError(
|
|
63
|
+
"Could not retrieve Entra credentials"
|
|
64
|
+
) from e
|
|
65
|
+
# Only update missing credentials
|
|
66
|
+
if not has_user and "user" in entra_creds:
|
|
67
|
+
cparams["user"] = entra_creds["user"]
|
|
68
|
+
if not has_password and "password" in entra_creds:
|
|
69
|
+
cparams["password"] = entra_creds["password"]
|
|
70
|
+
|
|
71
|
+
# Strip helper-only param before DBAPI connect to avoid 'invalid connection option'
|
|
72
|
+
if "credential" in cparams:
|
|
73
|
+
del cparams["credential"]
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# Copyright (c) Microsoft. All rights reserved.
|
|
2
|
+
|
|
3
|
+
from typing import Any
|
|
4
|
+
|
|
5
|
+
from azure.core.credentials import TokenCredential
|
|
6
|
+
|
|
7
|
+
try:
|
|
8
|
+
from sqlalchemy import Engine, event
|
|
9
|
+
from sqlalchemy.engine import Dialect
|
|
10
|
+
except ImportError as e:
|
|
11
|
+
raise ImportError(
|
|
12
|
+
"SQLAlchemy dependencies are not installed. "
|
|
13
|
+
"Install them with: pip install azurepg-entra[sqlalchemy]"
|
|
14
|
+
) from e
|
|
15
|
+
|
|
16
|
+
from azure_postgresql_auth.core import get_entra_conninfo
|
|
17
|
+
from azure_postgresql_auth.errors import (
|
|
18
|
+
CredentialValueError,
|
|
19
|
+
EntraConnectionValueError,
|
|
20
|
+
)
|
|
21
|
+
|
|
22
|
+
|
|
23
|
+
def enable_entra_authentication(engine: Engine) -> None:
|
|
24
|
+
"""
|
|
25
|
+
Enable Azure Entra ID authentication for a SQLAlchemy engine.
|
|
26
|
+
|
|
27
|
+
This function registers an event listener that automatically provides
|
|
28
|
+
Entra ID credentials for each database connection if they are not already set.
|
|
29
|
+
|
|
30
|
+
Args:
|
|
31
|
+
engine: The SQLAlchemy Engine to enable Entra authentication for
|
|
32
|
+
"""
|
|
33
|
+
|
|
34
|
+
@event.listens_for(engine, "do_connect")
|
|
35
|
+
def provide_token(
|
|
36
|
+
dialect: Dialect, conn_rec: Any, cargs: Any, cparams: dict[str, Any]
|
|
37
|
+
) -> None:
|
|
38
|
+
"""Event handler that provides Entra credentials for each connection.
|
|
39
|
+
|
|
40
|
+
Raises:
|
|
41
|
+
CredentialValueError: If the provided credential is not a valid TokenCredential.
|
|
42
|
+
EntraConnectionValueError: If Entra connection credentials cannot be retrieved
|
|
43
|
+
"""
|
|
44
|
+
credential = cparams.get("credential", None)
|
|
45
|
+
if credential and not isinstance(credential, (TokenCredential)):
|
|
46
|
+
raise CredentialValueError(
|
|
47
|
+
"credential must be a TokenCredential for sync connections"
|
|
48
|
+
)
|
|
49
|
+
# Check if credentials are already present
|
|
50
|
+
has_user = "user" in cparams
|
|
51
|
+
has_password = "password" in cparams
|
|
52
|
+
|
|
53
|
+
# Only get Entra credentials if user or password is missing
|
|
54
|
+
if not has_user or not has_password:
|
|
55
|
+
try:
|
|
56
|
+
entra_creds = get_entra_conninfo(credential)
|
|
57
|
+
except Exception as e:
|
|
58
|
+
raise EntraConnectionValueError(
|
|
59
|
+
"Could not retrieve Entra credentials"
|
|
60
|
+
) from e
|
|
61
|
+
# Only update missing credentials
|
|
62
|
+
if not has_user and "user" in entra_creds:
|
|
63
|
+
cparams["user"] = entra_creds["user"]
|
|
64
|
+
if not has_password and "password" in entra_creds:
|
|
65
|
+
cparams["password"] = entra_creds["password"]
|
|
66
|
+
|
|
67
|
+
# Remove the helper-only parameter so the DBAPI (psycopg/psycopg2) doesn't see an
|
|
68
|
+
# unknown connection option and raise 'invalid connection option "credential"'.
|
|
69
|
+
if "credential" in cparams:
|
|
70
|
+
del cparams["credential"]
|
|
@@ -0,0 +1,360 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: azure-postgresql-auth
|
|
3
|
+
Version: 1.0.0
|
|
4
|
+
Summary: Azure Entra ID authentication extension for Python database drivers
|
|
5
|
+
Author-email: Arjun Narendra <v-anarendra@microsoft.com>
|
|
6
|
+
License: MIT
|
|
7
|
+
Project-URL: Homepage, https://github.com/v-anarendra_microsoft/entra-id-integration-for-drivers
|
|
8
|
+
Project-URL: Issues, https://github.com/v-anarendra_microsoft/entra-id-integration-for-drivers/issues
|
|
9
|
+
Classifier: Programming Language :: Python :: 3
|
|
10
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
11
|
+
Classifier: Operating System :: OS Independent
|
|
12
|
+
Requires-Python: >=3.10
|
|
13
|
+
Description-Content-Type: text/markdown
|
|
14
|
+
Requires-Dist: azure-identity>=1.13.0
|
|
15
|
+
Requires-Dist: azure-core>=1.24.0
|
|
16
|
+
Provides-Extra: psycopg3
|
|
17
|
+
Requires-Dist: psycopg[binary]>=3.1.0; extra == "psycopg3"
|
|
18
|
+
Requires-Dist: aiohttp>=3.8.0; extra == "psycopg3"
|
|
19
|
+
Provides-Extra: psycopg2
|
|
20
|
+
Requires-Dist: psycopg2-binary>=2.9.0; extra == "psycopg2"
|
|
21
|
+
Provides-Extra: sqlalchemy
|
|
22
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == "sqlalchemy"
|
|
23
|
+
Requires-Dist: aiohttp>=3.8.0; extra == "sqlalchemy"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=7.0.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
27
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == "dev"
|
|
28
|
+
Requires-Dist: mypy~=1.15; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: types-psycopg2>=2.9.0; extra == "dev"
|
|
31
|
+
Requires-Dist: psycopg-pool>=3.1.0; extra == "dev"
|
|
32
|
+
Requires-Dist: testcontainers[postgres]>=3.7.0; extra == "dev"
|
|
33
|
+
Requires-Dist: psycopg[binary]>=3.1.0; extra == "dev"
|
|
34
|
+
Requires-Dist: psycopg2-binary>=2.9.0; extra == "dev"
|
|
35
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == "dev"
|
|
36
|
+
Provides-Extra: all
|
|
37
|
+
Requires-Dist: psycopg[binary]>=3.1.0; extra == "all"
|
|
38
|
+
Requires-Dist: aiohttp>=3.8.0; extra == "all"
|
|
39
|
+
Requires-Dist: psycopg2-binary>=2.9.0; extra == "all"
|
|
40
|
+
Requires-Dist: sqlalchemy>=2.0.0; extra == "all"
|
|
41
|
+
Requires-Dist: pytest>=7.0.0; extra == "all"
|
|
42
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "all"
|
|
43
|
+
Requires-Dist: python-dotenv>=1.0.0; extra == "all"
|
|
44
|
+
Requires-Dist: mypy~=1.15; extra == "all"
|
|
45
|
+
Requires-Dist: ruff>=0.8.0; extra == "all"
|
|
46
|
+
Requires-Dist: types-psycopg2>=2.9.0; extra == "all"
|
|
47
|
+
Requires-Dist: psycopg-pool>=3.1.0; extra == "all"
|
|
48
|
+
Requires-Dist: testcontainers[postgres]>=3.7.0; extra == "all"
|
|
49
|
+
|
|
50
|
+
# azurepg-entra: Azure Database for PostgreSQL Entra ID Authentication
|
|
51
|
+
|
|
52
|
+
This package provides seamless Azure Entra ID authentication for Python database drivers connecting to Azure Database for PostgreSQL. It supports both legacy and modern PostgreSQL drivers with automatic token management and connection pooling.
|
|
53
|
+
|
|
54
|
+
## Features
|
|
55
|
+
|
|
56
|
+
- **🔐 Azure Entra ID Authentication**: Automatic token acquisition and refresh for secure database connections
|
|
57
|
+
- **🔄 Multi-Driver Support**: Works with psycopg2, psycopg3, and SQLAlchemy
|
|
58
|
+
- **⚡ Connection Pooling**: Built-in support for both synchronous and asynchronous connection pools
|
|
59
|
+
- **🏗️ Clean Architecture**: Simple package structure with `azure_postgresql_auth.psycopg2`, `azure_postgresql_auth.psycopg3`, and `azure_postgresql_auth.sqlalchemy`
|
|
60
|
+
- **🔄 Automatic Token Management**: Handles token acquisition, validation, and refresh automatically
|
|
61
|
+
- **🌐 Cross-platform**: Works on Windows, Linux, and macOS
|
|
62
|
+
- **📦 Flexible Installation**: Optional dependencies for different driver combinations
|
|
63
|
+
|
|
64
|
+
## Installation
|
|
65
|
+
|
|
66
|
+
### Basic Installation
|
|
67
|
+
|
|
68
|
+
Install the core package (includes Azure Identity dependencies only):
|
|
69
|
+
```bash
|
|
70
|
+
pip install azurepg-entra
|
|
71
|
+
```
|
|
72
|
+
|
|
73
|
+
### Driver-Specific Installation
|
|
74
|
+
|
|
75
|
+
Choose the installation option based on which PostgreSQL drivers you need:
|
|
76
|
+
|
|
77
|
+
```bash
|
|
78
|
+
# For psycopg3 (modern psycopg, recommended for new projects)
|
|
79
|
+
pip install "azurepg-entra[psycopg3]"
|
|
80
|
+
|
|
81
|
+
# For psycopg2 (legacy support)
|
|
82
|
+
pip install "azurepg-entra[psycopg2]"
|
|
83
|
+
|
|
84
|
+
# For SQLAlchemy with psycopg3 backend
|
|
85
|
+
pip install "azurepg-entra[sqlalchemy]"
|
|
86
|
+
|
|
87
|
+
# All database drivers combined
|
|
88
|
+
pip install "azurepg-entra[drivers]"
|
|
89
|
+
|
|
90
|
+
# Everything including development tools
|
|
91
|
+
pip install "azurepg-entra[all]"
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Development Installation
|
|
95
|
+
|
|
96
|
+
Install from source for development:
|
|
97
|
+
```bash
|
|
98
|
+
git clone https://github.com/v-anarendra_microsoft/entra-id-integration-for-drivers.git
|
|
99
|
+
cd entra-id-integration-for-drivers/python
|
|
100
|
+
|
|
101
|
+
# Install with all dependencies for development
|
|
102
|
+
pip install -e ".[all]"
|
|
103
|
+
|
|
104
|
+
# Or install specific driver combinations
|
|
105
|
+
pip install -e ".[psycopg3,dev]"
|
|
106
|
+
```
|
|
107
|
+
|
|
108
|
+
## Configuration
|
|
109
|
+
|
|
110
|
+
### Environment Variables
|
|
111
|
+
|
|
112
|
+
The samples use environment variables to configure database connections.
|
|
113
|
+
|
|
114
|
+
Copy `.env.example` into a `.env` file in the same directory and update the variables.
|
|
115
|
+
```env
|
|
116
|
+
POSTGRES_SERVER=<your-server.postgres.database.azure.com>
|
|
117
|
+
POSTGRES_DATABASE=<your_database_name>
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
## Quick Start
|
|
121
|
+
|
|
122
|
+
### Running the Samples
|
|
123
|
+
|
|
124
|
+
The repository includes comprehensive working examples in the `samples/` directory:
|
|
125
|
+
|
|
126
|
+
- **`samples/psycopg2/getting_started/`**: psycopg2 (legacy driver support)
|
|
127
|
+
- **`samples/psycopg3/getting_started/`**: psycopg3 examples (modern driver, recommended)
|
|
128
|
+
- **`samples/sqlalchemy/getting_started/`**: SQLAlchemy examples with psycopg3 backend
|
|
129
|
+
|
|
130
|
+
Configure your environment variables first, then run the samples:
|
|
131
|
+
|
|
132
|
+
```bash
|
|
133
|
+
# Copy and configure environment
|
|
134
|
+
cp samples/psycopg3/getting_started/.env.example samples/psycopg3/getting_started/.env
|
|
135
|
+
# Edit .env with your Azure PostgreSQL server details
|
|
136
|
+
|
|
137
|
+
# Test psycopg2 (legacy driver)
|
|
138
|
+
python samples/psycopg2/getting_started/create_db_connection_psycopg2.py --mode both
|
|
139
|
+
|
|
140
|
+
# Test psycopg3 (modern driver, recommended)
|
|
141
|
+
python samples/psycopg3/getting_started/create_db_connection_psycopg.py --mode both
|
|
142
|
+
|
|
143
|
+
# Test SQLAlchemy
|
|
144
|
+
python samples/sqlalchemy/getting_started/create_db_connection_sqlalchemy.py --mode both
|
|
145
|
+
```
|
|
146
|
+
|
|
147
|
+
## Usage
|
|
148
|
+
|
|
149
|
+
Choose the driver that best fits your project needs:
|
|
150
|
+
|
|
151
|
+
- **psycopg3**: Modern PostgreSQL driver (recommended for new projects)
|
|
152
|
+
- **psycopg2**: Legacy PostgreSQL driver (for existing projects)
|
|
153
|
+
- **SQLAlchemy**: High-level ORM/Core interface
|
|
154
|
+
|
|
155
|
+
---
|
|
156
|
+
|
|
157
|
+
## psycopg2 Driver (Legacy Support)
|
|
158
|
+
|
|
159
|
+
> **Note**: psycopg2 is in maintenance mode. For new projects, consider using psycopg3 instead.
|
|
160
|
+
|
|
161
|
+
The psycopg2 integration provides synchronous connection support with Azure Entra ID authentication through connection pooling.
|
|
162
|
+
|
|
163
|
+
### Installation
|
|
164
|
+
```bash
|
|
165
|
+
pip install "azurepg-entra[psycopg2]"
|
|
166
|
+
```
|
|
167
|
+
|
|
168
|
+
### Connection Pooling (Recommended)
|
|
169
|
+
|
|
170
|
+
```python
|
|
171
|
+
from azure_postgresql_auth.psycopg2 import EntraConnection # import library
|
|
172
|
+
from psycopg2 import pool # import to use pooling
|
|
173
|
+
|
|
174
|
+
with pool.ThreadedConnectionPool(
|
|
175
|
+
minconn=1,
|
|
176
|
+
maxconn=5,
|
|
177
|
+
host="your-server.postgres.database.azure.com",
|
|
178
|
+
database="your_database",
|
|
179
|
+
connection_factory=EntraConnection
|
|
180
|
+
) as connection_pool:
|
|
181
|
+
```
|
|
182
|
+
|
|
183
|
+
### Direct Connection
|
|
184
|
+
|
|
185
|
+
```python
|
|
186
|
+
from azure_postgresql_auth.psycopg2 import EntraConnection # import library
|
|
187
|
+
|
|
188
|
+
with EntraConnection(
|
|
189
|
+
"postgresql://your-server.postgres.database.azure.com:5432/your_database"
|
|
190
|
+
) as conn
|
|
191
|
+
```
|
|
192
|
+
|
|
193
|
+
---
|
|
194
|
+
|
|
195
|
+
## psycopg3 Driver (Recommended)
|
|
196
|
+
|
|
197
|
+
psycopg3 is the modern, actively developed PostgreSQL driver with native async support and better performance.
|
|
198
|
+
|
|
199
|
+
### Installation
|
|
200
|
+
```bash
|
|
201
|
+
pip install "azurepg-entra[psycopg3]"
|
|
202
|
+
```
|
|
203
|
+
|
|
204
|
+
### Synchronous Connection
|
|
205
|
+
|
|
206
|
+
```python
|
|
207
|
+
from azure_postgresql_auth.psycopg3 import EntraConnection # import library
|
|
208
|
+
from psycopg_pool import ConnectionPool # import to use pooling
|
|
209
|
+
|
|
210
|
+
with ConnectionPool(
|
|
211
|
+
conninfo="postgresql://your-server.postgres.database.azure.com:5432/your_database",
|
|
212
|
+
connection_class=EntraConnection,
|
|
213
|
+
min_size=1, # keep at least 1 connection always open
|
|
214
|
+
max_size=5, # allow up to 5 concurrent connections
|
|
215
|
+
) as pool
|
|
216
|
+
```
|
|
217
|
+
|
|
218
|
+
### Asynchronous Connection
|
|
219
|
+
|
|
220
|
+
```python
|
|
221
|
+
from azure_postgresql_auth.psycopg3 import AsyncEntraConnection # import library
|
|
222
|
+
from psycopg_pool import AsyncConnectionPool # import to use pooling
|
|
223
|
+
|
|
224
|
+
async with AsyncConnectionPool(
|
|
225
|
+
conninfo="postgresql://your-server.postgres.database.azure.com:5432/your_database",
|
|
226
|
+
connection_class=AsyncEntraConnection,
|
|
227
|
+
min_size=1, # keep at least 1 connection always open
|
|
228
|
+
max_size=5, # allow up to 5 concurrent connections
|
|
229
|
+
) as pool
|
|
230
|
+
```
|
|
231
|
+
|
|
232
|
+
---
|
|
233
|
+
|
|
234
|
+
## SQLAlchemy Integration
|
|
235
|
+
|
|
236
|
+
SQLAlchemy integration uses psycopg3 as the backend driver with automatic Entra ID authentication through event listeners.
|
|
237
|
+
|
|
238
|
+
> **For more information**: See SQLAlchemy's documentation on [controlling how parameters are passed to the DBAPI connect function](https://docs.sqlalchemy.org/en/20/core/engines.html#controlling-how-parameters-are-passed-to-the-dbapi-connect-function).
|
|
239
|
+
|
|
240
|
+
### Installation
|
|
241
|
+
```bash
|
|
242
|
+
pip install "azurepg-entra[sqlalchemy]"
|
|
243
|
+
```
|
|
244
|
+
|
|
245
|
+
### Synchronous Engine
|
|
246
|
+
|
|
247
|
+
```python
|
|
248
|
+
from sqlalchemy import create_engine
|
|
249
|
+
from azure_postgresql_auth.sqlalchemy import enable_entra_authentication # import library
|
|
250
|
+
|
|
251
|
+
with create_engine("postgresql+psycopg://your-server.postgres.database.azure.com/your_database") as engine:
|
|
252
|
+
# Enable Entra ID authentication
|
|
253
|
+
enable_entra_authentication(engine)
|
|
254
|
+
|
|
255
|
+
# Core usage
|
|
256
|
+
with engine.connect() as conn:
|
|
257
|
+
|
|
258
|
+
# ORM usage
|
|
259
|
+
from sqlalchemy.orm import sessionmaker
|
|
260
|
+
Session = sessionmaker(bind=engine)
|
|
261
|
+
```
|
|
262
|
+
|
|
263
|
+
### Asynchronous Engine
|
|
264
|
+
|
|
265
|
+
```python
|
|
266
|
+
from sqlalchemy.ext.asyncio import create_async_engine
|
|
267
|
+
from azure_postgresql_auth.sqlalchemy import enable_entra_authentication_async # import library
|
|
268
|
+
|
|
269
|
+
async with create_async_engine("postgresql+psycopg://your-server.postgres.database.azure.com/your_database") as engine:
|
|
270
|
+
# Enable Entra ID authentication for async
|
|
271
|
+
enable_entra_authentication_async(engine)
|
|
272
|
+
|
|
273
|
+
# Async Core usage
|
|
274
|
+
async with engine.connect() as conn:
|
|
275
|
+
|
|
276
|
+
# Async ORM usage
|
|
277
|
+
from sqlalchemy.ext.asyncio import async_sessionmaker
|
|
278
|
+
AsyncSession = async_sessionmaker(engine, expire_on_commit=False)
|
|
279
|
+
```
|
|
280
|
+
|
|
281
|
+
## How It Works
|
|
282
|
+
|
|
283
|
+
### Authentication Flow
|
|
284
|
+
|
|
285
|
+
1. **Token Acquisition**: Uses Azure Identity libraries (`DefaultAzureCredential` by default) to acquire access tokens from Azure Entra ID
|
|
286
|
+
2. **Automatic Refresh**: Tokens are automatically refreshed before each new database connection
|
|
287
|
+
3. **Secure Transport**: Tokens are passed as passwords in PostgreSQL connection strings over SSL
|
|
288
|
+
4. **Server Validation**: Azure Database for PostgreSQL validates the token and establishes the authenticated connection
|
|
289
|
+
5. **User Mapping**: The token's user principal name (UPN) is mapped to a PostgreSQL user for authorization
|
|
290
|
+
|
|
291
|
+
### Token Scopes
|
|
292
|
+
|
|
293
|
+
The package automatically requests the correct OAuth2 scopes:
|
|
294
|
+
- **Database scope**: `https://ossrdbms-aad.database.windows.net/.default` (primary)
|
|
295
|
+
- **Management scope**: `https://management.azure.com/.default` (fallback for managed identities)
|
|
296
|
+
|
|
297
|
+
### Security Features
|
|
298
|
+
|
|
299
|
+
- **🔒 Token-based authentication**: No passwords stored or transmitted
|
|
300
|
+
- **⏰ Automatic expiration**: Tokens expire and are refreshed automatically
|
|
301
|
+
- **🛡️ SSL enforcement**: All connections require SSL encryption
|
|
302
|
+
- **🔑 Principle of least privilege**: Only database-specific scopes are requested
|
|
303
|
+
---
|
|
304
|
+
|
|
305
|
+
## Troubleshooting
|
|
306
|
+
|
|
307
|
+
### Common Issues
|
|
308
|
+
|
|
309
|
+
**Authentication Errors**
|
|
310
|
+
```bash
|
|
311
|
+
# Error: "password authentication failed"
|
|
312
|
+
# Solution: Ensure your Azure identity has been granted access to the database
|
|
313
|
+
# Run this SQL as a database administrator:
|
|
314
|
+
CREATE ROLE "your-user@your-domain.com" WITH LOGIN;
|
|
315
|
+
GRANT ALL PRIVILEGES ON DATABASE your_database TO "your-user@your-domain.com";
|
|
316
|
+
```
|
|
317
|
+
|
|
318
|
+
**Connection Timeouts**
|
|
319
|
+
```python
|
|
320
|
+
# Increase connection timeout for slow networks
|
|
321
|
+
conn = SyncEntraConnection.connect(
|
|
322
|
+
"postgresql://server:5432/db",
|
|
323
|
+
connect_timeout=30 # 30 seconds instead of default 10
|
|
324
|
+
)
|
|
325
|
+
```
|
|
326
|
+
|
|
327
|
+
**Windows Async Issues**
|
|
328
|
+
```python
|
|
329
|
+
# Fix Windows event loop compatibility
|
|
330
|
+
import asyncio
|
|
331
|
+
import sys
|
|
332
|
+
|
|
333
|
+
if sys.platform == "win32":
|
|
334
|
+
asyncio.set_event_loop_policy(asyncio.WindowsSelectorEventLoopPolicy())
|
|
335
|
+
```
|
|
336
|
+
|
|
337
|
+
### Debug Logging
|
|
338
|
+
|
|
339
|
+
Enable debug logging to troubleshoot authentication issues:
|
|
340
|
+
|
|
341
|
+
```python
|
|
342
|
+
import logging
|
|
343
|
+
logging.basicConfig(level=logging.DEBUG)
|
|
344
|
+
|
|
345
|
+
# This will show token acquisition and connection details
|
|
346
|
+
conn = SyncEntraConnection.connect("postgresql://server:5432/db")
|
|
347
|
+
```
|
|
348
|
+
|
|
349
|
+
---
|
|
350
|
+
|
|
351
|
+
## Contributing
|
|
352
|
+
|
|
353
|
+
We welcome contributions! Please see [CONTRIBUTING.md](../CONTRIBUTING.md) for guidelines.
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
---
|
|
357
|
+
|
|
358
|
+
## License
|
|
359
|
+
|
|
360
|
+
This project is licensed under the MIT License - see the [LICENSE](../LICENSE) file for details.
|
|
@@ -0,0 +1,16 @@
|
|
|
1
|
+
azure_postgresql_auth/__init__.py,sha256=2pRSes8cqoINx341xJCMrq6WmpUOs5QA3Nu3NPSaeBg,803
|
|
2
|
+
azure_postgresql_auth/core.py,sha256=zW8XqcogluaXW9QB0ETS5Nfo-DdWPw6qrF4uXnz2nbs,9381
|
|
3
|
+
azure_postgresql_auth/errors.py,sha256=rpu7SEJ6Uwldm4o48_7NTWsockJ_pBxe9cMLi-kD9BI,798
|
|
4
|
+
azure_postgresql_auth/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
5
|
+
azure_postgresql_auth/psycopg2/__init__.py,sha256=twUVzJBoboQ45liZby_UOrStftS5C602cjdypp11dOY,620
|
|
6
|
+
azure_postgresql_auth/psycopg2/entra_connection.py,sha256=Gm42vI4fWL6fdoPmUBDb95CK19amhHvjUFxoev-fFnE,3260
|
|
7
|
+
azure_postgresql_auth/psycopg3/__init__.py,sha256=ChSOYtVhTzj2SF3svmhNwKVj0B7j_9CfoB5hZF_zsDg,815
|
|
8
|
+
azure_postgresql_auth/psycopg3/async_entra_connection.py,sha256=jB4QIcMdkLIuxT9Y38yiNbn9oyb59yZZw-dRhLhASZQ,2972
|
|
9
|
+
azure_postgresql_auth/psycopg3/entra_connection.py,sha256=cpJJ6AcfJMOv6eXIy8IGfCLvyrn99oG5IAVtd0R-R7g,2888
|
|
10
|
+
azure_postgresql_auth/sqlalchemy/__init__.py,sha256=LttQhibGGG2rkaoBOOVjvpsYE4-szKT37PWoba42PS0,954
|
|
11
|
+
azure_postgresql_auth/sqlalchemy/async_entra_connection.py,sha256=L3RjsqsAuj8NaYQRhMaiJbkBnblVAA8cYMfvAQQiOC8,2924
|
|
12
|
+
azure_postgresql_auth/sqlalchemy/entra_connection.py,sha256=BcX5M6R0KaHF6Sflzl1V96BmwmVYjB-m80W7c1s1ydc,2743
|
|
13
|
+
azure_postgresql_auth-1.0.0.dist-info/METADATA,sha256=RhDK4cCjui_unpI_QDOVOtYJ4S_3PkwWNzr0AOzMwrQ,12500
|
|
14
|
+
azure_postgresql_auth-1.0.0.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
15
|
+
azure_postgresql_auth-1.0.0.dist-info/top_level.txt,sha256=tfXHngCqGdo4mArX4yjg3NJ1CQ_uUEmwnHaY9rkPnIc,22
|
|
16
|
+
azure_postgresql_auth-1.0.0.dist-info/RECORD,,
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
azure_postgresql_auth
|