aps-agent/server/auth/middleware.py

96 lines
3.3 KiB
Python

from __future__ import annotations
import os
from fastapi import Request
from fastapi.responses import JSONResponse
from server.auth.context import IdentityContext, bind_identity, reset_identity
from server.auth.licenses import get_license_provider, is_desktop_request
from server.auth.providers import AuthError, get_auth_provider
PUBLIC_API_PATHS = {
"/api/health",
"/api/auth/login",
"/api/auth/login/precheck",
"/api/auth/tenants",
"/api/auth/captcha",
"/api/auth/refresh",
"/api/auth/logout",
"/api/auth/license/activate",
}
SELF_AUTHORIZED_WRITE_PREFIXES = (
"/api/workspace",
"/api/projects",
"/api/sessions",
"/api/project-files",
"/api/drawings", # 图纸解析只读放行;候选入库走 P2 门禁
)
def authentication_enabled() -> bool:
"""Return whether browser/desktop requests must present login credentials."""
value = (os.environ.get("APS_AUTH_ENABLED") or "1").strip().lower()
return value not in {"0", "false", "no", "off"}
def bypass_identity() -> IdentityContext:
"""Single local administrator identity used only when auth is explicitly disabled."""
return IdentityContext(
user_id=1,
username="local-admin",
fullname="本地免登录管理员",
tenant_uuid="platform",
roles=("system", "admin", "planner", "approver", "auditor", "scheduler", "desktop"),
auth_kind="disabled",
)
class AuthenticationMiddleware:
"""ASGI middleware keeps identity bound for the full streaming response lifetime."""
def __init__(self, app) -> None:
self.app = app
async def __call__(self, scope, receive, send) -> None:
if scope.get("type") != "http":
await self.app(scope, receive, send)
return
path = scope.get("path") or ""
if scope.get("method") == "OPTIONS" or not path.startswith("/api/") or path in PUBLIC_API_PATHS:
await self.app(scope, receive, send)
return
if authentication_enabled():
request = Request(scope, receive=receive)
try:
provider = get_license_provider() if is_desktop_request(request) else get_auth_provider()
identity = await provider.authenticate(request)
except AuthError as exc:
response = JSONResponse(
{"error": {"code": exc.code, "message": exc.message}},
status_code=exc.status_code,
)
await response(scope, receive, send)
return
else:
identity = bypass_identity()
token = bind_identity(identity)
try:
method = scope.get("method") or "GET"
if method not in {"GET", "HEAD", "OPTIONS"} and not path.startswith(SELF_AUTHORIZED_WRITE_PREFIXES):
from server.state.projects import get_project_store
try:
get_project_store().require_active_write()
except PermissionError as exc:
response = JSONResponse(
{"error": {"code": "PROJECT_READ_ONLY", "message": str(exc)}},
status_code=403,
)
await response(scope, receive, send)
return
await self.app(scope, receive, send)
finally:
reset_identity(token)