Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,237 @@
|
||||
"""Contract tests for the generic non-interactive (bearer-token) auth seam.
|
||||
|
||||
Covers Task 2.0a: the reusable token-auth capability in the dashboard auth
|
||||
framework — NOT the drain plugin (that's 2.0b/2.1). Asserts the ABC capability
|
||||
flag, the registry filter, bearer extraction, provider stacking (verify_token),
|
||||
and the route-agnostic middleware seam's fail-closed / 503 / pass-through
|
||||
behaviour.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import asyncio
|
||||
from typing import Optional
|
||||
|
||||
import pytest
|
||||
|
||||
from hermes_cli.dashboard_auth import (
|
||||
DashboardAuthProvider,
|
||||
LoginStart,
|
||||
Session,
|
||||
TokenPrincipal,
|
||||
clear_providers,
|
||||
list_providers,
|
||||
list_session_providers,
|
||||
list_token_providers,
|
||||
register_provider,
|
||||
)
|
||||
from hermes_cli.dashboard_auth.base import ProviderError
|
||||
from hermes_cli.dashboard_auth import token_auth
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Test doubles
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
class _OAuthOnly(DashboardAuthProvider):
|
||||
"""A pure interactive provider — never token-authable."""
|
||||
|
||||
name = "oauth-only"
|
||||
display_name = "OAuth Only"
|
||||
|
||||
def start_login(self, *, redirect_uri):
|
||||
return LoginStart(redirect_url="x", cookie_payload={})
|
||||
|
||||
def complete_login(self, *, code, state, code_verifier, redirect_uri):
|
||||
return Session("u", "e", "n", "o", self.name, 0, "a", "r")
|
||||
|
||||
def verify_session(self, *, access_token):
|
||||
return None
|
||||
|
||||
def refresh_session(self, *, refresh_token):
|
||||
return Session("u", "e", "n", "o", self.name, 0, "a", "r")
|
||||
|
||||
def revoke_session(self, *, refresh_token):
|
||||
return None
|
||||
|
||||
|
||||
class _TokenProvider(_OAuthOnly):
|
||||
"""A token provider that accepts exactly one secret."""
|
||||
|
||||
name = "tok"
|
||||
display_name = "Token Provider"
|
||||
supports_token = True
|
||||
|
||||
def __init__(self, *, secret: str = "good-secret", scopes=("drain",)):
|
||||
self._secret = secret
|
||||
self._scopes = tuple(scopes)
|
||||
|
||||
def verify_token(self, *, token: str) -> Optional[TokenPrincipal]:
|
||||
if token == self._secret:
|
||||
return TokenPrincipal(
|
||||
principal=self.name, provider=self.name, scopes=self._scopes
|
||||
)
|
||||
return None
|
||||
|
||||
|
||||
class _UnreachableTokenProvider(_OAuthOnly):
|
||||
name = "tok-down"
|
||||
display_name = "Unreachable Token Provider"
|
||||
supports_token = True
|
||||
|
||||
def verify_token(self, *, token: str) -> Optional[TokenPrincipal]:
|
||||
raise ProviderError("backing store down")
|
||||
|
||||
|
||||
class _BuggyTokenProvider(_OAuthOnly):
|
||||
name = "tok-buggy"
|
||||
display_name = "Buggy Token Provider"
|
||||
supports_token = True
|
||||
|
||||
def verify_token(self, *, token: str) -> Optional[TokenPrincipal]:
|
||||
raise RuntimeError("kaboom")
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Fixtures
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
@pytest.fixture(autouse=True)
|
||||
def _isolated_state():
|
||||
clear_providers()
|
||||
token_auth.clear_token_routes()
|
||||
yield
|
||||
clear_providers()
|
||||
token_auth.clear_token_routes()
|
||||
|
||||
|
||||
class _FakeURL:
|
||||
def __init__(self, path):
|
||||
self.path = path
|
||||
|
||||
|
||||
class _FakeClient:
|
||||
host = "1.2.3.4"
|
||||
|
||||
|
||||
class _FakeRequest:
|
||||
"""Minimal Request stand-in for the seam (no real Starlette needed)."""
|
||||
|
||||
def __init__(self, path="/api/gateway/drain", headers=None):
|
||||
self.url = _FakeURL(path)
|
||||
self.headers = headers or {}
|
||||
self.client = _FakeClient()
|
||||
|
||||
class _State:
|
||||
pass
|
||||
|
||||
self.state = _State()
|
||||
|
||||
|
||||
def _run(coro):
|
||||
return asyncio.run(coro)
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# ABC + registry
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_oauth_provider_defaults_supports_token_false():
|
||||
assert _OAuthOnly().supports_token is False
|
||||
|
||||
|
||||
|
||||
|
||||
class _NonInteractiveProvider(_TokenProvider):
|
||||
"""A token-only credential with no interactive session."""
|
||||
|
||||
name = "svc-cred"
|
||||
display_name = "Service Credential"
|
||||
supports_session = False
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Bearer extraction
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# authenticate_token (provider stacking)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
def test_authenticate_token_accepts_valid():
|
||||
register_provider(_TokenProvider(secret="good-secret"))
|
||||
req = _FakeRequest(headers={"authorization": "Bearer good-secret"})
|
||||
principal, unreachable = token_auth.authenticate_token(req)
|
||||
assert unreachable is None
|
||||
assert principal is not None
|
||||
assert principal.provider == "tok"
|
||||
assert principal.scopes == ("drain",)
|
||||
|
||||
|
||||
def test_authenticate_token_rejects_wrong_secret():
|
||||
register_provider(_TokenProvider(secret="good-secret"))
|
||||
req = _FakeRequest(headers={"authorization": "Bearer wrong"})
|
||||
principal, unreachable = token_auth.authenticate_token(req)
|
||||
assert principal is None
|
||||
assert unreachable is None
|
||||
|
||||
|
||||
def test_authenticate_token_stacks_first_match_wins():
|
||||
register_provider(_TokenProvider(secret="aaa"))
|
||||
second = _TokenProvider(secret="bbb")
|
||||
second.name = "tok2"
|
||||
register_provider(second)
|
||||
req = _FakeRequest(headers={"authorization": "Bearer bbb"})
|
||||
principal, _ = token_auth.authenticate_token(req)
|
||||
assert principal is not None and principal.provider == "tok2"
|
||||
|
||||
|
||||
def test_authenticate_token_unreachable_then_valid_provider_wins():
|
||||
register_provider(_UnreachableTokenProvider())
|
||||
register_provider(_TokenProvider(secret="good"))
|
||||
req = _FakeRequest(headers={"authorization": "Bearer good"})
|
||||
principal, unreachable = token_auth.authenticate_token(req)
|
||||
# A later provider accepting the token beats the earlier outage.
|
||||
assert principal is not None and principal.provider == "tok"
|
||||
assert unreachable is None
|
||||
|
||||
|
||||
def test_authenticate_token_buggy_provider_does_not_crash():
|
||||
register_provider(_BuggyTokenProvider())
|
||||
register_provider(_TokenProvider(secret="good"))
|
||||
req = _FakeRequest(headers={"authorization": "Bearer good"})
|
||||
principal, unreachable = token_auth.authenticate_token(req)
|
||||
assert principal is not None and principal.provider == "tok"
|
||||
|
||||
|
||||
# --------------------------------------------------------------------------
|
||||
# Middleware seam (route-agnostic)
|
||||
# --------------------------------------------------------------------------
|
||||
|
||||
|
||||
async def _call_next_ok(request):
|
||||
from fastapi.responses import JSONResponse
|
||||
|
||||
return JSONResponse({"ok": True}, status_code=200)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
def test_seam_rejects_wrong_token_401():
|
||||
register_provider(_TokenProvider(secret="good"))
|
||||
token_auth.register_token_route("/api/gateway/drain")
|
||||
req = _FakeRequest(
|
||||
path="/api/gateway/drain", headers={"authorization": "Bearer bad"}
|
||||
)
|
||||
resp = _run(token_auth.token_auth_middleware(req, _call_next_ok))
|
||||
assert resp.status_code == 401
|
||||
|
||||
|
||||
Reference in New Issue
Block a user