From 442cca03cefc7ca2d9d2d50c912d869d4237c698 Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Thu, 4 Sep 2025 15:01:56 +0200 Subject: [PATCH 01/15] added basic snowflake workload identity federation --- dbt-snowflake/pyproject.toml | 3 +-- .../src/dbt/adapters/snowflake/connections.py | 13 +++++++++++++ 2 files changed, 14 insertions(+), 2 deletions(-) diff --git a/dbt-snowflake/pyproject.toml b/dbt-snowflake/pyproject.toml index bfaa52ee1..297ea1b42 100644 --- a/dbt-snowflake/pyproject.toml +++ b/dbt-snowflake/pyproject.toml @@ -25,8 +25,7 @@ classifiers = [ dependencies = [ "dbt-common>=1.10,<2.0", "dbt-adapters>=1.16,<2.0", - # lower bound pin due to CVE-2025-24794 - "snowflake-connector-python[secure-local-storage]>=3.13.1,<4.0.0", + "snowflake-connector-python[secure-local-storage]>=3.17.3,<4.0.0", "certifi<2025.4.26", # add dbt-core to ensure backwards compatibility of installation, this is not a functional dependency "dbt-core>=1.10.0rc0", diff --git a/dbt-snowflake/src/dbt/adapters/snowflake/connections.py b/dbt-snowflake/src/dbt/adapters/snowflake/connections.py index b83032a76..ae0dae9d6 100644 --- a/dbt-snowflake/src/dbt/adapters/snowflake/connections.py +++ b/dbt-snowflake/src/dbt/adapters/snowflake/connections.py @@ -38,6 +38,8 @@ BindUploadError, ) +from snowflake.connector.network import WORKLOAD_IDENTITY_AUTHENTICATOR + from dbt_common.exceptions import ( DbtInternalError, DbtRuntimeError, @@ -114,6 +116,7 @@ class SnowflakeCredentials(Credentials): # this needs to default to `None` so that we can tell if the user set it; see `__post_init__()` reuse_connections: Optional[bool] = None s3_stage_vpce_dns_name: Optional[str] = None + workload_identity_provider: Optional[str] = None def __post_init__(self): if self.authenticator != "oauth" and (self.oauth_client_secret or self.oauth_client_id): @@ -182,6 +185,7 @@ def _connection_keys(self): "insecure_mode", "reuse_connections", "s3_stage_vpce_dns_name", + "workload_identity_provider", ) def auth_args(self): @@ -232,6 +236,15 @@ def auth_args(self): result["token"] = self.token result["authenticator"] = "oauth" + elif self.authenticator.lower() == "workload_identity": + result["authenticator"] = WORKLOAD_IDENTITY_AUTHENTICATOR + + if not self.workload_identity_provider: + raise DbtConfigError( + "workload_identity_provider must be set if authenticator='workload_identity'!" + ) + result["workload_identity_provider"] = self.workload_identity_provider + # enable id token cache for linux result["client_store_temporary_credential"] = True # enable mfa token cache for linux From f4e1799209b84ce05600cb8a04e2393f18af9a68 Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Thu, 4 Sep 2025 15:25:27 +0200 Subject: [PATCH 02/15] addd entra details --- dbt-snowflake/src/dbt/adapters/snowflake/connections.py | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/dbt-snowflake/src/dbt/adapters/snowflake/connections.py b/dbt-snowflake/src/dbt/adapters/snowflake/connections.py index ae0dae9d6..2f3b4a2a2 100644 --- a/dbt-snowflake/src/dbt/adapters/snowflake/connections.py +++ b/dbt-snowflake/src/dbt/adapters/snowflake/connections.py @@ -117,6 +117,7 @@ class SnowflakeCredentials(Credentials): reuse_connections: Optional[bool] = None s3_stage_vpce_dns_name: Optional[str] = None workload_identity_provider: Optional[str] = None + workload_identity_entra_resource: Optional[str] = None def __post_init__(self): if self.authenticator != "oauth" and (self.oauth_client_secret or self.oauth_client_id): @@ -186,6 +187,7 @@ def _connection_keys(self): "reuse_connections", "s3_stage_vpce_dns_name", "workload_identity_provider", + "workload_identity_entra_resource", ) def auth_args(self): @@ -245,6 +247,13 @@ def auth_args(self): ) result["workload_identity_provider"] = self.workload_identity_provider + if self.token: + result["token"] = self.token + if self.workload_identity_entra_resource: + result["workload_identity_entra_resource"] = ( + self.workload_identity_entra_resource + ) + # enable id token cache for linux result["client_store_temporary_credential"] = True # enable mfa token cache for linux From 6c85b3baf3fe14060a88fd9fe6ff5e819955259a Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Thu, 4 Sep 2025 15:39:37 +0200 Subject: [PATCH 03/15] add tests --- .../test_workload_identity_federation.py | 84 +++++++++++++++++++ dbt-snowflake/tests/unit/test_connections.py | 35 ++++++++ 2 files changed, 119 insertions(+) create mode 100644 dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation.py diff --git a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation.py b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation.py new file mode 100644 index 000000000..f2c39a3a8 --- /dev/null +++ b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation.py @@ -0,0 +1,84 @@ +""" +Functional tests for Snowflake Workload Identity Federation (WIF) with AWS authentication. +Prerequisites for testing WIF with AWS: +1. **AWS IAM Configuration:** + Create an IAM role that can be assumed by the EC2 service. An example trust policy below: + ``` + { + "Version": "2012-10-17", + "Statement": [ + { + "Effect": "Allow", + "Principal": { + "Service": "ec2.amazonaws.com" // or your specific service + }, + "Action": "sts:AssumeRole" + } + ] + } + ``` +2. **EC2 Instance:** + Launch an EC2 instance with the IAM role attached as an instance profile. + Connect to the EC2 instance and + + +3. **Snowflake User Configuration:** + Create a service user in Snowflake with WIF enabled: + ```sql + CREATE USER + WORKLOAD_IDENTITY = ( + TYPE = AWS + ARN = '' + ) + TYPE = SERVICE + DEFAULT_ROLE = ; + ``` + Replace `` with your desired username and `` + with the ARN of your AWS IAM role. +4. **AWS Environment:** + This test must run from within the configured EC2 environment. + Connect to the EC2 instance using SSH or similar. +5. **Environment Variables:** + Set the following environment variables for testing: + - SNOWFLAKE_TEST_WIF_ACCOUNT: Your Snowflake account identifier + - SNOWFLAKE_TEST_WIF_USER: The Snowflake service user created for WIF + - SNOWFLAKE_TEST_WIF_DATABASE: Test database name + - SNOWFLAKE_TEST_WIF_WAREHOUSE: Test warehouse name + - SNOWFLAKE_TEST_WIF_ROLE: Snowflake Role for the user (optional) + - SNOWFLAKE_TEST_WIF_SCHEMA: Schema for testing (optional, defaults to schema in profile) +Note: WIF authentication relies on being in the AWS environment, so these tests can't be run locally or in the CI/CD pipeline. +""" + +import os +from dbt.tests.util import run_dbt +import pytest + + +_MODELS__MODEL_1_SQL = """ +select 1 as id, 'wif_test' as source +""" + + +class TestSnowflakeWorkloadIdentityFederation: + @pytest.fixture(scope="class", autouse=True) + def dbt_profile_target(self): + return { + "type": "snowflake", + "threads": 4, + "account": os.getenv("SNOWFLAKE_TEST_ACCOUNT"), + "user": os.getenv("SNOWFLAKE_TEST_WIF_USER"), + "database": os.getenv("SNOWFLAKE_TEST_DATABASE"), + "warehouse": os.getenv("SNOWFLAKE_TEST_WAREHOUSE"), + "authenticator": "workload_identity", + "workload_identity_provider": "aws", + } + + @pytest.fixture(scope="class") + def models(self): + return { + "model_1.sql": _MODELS__MODEL_1_SQL, + } + + def test_snowflake_wif_basic_functionality(self, project): + """Test basic dbt functionality with WIF authentication""" + run_dbt() diff --git a/dbt-snowflake/tests/unit/test_connections.py b/dbt-snowflake/tests/unit/test_connections.py index fb9c57615..53dabb9e5 100644 --- a/dbt-snowflake/tests/unit/test_connections.py +++ b/dbt-snowflake/tests/unit/test_connections.py @@ -4,6 +4,7 @@ from unittest.mock import Mock, patch import multiprocessing from dbt.adapters.exceptions.connection import FailedToConnectError +from dbt_common.exceptions import DbtConfigError import dbt.adapters.snowflake.connections as connections import dbt.adapters.events.logging @@ -67,3 +68,37 @@ def test_snowflake_oauth_expired_token_raises_error(): with pytest.raises(FailedToConnectError): adapter.open() + + +def test_connnections_credentials_passes_through_wif_params(): + credentials = { + "account": "account_id_with_underscores", + "database": "database", + "warehouse": "warehouse", + "schema": "schema", + "authenticator": "workload_identity", + "workload_identity_provider": "azure", + "workload_identity_entra_resource": "app://123", + "token": "test_token", + } + auth_args = connections.SnowflakeCredentials(**credentials).auth_args() + assert auth_args["authenticator"] == "WORKLOAD_IDENTITY" + assert auth_args["workload_identity_provider"] == "azure" + assert auth_args["workload_identity_entra_resource"] == "app://123" + assert auth_args["token"] == "test_token" + + +def test_connnections_credentials_wif_authenticator_fails_without_provider(): + credentials = { + "account": "account_id_with_underscores", + "database": "database", + "warehouse": "warehouse", + "schema": "schema", + "authenticator": "workload_identity", + # Missing workload_identity_provider + } + with pytest.raises(DbtConfigError) as excinfo: + connections.SnowflakeCredentials(**credentials).auth_args() + assert "workload_identity_provider must be set if authenticator='workload_identity'" in str( + excinfo + ) From 20f7f65172703dee5cee9906b5aa5e999bff33f6 Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Thu, 4 Sep 2025 15:55:07 +0200 Subject: [PATCH 04/15] added wif aws test --- ...eration.py => test_workload_identity_federation_aws copy.py} | 2 ++ 1 file changed, 2 insertions(+) rename dbt-snowflake/tests/functional/auth_tests/{test_workload_identity_federation.py => test_workload_identity_federation_aws copy.py} (93%) diff --git a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation.py b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws copy.py similarity index 93% rename from dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation.py rename to dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws copy.py index f2c39a3a8..24c7e2807 100644 --- a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation.py +++ b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws copy.py @@ -38,6 +38,8 @@ 4. **AWS Environment:** This test must run from within the configured EC2 environment. Connect to the EC2 instance using SSH or similar. + Clone this repository, run the setup, and execute this test e.g. + `hatch run pytest tests/functional/auth_tests/test_workload_identity_federation.py::test_snowflake_wif_basic_functionality` 5. **Environment Variables:** Set the following environment variables for testing: - SNOWFLAKE_TEST_WIF_ACCOUNT: Your Snowflake account identifier From 53d734cce70639f96f526f4f283bf008b7fb806a Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Thu, 4 Sep 2025 15:57:04 +0200 Subject: [PATCH 05/15] fix file name --- ...ation_aws copy.py => test_workload_identity_federation_aws.py} | 0 1 file changed, 0 insertions(+), 0 deletions(-) rename dbt-snowflake/tests/functional/auth_tests/{test_workload_identity_federation_aws copy.py => test_workload_identity_federation_aws.py} (100%) diff --git a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws copy.py b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py similarity index 100% rename from dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws copy.py rename to dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py From e273ffe16891c1df031b7cdda10eb5fa675a6d46 Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Thu, 4 Sep 2025 16:16:40 +0200 Subject: [PATCH 06/15] add role to test --- .../auth_tests/test_workload_identity_federation_aws.py | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py index 24c7e2807..e6d0bd0c6 100644 --- a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py +++ b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py @@ -39,7 +39,7 @@ This test must run from within the configured EC2 environment. Connect to the EC2 instance using SSH or similar. Clone this repository, run the setup, and execute this test e.g. - `hatch run pytest tests/functional/auth_tests/test_workload_identity_federation.py::test_snowflake_wif_basic_functionality` + `hatch run pytest tests/functional/auth_tests/test_workload_identity_federation_aws.py::test_snowflake_wif_basic_functionality` 5. **Environment Variables:** Set the following environment variables for testing: - SNOWFLAKE_TEST_WIF_ACCOUNT: Your Snowflake account identifier @@ -71,6 +71,7 @@ def dbt_profile_target(self): "user": os.getenv("SNOWFLAKE_TEST_WIF_USER"), "database": os.getenv("SNOWFLAKE_TEST_DATABASE"), "warehouse": os.getenv("SNOWFLAKE_TEST_WAREHOUSE"), + "role": os.getenv("SNOWFLAKE_TEST_ROLE"), "authenticator": "workload_identity", "workload_identity_provider": "aws", } From ac4affba895b423a186184941d177bc8fa6ca42a Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Thu, 4 Sep 2025 16:37:52 +0200 Subject: [PATCH 07/15] add OIDC test --- .github/workflows/run_snowflake_wif_test.yml | 42 +++++++++++++ .../test_workload_identity_federation_aws.py | 10 +-- .../test_workload_identity_federation_oidc.py | 63 +++++++++++++++++++ 3 files changed, 110 insertions(+), 5 deletions(-) create mode 100644 .github/workflows/run_snowflake_wif_test.yml create mode 100644 dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py diff --git a/.github/workflows/run_snowflake_wif_test.yml b/.github/workflows/run_snowflake_wif_test.yml new file mode 100644 index 000000000..ae607593d --- /dev/null +++ b/.github/workflows/run_snowflake_wif_test.yml @@ -0,0 +1,42 @@ +name: Run Snowflake Workload Identity Federation (WIF) Test +on: + workflow_dispatch: + push: + branches: [ main ] + +permissions: + contents: read + id-token: write + +jobs: + run-snowflake: + runs-on: ubuntu-latest + env: + SNOWFLAKE_TEST_ACCOUNT: TEDYZXG-INFINITYWORKSPARTNER + SNOWFLAKE_TEST_DATABASE: DBT_WORKSHOP + SNOWFLAKE_TEST_WAREHOUSE: TRANSFORMER + SNOWFLAKE_TEST_ROLE: DBT_WORKSHOP_ROLE + SNOWFLAKE_TEST_WIF_USER: GHA_WIF_DBT_USER + + steps: + - uses: actions/checkout@v4 + + - name: Get OIDC token for Snowflake + id: oidc + uses: actions/github-script@v7 + with: + script: | + const token = await core.getIDToken('snowflakecomputing.com'); + core.setOutput('id_token', token); + + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - uses: pypa/hatch@install + + - run: hatch build && hatch run build:check-all + working-directory: ./dbt-snowflake + + - run: hatch run python -m pytest tests/functional/auth_tests/test_workload_identity_federation_oidc.py diff --git a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py index e6d0bd0c6..4ba9fb089 100644 --- a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py +++ b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py @@ -42,12 +42,12 @@ `hatch run pytest tests/functional/auth_tests/test_workload_identity_federation_aws.py::test_snowflake_wif_basic_functionality` 5. **Environment Variables:** Set the following environment variables for testing: - - SNOWFLAKE_TEST_WIF_ACCOUNT: Your Snowflake account identifier + - SNOWFLAKE_TEST_ACCOUNT: Your Snowflake account identifier - SNOWFLAKE_TEST_WIF_USER: The Snowflake service user created for WIF - - SNOWFLAKE_TEST_WIF_DATABASE: Test database name - - SNOWFLAKE_TEST_WIF_WAREHOUSE: Test warehouse name - - SNOWFLAKE_TEST_WIF_ROLE: Snowflake Role for the user (optional) - - SNOWFLAKE_TEST_WIF_SCHEMA: Schema for testing (optional, defaults to schema in profile) + - SNOWFLAKE_TEST_DATABASE: Test database name + - SNOWFLAKE_TEST_WAREHOUSE: Test warehouse name + - SNOWFLAKE_TEST_ROLE: Snowflake Role for the user (optional) + - SNOWFLAKE_TEST_SCHEMA: Schema for testing (optional, defaults to schema in profile) Note: WIF authentication relies on being in the AWS environment, so these tests can't be run locally or in the CI/CD pipeline. """ diff --git a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py new file mode 100644 index 000000000..dd32b905f --- /dev/null +++ b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py @@ -0,0 +1,63 @@ +""" +Functional tests for Snowflake Workload Identity Federation (WIF) with OIDC authentication. +Prerequisites for testing WIF with OIDC: + +1. **Create a Snowflake User with OIDC Auth** + + Create a service user in Snowflake with WIF enabled: + ```sql + CREATE USER + TYPE = SERVICE + WORKLOAD_IDENTITY = ( + TYPE = OIDC, + ISSUER = 'https://token.actions.githubusercontent.com', + SUBJECT = 'repo:/dbt-adapters:ref:refs/heads/main', + OIDC_AUDIENCE_LIST = ('snowflakecomputing.com') + ); + ``` + +2. **Create a GitHub Actions that generates the OIDC token and runs the test ** + + ```yaml + + # TODO + + ``` + + +""" + +import os +from dbt.tests.util import run_dbt +import pytest + + +_MODELS__MODEL_1_SQL = """ +select 1 as id, 'wif_test' as source +""" + + +class TestSnowflakeWorkloadIdentityFederation: + @pytest.fixture(scope="class", autouse=True) + def dbt_profile_target(self): + return { + "type": "snowflake", + "threads": 4, + "account": os.getenv("SNOWFLAKE_TEST_ACCOUNT"), + "user": os.getenv("SNOWFLAKE_TEST_WIF_USER"), + "database": os.getenv("SNOWFLAKE_TEST_DATABASE"), + "warehouse": os.getenv("SNOWFLAKE_TEST_WAREHOUSE"), + "authenticator": "workload_identity", + "workload_identity_provider": "oidc", + "token": os.getenv("ODIC_TOKEN"), + } + + @pytest.fixture(scope="class") + def models(self): + return { + "model_1.sql": _MODELS__MODEL_1_SQL, + } + + def test_snowflake_wif_basic_functionality(self, project): + """Test basic dbt functionality with WIF authentication""" + run_dbt() From 595d91e9bcad9aa79840e630977da4245610615f Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Thu, 4 Sep 2025 16:39:43 +0200 Subject: [PATCH 08/15] change hatch step --- .github/workflows/run_snowflake_wif_test.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/run_snowflake_wif_test.yml b/.github/workflows/run_snowflake_wif_test.yml index ae607593d..39a88a03b 100644 --- a/.github/workflows/run_snowflake_wif_test.yml +++ b/.github/workflows/run_snowflake_wif_test.yml @@ -36,7 +36,7 @@ jobs: - uses: pypa/hatch@install - - run: hatch build && hatch run build:check-all + - run: hatch run setup working-directory: ./dbt-snowflake - run: hatch run python -m pytest tests/functional/auth_tests/test_workload_identity_federation_oidc.py From cd1bf6f4dfcea131a1c61f546addeb1508d3b921 Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Thu, 4 Sep 2025 16:41:56 +0200 Subject: [PATCH 09/15] output token --- .github/workflows/run_snowflake_wif_test.yml | 3 +++ 1 file changed, 3 insertions(+) diff --git a/.github/workflows/run_snowflake_wif_test.yml b/.github/workflows/run_snowflake_wif_test.yml index 39a88a03b..3c8b3ea8e 100644 --- a/.github/workflows/run_snowflake_wif_test.yml +++ b/.github/workflows/run_snowflake_wif_test.yml @@ -40,3 +40,6 @@ jobs: working-directory: ./dbt-snowflake - run: hatch run python -m pytest tests/functional/auth_tests/test_workload_identity_federation_oidc.py + working-directory: ./dbt-snowflake + env: + ODIC_TOKEN: ${{ steps.oidc.outputs.id_token }} From 8d662e067e5d3a6b56538834cc0893fc3a607ad2 Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Thu, 4 Sep 2025 16:43:40 +0200 Subject: [PATCH 10/15] add role to test steps --- .../auth_tests/test_workload_identity_federation_oidc.py | 1 + 1 file changed, 1 insertion(+) diff --git a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py index dd32b905f..67df4c1a1 100644 --- a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py +++ b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py @@ -47,6 +47,7 @@ def dbt_profile_target(self): "user": os.getenv("SNOWFLAKE_TEST_WIF_USER"), "database": os.getenv("SNOWFLAKE_TEST_DATABASE"), "warehouse": os.getenv("SNOWFLAKE_TEST_WAREHOUSE"), + "role": os.getenv("SNOWFLAKE_TEST_ROLE"), "authenticator": "workload_identity", "workload_identity_provider": "oidc", "token": os.getenv("ODIC_TOKEN"), From 5c17e99c59ca6a35718015f6057088ef896e1d80 Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Thu, 4 Sep 2025 16:53:44 +0200 Subject: [PATCH 11/15] add wif oidc test steps --- .github/workflows/run_snowflake_wif_test.yml | 45 ------------------ .../test_workload_identity_federation_oidc.py | 46 ++++++++++++++++++- 2 files changed, 44 insertions(+), 47 deletions(-) delete mode 100644 .github/workflows/run_snowflake_wif_test.yml diff --git a/.github/workflows/run_snowflake_wif_test.yml b/.github/workflows/run_snowflake_wif_test.yml deleted file mode 100644 index 3c8b3ea8e..000000000 --- a/.github/workflows/run_snowflake_wif_test.yml +++ /dev/null @@ -1,45 +0,0 @@ -name: Run Snowflake Workload Identity Federation (WIF) Test -on: - workflow_dispatch: - push: - branches: [ main ] - -permissions: - contents: read - id-token: write - -jobs: - run-snowflake: - runs-on: ubuntu-latest - env: - SNOWFLAKE_TEST_ACCOUNT: TEDYZXG-INFINITYWORKSPARTNER - SNOWFLAKE_TEST_DATABASE: DBT_WORKSHOP - SNOWFLAKE_TEST_WAREHOUSE: TRANSFORMER - SNOWFLAKE_TEST_ROLE: DBT_WORKSHOP_ROLE - SNOWFLAKE_TEST_WIF_USER: GHA_WIF_DBT_USER - - steps: - - uses: actions/checkout@v4 - - - name: Get OIDC token for Snowflake - id: oidc - uses: actions/github-script@v7 - with: - script: | - const token = await core.getIDToken('snowflakecomputing.com'); - core.setOutput('id_token', token); - - - name: Set up Python - uses: actions/setup-python@v5 - with: - python-version: '3.11' - - - uses: pypa/hatch@install - - - run: hatch run setup - working-directory: ./dbt-snowflake - - - run: hatch run python -m pytest tests/functional/auth_tests/test_workload_identity_federation_oidc.py - working-directory: ./dbt-snowflake - env: - ODIC_TOKEN: ${{ steps.oidc.outputs.id_token }} diff --git a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py index 67df4c1a1..903b0f069 100644 --- a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py +++ b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py @@ -20,10 +20,52 @@ ```yaml - # TODO +name: Run Snowflake Workload Identity Federation (WIF) Test +on: + workflow_dispatch: + push: + branches: [ main ] - ``` +permissions: + contents: read + id-token: write + +jobs: + run-snowflake: + runs-on: ubuntu-latest + env: + SNOWFLAKE_TEST_ACCOUNT: + SNOWFLAKE_TEST_DATABASE: + SNOWFLAKE_TEST_WAREHOUSE: + SNOWFLAKE_TEST_ROLE: + SNOWFLAKE_TEST_WIF_USER: + + steps: + - uses: actions/checkout@v4 + + - name: Get OIDC token for Snowflake + id: oidc + uses: actions/github-script@v7 + with: + script: | + const token = await core.getIDToken('snowflakecomputing.com'); + core.setOutput('id_token', token); + - name: Set up Python + uses: actions/setup-python@v5 + with: + python-version: '3.11' + + - uses: pypa/hatch@install + + - run: hatch run setup + working-directory: ./dbt-snowflake + + - run: hatch run python -m pytest tests/functional/auth_tests/test_workload_identity_federation_oidc.py + working-directory: ./dbt-snowflake + env: + ODIC_TOKEN: ${{ steps.oidc.outputs.id_token }} + ``` """ From 126bd8b02fac05189dfde291e0058ca2bd273252 Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Thu, 4 Sep 2025 18:01:13 +0200 Subject: [PATCH 12/15] update profile template --- dbt-snowflake/src/dbt/include/snowflake/profile_template.yml | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/dbt-snowflake/src/dbt/include/snowflake/profile_template.yml b/dbt-snowflake/src/dbt/include/snowflake/profile_template.yml index b437853e7..6596bf42c 100644 --- a/dbt-snowflake/src/dbt/include/snowflake/profile_template.yml +++ b/dbt-snowflake/src/dbt/include/snowflake/profile_template.yml @@ -20,6 +20,10 @@ prompts: authenticator: hint: "'externalbrowser' or a valid Okta URL" default: 'externalbrowser' + workload_identity: + _fixed_authenticator: workload_identity + workload_identity_provider: + hint: Must be one of the following - [OIDC, AWS, AZURE, GCP] role: hint: 'dev role' warehouse: From f35c86bfaa2bd25b50caf66f2febf5055ca5da2f Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Tue, 9 Sep 2025 21:48:00 +0100 Subject: [PATCH 13/15] add better error handing and messaging for WIF --- .../src/dbt/adapters/snowflake/connections.py | 25 +++++++++++- .../test_workload_identity_federation_aws.py | 4 +- .../test_workload_identity_federation_oidc.py | 4 +- dbt-snowflake/tests/unit/test_connections.py | 40 ++++++++++++++++++- 4 files changed, 65 insertions(+), 8 deletions(-) diff --git a/dbt-snowflake/src/dbt/adapters/snowflake/connections.py b/dbt-snowflake/src/dbt/adapters/snowflake/connections.py index 2f3b4a2a2..6bd3a2ece 100644 --- a/dbt-snowflake/src/dbt/adapters/snowflake/connections.py +++ b/dbt-snowflake/src/dbt/adapters/snowflake/connections.py @@ -241,15 +241,36 @@ def auth_args(self): elif self.authenticator.lower() == "workload_identity": result["authenticator"] = WORKLOAD_IDENTITY_AUTHENTICATOR - if not self.workload_identity_provider: + accepted_workload_identity_providers = [ + "OIDC", + "AZURE", + "GCP", + "AWS", + ] + + if ( + not self.workload_identity_provider + or self.workload_identity_provider.upper() + not in accepted_workload_identity_providers + ): + raise DbtConfigError( - "workload_identity_provider must be set if authenticator='workload_identity'!" + "workload_identity_provider must be set to one of the following values if authenticator='workload_identity'!:\n\n" + f"{'\n'.join(accepted_workload_identity_providers)}\n\n" + f"Provided workload_identity_provider was '{self.workload_identity_provider}'" ) + result["workload_identity_provider"] = self.workload_identity_provider if self.token: result["token"] = self.token + if self.workload_identity_entra_resource: + if self.workload_identity_provider.upper() != "AZURE": + raise DbtConfigError( + "workload_identity_entra_resource can only be set if workload_identity_provider is Azure" + ) + result["workload_identity_entra_resource"] = ( self.workload_identity_entra_resource ) diff --git a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py index 4ba9fb089..f972ac494 100644 --- a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py +++ b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_aws.py @@ -43,7 +43,7 @@ 5. **Environment Variables:** Set the following environment variables for testing: - SNOWFLAKE_TEST_ACCOUNT: Your Snowflake account identifier - - SNOWFLAKE_TEST_WIF_USER: The Snowflake service user created for WIF + - SNOWFLAKE_TEST_USER: The Snowflake service user created for WIF - SNOWFLAKE_TEST_DATABASE: Test database name - SNOWFLAKE_TEST_WAREHOUSE: Test warehouse name - SNOWFLAKE_TEST_ROLE: Snowflake Role for the user (optional) @@ -68,7 +68,7 @@ def dbt_profile_target(self): "type": "snowflake", "threads": 4, "account": os.getenv("SNOWFLAKE_TEST_ACCOUNT"), - "user": os.getenv("SNOWFLAKE_TEST_WIF_USER"), + "user": os.getenv("SNOWFLAKE_TEST_USER"), "database": os.getenv("SNOWFLAKE_TEST_DATABASE"), "warehouse": os.getenv("SNOWFLAKE_TEST_WAREHOUSE"), "role": os.getenv("SNOWFLAKE_TEST_ROLE"), diff --git a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py index 903b0f069..85da7c1d9 100644 --- a/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py +++ b/dbt-snowflake/tests/functional/auth_tests/test_workload_identity_federation_oidc.py @@ -38,7 +38,7 @@ SNOWFLAKE_TEST_DATABASE: SNOWFLAKE_TEST_WAREHOUSE: SNOWFLAKE_TEST_ROLE: - SNOWFLAKE_TEST_WIF_USER: + SNOWFLAKE_TEST_USER: steps: - uses: actions/checkout@v4 @@ -86,7 +86,7 @@ def dbt_profile_target(self): "type": "snowflake", "threads": 4, "account": os.getenv("SNOWFLAKE_TEST_ACCOUNT"), - "user": os.getenv("SNOWFLAKE_TEST_WIF_USER"), + "user": os.getenv("SNOWFLAKE_TEST_USER"), "database": os.getenv("SNOWFLAKE_TEST_DATABASE"), "warehouse": os.getenv("SNOWFLAKE_TEST_WAREHOUSE"), "role": os.getenv("SNOWFLAKE_TEST_ROLE"), diff --git a/dbt-snowflake/tests/unit/test_connections.py b/dbt-snowflake/tests/unit/test_connections.py index 53dabb9e5..0998e0fcf 100644 --- a/dbt-snowflake/tests/unit/test_connections.py +++ b/dbt-snowflake/tests/unit/test_connections.py @@ -99,6 +99,42 @@ def test_connnections_credentials_wif_authenticator_fails_without_provider(): } with pytest.raises(DbtConfigError) as excinfo: connections.SnowflakeCredentials(**credentials).auth_args() - assert "workload_identity_provider must be set if authenticator='workload_identity'" in str( - excinfo + assert ( + "workload_identity_provider must be set to one of the following values if authenticator='workload_identity'!" + in str(excinfo) + ) + + +def test_connnections_credentials_wif_authenticator_fails_with_invalid_provider(): + credentials = { + "account": "account_id_with_underscores", + "database": "database", + "warehouse": "warehouse", + "schema": "schema", + "authenticator": "workload_identity", + "workload_identity_provider": "some_non_existent_cloud_provider", + } + with pytest.raises(DbtConfigError) as excinfo: + connections.SnowflakeCredentials(**credentials).auth_args() + assert ( + "workload_identity_provider must be set to one of the following values if authenticator='workload_identity'!" + in str(excinfo) + ) + + +def test_connnections_credentials_wif_authenticator_fails_with_entra_resource_and_non_azure_provider(): + credentials = { + "account": "account_id_with_underscores", + "database": "database", + "warehouse": "warehouse", + "schema": "schema", + "authenticator": "workload_identity", + "workload_identity_provider": "aws", + "workload_identity_entra_resource": "app://123", + } + with pytest.raises(DbtConfigError) as excinfo: + connections.SnowflakeCredentials(**credentials).auth_args() + assert ( + "workload_identity_entra_resource can only be set if workload_identity_provider is Azure" + in str(excinfo) ) From c232493d8c7a6e7c5bf9772da72d376f19d4c7e6 Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Tue, 9 Sep 2025 22:07:21 +0100 Subject: [PATCH 14/15] fix fstring linting issue --- dbt-snowflake/src/dbt/adapters/snowflake/connections.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/dbt-snowflake/src/dbt/adapters/snowflake/connections.py b/dbt-snowflake/src/dbt/adapters/snowflake/connections.py index 6bd3a2ece..8291b30d2 100644 --- a/dbt-snowflake/src/dbt/adapters/snowflake/connections.py +++ b/dbt-snowflake/src/dbt/adapters/snowflake/connections.py @@ -255,8 +255,8 @@ def auth_args(self): ): raise DbtConfigError( - "workload_identity_provider must be set to one of the following values if authenticator='workload_identity'!:\n\n" - f"{'\n'.join(accepted_workload_identity_providers)}\n\n" + "workload_identity_provider must be set to one of the following values if authenticator='workload_identity'!:\n" + f"{', '.join(accepted_workload_identity_providers)}\n\n" f"Provided workload_identity_provider was '{self.workload_identity_provider}'" ) From d0da874b7c9f8e1bf3a9982d3275a9e82793d1db Mon Sep 17 00:00:00 2001 From: Rory Donaldson Date: Tue, 9 Sep 2025 22:10:36 +0100 Subject: [PATCH 15/15] add changie entry --- .../.changes/unreleased/Features-20250909-221007.yaml | 6 ++++++ 1 file changed, 6 insertions(+) create mode 100644 dbt-snowflake/.changes/unreleased/Features-20250909-221007.yaml diff --git a/dbt-snowflake/.changes/unreleased/Features-20250909-221007.yaml b/dbt-snowflake/.changes/unreleased/Features-20250909-221007.yaml new file mode 100644 index 000000000..c0b67949a --- /dev/null +++ b/dbt-snowflake/.changes/unreleased/Features-20250909-221007.yaml @@ -0,0 +1,6 @@ +kind: Features +body: Introduction of Workload Identity Federation as a supported method of authentication to Snowflake +time: 2025-09-09T22:10:07.504016+01:00 +custom: + Author: roryjbd,sfc-gh-pmansour + Issue: "1234"