Major changes: - Consolidate 3 different User models into single unified model (models/user.py) - Use UUID primary keys throughout (matches existing database schema) - Add comprehensive authentication fields while preserving existing data - Remove duplicate User model from auth.py, keep APIKey/RefreshToken/TokenBlacklist - Update all imports to use unified User model consistently - Create database migration (002_add_auth_fields.sql) for safe schema upgrade - Fix frontend User interface to handle UUID string IDs - Add backward compatibility fields (name property, role field) - Maintain relationships for authentication features (api_keys, refresh_tokens) Schema conflicts resolved: ✅ Migration schema (UUID, 7 fields) + Basic model (Integer, 6 fields) + Auth model (Integer, 10 fields) → Unified model (UUID, 12 fields with full backward compatibility) ✅ Field inconsistencies (name vs full_name) resolved with compatibility property ✅ Database foreign key constraints updated for UUID relationships ✅ JWT token handling fixed for UUID user IDs This completes the holistic database schema unification requested after quick patching caused conflicts. All existing data preserved, full auth system functional. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
199 lines
6.4 KiB
Python
199 lines
6.4 KiB
Python
"""
|
|
Authentication dependencies for FastAPI endpoints.
|
|
Provides dependency injection for authentication and authorization.
|
|
"""
|
|
|
|
from typing import Optional, Dict, Any, List
|
|
from fastapi import Depends, HTTPException, status, Request, Header
|
|
from fastapi.security import HTTPBearer, HTTPAuthorizationCredentials
|
|
from sqlalchemy.orm import Session
|
|
from app.core.database import get_db
|
|
from app.core.security import AuthManager, AuthenticationError
|
|
|
|
security = HTTPBearer(auto_error=False)
|
|
|
|
|
|
def get_api_key_from_header(x_api_key: Optional[str] = Header(None)) -> Optional[str]:
|
|
"""Extract API key from X-API-Key header."""
|
|
return x_api_key
|
|
|
|
|
|
def get_current_user_context(
|
|
request: Request,
|
|
db: Session = Depends(get_db),
|
|
authorization: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
|
api_key: Optional[str] = Depends(get_api_key_from_header),
|
|
) -> Dict[str, Any]:
|
|
"""
|
|
Get current authenticated user context.
|
|
Supports both JWT Bearer tokens and API key authentication.
|
|
"""
|
|
try:
|
|
user_context = AuthManager.authenticate_request(
|
|
session=db,
|
|
authorization=authorization,
|
|
api_key=api_key
|
|
)
|
|
|
|
# Add request metadata
|
|
user_context["request_ip"] = request.client.host if request.client else None
|
|
user_context["user_agent"] = request.headers.get("user-agent")
|
|
|
|
return user_context
|
|
|
|
except AuthenticationError as e:
|
|
raise HTTPException(
|
|
status_code=e.status_code,
|
|
detail=e.message,
|
|
headers={"WWW-Authenticate": "Bearer"},
|
|
)
|
|
|
|
|
|
def get_current_user(
|
|
user_context: Dict[str, Any] = Depends(get_current_user_context),
|
|
) -> Dict[str, Any]:
|
|
"""Get current authenticated user (alias for get_current_user_context)."""
|
|
return user_context
|
|
|
|
|
|
def get_current_active_user(
|
|
user_context: Dict[str, Any] = Depends(get_current_user_context),
|
|
db: Session = Depends(get_db),
|
|
) -> Dict[str, Any]:
|
|
"""Get current authenticated and active user."""
|
|
from app.models.user import User
|
|
|
|
user = db.query(User).filter(User.id == user_context["user_id"]).first()
|
|
if not user:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_404_NOT_FOUND,
|
|
detail="User not found"
|
|
)
|
|
|
|
if not user.is_active:
|
|
raise HTTPException(
|
|
status_code=status.HTTP_400_BAD_REQUEST,
|
|
detail="Inactive user"
|
|
)
|
|
|
|
return user_context
|
|
|
|
|
|
def get_current_superuser(
|
|
user_context: Dict[str, Any] = Depends(get_current_active_user),
|
|
) -> Dict[str, Any]:
|
|
"""Get current authenticated superuser."""
|
|
if not user_context.get("is_superuser", False):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail="Not enough permissions"
|
|
)
|
|
return user_context
|
|
|
|
|
|
def require_scope(required_scope: str):
|
|
"""
|
|
Dependency factory to require specific scope for an endpoint.
|
|
|
|
Usage:
|
|
@app.get("/admin/users", dependencies=[Depends(require_scope("admin"))])
|
|
async def get_users():
|
|
...
|
|
"""
|
|
def scope_dependency(
|
|
user_context: Dict[str, Any] = Depends(get_current_active_user)
|
|
) -> Dict[str, Any]:
|
|
from app.core.security import APIKeyManager
|
|
|
|
user_scopes = user_context.get("scopes", [])
|
|
|
|
if not APIKeyManager.check_scope_permission(user_scopes, required_scope):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"Insufficient permissions. Required scope: {required_scope}"
|
|
)
|
|
|
|
return user_context
|
|
|
|
return scope_dependency
|
|
|
|
|
|
def require_scopes(required_scopes: List[str]):
|
|
"""
|
|
Dependency factory to require multiple scopes for an endpoint.
|
|
User must have ALL specified scopes.
|
|
"""
|
|
def scopes_dependency(
|
|
user_context: Dict[str, Any] = Depends(get_current_active_user)
|
|
) -> Dict[str, Any]:
|
|
from app.core.security import APIKeyManager
|
|
|
|
user_scopes = user_context.get("scopes", [])
|
|
|
|
for scope in required_scopes:
|
|
if not APIKeyManager.check_scope_permission(user_scopes, scope):
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"Insufficient permissions. Required scopes: {', '.join(required_scopes)}"
|
|
)
|
|
|
|
return user_context
|
|
|
|
return scopes_dependency
|
|
|
|
|
|
def require_any_scope(required_scopes: List[str]):
|
|
"""
|
|
Dependency factory to require at least one of the specified scopes.
|
|
User must have ANY of the specified scopes.
|
|
"""
|
|
def any_scope_dependency(
|
|
user_context: Dict[str, Any] = Depends(get_current_active_user)
|
|
) -> Dict[str, Any]:
|
|
from app.core.security import APIKeyManager
|
|
|
|
user_scopes = user_context.get("scopes", [])
|
|
|
|
for scope in required_scopes:
|
|
if APIKeyManager.check_scope_permission(user_scopes, scope):
|
|
return user_context
|
|
|
|
raise HTTPException(
|
|
status_code=status.HTTP_403_FORBIDDEN,
|
|
detail=f"Insufficient permissions. Required one of: {', '.join(required_scopes)}"
|
|
)
|
|
|
|
return any_scope_dependency
|
|
|
|
|
|
# Optional authentication (won't raise error if not authenticated)
|
|
def get_optional_user_context(
|
|
db: Session = Depends(get_db),
|
|
authorization: Optional[HTTPAuthorizationCredentials] = Depends(security),
|
|
api_key: Optional[str] = Depends(get_api_key_from_header),
|
|
) -> Optional[Dict[str, Any]]:
|
|
"""
|
|
Get current user context if authenticated, None otherwise.
|
|
Useful for endpoints that work with or without authentication.
|
|
"""
|
|
try:
|
|
return AuthManager.authenticate_request(
|
|
session=db,
|
|
authorization=authorization,
|
|
api_key=api_key
|
|
)
|
|
except AuthenticationError:
|
|
return None
|
|
|
|
|
|
# Common scope dependencies for convenience
|
|
require_admin = require_scope("admin")
|
|
require_agents_read = require_scope("agents:read")
|
|
require_agents_write = require_scope("agents:write")
|
|
require_workflows_read = require_scope("workflows:read")
|
|
require_workflows_write = require_scope("workflows:write")
|
|
require_tasks_read = require_scope("tasks:read")
|
|
require_tasks_write = require_scope("tasks:write")
|
|
require_metrics_read = require_scope("metrics:read")
|
|
require_system_read = require_scope("system:read")
|
|
require_system_write = require_scope("system:write") |