diff --git a/contextual_orchestrator/review_gateway.py b/contextual_orchestrator/review_gateway.py index bd7ad279b..bddd94518 100644 --- a/contextual_orchestrator/review_gateway.py +++ b/contextual_orchestrator/review_gateway.py @@ -379,9 +379,23 @@ def main() -> None: inference_token = get_credential(args.inference_token_key) if not admin_token or not inference_token or admin_token == inference_token: parser.error("--production requires distinct admin and inference KV credentials") + + def current_scope(token: str) -> str | None: + admin = get_credential(args.admin_token_key) + inference = get_credential(args.inference_token_key) + if not token or not admin or not inference or admin == inference: + return None + if SecurityConfig._constant_time_token_match(token, admin): + return "admin" + if SecurityConfig._constant_time_token_match(token, inference): + return "inference" + return None + security = SecurityConfig( admin_token=admin_token, inference_token=inference_token, + bearer_verifier=lambda token, scope: current_scope(token) == scope, + principal_resolver=lambda token: "review-gateway" if current_scope(token) else None, allow_public_bind=args.allow_public_bind, max_body_bytes=REVIEW_MAX_BODY_BYTES, ) diff --git a/tests/test_review_gateway.py b/tests/test_review_gateway.py index 71375af52..9aa444cd0 100644 --- a/tests/test_review_gateway.py +++ b/tests/test_review_gateway.py @@ -15,6 +15,7 @@ from contextual_orchestrator.credentials import ( InMemoryCredentialBackend, NotConfigured, + delete_credential, get_credential, register_credential, set_backend, @@ -389,6 +390,7 @@ def request(path, token, body=None): assert request("/v1/models", None)[0] == 401 assert request("/v1/models", "admin-secret")[0] == 401 + assert request("/admin/state", "admin-secret")[0] == 200 assert provider_headers == [] status, models = request("/v1/models", "inference-secret") assert status == 200 @@ -402,6 +404,20 @@ def request(path, token, body=None): assert answer["model"] == "orchestrator/free" assert provider_headers assert set(provider_headers) == {"Bearer stored-router-secret"} + sent_before_revocation = len(provider_headers) + delete_credential(review_gateway.REVIEW_INFERENCE_CREDENTIAL_NAME) + assert request("/v1/models", "inference-secret")[0] == 401 + assert request( + "/v1/chat/completions", + "inference-secret", + {"model": "orchestrator/free", "messages": [{"role": "user", "content": "review"}]}, + )[0] == 401 + assert len(provider_headers) == sent_before_revocation + register_credential(review_gateway.REVIEW_INFERENCE_CREDENTIAL_NAME, "rotated-inference-secret") + assert request("/v1/models", "inference-secret")[0] == 401 + assert request("/v1/models", "rotated-inference-secret")[0] == 200 + delete_credential(review_gateway.REVIEW_ADMIN_CREDENTIAL_NAME) + assert request("/admin/state", "admin-secret")[0] == 401 finally: server.shutdown() captured["thread"].join(timeout=5)