Connecting to Azure SQL Database in a Python script
Both when developing locally and when an app is deployed on-premises (e.g. on Streamlit), authenticating an app using an application service principal is an option. In fact, this is the only option when deploying on-premises.
- In ‘App registration’ in the Azure portal, create a new registration with a client secret following step 1 here
- Copy the ‘Application (client) ID’, ‘Directory (tenant) ID’ and client secret and save them as environment variables locally and/or within the app that will be connecting to the database (e.g. GitHub Codespaces, Streamlit)
- Add the application service principal as a user in the database and give it permissions. E.g.:
1
2
3
4
5
create user python_application_service_principal from external provider
alter role db_datareader add member python_application_service_principal;
alter role db_datawriter add member python_application_service_principal;
There are then (at least) two ways of connecting to a database using an application service principal.
Using the ‘Application (client) ID’ and client secret to connecting using authentication type ActiveDirectoryServicePrincipal
1
2
3
4
5
6
7
8
9
10
11
12
13
14
from ds_utils import database_operations as dbo
# %%
# CONNECT TO D/B
connection = dbo.connect_sql_db(
driver='pyodbc',
driver_version=os.environ['ODBC_DRIVER'],
dialect='mssql',
server=os.environ['ODBC_SERVER'],
database=os.environ['ODBC_DATABASE'],
authentication=os.environ['ODBC_AUTHENTICATION'],
username=os.environ['AZURE_CLIENT_ID'],
password=os.environ['AZURE_CLIENT_SECRET'],
)
where ['ODBC_AUTHENTICATION']
is ActiveDirectoryServicePrincipal
.
Using access tokens
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
import struct
from azure.identity import DefaultAzureCredential
from ds_utils import database_operations as dbo
# %%
# CONNECT TO D/B
# Create SQLAlchemy engine
connection = dbo.connect_sql_db(
driver='pyodbc',
driver_version=os.environ['ODBC_DRIVER'],
dialect='mssql',
server=os.environ['ODBC_SERVER'],
database=os.environ['ODBC_DATABASE']
)
# %%
# Handle token creation
# NB: The first two parameters are defaults required for connecting to Azure SQL Database
# Ref.: https://docs.sqlalchemy.org/en/20/dialects/mssql.html#connecting-to-databases-with-access-tokens # noqa
SQL_COPT_SS_ACCESS_TOKEN = 1256
TOKEN_URL = "https://database.windows.net/.default"
azure_credentials = DefaultAzureCredential()
@sqlalchemy.event.listens_for(connection, "do_connect")
def provide_token(dialect, conn_rec, cargs, cparams):
# Remove the "Trusted_Connection" parameter that SQLAlchemy adds
cargs[0] = cargs[0].replace(";Trusted_Connection=Yes", "")
# Create token credential
raw_token = azure_credentials.get_token(TOKEN_URL).token.encode("utf-16-le")
token_struct = struct.pack(f"<I{len(raw_token)}s", len(raw_token), raw_token)
# Apply it to keyword arguments
cparams["attrs_before"] = {SQL_COPT_SS_ACCESS_TOKEN: token_struct}
(Ref.. Note that this appears to be a modified version of the instructions here.)
with
1
2
3
4
5
6
connection_string = (
f'{dialect}+{driver}:///?odbc_connect=' +
urllib.parse.quote_plus(
f'DRIVER={driver_version};SERVER={server};DATABASE={database};'
)
)
in database_operations
.
This post is licensed under CC BY 4.0 by the author.