mirror of
https://github.com/BerriAI/litellm.git
synced 2026-09-11 22:51:28 +00:00
fix(anthropic_wif): let deployment token refs beat the env identity source kind and surface signing ImportError detail
This commit is contained in:
parent
f8b31844bb
commit
ff59884bc1
4 changed files with 81 additions and 5 deletions
|
|
@ -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")
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
|
|||
|
|
@ -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(
|
||||
|
|
|
|||
|
|
@ -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")],
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue