This is an automated email from the ASF dual-hosted git repository.

shahar1 pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/airflow.git


The following commit(s) were added to refs/heads/main by this push:
     new 24ff1f58d18 Report missing Anthropic WIF fields clearly (#71359)
24ff1f58d18 is described below

commit 24ff1f58d189c7be28fc0497fd79a0cdeb1b8281
Author: yuyua9 <[email protected]>
AuthorDate: Sat Sep 19 00:19:33 2026 +0800

    Report missing Anthropic WIF fields clearly (#71359)
---
 .../airflow/providers/anthropic/hooks/anthropic.py | 16 +++++-
 .../tests/unit/anthropic/hooks/test_anthropic.py   | 63 ++++++++++++++++++++++
 2 files changed, 77 insertions(+), 2 deletions(-)

diff --git 
a/providers/anthropic/src/airflow/providers/anthropic/hooks/anthropic.py 
b/providers/anthropic/src/airflow/providers/anthropic/hooks/anthropic.py
index 023729f2636..a6d5345e930 100644
--- a/providers/anthropic/src/airflow/providers/anthropic/hooks/anthropic.py
+++ b/providers/anthropic/src/airflow/providers/anthropic/hooks/anthropic.py
@@ -304,7 +304,7 @@ class AnthropicHook(BaseHook):
     - ``workload_identity``: configure `Workload Identity Federation
       
<https://platform.claude.com/docs/en/manage-claude/workload-identity-federation>`__
       (keyless OIDC auth) with ``identity_token_file``, ``federation_rule_id``,
-      ``organization_id``, ``service_account_id`` and optional 
``workspace_id`` / ``scope``.
+      ``organization_id`` and optional ``service_account_id``, 
``workspace_id`` / ``scope``.
 
     When the ``anthropic`` platform has no API Key and no 
``workload_identity`` block, the
     client is built with no static credential so the SDK resolves them from 
the environment
@@ -408,12 +408,24 @@ class AnthropicHook(BaseHook):
         Anthropic access token. See
         
https://platform.claude.com/docs/en/manage-claude/workload-identity-federation.
         """
+        required_fields = (
+            "identity_token_file",
+            "federation_rule_id",
+            "organization_id",
+        )
+        missing_fields = [field for field in required_fields if not 
wif.get(field)]
+        if missing_fields:
+            raise AnthropicError(
+                "The workload_identity configuration is missing or has empty 
required fields: "
+                f"{', '.join(missing_fields)}."
+            )
         kwargs: dict[str, Any] = {
             "identity_token_provider": 
IdentityTokenFile(wif["identity_token_file"]),
             "federation_rule_id": wif["federation_rule_id"],
             "organization_id": wif["organization_id"],
-            "service_account_id": wif["service_account_id"],
         }
+        if wif.get("service_account_id"):
+            kwargs["service_account_id"] = wif["service_account_id"]
         if wif.get("workspace_id"):
             kwargs["workspace_id"] = wif["workspace_id"]
         if wif.get("scope"):
diff --git a/providers/anthropic/tests/unit/anthropic/hooks/test_anthropic.py 
b/providers/anthropic/tests/unit/anthropic/hooks/test_anthropic.py
index 3ba0842d7e6..84b4e37a220 100644
--- a/providers/anthropic/tests/unit/anthropic/hooks/test_anthropic.py
+++ b/providers/anthropic/tests/unit/anthropic/hooks/test_anthropic.py
@@ -762,6 +762,69 @@ class TestAnthropicHookGetConn:
         )
         
mock_anthropic.assert_called_once_with(credentials=mock_wic.return_value, 
base_url=None)
 
+    @pytest.mark.parametrize("service_account_id", [None, ""])
+    @mock.patch(f"{HOOK_PATH}.Anthropic")
+    @mock.patch(f"{HOOK_PATH}.IdentityTokenFile")
+    @mock.patch(f"{HOOK_PATH}.WorkloadIdentityCredentials")
+    @mock.patch.object(AnthropicHook, "get_connection")
+    def test_workload_identity_federation_service_account_is_optional(
+        self, mock_get_connection, mock_wic, mock_itf, mock_anthropic, 
service_account_id
+    ):
+        workload_identity = {
+            "identity_token_file": "/var/run/secrets/anthropic.com/token",
+            "federation_rule_id": "fdrl_x",
+            "organization_id": "org_x",
+        }
+        if service_account_id is not None:
+            workload_identity["service_account_id"] = service_account_id
+        mock_get_connection.return_value = _conn(
+            password=None,
+            extra={"workload_identity": workload_identity},
+        )
+
+        AnthropicHook().get_conn()
+
+        mock_wic.assert_called_once_with(
+            identity_token_provider=mock_itf.return_value,
+            federation_rule_id="fdrl_x",
+            organization_id="org_x",
+        )
+        
mock_anthropic.assert_called_once_with(credentials=mock_wic.return_value, 
base_url=None)
+
+    @pytest.mark.parametrize(
+        ("missing_field", "missing_value"),
+        [
+            pytest.param(field, value, id=f"{field}-{'absent' if value is None 
else 'empty'}")
+            for field in (
+                "identity_token_file",
+                "federation_rule_id",
+                "organization_id",
+            )
+            for value in (None, "")
+        ],
+    )
+    @mock.patch.object(AnthropicHook, "get_connection")
+    def test_workload_identity_federation_requires_fields(
+        self, mock_get_connection, missing_field, missing_value
+    ):
+        workload_identity = {
+            "identity_token_file": "/var/run/secrets/anthropic.com/token",
+            "federation_rule_id": "fdrl_x",
+            "organization_id": "org_x",
+            "service_account_id": "svac_x",
+        }
+        if missing_value is None:
+            workload_identity.pop(missing_field)
+        else:
+            workload_identity[missing_field] = missing_value
+        mock_get_connection.return_value = _conn(
+            password=None,
+            extra={"workload_identity": workload_identity},
+        )
+
+        with pytest.raises(AnthropicError, match=missing_field):
+            AnthropicHook().get_conn()
+
     @mock.patch(f"{HOOK_PATH}.Anthropic")
     @mock.patch.object(AnthropicHook, "get_connection")
     def test_no_key_lets_sdk_resolve_credentials(self, mock_get_connection, 
mock_anthropic):

Reply via email to