Files
virtual_board_member/app/core/auth.py
Jonathan Pressnell 1a8ec37bed feat: Complete Week 2 - Document Processing Pipeline
- Implement multi-format document support (PDF, XLSX, CSV, PPTX, TXT, Images)
- Add S3-compatible storage service with tenant isolation
- Create document organization service with hierarchical folders and tagging
- Implement advanced document processing with table/chart extraction
- Add batch upload capabilities (up to 50 files)
- Create comprehensive document validation and security scanning
- Implement automatic metadata extraction and categorization
- Add document version control system
- Update DEVELOPMENT_PLAN.md to mark Week 2 as completed
- Add WEEK2_COMPLETION_SUMMARY.md with detailed implementation notes
- All tests passing (6/6) - 100% success rate
2025-08-08 15:47:43 -04:00

209 lines
7.5 KiB
Python

"""
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