From a17772ade5a67457a8b6f81a9aac7cb3051d87da Mon Sep 17 00:00:00 2001 From: Agent Zero Date: Fri, 18 Sep 2026 08:04:41 +0200 Subject: [PATCH] =?UTF-8?q?fix(security):=20F05=20(Astra=20P1)=20=E2=80=94?= =?UTF-8?q?=20Plugin-Gate=20laeuft=20NACH=20Authentisierung,=20fail-closed?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Vorher: require_active_plugin hing nicht an einer Auth-Dependency — FastAPI konnte die Plugin-Pruefung VOR der Authentisierung ausfuehren. Der Mandant wurde aus dem DB-Kontext gelesen (current_setting), der zu diesem Zeitpunkt oft fehlt → stiller Return = Plugin aktiv. Der Code trug sogar ein TODO: Fix in production. Astra-Repro: Endpunkt antwortete HTTP 200 ohne Mandantenkontext. Fix: - _check haengt an get_current_user_or_bearer (Cookie- UND Bearer-Auth) → FastAPI aufloesungsbedingt immer authentifiziert vor dem Gate - Mandant kommt aus dem authentifizierten User-Kontext, nie aus current_setting - Fehlender Mandanten-Kontext → 403 plugin_gate_no_tenant (fail-closed, war: stiller Durchlass) - Public-Routen (is_public) umgehen das Gate weiterhin korrekt Nebenwirkung positiv: Bearer-Clients (External-API, MCP) laufen nicht mehr gegen den Cookie-Zwang des Gates. Verifikation: Syntax OK, ruff clean, test_s1_security_guards + test_auth 21/21. Der Gate-Order-Beweis ist ein Integrationstest-Verhalten (HTTP) — Plugin-Inactive-Faelle werden bereits durch die permission_system_live-Suite abgedeckt. --- app/deps.py | 36 +++++++++++++++++++++++------------- 1 file changed, 23 insertions(+), 13 deletions(-) diff --git a/app/deps.py b/app/deps.py index b5e7653..db05fbc 100644 --- a/app/deps.py +++ b/app/deps.py @@ -438,7 +438,16 @@ def require_active_plugin(plugin_name: str): """ async def _check( db: AsyncSession = Depends(get_db), + current_user: dict[str, Any] = Depends(get_current_user_or_bearer), ) -> None: + """F05 (Astra P1): the plugin gate runs AFTER authentication. + + Depending on ``get_current_user_or_bearer`` guarantees FastAPI + resolves the authenticated user context BEFORE this check — the + previous version read the tenant from the DB session before auth + had run (context missing → silent allow). Both auth paths (cookie + and Bearer) set the tenant context on this same ``db`` session. + """ from app.core.permission_registry import get_permission_registry try: registry = get_permission_registry() @@ -450,19 +459,20 @@ def require_active_plugin(plugin_name: str): "code": "plugin_inactive", }, ) - # Get tenant_id from existing db session (NOT a new session) - # The tenant context is set by middleware/get_current_user on this same session - from sqlalchemy import text as sa_text - - result = await db.execute( - sa_text("SELECT NULLIF(current_setting('app.current_tenant_id', true), '')::uuid") - ) - tenant_id = result.scalar() - - if tenant_id is None: - # No tenant context — plugin is active by default (backward compatible) - # TODO: Fix in production to deny access when no tenant context - return + # Tenant comes from the AUTHENTICATED user context — never from + # the DB session (which may not have the context set yet). + raw_tid = current_user.get("tenant_id") + if not raw_tid: + # Fail-closed: no authenticated tenant context → reject. + # (Previously this returned silently = plugin active.) + raise HTTPException( + status_code=status.HTTP_403_FORBIDDEN, + detail={ + "detail": "Plugin gate requires an authenticated tenant context", + "code": "plugin_gate_no_tenant", + }, + ) + tenant_id = uuid.UUID(str(raw_tid)) # Per-tenant activation check with Redis cache import json