fix(security): F05 (Astra P1) — Plugin-Gate laeuft NACH Authentisierung, fail-closed
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.
This commit is contained in:
+23
-13
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user