auth_middleware.py 2.7 KB

12345678910111213141516171819202122232425262728293031323334353637383940414243444546474849505152535455565758596061626364656667686970717273747576777879808182
  1. from __future__ import annotations
  2. import re
  3. from collections.abc import Awaitable, Callable
  4. from fastapi import Request
  5. from starlette.concurrency import run_in_threadpool
  6. from starlette.middleware.base import BaseHTTPMiddleware
  7. from starlette.responses import JSONResponse, Response
  8. from api.services.auth import ADMIN_ROLE, SESSION_COOKIE_NAME, resolve_session
  9. _PUBLIC_PATHS = {
  10. "/api/auth/login",
  11. }
  12. _AUTHENTICATED_USER_PATHS = {
  13. ("GET", "/api/auth/me"),
  14. ("POST", "/api/auth/logout"),
  15. }
  16. _NORMAL_USER_PATHS = {
  17. ("GET", "/api/category-tree"),
  18. ("GET", "/api/demand-grade"),
  19. ("GET", "/api/video-discovery/demands"),
  20. ("GET", "/api/video-discovery/runs"),
  21. ("GET", "/api/video-discovery/feedback"),
  22. ("GET", "/api/business-harness/packages/latest"),
  23. ("POST", "/api/video-discovery/feedback"),
  24. }
  25. _NORMAL_USER_PATTERNS = (
  26. re.compile(r"^/api/video-discovery/demands/\d+$"),
  27. re.compile(r"^/api/video-discovery/runs/[^/]+$"),
  28. re.compile(r"^/api/video-discovery/runs/[^/]+/searches$"),
  29. re.compile(r"^/api/video-discovery/runs/[^/]+/candidates$"),
  30. re.compile(r"^/api/demand-grade/\d+/videos$"),
  31. re.compile(r"^/api/business-harness/demands/[^/]+/history$"),
  32. )
  33. def normal_user_can_access(method: str, path: str) -> bool:
  34. if (method, path) in _AUTHENTICATED_USER_PATHS:
  35. return True
  36. if (method, path) in _NORMAL_USER_PATHS:
  37. return True
  38. return method == "GET" and any(pattern.fullmatch(path) for pattern in _NORMAL_USER_PATTERNS)
  39. def _is_protected_path(path: str) -> bool:
  40. return (
  41. path == "/api"
  42. or path.startswith("/api/")
  43. or path in {"/docs", "/redoc", "/openapi.json"}
  44. )
  45. class AuthenticationMiddleware(BaseHTTPMiddleware):
  46. """Authenticate API requests and enforce the two fixed application roles."""
  47. async def dispatch(
  48. self,
  49. request: Request,
  50. call_next: Callable[[Request], Awaitable[Response]],
  51. ) -> Response:
  52. path = request.url.path
  53. method = request.method.upper()
  54. if method == "OPTIONS" or not _is_protected_path(path) or path in _PUBLIC_PATHS:
  55. return await call_next(request)
  56. token = request.cookies.get(SESSION_COOKIE_NAME)
  57. user = await run_in_threadpool(resolve_session, token) if token else None
  58. if user is None:
  59. return JSONResponse(
  60. status_code=401,
  61. content={"detail": "authentication required"},
  62. )
  63. request.state.current_user = user
  64. if user["role"] == ADMIN_ROLE or normal_user_can_access(method, path):
  65. return await call_next(request)
  66. return JSONResponse(
  67. status_code=403,
  68. content={"detail": "permission denied"},
  69. )