Context
The current credential path eagerly calls azure_credential.get_token(), manually builds the ODBC access-token struct, and passes it through attrs_before:
|
# ODBC attribute used to inject a pre-acquired Entra ID access token, kept for a future explicit |
|
# access-token feature. Not used by any authentication method today. |
|
# https://learn.microsoft.com/sql/connect/odbc/using-azure-active-directory#authenticating-with-an-access-token |
|
SQL_COPT_SS_ACCESS_TOKEN = 1256 |
|
SQL_TOKEN_SCOPE = "https://database.windows.net/.default" |
|
|
|
# Entra ID authentication methods supported by mssql-python's `Authentication=` connection |
|
# option. mssql-python performs the sign-in for all of them; dlt only builds the DSN. |
|
SUPPORTED_AUTHENTICATION = frozenset( |
|
{ |
|
"ActiveDirectoryServicePrincipal", |
|
"ActiveDirectoryPassword", |
|
"ActiveDirectoryIntegrated", |
|
"ActiveDirectoryInteractive", |
|
"ActiveDirectoryMsi", |
|
"ActiveDirectoryDefault", |
|
"ActiveDirectoryDeviceCode", |
|
} |
|
) |
|
|
|
# Thin alias for `ActiveDirectoryDefault`, resolved by `_normalize_authentication`. |
|
_AUTHENTICATION_ALIASES = { |
|
"default": "ActiveDirectoryDefault", |
|
} |
|
|
|
|
|
def _normalize_authentication(authentication: str) -> str: |
|
"""Resolve the thin `default` alias to the canonical `ActiveDirectoryDefault` name.""" |
|
return _AUTHENTICATION_ALIASES.get(authentication.lower(), authentication) |
|
|
|
|
|
def build_token_attrs_before(credentials: Any) -> dict[int, bytes] | None: |
|
"""Return `attrs_before` with a directly injected Entra ID access token, or None.""" |
|
if credentials.access_token: |
|
token = str(credentials.access_token) |
|
elif credentials.azure_credential: |
|
token = credentials.azure_credential.get_token(SQL_TOKEN_SCOPE).token |
|
else: |
|
return None |
|
encoded_token = token.encode("utf-16-le") |
|
token_struct = struct.pack(f"<I{len(encoded_token)}s", len(encoded_token), encoded_token) |
|
return {SQL_COPT_SS_ACCESS_TOKEN: token_struct} |
|
access_token: Optional[TSecretStrValue] = None |
|
"""Pre-acquired Entra ID access token, injected as-is. Takes precedence over `azure_credential` and `authentication`""" |
|
|
|
azure_credential: Annotated[Optional[Any], NotResolved()] = None |
|
"""A `TokenCredential` injected at runtime, e.g. `DefaultAzureCredential()`. Takes precedence over `authentication` but not over `access_token`""" |
|
def to_odbc_dsn(self) -> str: |
|
params = self.get_odbc_dsn_dict() |
|
return build_odbc_dsn(params) |
|
|
|
def to_odbc_attrs_before(self) -> dict[int, bytes] | None: |
|
"""Return `attrs_before` with a directly injected Entra ID access token, or None.""" |
|
return build_token_attrs_before(self) |
|
def open_connection(self) -> mssql_python.Connection: |
|
# mssql-python bundles its own driver, so the connection string carries no DRIVER, and it |
|
# signs in for every supported Entra ID authentication method itself from the |
|
# `Authentication=` DSN keyword — dlt injects no `attrs_before` for those. |
|
# |
|
# mssql-python auto-enables connection pooling (default: 100 connections, 600s idle |
|
# timeout) on the first connection any process opens, unless the application calls |
|
# `mssql_python.pooling()` first — which dlt does not do, since these defaults are |
|
# already sane for our workload. The pool matches purely on connection-string text, so |
|
# our credentials building a stable DSN is what makes reuse actually happen. |
|
self._conn = mssql_python.connect( |
|
self.credentials.to_odbc_dsn(), |
|
autocommit=True, |
|
attrs_before=self.credentials.to_odbc_attrs_before(), # type: ignore[arg-type] |
|
timeout=self.credentials.connect_timeout, |
|
) |
mssql-python v1.13.0 / PR #603 adds a public token_provider= parameter accepting any synchronous object with .get_token(scope). It centralizes token acquisition, validation, expiry capture, error translation, token struct construction, and fresh-token acquisition for bulk-copy operations.
Our azure_credential objects already implement this protocol, including FabNotebookUtilsCredential. Continuing to serialize those credentials in dlt duplicates driver code and prevents the driver from retaining the provider for later operations.
This depends on #10.
Proposed change
- Pass
MsSqlCredentials.azure_credential to mssql_python.connect(..., token_provider=...).
- Keep
access_token on the existing attrs_before[SQL_COPT_SS_ACCESS_TOKEN] path:
- it is explicitly a pre-acquired token rather than a provider;
- it preserves the v1.13 escape hatch for sovereign-cloud tokens, because
token_provider uses the Azure commercial SQL scope.
- Split the current helper so only
access_token needs UTF-16LE/length-prefix packing; remove manual get_token(SQL_TOKEN_SCOPE) and its dlt-owned scope constant from the azure_credential path.
- Continue omitting
Authentication=, UID, and PWD from the DSN when either explicit token mechanism is selected.
- Keep the existing precedence explicit:
access_token wins over azure_credential, which wins over authentication.
Do not claim that token_provider makes token-hash pools expiry-refreshable. In v1.13 custom providers are still keyed by the token hash; token rotation opens a new pool bucket. The concrete value here is a smaller dlt authentication surface, driver-owned validation/lifecycle metadata, and correct token reacquisition for the new bulk-copy API.
Acceptance criteria
azure_credential is passed unchanged as token_provider and dlt does not call get_token() while building credentials or connection arguments.
access_token still produces the exact binary attrs_before value and never sets token_provider.
authentication=ActiveDirectory* continues to use only the DSN and never sets either explicit token argument.
FabNotebookUtilsCredential("sql"), DefaultAzureCredential, and a minimal custom get_token(scope) provider are covered.
- Tests cover precedence and prove that
token_provider, Authentication=, and access-token attrs_before are never sent together.
- Driver acquisition failures retain the original credential error as their cause and are mapped through the existing dlt database exception boundary.
- Documentation calls out that
token_provider supports only the Azure commercial SQL scope; sovereign clouds require access_token.
Context
The current credential path eagerly calls
azure_credential.get_token(), manually builds the ODBC access-token struct, and passes it throughattrs_before:dlt/dlt/destinations/impl/mssql/configuration.py
Lines 15 to 56 in a831096
dlt/dlt/destinations/impl/mssql/configuration.py
Lines 181 to 185 in a831096
dlt/dlt/destinations/impl/mssql/configuration.py
Lines 242 to 248 in a831096
dlt/dlt/destinations/impl/mssql/sql_client.py
Lines 39 to 54 in a831096
mssql-python v1.13.0 / PR #603 adds a public
token_provider=parameter accepting any synchronous object with.get_token(scope). It centralizes token acquisition, validation, expiry capture, error translation, token struct construction, and fresh-token acquisition for bulk-copy operations.Our
azure_credentialobjects already implement this protocol, includingFabNotebookUtilsCredential. Continuing to serialize those credentials in dlt duplicates driver code and prevents the driver from retaining the provider for later operations.This depends on #10.
Proposed change
MsSqlCredentials.azure_credentialtomssql_python.connect(..., token_provider=...).access_tokenon the existingattrs_before[SQL_COPT_SS_ACCESS_TOKEN]path:token_provideruses the Azure commercial SQL scope.access_tokenneeds UTF-16LE/length-prefix packing; remove manualget_token(SQL_TOKEN_SCOPE)and its dlt-owned scope constant from theazure_credentialpath.Authentication=,UID, andPWDfrom the DSN when either explicit token mechanism is selected.access_tokenwins overazure_credential, which wins overauthentication.Do not claim that
token_providermakes token-hash pools expiry-refreshable. In v1.13 custom providers are still keyed by the token hash; token rotation opens a new pool bucket. The concrete value here is a smaller dlt authentication surface, driver-owned validation/lifecycle metadata, and correct token reacquisition for the new bulk-copy API.Acceptance criteria
azure_credentialis passed unchanged astoken_providerand dlt does not callget_token()while building credentials or connection arguments.access_tokenstill produces the exact binaryattrs_beforevalue and never setstoken_provider.authentication=ActiveDirectory*continues to use only the DSN and never sets either explicit token argument.FabNotebookUtilsCredential("sql"),DefaultAzureCredential, and a minimal customget_token(scope)provider are covered.token_provider,Authentication=, and access-tokenattrs_beforeare never sent together.token_providersupports only the Azure commercial SQL scope; sovereign clouds requireaccess_token.