-
Notifications
You must be signed in to change notification settings - Fork 101
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
add support for Azure SQL by adding Active Directory authentication methods #53
Changes from all commits
585751b
76251f9
bc1b7dc
2f70242
25b6436
8e869bf
06aa91e
ae1f191
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,18 +1,39 @@ | ||
from contextlib import contextmanager | ||
|
||
import pyodbc | ||
import os | ||
import time | ||
import struct | ||
|
||
import dbt.exceptions | ||
from dbt.adapters.base import Credentials | ||
from dbt.adapters.sql import SQLConnectionManager | ||
from azure.identity import DefaultAzureCredential | ||
|
||
from dbt.logger import GLOBAL_LOGGER as logger | ||
|
||
from dataclasses import dataclass | ||
from typing import Optional | ||
|
||
|
||
def create_token(tenant_id, client_id, client_secret): | ||
# bc DefaultAzureCredential will look in env variables | ||
os.environ['AZURE_TENANT_ID'] = tenant_id | ||
os.environ['AZURE_CLIENT_ID'] = client_id | ||
os.environ['AZURE_CLIENT_SECRET'] = client_secret | ||
|
||
token = DefaultAzureCredential().get_token( | ||
'https://database.windows.net//.default') | ||
# convert to byte string interspersed with the 1-byte | ||
# TODO decide which is cleaner? | ||
# exptoken=b''.join([bytes({i})+bytes(1) for i in bytes(token.token, "UTF-8")]) | ||
exptoken = bytes(1).join([bytes(i, "UTF-8") for i in token.token])+bytes(1) | ||
# make c object with bytestring length prefix | ||
tokenstruct = struct.pack("=i", len(exptoken)) + exptoken | ||
|
||
return tokenstruct | ||
|
||
|
||
@dataclass | ||
class SQLServerCredentials(Credentials): | ||
driver: str | ||
|
@@ -23,14 +44,16 @@ class SQLServerCredentials(Credentials): | |
UID: Optional[str] = None | ||
PWD: Optional[str] = None | ||
windows_login: Optional[bool] = False | ||
tenant_id: Optional[str] = None | ||
client_id: Optional[str] = None | ||
client_secret: Optional[str] = None | ||
# "sql", "ActiveDirectoryPassword" or "ActiveDirectoryInteractive", or | ||
# "ServicePrincipal" | ||
authentication: Optional[str] = "sql" | ||
encrypt: Optional[str] = "yes" | ||
|
||
_ALIASES = { | ||
'user': 'UID' | ||
, 'username': 'UID' | ||
, 'pass': 'PWD' | ||
, 'password': 'PWD' | ||
, 'server': 'host' | ||
, 'trusted_connection': 'windows_login' | ||
'user': 'UID', 'username': 'UID', 'pass': 'PWD', 'password': 'PWD', 'server': 'host', 'trusted_connection': 'windows_login', 'auth': 'authentication', 'app_id': 'client_id', 'app_secret': 'client_secret' | ||
} | ||
|
||
@property | ||
|
@@ -40,11 +63,17 @@ def type(self): | |
def _connection_keys(self): | ||
# return an iterator of keys to pretty-print in 'dbt debug' | ||
# raise NotImplementedError | ||
return 'server', 'database', 'schema', 'port', 'UID', 'windows_login' | ||
if self.windows_login is True: | ||
self.authentication = "Windows Login" | ||
|
||
|
||
return 'server', 'database', 'schema', 'port', 'UID', \ | ||
'authentication', 'encrypt' | ||
|
||
|
||
class SQLServerConnectionManager(SQLConnectionManager): | ||
TYPE = 'sqlserver' | ||
TOKEN = None | ||
|
||
@contextmanager | ||
def exception_handler(self, sql): | ||
|
@@ -97,16 +126,55 @@ def open(cls, connection): | |
|
||
con_str.append(f"Database={credentials.database}") | ||
|
||
if not getattr(credentials, 'windows_login', False): | ||
con_str.append(f"UID={credentials.UID}") | ||
con_str.append(f"PWD={credentials.PWD}") | ||
else: | ||
type_auth = getattr(credentials, 'authentication', 'sql') | ||
|
||
if 'ActiveDirectory' in type_auth: | ||
con_str.append(f"Authentication={credentials.authentication}") | ||
|
||
if type_auth == "ActiveDirectoryPassword": | ||
con_str.append(f"UID={{{credentials.UID}}}") | ||
con_str.append(f"PWD={{{credentials.PWD}}}") | ||
elif type_auth == "ActiveDirectoryInteractive": | ||
con_str.append(f"UID={{{credentials.UID}}}") | ||
elif type_auth == "ActiveDirectoryIntegrated": | ||
# why is this necessary??? | ||
con_str.remove("UID={None}") | ||
elif type_auth == "ActiveDirectoryMsi": | ||
raise ValueError("ActiveDirectoryMsi is not supported yet") | ||
|
||
elif type_auth == 'ServicePrincipal': | ||
app_id = getattr(credentials, 'AppId', None) | ||
app_secret = getattr(credentials, 'AppSecret', None) | ||
|
||
elif getattr(credentials, 'windows_login', False): | ||
con_str.append(f"trusted_connection=yes") | ||
elif type_auth == 'sql': | ||
con_str.append("Authentication=SqlPassword") | ||
con_str.append(f"UID={{{credentials.UID}}}") | ||
con_str.append(f"PWD={{{credentials.PWD}}}") | ||
|
||
if not getattr(credentials, 'encrypt', False): | ||
con_str.append(f"Encrypt={credentials.encrypt}") | ||
|
||
con_str_concat = ';'.join(con_str) | ||
logger.debug(f'Using connection string: {con_str_concat}') | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This code |
||
|
||
handle = pyodbc.connect(con_str_concat, autocommit=True) | ||
if type_auth != 'ServicePrincipal': | ||
handle = pyodbc.connect(con_str_concat, autocommit=True) | ||
|
||
elif type_auth == 'ServicePrincipal': | ||
|
||
# create token if it does not exist | ||
if cls.TOKEN is None: | ||
tenant_id = getattr(credentials, 'tenant_id', None) | ||
client_id = getattr(credentials, 'client_id', None) | ||
client_secret = getattr(credentials, 'client_secret', None) | ||
|
||
cls.TOKEN = create_token(tenant_id, client_id, client_secret) | ||
|
||
handle = pyodbc.connect(con_str_concat, | ||
attrs_before = {1256:cls.TOKEN}, | ||
autocommit=True) | ||
|
||
connection.state = 'open' | ||
connection.handle = handle | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -27,5 +27,6 @@ | |
install_requires=[ | ||
'dbt-core>=0.18.0', | ||
'pyodbc>=4.0.27', | ||
'azure-identity>=1.4.0' | ||
] | ||
) |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
So instead of displaying
windows_login
as True indbt debug
output , this changes would displayauthentication: Windows Login
We could have added windows auth as an Authentication value within the profiles config but
to have backward compatibility for Windows Auth with the config property :
windows_login
otherwisedbt debug
window would displayauthentication=sql
@mikaelene