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):