"""Dashboard authentication dependency for tenant dashboard-to-orchestrator communication.""" import hashlib import secrets from typing import Annotated from uuid import UUID from fastapi import Depends, Header, HTTPException, Path, status from sqlalchemy import select from sqlalchemy.ext.asyncio import AsyncSession from app.db import get_db from app.models.tenant import Tenant async def verify_dashboard_token( tenant_id: Annotated[UUID, Path(...)], x_dashboard_token: Annotated[str, Header(alias="X-Dashboard-Token")], db: AsyncSession = Depends(get_db), ) -> Tenant: """ Verify per-tenant dashboard token for tenant dashboard endpoints. The dashboard token is used by the tenant's dashboard application (Hub Dashboard or Control Panel) to authenticate requests to the Orchestrator for task execution and status queries. Args: tenant_id: The tenant UUID from the path x_dashboard_token: The raw dashboard token from header db: Database session Returns: The verified Tenant object Raises: HTTPException: 401 if token is missing, invalid, or tenant not found """ # Fetch tenant result = await db.execute( select(Tenant).where(Tenant.id == tenant_id) ) tenant = result.scalar_one_or_none() if not tenant: raise HTTPException( status_code=status.HTTP_404_NOT_FOUND, detail="Tenant not found", ) if not tenant.dashboard_token_hash: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Dashboard token not configured for this tenant", headers={"WWW-Authenticate": "DashboardToken"}, ) # Compute SHA-256 hash of provided token provided_hash = hashlib.sha256(x_dashboard_token.encode()).hexdigest() # Use timing-safe comparison to prevent timing attacks if not secrets.compare_digest(tenant.dashboard_token_hash, provided_hash): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid dashboard token", headers={"WWW-Authenticate": "DashboardToken"}, ) return tenant # Type alias for dependency injection DashboardAuthDep = Annotated[Tenant, Depends(verify_dashboard_token)]