mirror of
https://github.com/evilsocket/audit
synced 2026-06-06 15:44:27 +00:00
265 lines
11 KiB
Python
265 lines
11 KiB
Python
"""Auth setup tests — env scrubbing + the auth modes.
|
|
|
|
Modes: gateway, api_key (opt-in), oauth_token, keychain_login,
|
|
macos_keychain_login.
|
|
|
|
The api_key mode requires the caller to pass `allow_api_key=True` to
|
|
configure_auth(). Without it, ANTHROPIC_API_KEY is scrubbed in favor of
|
|
subscription auth, matching the original "subscription only" behavior.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
import shutil
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from audit import auth as auth_mod
|
|
from audit.auth import AuthError, configure_auth
|
|
|
|
|
|
def _empty_env(tmp_path: Path) -> Path:
|
|
p = tmp_path / ".env"
|
|
p.write_text("")
|
|
return p
|
|
|
|
|
|
def _require_claude_cli() -> None:
|
|
if shutil.which("claude") is None:
|
|
pytest.skip("claude CLI not installed")
|
|
|
|
|
|
def _clear_all_auth_env(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Wipe every env var that influences auth-mode selection."""
|
|
for var in (
|
|
"CLAUDE_CODE_OAUTH_TOKEN",
|
|
"ANTHROPIC_API_KEY",
|
|
"ANTHROPIC_BASE_URL",
|
|
"ANTHROPIC_AUTH_TOKEN",
|
|
):
|
|
monkeypatch.delenv(var, raising=False)
|
|
|
|
def _force_non_macos(monkeypatch: pytest.MonkeyPatch) -> None:
|
|
"""Force tests that expect no implicit local login fallback to run as Linux."""
|
|
monkeypatch.setattr(auth_mod.platform, "system", lambda: "Linux")
|
|
|
|
# ---------- absence ----------
|
|
|
|
|
|
def test_missing_everything_raises(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
_clear_all_auth_env(monkeypatch)
|
|
_force_non_macos(monkeypatch)
|
|
monkeypatch.setattr(auth_mod, "CREDENTIALS_PATH", tmp_path / "no_creds.json")
|
|
with pytest.raises(AuthError, match="No auth available"):
|
|
configure_auth(env_file=_empty_env(tmp_path))
|
|
|
|
|
|
def test_missing_claude_cli(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "fake-test-token")
|
|
monkeypatch.setenv("PATH", "/nonexistent")
|
|
with pytest.raises(AuthError, match="claude.*CLI"):
|
|
configure_auth(env_file=_empty_env(tmp_path))
|
|
|
|
|
|
# ---------- default behavior (allow_api_key=False, preserves upstream) ----------
|
|
|
|
|
|
def test_default_scrubs_api_key_with_oauth(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""Default behavior: ANTHROPIC_API_KEY is scrubbed even when OAuth is
|
|
present. Subscription auth wins. Matches upstream evilsocket/audit."""
|
|
_clear_all_auth_env(monkeypatch)
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "fake-test-token")
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-should-be-deleted")
|
|
monkeypatch.setattr(auth_mod, "CREDENTIALS_PATH", tmp_path / "no_creds.json")
|
|
_require_claude_cli()
|
|
status = configure_auth(env_file=_empty_env(tmp_path))
|
|
assert status.auth_mode == "oauth_token"
|
|
assert status.api_key_scrubbed is True
|
|
assert "ANTHROPIC_API_KEY" not in os.environ
|
|
|
|
|
|
def test_default_scrubs_api_key_even_alone(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""Default behavior: ANTHROPIC_API_KEY alone (no other auth, no opt-in)
|
|
is scrubbed and yields AuthError with a hint about --allow-api-key."""
|
|
_clear_all_auth_env(monkeypatch)
|
|
_force_non_macos(monkeypatch)
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-should-be-deleted")
|
|
monkeypatch.setattr(auth_mod, "CREDENTIALS_PATH", tmp_path / "no_creds.json")
|
|
_require_claude_cli()
|
|
with pytest.raises(AuthError, match="--allow-api-key"):
|
|
configure_auth(env_file=_empty_env(tmp_path))
|
|
# And the key was scrubbed before the raise.
|
|
assert "ANTHROPIC_API_KEY" not in os.environ
|
|
|
|
|
|
def test_oauth_token_mode(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
"""OAuth token alone selects oauth_token mode."""
|
|
_clear_all_auth_env(monkeypatch)
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "fake-test-token")
|
|
monkeypatch.setattr(auth_mod, "CREDENTIALS_PATH", tmp_path / "no_creds.json")
|
|
_require_claude_cli()
|
|
status = configure_auth(env_file=_empty_env(tmp_path))
|
|
assert status.auth_mode == "oauth_token"
|
|
assert status.api_key_scrubbed is False
|
|
|
|
|
|
def test_keychain_mode(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
_clear_all_auth_env(monkeypatch)
|
|
creds = tmp_path / "creds.json"
|
|
creds.write_text("{}")
|
|
monkeypatch.setattr(auth_mod, "CREDENTIALS_PATH", creds)
|
|
_require_claude_cli()
|
|
status = configure_auth(env_file=_empty_env(tmp_path))
|
|
assert status.auth_mode == "keychain_login"
|
|
assert status.credentials_file == creds
|
|
|
|
def test_macos_keychain_login_mode(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""On macOS, Claude Code /login credentials live in the macOS Keychain,
|
|
not in ~/.claude/.credentials.json. Allow this path through preflight so
|
|
the Claude Agent SDK can use the active first-party login."""
|
|
_clear_all_auth_env(monkeypatch)
|
|
monkeypatch.setattr(auth_mod, "CREDENTIALS_PATH", tmp_path / "no_creds.json")
|
|
monkeypatch.setattr(auth_mod.platform, "system", lambda: "Darwin")
|
|
_require_claude_cli()
|
|
status = configure_auth(env_file=_empty_env(tmp_path))
|
|
assert status.auth_mode == "macos_keychain_login"
|
|
assert status.credentials_file is None
|
|
|
|
|
|
# ---------- opt-in api_key mode ----------
|
|
|
|
|
|
def test_api_key_mode_opt_in(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
"""ANTHROPIC_API_KEY with allow_api_key=True selects api_key mode
|
|
and leaves the key in the env so the SDK can use it."""
|
|
_clear_all_auth_env(monkeypatch)
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-api03-fake")
|
|
monkeypatch.setattr(auth_mod, "CREDENTIALS_PATH", tmp_path / "no_creds.json")
|
|
_require_claude_cli()
|
|
status = configure_auth(env_file=_empty_env(tmp_path), allow_api_key=True)
|
|
assert status.auth_mode == "api_key"
|
|
assert status.api_key_scrubbed is False
|
|
# CRITICAL: the key MUST still be in the env so the SDK can use it
|
|
assert os.environ.get("ANTHROPIC_API_KEY") == "sk-ant-api03-fake"
|
|
|
|
|
|
def test_api_key_outranks_oauth_when_opted_in(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""With allow_api_key=True, ANTHROPIC_API_KEY wins over
|
|
CLAUDE_CODE_OAUTH_TOKEN. Matches SDK precedence (rung 3 > rung 5)."""
|
|
_clear_all_auth_env(monkeypatch)
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-api03-fake")
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "fake-oauth-token")
|
|
monkeypatch.setattr(auth_mod, "CREDENTIALS_PATH", tmp_path / "no_creds.json")
|
|
_require_claude_cli()
|
|
status = configure_auth(env_file=_empty_env(tmp_path), allow_api_key=True)
|
|
assert status.auth_mode == "api_key"
|
|
assert os.environ.get("ANTHROPIC_API_KEY") == "sk-ant-api03-fake"
|
|
|
|
|
|
def test_api_key_scrubs_stale_auth_token(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""In api_key mode, a stale ANTHROPIC_AUTH_TOKEN must be scrubbed
|
|
so it can't outrank the API key (rung 2 > rung 3)."""
|
|
_clear_all_auth_env(monkeypatch)
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-api03-fake")
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", "stale-token-must-go")
|
|
monkeypatch.setattr(auth_mod, "CREDENTIALS_PATH", tmp_path / "no_creds.json")
|
|
_require_claude_cli()
|
|
status = configure_auth(env_file=_empty_env(tmp_path), allow_api_key=True)
|
|
assert status.auth_mode == "api_key"
|
|
assert status.auth_token_scrubbed is True
|
|
assert "ANTHROPIC_AUTH_TOKEN" not in os.environ
|
|
assert os.environ.get("ANTHROPIC_API_KEY") == "sk-ant-api03-fake"
|
|
|
|
|
|
def test_allow_api_key_with_no_key_falls_back_to_oauth(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""Passing allow_api_key=True without setting ANTHROPIC_API_KEY is
|
|
a no-op — falls through to subscription auth normally."""
|
|
_clear_all_auth_env(monkeypatch)
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "fake-test-token")
|
|
monkeypatch.setattr(auth_mod, "CREDENTIALS_PATH", tmp_path / "no_creds.json")
|
|
_require_claude_cli()
|
|
status = configure_auth(env_file=_empty_env(tmp_path), allow_api_key=True)
|
|
assert status.auth_mode == "oauth_token"
|
|
|
|
|
|
# ---------- gateway mode ----------
|
|
|
|
|
|
def test_gateway_mode_openrouter(monkeypatch: pytest.MonkeyPatch, tmp_path: Path) -> None:
|
|
"""When ANTHROPIC_BASE_URL points at a non-anthropic host AND
|
|
ANTHROPIC_AUTH_TOKEN is set, leave the gateway env intact and
|
|
don't scrub the token."""
|
|
_clear_all_auth_env(monkeypatch)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", "https://openrouter.ai/api")
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", "or-sk-xxx")
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "should-be-deleted")
|
|
_require_claude_cli()
|
|
status = configure_auth(env_file=_empty_env(tmp_path))
|
|
assert status.auth_mode == "gateway"
|
|
assert status.gateway_base_url == "https://openrouter.ai/api"
|
|
assert status.api_key_scrubbed is True
|
|
assert status.auth_token_scrubbed is False
|
|
# CRITICAL: the gateway token MUST still be in the env so the SDK can use it
|
|
assert os.environ.get("ANTHROPIC_AUTH_TOKEN") == "or-sk-xxx"
|
|
assert os.environ.get("ANTHROPIC_BASE_URL") == "https://openrouter.ai/api"
|
|
assert "ANTHROPIC_API_KEY" not in os.environ
|
|
|
|
|
|
def test_gateway_beats_api_key_even_when_opted_in(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""Gateway mode outranks api_key mode even with allow_api_key=True.
|
|
Mirrors SDK precedence (ANTHROPIC_AUTH_TOKEN at rung 2 > API key at 3)."""
|
|
_clear_all_auth_env(monkeypatch)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", "https://openrouter.ai/api")
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", "or-sk-xxx")
|
|
monkeypatch.setenv("ANTHROPIC_API_KEY", "sk-ant-api03-fake")
|
|
_require_claude_cli()
|
|
status = configure_auth(env_file=_empty_env(tmp_path), allow_api_key=True)
|
|
assert status.auth_mode == "gateway"
|
|
assert status.api_key_scrubbed is True
|
|
assert "ANTHROPIC_API_KEY" not in os.environ
|
|
|
|
|
|
def test_gateway_mode_requires_both_url_and_token(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""A base URL without a token doesn't trigger gateway mode."""
|
|
_clear_all_auth_env(monkeypatch)
|
|
_force_non_macos(monkeypatch)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", "https://openrouter.ai/api")
|
|
monkeypatch.setattr(auth_mod, "CREDENTIALS_PATH", tmp_path / "no_creds.json")
|
|
_require_claude_cli()
|
|
with pytest.raises(AuthError):
|
|
configure_auth(env_file=_empty_env(tmp_path))
|
|
|
|
|
|
def test_anthropic_base_url_does_not_trigger_gateway(
|
|
monkeypatch: pytest.MonkeyPatch, tmp_path: Path
|
|
) -> None:
|
|
"""A base URL pointing AT anthropic.com is normal — not gateway mode.
|
|
Subscription scrubbing should still happen for the auth token."""
|
|
_clear_all_auth_env(monkeypatch)
|
|
monkeypatch.setenv("ANTHROPIC_BASE_URL", "https://api.anthropic.com")
|
|
monkeypatch.setenv("ANTHROPIC_AUTH_TOKEN", "should-be-scrubbed")
|
|
monkeypatch.setenv("CLAUDE_CODE_OAUTH_TOKEN", "fake-token")
|
|
_require_claude_cli()
|
|
status = configure_auth(env_file=_empty_env(tmp_path))
|
|
assert status.auth_mode == "oauth_token"
|
|
assert status.auth_token_scrubbed is True
|
|
assert "ANTHROPIC_AUTH_TOKEN" not in os.environ
|