fix(anthropic_wif): let deployment token refs beat the env identity source kind and surface signing ImportError detail

This commit is contained in:
mateo-berri 2026-08-29 16:17:01 -07:00
parent f8b31844bb
commit ff59884bc1
4 changed files with 81 additions and 5 deletions

View file

@ -54,6 +54,8 @@ _DEFAULT_TRUSTED_EXCHANGE_HOST: Final = "api.anthropic.com"
_REJECTED_REF_PREFIX: Final = "oidc/env_path/"
_IDENTITY_SOURCE_PARAM: Final = "anthropic_identity_source"
_IDENTITY_SOURCE_ENV: Final = "ANTHROPIC_IDENTITY_SOURCE"
_IDENTITY_TOKEN_FILE_PARAM: Final = "anthropic_identity_token_file"
_IDENTITY_TOKEN_PARAM: Final = "anthropic_identity_token"
# litellm_params key -> InternalIssuerSource/KeycloakSource field name. Every key here must
# also be listed in ANTHROPIC_WIF_KWARGS_KEYS (get_litellm_params.py), which is what makes it
@ -136,8 +138,10 @@ def _resolve_identity_source(
frozen config, hashes it into the ``oidc/<kind>/<hash>`` cache-key ref (``identity_source_ref``),
and closes the source's fetch/mint function over it. An unset-but-invalid config (unknown
kind, a missing required field, or a field from the other variant) fails closed here rather
than silently falling back to token_file."""
source_kind: Final = _config_value(litellm_params, _IDENTITY_SOURCE_PARAM, _IDENTITY_SOURCE_ENV)
than silently falling back to token_file. A deployment whose params carry a legacy token or
token_file ref stays on legacy resolution even when ``ANTHROPIC_IDENTITY_SOURCE`` names a
fleet-wide kind: the env kind only governs deployments that set no identity params of their own."""
source_kind: Final = _resolve_source_kind(litellm_params)
if source_kind is None:
legacy_ref: Final = _resolve_assertion_ref(litellm_params)
return (legacy_ref, None) if legacy_ref is not None else None
@ -166,6 +170,16 @@ def _resolve_identity_source(
)
def _resolve_source_kind(litellm_params: Mapping[str, object] | None) -> str | None:
param_kind: Final = _param_str(litellm_params, _IDENTITY_SOURCE_PARAM)
if param_kind is not None:
return param_kind
has_param_legacy_ref: Final = any(
_param_str(litellm_params, key) is not None for key in (_IDENTITY_TOKEN_FILE_PARAM, _IDENTITY_TOKEN_PARAM)
)
return None if has_param_legacy_ref else _env_str(_IDENTITY_SOURCE_ENV)
def _reject_foreign_variant_fields(
litellm_params: Mapping[str, object], foreign_field_map: Mapping[str, str], chosen_kind: str
) -> None:
@ -360,10 +374,10 @@ def _env_str(name: str) -> str | None:
def _resolve_assertion_ref(litellm_params: Mapping[str, object] | None) -> str | None:
file_param: Final = _param_str(litellm_params, "anthropic_identity_token_file")
file_param: Final = _param_str(litellm_params, _IDENTITY_TOKEN_FILE_PARAM)
if file_param is not None:
return f"oidc/file/{file_param}"
inline_param: Final = _param_str(litellm_params, "anthropic_identity_token")
inline_param: Final = _param_str(litellm_params, _IDENTITY_TOKEN_PARAM)
if inline_param is not None:
return _validated_inline_ref(inline_param)
file_env: Final = _env_str("ANTHROPIC_IDENTITY_TOKEN_FILE")

View file

@ -305,7 +305,7 @@ def _read_assertion(fetch: AssertionSource, ref: str) -> SecretStr | AssertionSo
raw: Final = fetch()
except OidcPathNotAllowedError:
return AssertionSourceError(kind="disallowed_path", source_ref=ref)
except ValueError as e:
except (ValueError, ImportError) as e:
return AssertionSourceError(kind="unreadable", source_ref=ref, detail=str(e)[:_REDACTION_CAP])
except Exception: # noqa: BLE001 # injected readers (secret managers) raise arbitrarily; all failures become values
return AssertionSourceError(kind="unreadable", source_ref=ref)

View file

@ -436,6 +436,55 @@ class TestResolutionMatrix:
assert params is not None
assert params.assertion_ref == "oidc/file//var/run/secrets/env-tok"
def test_param_token_ref_beats_env_identity_source(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("ANTHROPIC_IDENTITY_SOURCE", "internal_issuer")
params = resolve_anthropic_wif_params(
{
"anthropic_federation_rule_id": "fdrl_1",
"anthropic_organization_id": "org-1",
"anthropic_identity_token_file": "/var/run/secrets/dep-tok",
}
)
assert params is not None
assert params.assertion_ref == "oidc/file//var/run/secrets/dep-tok"
assert params.assertion_source is None
def test_param_inline_token_beats_env_identity_source(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("ANTHROPIC_IDENTITY_SOURCE", "internal_issuer")
params = resolve_anthropic_wif_params(
{
"anthropic_federation_rule_id": "fdrl_1",
"anthropic_organization_id": "org-1",
"anthropic_identity_token": "oidc/env/OTHER",
}
)
assert params is not None
assert params.assertion_ref == "oidc/env/OTHER"
assert params.assertion_source is None
def test_env_identity_source_beats_env_token_refs(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("ANTHROPIC_IDENTITY_SOURCE", "internal_issuer")
monkeypatch.setenv("ANTHROPIC_IDENTITY_TOKEN_FILE", "/var/run/secrets/env-tok")
with pytest.raises(litellm.AuthenticationError):
resolve_anthropic_wif_params(
{"anthropic_federation_rule_id": "fdrl_1", "anthropic_organization_id": "org-1"}
)
def test_env_identity_source_dispatches_param_issuer_fields(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("ANTHROPIC_IDENTITY_SOURCE", "internal_issuer")
params = resolve_anthropic_wif_params(
{
"anthropic_federation_rule_id": "fdrl_1",
"anthropic_organization_id": "org-1",
"anthropic_issuer_url": "https://issuer.internal.example",
"anthropic_issuer_subject": "workload-a",
"anthropic_issuer_signing_key_ref": ISSUER_SIGNING_KEY_REF,
}
)
assert params is not None
assert params.assertion_ref.startswith("oidc/internal_issuer/")
assert params.assertion_source is not None
def test_empty_workspace_env_coerced_to_none(self, monkeypatch: pytest.MonkeyPatch):
monkeypatch.setenv("ANTHROPIC_WORKSPACE_ID", "")
params = resolve_anthropic_wif_params(

View file

@ -840,6 +840,7 @@ class TestAssertionGuards:
[
(OidcPathNotAllowedError("path outside allowed credential directories"), "disallowed_path"),
(ValueError("Environment variable ANTHROPIC_IDENTITY_TOKEN not found"), "unreadable"),
(ImportError("needs PyJWT and cryptography: pip install 'litellm[proxy]'"), "unreadable"),
(OSError("permission denied"), "unreadable"),
],
)
@ -866,6 +867,18 @@ class TestAssertionGuards:
assert isinstance(result, AssertionSourceError)
assert result.detail == "Keycloak token endpoint returned invalid_client"
def test_import_error_message_is_captured_as_detail(self):
poster = ScriptedPoster([token_response()])
def reader(ref: str) -> str | None:
raise ImportError("the internal_issuer identity source needs PyJWT and cryptography: pip install 'litellm[proxy]'")
result = make_engine(poster, reader=reader).get_token(make_spec())
assert isinstance(result, AssertionSourceError)
assert result.detail is not None
assert "litellm[proxy]" in result.detail
@pytest.mark.parametrize(
"raised",
[OidcPathNotAllowedError("path outside allowed credential directories"), OSError("permission denied")],