""" Authentication and authorization service for the Virtual Board Member AI System. """ import logging from datetime import datetime, timedelta from typing import Optional, Dict, Any from fastapi import HTTPException, Depends, status from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials from jose import JWTError, jwt from passlib.context import CryptContext from sqlalchemy.orm import Session import redis.asyncio as redis from app.core.config import settings from app.core.database import get_db from app.models.user import User from app.models.tenant import Tenant logger = logging.getLogger(__name__) # Security configurations security = HTTPBearer() pwd_context = CryptContext(schemes=["bcrypt"], deprecated="auto") class AuthService: """Authentication service with tenant-aware authentication.""" def __init__(self): self.redis_client = None self._init_redis() async def _init_redis(self): """Initialize Redis connection for session management.""" try: self.redis_client = redis.from_url( settings.REDIS_URL, encoding="utf-8", decode_responses=True ) await self.redis_client.ping() logger.info("Redis connection established for auth service") except Exception as e: logger.error(f"Failed to connect to Redis: {e}") self.redis_client = None def verify_password(self, plain_password: str, hashed_password: str) -> bool: """Verify a password against its hash.""" return pwd_context.verify(plain_password, hashed_password) def get_password_hash(self, password: str) -> str: """Generate password hash.""" return pwd_context.hash(password) def create_access_token(self, data: Dict[str, Any], expires_delta: Optional[timedelta] = None) -> str: """Create JWT access token.""" to_encode = data.copy() if expires_delta: expire = datetime.utcnow() + expires_delta else: expire = datetime.utcnow() + timedelta(minutes=settings.ACCESS_TOKEN_EXPIRE_MINUTES) to_encode.update({"exp": expire}) encoded_jwt = jwt.encode(to_encode, settings.SECRET_KEY, algorithm=settings.ALGORITHM) return encoded_jwt def verify_token(self, token: str) -> Dict[str, Any]: """Verify and decode JWT token.""" try: payload = jwt.decode(token, settings.SECRET_KEY, algorithms=[settings.ALGORITHM]) return payload except JWTError as e: logger.error(f"Token verification failed: {e}") raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Could not validate credentials", headers={"WWW-Authenticate": "Bearer"}, ) async def create_session(self, user_id: str, tenant_id: str, token: str) -> bool: """Create user session in Redis.""" if not self.redis_client: logger.warning("Redis not available, session not created") return False try: session_key = f"session:{user_id}:{tenant_id}" session_data = { "user_id": user_id, "tenant_id": tenant_id, "token": token, "created_at": datetime.utcnow().isoformat(), "expires_at": (datetime.utcnow() + timedelta(hours=24)).isoformat() } await self.redis_client.hset(session_key, mapping=session_data) await self.redis_client.expire(session_key, 86400) # 24 hours logger.info(f"Session created for user {user_id} in tenant {tenant_id}") return True except Exception as e: logger.error(f"Failed to create session: {e}") return False async def get_session(self, user_id: str, tenant_id: str) -> Optional[Dict[str, Any]]: """Get user session from Redis.""" if not self.redis_client: return None try: session_key = f"session:{user_id}:{tenant_id}" session_data = await self.redis_client.hgetall(session_key) if session_data: expires_at = datetime.fromisoformat(session_data["expires_at"]) if datetime.utcnow() < expires_at: return session_data else: await self.redis_client.delete(session_key) return None except Exception as e: logger.error(f"Failed to get session: {e}") return None async def invalidate_session(self, user_id: str, tenant_id: str) -> bool: """Invalidate user session.""" if not self.redis_client: return False try: session_key = f"session:{user_id}:{tenant_id}" await self.redis_client.delete(session_key) logger.info(f"Session invalidated for user {user_id} in tenant {tenant_id}") return True except Exception as e: logger.error(f"Failed to invalidate session: {e}") return False # Global auth service instance auth_service = AuthService() async def get_current_user( credentials: HTTPAuthorizationCredentials = Depends(security), db: Session = Depends(get_db) ) -> User: """Get current authenticated user with tenant context.""" token = credentials.credentials payload = auth_service.verify_token(token) user_id: str = payload.get("sub") tenant_id: str = payload.get("tenant_id") if user_id is None or tenant_id is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Invalid token payload", headers={"WWW-Authenticate": "Bearer"}, ) # Verify session exists session = await auth_service.get_session(user_id, tenant_id) if not session: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="Session expired or invalid", headers={"WWW-Authenticate": "Bearer"}, ) # Get user from database user = db.query(User).filter( User.id == user_id, User.tenant_id == tenant_id ).first() if user is None: raise HTTPException( status_code=status.HTTP_401_UNAUTHORIZED, detail="User not found", headers={"WWW-Authenticate": "Bearer"}, ) return user async def get_current_active_user(current_user: User = Depends(get_current_user)) -> User: """Get current active user.""" if not current_user.is_active: raise HTTPException( status_code=status.HTTP_400_BAD_REQUEST, detail="Inactive user" ) return current_user def require_role(required_role: str): """Decorator to require specific user role.""" def role_checker(current_user: User = Depends(get_current_active_user)) -> User: if current_user.role != required_role and current_user.role != "admin": raise HTTPException( status_code=status.HTTP_403_FORBIDDEN, detail="Insufficient permissions" ) return current_user return role_checker def require_tenant_access(): """Decorator to ensure user has access to the specified tenant.""" def tenant_checker(current_user: User = Depends(get_current_active_user)) -> User: # Additional tenant-specific checks can be added here return current_user return tenant_checker