Import AITURK IDE 1.0.0-beta.1 from Hermes 63279301; preserve MIT license
This commit is contained in:
@@ -0,0 +1,156 @@
|
||||
"""#94558 — a non-JWT bearer must not be reported as "Auth provider unreachable".
|
||||
|
||||
Hosted agents answered every opaque/peer bearer on the gated API with a fast
|
||||
HTTP 503 ``{"detail": "Auth provider 'nous' unreachable"}`` while Portal was
|
||||
perfectly healthy: ``NousDashboardAuthProvider._verify_jwt`` folded *every*
|
||||
``PyJWKClient`` failure — including ``DecodeError('Not enough segments')`` for
|
||||
a token that is not a JWT at all — into ``ProviderError``. Only a transport
|
||||
failure fetching the JWKS is "unreachable"; anything else means "not my
|
||||
token" (``verify_session`` -> None -> 401 / next provider).
|
||||
|
||||
Real ``NousDashboardAuthProvider`` + real ``SelfHostedOIDCProvider`` JWKS path,
|
||||
a real local HTTP JWKS server (reachable case) or a closed port (unreachable),
|
||||
and the real gated web_server app for the HTTP-level assertion.
|
||||
"""
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import threading
|
||||
from http.server import BaseHTTPRequestHandler, HTTPServer
|
||||
|
||||
import jwt
|
||||
import pytest
|
||||
from starlette.testclient import TestClient
|
||||
|
||||
from hermes_cli import web_server
|
||||
from hermes_cli.dashboard_auth import (
|
||||
InvalidCodeError,
|
||||
ProviderError,
|
||||
classify_jwks_lookup_error,
|
||||
clear_providers,
|
||||
register_provider,
|
||||
)
|
||||
from hermes_cli.dashboard_auth.cookies import SESSION_AT_COOKIE
|
||||
import plugins.dashboard_auth.nous as nous_plugin
|
||||
|
||||
OPAQUE_PEER_KEY = "hk_live_opaque_peer_key_0123456789abcdef"
|
||||
# Well-formed RS256 JWT header with an unknown kid, bogus payload/signature.
|
||||
FOREIGN_KID_JWT = "eyJhbGciOiJSUzI1NiIsImtpZCI6Inp6eiJ9.e30.sig"
|
||||
|
||||
|
||||
@pytest.fixture(scope="module")
|
||||
def empty_jwks_server():
|
||||
"""A reachable JWKS endpoint that knows no keys."""
|
||||
|
||||
class _H(BaseHTTPRequestHandler):
|
||||
def do_GET(self): # noqa: N802
|
||||
self.send_response(200)
|
||||
self.send_header("content-type", "application/json")
|
||||
self.end_headers()
|
||||
self.wfile.write(json.dumps({"keys": []}).encode())
|
||||
|
||||
def log_message(self, *a): # silence
|
||||
pass
|
||||
|
||||
srv = HTTPServer(("127.0.0.1", 0), _H)
|
||||
t = threading.Thread(target=srv.serve_forever, daemon=True)
|
||||
t.start()
|
||||
yield f"http://127.0.0.1:{srv.server_address[1]}"
|
||||
srv.shutdown()
|
||||
|
||||
|
||||
def _nous(portal_url: str) -> nous_plugin.NousDashboardAuthProvider:
|
||||
return nous_plugin.NousDashboardAuthProvider(client_id="agent:test-instance", portal_url=portal_url)
|
||||
|
||||
|
||||
# ── classifier ────────────────────────────────────────────────────────────
|
||||
|
||||
def test_classifier_maps_transport_failure_to_provider_error():
|
||||
exc = jwt.PyJWKClientConnectionError("Fail to fetch data from the url")
|
||||
assert isinstance(classify_jwks_lookup_error(exc), ProviderError)
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"exc",
|
||||
[
|
||||
jwt.DecodeError("Not enough segments"),
|
||||
jwt.PyJWKSetError("The JWK Set did not contain any keys"),
|
||||
jwt.InvalidTokenError("bad"),
|
||||
],
|
||||
)
|
||||
def test_classifier_maps_unverifiable_token_to_invalid_code(exc):
|
||||
assert isinstance(classify_jwks_lookup_error(exc), InvalidCodeError)
|
||||
|
||||
|
||||
def test_classifier_keeps_bare_jwk_client_error_as_provider_fault():
|
||||
assert isinstance(classify_jwks_lookup_error(jwt.PyJWKClientError("weird JWKS shape")), ProviderError)
|
||||
|
||||
|
||||
# ── Nous provider ─────────────────────────────────────────────────────────
|
||||
|
||||
def test_opaque_bearer_with_healthy_portal_is_not_unreachable(empty_jwks_server):
|
||||
provider = _nous(empty_jwks_server)
|
||||
assert provider.verify_session(access_token=OPAQUE_PEER_KEY) is None
|
||||
|
||||
|
||||
def test_foreign_kid_jwt_with_healthy_portal_is_not_unreachable(empty_jwks_server):
|
||||
provider = _nous(empty_jwks_server)
|
||||
assert provider.verify_session(access_token=FOREIGN_KID_JWT) is None
|
||||
|
||||
|
||||
def test_real_jwt_with_unreachable_portal_still_raises_provider_error():
|
||||
provider = _nous("http://127.0.0.1:9") # discard port: connection refused
|
||||
with pytest.raises(ProviderError):
|
||||
provider.verify_session(access_token=FOREIGN_KID_JWT)
|
||||
|
||||
|
||||
def test_opaque_bearer_with_unreachable_portal_is_still_just_not_ours():
|
||||
"""No network call is even needed to know an opaque string is not our JWT."""
|
||||
provider = _nous("http://127.0.0.1:9")
|
||||
assert provider.verify_session(access_token=OPAQUE_PEER_KEY) is None
|
||||
|
||||
|
||||
# ── self-hosted OIDC provider (sibling site of the same hunk) ──────────────
|
||||
|
||||
def test_self_hosted_provider_shares_the_classification(empty_jwks_server, monkeypatch):
|
||||
import plugins.dashboard_auth.self_hosted as sh
|
||||
|
||||
provider = object.__new__(sh.SelfHostedOIDCProvider)
|
||||
provider._jwks_client = None
|
||||
provider._client_id = "hermes"
|
||||
monkeypatch.setattr(
|
||||
provider, "_get_discovery",
|
||||
lambda: {"jwks_uri": f"{empty_jwks_server}/jwks", "issuer": empty_jwks_server},
|
||||
)
|
||||
with pytest.raises(InvalidCodeError):
|
||||
provider._verify_id_token(OPAQUE_PEER_KEY)
|
||||
|
||||
|
||||
# ── HTTP level: the gated API answers 401, not 503 ────────────────────────
|
||||
|
||||
@pytest.fixture
|
||||
def _gated_nous(empty_jwks_server):
|
||||
clear_providers()
|
||||
prev = {k: getattr(web_server.app.state, k, None) for k in ("bound_host", "bound_port", "auth_required")}
|
||||
web_server.app.state.bound_host = "agent.example.test"
|
||||
web_server.app.state.bound_port = 443
|
||||
web_server.app.state.auth_required = True
|
||||
register_provider(_nous(empty_jwks_server))
|
||||
yield TestClient(web_server.app, base_url="https://agent.example.test")
|
||||
clear_providers()
|
||||
for k, v in prev.items():
|
||||
setattr(web_server.app.state, k, v)
|
||||
|
||||
|
||||
def test_gated_api_rejects_opaque_bearer_with_401_not_503(_gated_nous):
|
||||
r = _gated_nous.get("/api/auth/me", headers={"Authorization": f"Bearer {OPAQUE_PEER_KEY}"})
|
||||
assert r.status_code != 503, r.text
|
||||
assert r.status_code == 401
|
||||
assert "unreachable" not in r.text.lower()
|
||||
|
||||
|
||||
def test_gated_api_rejects_opaque_cookie_with_401_not_503(_gated_nous):
|
||||
_gated_nous.cookies.set(SESSION_AT_COOKIE, OPAQUE_PEER_KEY)
|
||||
r = _gated_nous.get("/api/auth/me")
|
||||
assert r.status_code != 503, r.text
|
||||
assert "unreachable" not in r.text.lower()
|
||||
Reference in New Issue
Block a user