from fastapi import Depends, HTTPException, status, Header, Request
import logging
from typing import List, Any, Optional, Annotated
from sqlalchemy.orm import Session
from app.db import models
from app.db.session import SessionLocal
from app.core.retrievers.base_retriever import Retriever
from app.core.services.document import DocumentService
from app.core.services.rag import RAGService
from app.core.vector_store.faiss_store import FaissVectorStore
# This is a dependency
def get_db():
db = SessionLocal()
try:
yield db
finally:
db.close()
# Dependency to get current user object (Prioritizes Authorization: Bearer <JWT>)
async def get_current_user(
request: Request,
db: Session = Depends(get_db),
authorization: Annotated[Optional[str], Header()] = None,
x_user_id: Annotated[Optional[str], Header()] = None,
x_proxy_secret: Annotated[Optional[str], Header()] = None,
) -> models.User:
from app.config import settings
# 1. Try to resolve user via JWT (Bearer Token)
token = None
if authorization and authorization.startswith("Bearer "):
token = authorization.split(" ")[1]
# Also support token in query param or X-User-ID header (if it looks like a JWT)
if not token:
token = request.query_params.get("token")
if not token and x_user_id and "." in x_user_id:
token = x_user_id
if token and "." in token:
try:
# Use request.app.state.services to avoid circular imports with app.main
global_services = getattr(request.app.state, "services", None)
if not global_services:
from app.main import services as global_services
import jwt
unverified = jwt.decode(token, options={"verify_signature": False})
# Local Session (HS256)
if unverified.get("iss") == "cortex-hub-internal":
decoded = jwt.decode(token, settings.SECRET_KEY, algorithms=["HS256"])
user_id = decoded.get("sub")
user = db.query(models.User).filter(models.User.id == user_id).first()
if not user:
raise HTTPException(status_code=401, detail="User in session token no longer exists.")
return user
# OIDC Token (RS256)
user = await global_services.auth_service.verify_id_token(token, db)
return user
except Exception as e:
logging.warning(f"JWT Verification failed: {e}")
if settings.OIDC_ENABLED:
raise HTTPException(status_code=401, detail=f"Invalid authentication token: {str(e)}")
# 2. Fallback to X-User-ID (Legacy / Proxy Identity Claim)
if not x_user_id:
logging.debug("No identity provided (no Authorization and no X-User-ID)")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Authentication required: Provide a Bearer token (JWT) or X-User-ID header."
)
# HARDENING: In OIDC mode, we strictly reject plain X-User-ID.
# Identity must be verified via JWT (ID Token) to prevent spoofing.
if settings.OIDC_ENABLED:
if not x_proxy_secret or x_proxy_secret != settings.SECRET_KEY:
logging.warning(f"Insecure X-User-ID '{x_user_id}' rejected in strict OIDC mode.")
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Insecure Identity Claim: Provide a valid OIDC ID Token (JWT) in the Authorization header."
)
# 3. Final verification of Identity Claim
user = db.query(models.User).filter(models.User.id == x_user_id).first()
if not user:
raise HTTPException(
status_code=status.HTTP_404_NOT_FOUND,
detail="User not found"
)
if not user.password_hash and not settings.OIDC_ENABLED:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Account disabled: OIDC is inactive and no password is set."
)
return user
async def get_optional_user(
request: Request,
db: Session = Depends(get_db),
authorization: Annotated[Optional[str], Header()] = None,
x_user_id: Annotated[Optional[str], Header()] = None,
x_proxy_secret: Annotated[Optional[str], Header()] = None,
) -> Optional[models.User]:
"""Soft-auth version of get_current_user. Returns None instead of raising if no valid identity is found."""
try:
return await get_current_user(request, db, authorization, x_user_id, x_proxy_secret)
except Exception:
return None
async def get_current_admin(
current_user: models.User = Depends(get_current_user)
) -> models.User:
if current_user.role != "admin":
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Admin role required"
)
return current_user
class ServiceContainer:
"""
A flexible container for managing and providing various application services.
Services are added dynamically using the `with_service` method.
"""
def __init__(self):
# Use a dictionary to store services, mapping their names to instances
self._services = {}
# Explicitly declare known services
self.document_service = None
self.rag_service = None
self.orchestrator = None
self.settings = None
self.node_registry_service = None
self.browser_service = None
self.tool_service = None
self.stt_service = None
self.tts_service = None
self.prompt_service = None
self.session_service = None
self.user_service = None
self.mesh_service = None
self.auth_service = None
self.preference_service = None
self.agent_scheduler = None
self.agent_service = None
def with_service(self, name: str, service: Any):
"""
Adds a service to the container.
Args:
name (str): The name to assign to the service (e.g., 'tts_service').
service (Any): The service instance to add.
"""
setattr(self, name, service)
return self
def with_document_service(self, vector_store: Optional[FaissVectorStore]):
"""
Adds a DocumentService instance to the container.
"""
if vector_store:
self.document_service = DocumentService(vector_store=vector_store)
return self
def with_rag_service(self, retrievers: List[Retriever], prompt_service = None, tool_service = None, node_registry_service = None):
"""
Adds a RAGService instance to the container.
"""
self.rag_service = RAGService(retrievers=retrievers, prompt_service=prompt_service, tool_service=tool_service, node_registry_service=node_registry_service, services=self)
return self
def __getattr__(self, name: str) -> Any:
"""
Allows services to be accessed directly as attributes (e.g., container.rag_service).
"""
raise AttributeError(f"'{self.__class__.__name__}' object has no service named '{name}'")