94057c3d3e
PR Test (NPU) / check-changes (push) Has been cancelled
PR Test (NPU) / pr-gate (push) Has been cancelled
PR Test (NPU) / set-image-config (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-1-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (0) (push) Has been cancelled
PR Test (NPU) / stage-b-test-2-npu-a2 (1) (push) Has been cancelled
PR Test (NPU) / stage-b-test-4-npu-a3 (push) Has been cancelled
PR Test (NPU) / stage-b-test-16-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-1-npu-a3 (push) Has been cancelled
PR Test (NPU) / multimodal-gen-test-2-npu-a3 (push) Has been cancelled
PR Test (Arm64) / pr-gate (push) Has been cancelled
PR Test (Arm64) / check-changes (push) Has been cancelled
PR Test (Arm64) / build-test (push) Has been cancelled
PR Test (sgl-router) / gate (push) Has been cancelled
PR Test (sgl-router) / tier-1 — lint (push) Has been cancelled
PR Test (sgl-router) / tier-2 — build + test (push) Has been cancelled
PR Test (sgl-router) / tier-3 — docker (placeholder) (push) Has been cancelled
PR Test (sgl-router) / tier-3 — k8s integration (push) Has been cancelled
PR Test (sgl-router) / tier-3 — e2e (push) Has been cancelled
PR Test (sgl-router) / finish (push) Has been cancelled
PR Test (NPU) / single-node-poc (map[name:qwen3_6_27b_w8a8_1p_in64k_out1k_50ms runner:linux-aarch64-a3-2 test_case:test/registered/ascend/performance/qwen3_6_27b/test_npu_qwen3_6_27b_w8a8_1p_in64k_out1k_50ms.py test_type:perf]) (push) Has been cancelled
PR Test (NPU) / pr-test-npu-finish (push) Has been cancelled
PR Test (Xeon) / pr-gate (push) Has been cancelled
PR Test (Xeon) / check-changes (push) Has been cancelled
PR Test (Xeon) / build-test (, xeon-gnr, base-b-test-cpu) (push) Has been cancelled
PR Test (XPU) / check-changes (push) Has been cancelled
PR Test (XPU) / pr-gate (push) Has been cancelled
PR Test (XPU) / stage-a-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / wait-for-stage-a (push) Has been cancelled
PR Test (XPU) / stage-b-test-1-gpu-xpu (push) Has been cancelled
PR Test (XPU) / finish (push) Has been cancelled
CI Model Inventory / build-inventory (push) Has been cancelled
Lint / lint (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Compilation Check (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Manual Policy (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark - Request Processing (push) Has been cancelled
PR Benchmark (SMG Components) / Benchmark Summary (push) Has been cancelled
PR Test (SMG) / build-wheel (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on windows (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (x86_64 - auto) (push) Has been cancelled
PR Test (SMG) / python-unit-tests (push) Has been cancelled
PR Test (SMG) / unit-tests (push) Has been cancelled
PR Test (SMG) / benchmarks (push) Has been cancelled
PR Test (SMG) / chat-completions (push) Has been cancelled
PR Test (SMG) / chat-completions-4gpu (push) Has been cancelled
PR Test (SMG) / e2e (push) Has been cancelled
PR Test (SMG) / docker-build-test (push) Has been cancelled
PR Test (SMG) / k8s-integration (push) Has been cancelled
PR Test (SMG) / finish (push) Has been cancelled
PR Test (SMG) / summarize-benchmarks (push) Has been cancelled
Release SGLang Model Gateway Docker Image / publish (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on macos (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - auto) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (aarch64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / build on linux (x86_64 - musllinux_1_1) (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Build SDist (push) Has been cancelled
Release SGLang Model Gateway to PyPI / Upload to PyPI (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (aarch64, 12.9, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu129-matrix (x86_64, 12.9, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu129 (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (aarch64, 13.0, 3.10, arm-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / build-cu130-matrix (x86_64, 13.0, 3.10, x64-kernel-build-node) (push) Has been cancelled
Release SGLang Kernels / release-cu130 (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 700) (push) Has been cancelled
Release SGLang Kernels / build-rocm-matrix (3.10, 720) (push) Has been cancelled
Release SGLang Kernels / release-rocm700 (push) Has been cancelled
Release SGLang Kernels / release-rocm720 (push) Has been cancelled
Release SGLang Kernels / build-musa43 (43, 3.10) (push) Has been cancelled
Release SGLang Kernels / release-musa43 (push) Has been cancelled
209 lines
7.1 KiB
Python
209 lines
7.1 KiB
Python
"""Auth utilities for HTTP servers.
|
|
|
|
This module is intentionally lightweight (no torch import) so it can be used in unit tests.
|
|
"""
|
|
|
|
from __future__ import annotations
|
|
|
|
import secrets
|
|
from dataclasses import dataclass
|
|
from enum import Enum
|
|
from typing import Any, Optional
|
|
|
|
|
|
@dataclass(frozen=True)
|
|
class AuthDecision:
|
|
allowed: bool
|
|
error_status_code: int = 401 # Only meaningful when allowed=False
|
|
|
|
|
|
class AuthLevel(str, Enum):
|
|
"""Per-endpoint auth level (attached to endpoint function via `@auth_level`)."""
|
|
|
|
NORMAL = "normal"
|
|
ADMIN_OPTIONAL = "admin_optional"
|
|
ADMIN_FORCE = "admin_force"
|
|
|
|
|
|
def auth_level(level: AuthLevel):
|
|
"""Mark endpoint with auth level (stored in endpoint metadata)."""
|
|
|
|
def decorator(func):
|
|
func._auth_level = level
|
|
return func
|
|
|
|
return decorator
|
|
|
|
|
|
def _get_auth_level_from_app_and_scope(app: Any, scope: dict) -> AuthLevel:
|
|
"""Best-effort resolve auth level by matching the request to a route."""
|
|
# Import lazily to keep this module unit-test friendly (FastAPI/Starlette are not
|
|
# required unless you actually use the middleware / route matching).
|
|
from starlette.routing import Match
|
|
|
|
# Prefer app.router.routes when available; fall back to app.routes.
|
|
routes = getattr(getattr(app, "router", None), "routes", None) or getattr(
|
|
app, "routes", []
|
|
)
|
|
|
|
for route in routes:
|
|
try:
|
|
match, child_scope = route.matches(scope)
|
|
except Exception:
|
|
continue
|
|
if match == Match.FULL:
|
|
endpoint = child_scope.get("endpoint") or getattr(route, "endpoint", None)
|
|
level = getattr(endpoint, "_auth_level", None)
|
|
return level if isinstance(level, AuthLevel) else AuthLevel.NORMAL
|
|
|
|
return AuthLevel.NORMAL
|
|
|
|
|
|
def app_has_admin_force_endpoints(app: Any) -> bool:
|
|
"""Return True if any route endpoint is marked as ADMIN_FORCE."""
|
|
routes = getattr(getattr(app, "router", None), "routes", None) or getattr(
|
|
app, "routes", []
|
|
)
|
|
for route in routes:
|
|
endpoint = getattr(route, "endpoint", None)
|
|
if getattr(endpoint, "_auth_level", None) == AuthLevel.ADMIN_FORCE:
|
|
return True
|
|
return False
|
|
|
|
|
|
def decide_request_auth(
|
|
*,
|
|
method: str,
|
|
path: str,
|
|
authorization_header: Optional[str],
|
|
api_key: Optional[str],
|
|
admin_api_key: Optional[str],
|
|
auth_level: AuthLevel,
|
|
) -> AuthDecision:
|
|
"""Pure auth decision function (easy to unit test).
|
|
|
|
Auth levels:
|
|
- NORMAL: legacy behavior (api_key protects all endpoints when configured)
|
|
- ADMIN_OPTIONAL: can be accessed without any key (if no keys configured),
|
|
or with api_key/admin_api_key depending on server config.
|
|
- ADMIN_FORCE: requires admin_api_key; if admin_api_key is NOT configured,
|
|
it must be rejected (403) even if api_key is provided.
|
|
|
|
NOTE :
|
|
- Health/metrics endpoints are always allowed (even when api_key/admin_api_key is set),
|
|
to support k8s/liveness/readiness and Prometheus scraping without embedding secrets.
|
|
- We match them by prefix to cover common variants like /health_generate.
|
|
"""
|
|
if method == "OPTIONS":
|
|
return AuthDecision(allowed=True)
|
|
|
|
if path.startswith("/health") or path.startswith("/metrics"):
|
|
return AuthDecision(allowed=True)
|
|
|
|
def _check_bearer_token(
|
|
authorization_header: Optional[str], expected_token: str
|
|
) -> bool:
|
|
"""Check bearer token with constant-time comparison."""
|
|
if not authorization_header:
|
|
return False
|
|
parts = authorization_header.split(" ", 1)
|
|
if len(parts) != 2 or parts[0].lower() != "bearer":
|
|
return False
|
|
return secrets.compare_digest(parts[1], expected_token)
|
|
|
|
# Force-auth endpoints: only admin_api_key can unlock them; if admin_api_key is unset,
|
|
# reject them unconditionally (explicitly "not allowed").
|
|
if auth_level == AuthLevel.ADMIN_FORCE:
|
|
if not admin_api_key:
|
|
return AuthDecision(allowed=False, error_status_code=403)
|
|
if not _check_bearer_token(authorization_header, admin_api_key):
|
|
return AuthDecision(allowed=False)
|
|
return AuthDecision(allowed=True)
|
|
|
|
# Optional-auth endpoints:
|
|
# - no keys configured: allow
|
|
# - only api_key: require api_key
|
|
# - only admin_api_key: require admin_api_key
|
|
# - both: require admin_api_key (api_key is NOT accepted)
|
|
if auth_level == AuthLevel.ADMIN_OPTIONAL:
|
|
if admin_api_key:
|
|
return AuthDecision(
|
|
allowed=_check_bearer_token(authorization_header, admin_api_key)
|
|
)
|
|
elif api_key:
|
|
return AuthDecision(
|
|
allowed=_check_bearer_token(authorization_header, api_key)
|
|
)
|
|
else:
|
|
return AuthDecision(allowed=True)
|
|
|
|
# Normal endpoints:
|
|
# - if api_key is configured, require api_key (even if admin_api_key is also configured)
|
|
# - otherwise allow (including the "admin_api_key only" case)
|
|
if api_key:
|
|
return AuthDecision(allowed=_check_bearer_token(authorization_header, api_key))
|
|
|
|
return AuthDecision(allowed=True)
|
|
|
|
|
|
def add_api_key_middleware(
|
|
app,
|
|
*,
|
|
api_key: Optional[str],
|
|
admin_api_key: Optional[str],
|
|
):
|
|
"""Add middleware for three endpoint auth levels: normal/admin_optional/admin_force."""
|
|
# Import lazily so `decide_request_auth()` can be unit-tested without FastAPI installed.
|
|
from fastapi.responses import ORJSONResponse
|
|
from starlette.requests import Request
|
|
|
|
class _ApiKeyASGIMiddleware:
|
|
"""ASGI-native middleware to preserve client disconnect events."""
|
|
|
|
def __init__(self, app, *, api_key, admin_api_key, fastapi_app):
|
|
self.app = app
|
|
self.api_key = api_key
|
|
self.admin_api_key = admin_api_key
|
|
self.fastapi_app = fastapi_app
|
|
|
|
async def __call__(self, scope, receive, send):
|
|
if scope["type"] != "http":
|
|
await self.app(scope, receive, send)
|
|
return
|
|
|
|
request = Request(scope, receive=receive)
|
|
path = request.url.path
|
|
authz = request.headers.get("Authorization")
|
|
level = _get_auth_level_from_app_and_scope(self.fastapi_app, scope)
|
|
decision = decide_request_auth(
|
|
method=request.method,
|
|
path=path,
|
|
authorization_header=authz,
|
|
api_key=self.api_key,
|
|
admin_api_key=self.admin_api_key,
|
|
auth_level=level,
|
|
)
|
|
|
|
if not decision.allowed:
|
|
response = ORJSONResponse(
|
|
content={
|
|
"error": (
|
|
"Unauthorized"
|
|
if decision.error_status_code == 401
|
|
else "Forbidden"
|
|
)
|
|
},
|
|
status_code=decision.error_status_code,
|
|
)
|
|
await response(scope, receive, send)
|
|
return
|
|
|
|
await self.app(scope, receive, send)
|
|
|
|
app.add_middleware(
|
|
_ApiKeyASGIMiddleware,
|
|
api_key=api_key,
|
|
admin_api_key=admin_api_key,
|
|
fastapi_app=app,
|
|
)
|