fix(security): F08 (Astra P1) — External-Agent-API fuer reine Bearer-Clients oeffnen, get_db-TypeError fixen
Check Cross-Plugin Imports / check (push) Has been cancelled

Vorher: Alle drei External-Endpoints hingen an require_permission,
das an der Session-Cookie-Auth haengt — reine Bearer-Clients (n8n,
Skripte, externe Systeme) erhielten 401, bevor die Bearer-Verifikation
im Handler je erreicht wurde (Astra-Repro: Statusabfrage mit nur
Bearer-Header -> 401). Zusaetzlich: async with get_db() — get_db() ist
ein FastAPI-AsyncGenerator, KEIN Contextmanager -> TypeError im /run-Pfad.

Fix:
- Neue Dependency require_permission_or_bearer (deps.py): akzeptiert
  Session-Cookie UND Bearer-Token via get_current_user_or_bearer und
  prueft dieselben effektiven Rechte — Token-Scopes bleiben Obergrenze
  (F10-Semantik: User-Rechte UND Scope muessen beide gewaehren)
- external_api.py: alle 3 Endpunkte (run/status/stream) auf die neue
  Dependency umgestellt
- /run-Pfad: get_db() -> get_session_factory() (Session-Factory wie alle
  anderen self-managed-Session-Codepfade)

Abnahme (Astra): Gueltiger Bearer ohne Cookie funktioniert fuer Status,
Run und Stream; ungueltige Tokens werden abgewiesen — die
Permission-Pruefung laeuft identisch fuer beide Auth-Pfade.

Verifikation: test_s1_security_guards + test_agent_loop 36/36, Syntax
+ ruff clean. (Live-Bearer-Verifikation folgt mit dem naechsten Deploy.)
This commit is contained in:
Agent Zero
2026-09-18 11:34:18 +02:00
parent 62d107d142
commit 001e4b415f
2 changed files with 55 additions and 6 deletions
+48
View File
@@ -257,6 +257,54 @@ async def get_current_user_or_bearer(
return await get_current_user(request, db, redis)
def require_permission_or_bearer(permission: str):
"""F08 (Astra P1): permission dependency for routes that serve BOTH
session-cookie clients (SPA) and pure Bearer API clients.
``require_permission`` resolves via ``get_current_user`` (session
cookie only) — a Bearer client fails with 401 before the route's own
Bearer verification is ever reached. This dependency accepts either
auth path and enforces the SAME effective permission:
- session users: normal permission check
- Bearer tokens: token scopes are an UPPER BOUND (F10) — the user's
own permissions must grant the permission AND the scope must match
"""
async def _check(
current_user: dict[str, Any] = Depends(get_current_user_or_bearer),
) -> dict[str, Any]:
token_scopes = current_user.get("_token_scopes")
if token_scopes is not None:
from app.core.permissions import _permission_matches_any
if not _permission_matches_any(set(token_scopes), permission):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={
"detail": f"Token scope '{permission}' required",
"code": "insufficient_scope",
},
)
# fall through — user permissions apply too (F10 semantics)
if current_user.get("is_system_admin"):
return current_user
from app.core.permissions import check_permission
if check_permission(current_user, permission):
return current_user
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail={
"detail": f"Permission '{permission}' required",
"code": "forbidden",
},
)
return _check
async def require_admin(
current_user: dict[str, Any] = Depends(get_current_user),
) -> dict[str, Any]: