diff --git a/.github/workflows/integration-tests-azure.yml b/.github/workflows/integration-tests-azure.yml index 0691777..dfad12d 100644 --- a/.github/workflows/integration-tests-azure.yml +++ b/.github/workflows/integration-tests-azure.yml @@ -1,32 +1,56 @@ ---- -name: Integration tests on Azure -on: # yamllint disable-line rule:truthy +name: Integration tests on Fabric DW +on: # yamllint disable-line rule:truthy workflow_dispatch: pull_request: branches: - - main + - oidc_connect jobs: - integration-tests-azure: + integration-tests-fabric-dw: name: Regular strategy: fail-fast: false max-parallel: 1 matrix: - profile: ["ci_azure_auto"] + profile: ["integration_tests"] python_version: ["3.11"] - msodbc_version: ["17", "18"] + msodbc_version: ["18"] runs-on: ubuntu-latest + permissions: + contents: read # Required to access repository files + packages: read # Grant explicit read access to packages + id-token: write # Needed if using OIDC authentication container: image: ghcr.io/${{ github.repository }}:CI-${{ matrix.python_version }}-msodbc${{ matrix.msodbc_version }} steps: - - name: AZ CLI login - run: az login --service-principal --username="${AZURE_CLIENT_ID}" --password="${AZURE_CLIENT_SECRET}" --tenant="${AZURE_TENANT_ID}" - env: - AZURE_CLIENT_ID: ${{ secrets.DBT_AZURE_SP_NAME }} - AZURE_CLIENT_SECRET: ${{ secrets.DBT_AZURE_SP_SECRET }} - AZURE_TENANT_ID: ${{ secrets.DBT_AZURE_TENANT }} + # Azure login using federated credentials + - name: Azure login with OIDC + uses: azure/login@v2 + with: + client-id: ${{ secrets.DBT_AZURE_SP_NAME }} + tenant-id: ${{ secrets.DBT_AZURE_TENANT }} + allow-no-subscriptions: true + federated-token: true + + - name: Test Connection To Fabric Data Warehouse + id: fetch_token + run: | + pip install azure-identity pyodbc azure-core + + python - < AccessToken: } -def get_pyodbc_attrs_before(credentials: FabricCredentials) -> Dict: +def get_pyodbc_attrs_before_credentials(credentials: FabricCredentials) -> Dict: """ Get the pyodbc attrs before. @@ -220,6 +220,36 @@ def get_pyodbc_attrs_before(credentials: FabricCredentials) -> Dict: return attrs_before +def get_pyodbc_attrs_before_accesstoken(accessToken: str) -> Dict: + """ + Get the pyodbc attrs before. + + Parameters + ---------- + credentials : Access Token for Integration Tests + Credentials. + + Returns + ------- + out : Dict + The pyodbc attrs before. + + Source + ------ + Authentication for SQL server with an access token: + https://docs.microsoft.com/en-us/sql/connect/odbc/using-azure-active-directory?view=sql-server-ver15#authenticating-with-an-access-token + """ + + access_token_utf16 = accessToken.encode("utf-16-le") + token_struct = struct.pack( + f" str: """ Convert a boolean to a connection string argument. @@ -323,7 +353,7 @@ def open(cls, connection: Connection) -> Connection: con_str.append(f"Database={credentials.database}") - #Enabling trace flag + # Enabling trace flag if credentials.trace_flag: con_str.append("SQL_ATTR_TRACE=SQL_OPT_TRACE_ON") else: @@ -331,7 +361,10 @@ def open(cls, connection: Connection) -> Connection: assert credentials.authentication is not None - if "ActiveDirectory" in credentials.authentication: + if ( + "ActiveDirectory" in credentials.authentication + and credentials.authentication != "ActiveDirectoryAccessToken" + ): con_str.append(f"Authentication={credentials.authentication}") if credentials.authentication == "ActiveDirectoryPassword": @@ -395,7 +428,11 @@ def open(cls, connection: Connection) -> Connection: def connect(): logger.debug(f"Using connection string: {con_str_display}") - attrs_before = get_pyodbc_attrs_before(credentials) + if credentials.authentication == "ActiveDirectoryAccessToken": + attrs_before = get_pyodbc_attrs_before_accesstoken(credentials.access_token) + else: + attrs_before = get_pyodbc_attrs_before_credentials(credentials) + handle = pyodbc.connect( con_str_concat, attrs_before=attrs_before, diff --git a/dbt/adapters/fabric/fabric_credentials.py b/dbt/adapters/fabric/fabric_credentials.py index a824fac..138e3bd 100644 --- a/dbt/adapters/fabric/fabric_credentials.py +++ b/dbt/adapters/fabric/fabric_credentials.py @@ -17,6 +17,7 @@ class FabricCredentials(Credentials): tenant_id: Optional[str] = None client_id: Optional[str] = None client_secret: Optional[str] = None + access_token: Optional[str] = None authentication: Optional[str] = "ActiveDirectoryServicePrincipal" encrypt: Optional[bool] = True # default value in MS ODBC Driver 18 as well trust_cert: Optional[bool] = False # default value in MS ODBC Driver 18 as well diff --git a/dev_requirements.txt b/dev_requirements.txt index d3313a4..f28c23a 100644 --- a/dev_requirements.txt +++ b/dev_requirements.txt @@ -1,9 +1,8 @@ # install latest changes in dbt-core # TODO: how to automate switching from develop to version branches? -git+https://github.com/dbt-labs/dbt-core.git@v1.8.0#egg=dbt-core&subdirectory=core +git+https://github.com/dbt-labs/dbt-core.git#egg=dbt-core&subdirectory=core git+https://github.com/dbt-labs/dbt-adapters.git git+https://github.com/dbt-labs/dbt-adapters.git#subdirectory=dbt-tests-adapter -git+https://github.com/dbt-labs/dbt-common.git pytest==8.0.1 twine==5.1.1 diff --git a/tests/conftest.py b/tests/conftest.py index 3e60ce0..adbcbb0 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -27,6 +27,8 @@ def dbt_profile_target(request: FixtureRequest, dbt_profile_target_update): target = _profile_ci_azure_environment() elif profile == "user_azure": target = _profile_user_azure() + elif profile == "integration_tests": + target = _profile_integration_tests() else: raise ValueError(f"Unknown profile: {profile}") @@ -55,7 +57,7 @@ def _profile_ci_azure_base(): "database": os.getenv("DBT_AZURESQL_DB"), "encrypt": True, "trust_cert": True, - "trace_flag":False, + "trace_flag": False, }, } @@ -104,6 +106,17 @@ def _profile_user_azure(): return profile +def _profile_integration_tests(): + profile = { + **_profile_ci_azure_base(), + **{ + "authentication": os.getenv("FABRIC_TEST_AUTH", "ActiveDirectoryAccessToken"), + "access_token": os.getenv("FABRIC_INTEGRATION_TESTS_TOKEN"), + }, + } + return profile + + @pytest.fixture(autouse=True) def skip_by_profile_type(request: FixtureRequest): profile_type = request.config.getoption("--profile")