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