rayhpeng 0f82f8a3a2 feat(app): add plugin system with auth plugin and static assets
Add new application structure:
- app/main.py - application entry point
- app/plugins/ - plugin system with auth plugin:
  - api/ - REST API endpoints and schemas
  - authorization/ - auth policies, providers, hooks
  - domain/ - business logic (service, models, jwt, password)
  - injection/ - route injection and guards
  - ops/ - operational utilities
  - runtime/ - runtime configuration
  - security/ - middleware, CSRF, dependencies
  - storage/ - user repositories and models
- app/static/ - static assets (scalar.js for API docs)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-04-22 11:31:42 +08:00

103 lines
3.7 KiB
Python

"""Runtime route guard backed by the auth plugin's route policy registry."""
from __future__ import annotations
from collections.abc import Awaitable, Callable
from fastapi import HTTPException, Request
from app.plugins.auth.authorization.authentication import (
authenticate_request,
get_auth_context,
set_auth_context,
)
from app.plugins.auth.authorization.authorization import ensure_authenticated
from app.plugins.auth.authorization.hooks import get_authz_hooks
from app.plugins.auth.authorization.policies import require_run_owner, require_thread_owner
from app.plugins.auth.injection.registry_loader import RoutePolicyRegistry, RoutePolicySpec
PolicyGuard = Callable[[Request, RoutePolicySpec], Awaitable[None]]
async def _check_capability(request: Request, spec: RoutePolicySpec) -> None:
if not spec.capability:
return
auth = get_auth_context(request)
if auth is None:
raise HTTPException(status_code=500, detail="Missing auth context")
if ":" not in spec.capability:
raise RuntimeError(f"Invalid capability format: {spec.capability}")
resource, action = spec.capability.split(":", 1)
if not auth.has_permission(resource, action):
raise HTTPException(status_code=403, detail=f"Permission denied: {spec.capability}")
async def _guard_thread_owner(request: Request, spec: RoutePolicySpec) -> None:
auth = get_auth_context(request)
if auth is None:
raise HTTPException(status_code=500, detail="Missing auth context")
thread_id = request.path_params.get("thread_id")
if not isinstance(thread_id, str):
raise RuntimeError("owner:thread policy requires thread_id path parameter")
await require_thread_owner(request, auth, thread_id=thread_id, require_existing=spec.require_existing)
async def _guard_run_owner(request: Request, spec: RoutePolicySpec) -> None:
auth = get_auth_context(request)
if auth is None:
raise HTTPException(status_code=500, detail="Missing auth context")
thread_id = request.path_params.get("thread_id")
run_id = request.path_params.get("run_id")
if not isinstance(thread_id, str) or not isinstance(run_id, str):
raise RuntimeError("owner:run policy requires thread_id and run_id path parameters")
await require_run_owner(
request,
auth,
thread_id=thread_id,
run_id=run_id,
require_existing=spec.require_existing,
)
_POLICY_GUARDS: dict[str, PolicyGuard] = {
"owner:thread": _guard_thread_owner,
"owner:run": _guard_run_owner,
}
async def enforce_route_policy(request: Request) -> None:
registry = getattr(request.app.state, "auth_route_policy_registry", None)
if not isinstance(registry, RoutePolicyRegistry):
raise RuntimeError("Auth route policy registry is not configured")
route = request.scope.get("route")
path_template = getattr(route, "path", None)
if not isinstance(path_template, str):
raise RuntimeError("Unable to resolve route path for authorization")
spec = registry.get(request.method, path_template)
if spec is None:
raise RuntimeError(f"Missing auth route policy for {request.method} {path_template}")
if spec.public:
return
auth = get_auth_context(request)
if auth is None:
hooks = get_authz_hooks(request)
auth = await authenticate_request(request, permission_provider=hooks.permission_provider)
set_auth_context(request, auth)
ensure_authenticated(auth)
await _check_capability(request, spec)
for policy_name in spec.policies:
guard = _POLICY_GUARDS.get(policy_name)
if guard is None:
raise RuntimeError(f"Unknown route policy guard: {policy_name}")
await guard(request, spec)
__all__ = ["enforce_route_policy"]