from __future__ import annotations import hmac import re from collections.abc import Generator from dataclasses import dataclass from fastapi import Depends, Header, HTTPException, Request, status from sqlalchemy.orm import Session from app.core.config import get_settings from app.core.db import SessionLocal from app.core.security import SessionPayload, read_session_token from app.models.user import User from app.schemas import CurrentUser, Role from app.services.sessions import is_session_revoked settings = get_settings() _VALID_ROLES = frozenset(Role.__args__) # type: ignore[attr-defined] def get_db() -> Generator[Session, None, None]: db = SessionLocal() try: yield db finally: db.close() def get_current_user(request: Request, db: Session = Depends(get_db)) -> CurrentUser: token = request.cookies.get(settings.session_cookie_name) payload: SessionPayload | None = read_session_token(token) if token else None revoked = token is not None and is_session_revoked(db, token) if payload is None or payload.role not in _VALID_ROLES or revoked: raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") # Demo reset deliberately rebuilds the deterministic users table. Retaining the # signed demo session until the reset endpoint clears its cookie keeps existing demo # workflows stable; operational sessions are always checked against the live record. if settings.mobilityops_demo_mode: demo_role: Role = payload.role # type: ignore[assignment] return CurrentUser( public_ref=payload.public_ref, display_name=payload.display_name, role=demo_role, ) user = db.get(User, payload.user_id) if ( user is None or not user.active or user.public_ref != payload.public_ref or user.role not in _VALID_ROLES ): raise HTTPException(status_code=status.HTTP_401_UNAUTHORIZED, detail="Not authenticated") role: Role = user.role # type: ignore[assignment] return CurrentUser(public_ref=user.public_ref, display_name=user.display_name, role=role) def require_operations_manager( user: CurrentUser = Depends(get_current_user), ) -> CurrentUser: if user.role != "operations_manager": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Operations Manager role required" ) return user @dataclass(frozen=True) class McpClientContext: client_id: str tenant: str _MCP_CLIENT_ID = re.compile( r"^itworx-mcp-hub:(?:readiness|mobilityops:[A-Za-z0-9][A-Za-z0-9._:-]{0,127})$" ) def require_mcp_service_token( x_service_token: str = Header(..., alias="X-Service-Token"), x_client_id: str = Header(..., alias="X-Client-Id", min_length=1, max_length=180), x_tenant_id: str | None = Header(default=None, alias="X-Tenant-Id", max_length=120), ) -> McpClientContext: if not hmac.compare_digest(x_service_token, settings.mcp_hub_service_token): raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid service token" ) if not _MCP_CLIENT_ID.fullmatch(x_client_id): raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Untrusted MCP client identity" ) if x_tenant_id is not None and x_tenant_id != settings.ragcore_tenant: raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Tenant mismatch") return McpClientContext(client_id=x_client_id, tenant=settings.ragcore_tenant)