| """
|
| Public API Authentication
|
|
|
| API Key authentication for the public Evaluation-as-a-Service API.
|
| """
|
|
|
| import hashlib
|
| import secrets
|
| import uuid
|
| from datetime import datetime
|
| from typing import Optional, Any
|
|
|
| from fastapi import Depends, HTTPException, status
|
| from fastapi.security import APIKeyHeader
|
| from sqlalchemy import select
|
| from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
| from backend.api.dependencies import get_db
|
| from backend.db.models import APIKey as APIKeyModel
|
| from backend.logging.logger import get_logger
|
|
|
|
|
|
|
| logger = get_logger("public_api_auth", component="api")
|
|
|
|
|
|
|
|
|
|
|
|
|
| API_KEY_HEADER = APIKeyHeader(
|
| name="Authorization",
|
| auto_error=False,
|
| description="API Key in format: Bearer <API_KEY>"
|
| )
|
|
|
|
|
|
|
|
|
|
|
|
|
| def hash_api_key(key: str) -> str:
|
| """
|
| Hash an API key using SHA-256.
|
|
|
| Args:
|
| key: The raw API key to hash
|
|
|
| Returns:
|
| The hexadecimal hash of the key
|
| """
|
| return hashlib.sha256(key.encode()).hexdigest()
|
|
|
|
|
| def generate_api_key() -> str:
|
| """
|
| Generate a new random API key.
|
|
|
| Returns:
|
| A random 32-byte API key encoded as a hex string
|
| """
|
| return secrets.token_hex(32)
|
|
|
|
|
| async def verify_api_key(
|
| key: str,
|
| db: AsyncSession
|
| ) -> Optional[APIKeyModel]:
|
| """
|
| Verify an API key against the database.
|
|
|
| Args:
|
| key: The raw API key to verify
|
| db: Database session
|
|
|
| Returns:
|
| The APIKeyModel if valid, None otherwise
|
| """
|
| key_hash = hash_api_key(key)
|
|
|
| result = await db.execute(
|
| select(APIKeyModel).where(
|
| APIKeyModel.key_hash == key_hash,
|
| APIKeyModel.active == True
|
| )
|
| )
|
| api_key = result.scalar_one_or_none()
|
|
|
| if api_key:
|
|
|
| api_key.last_used = datetime.utcnow()
|
| await db.commit()
|
|
|
| logger.info(
|
| "API key verified",
|
| metadata={
|
| "api_key_id": str(api_key.id),
|
| "owner": api_key.owner
|
| }
|
| )
|
|
|
| return api_key
|
|
|
|
|
| async def get_current_api_key(
|
| api_key_header: Optional[str] = Depends(API_KEY_HEADER),
|
| db: AsyncSession = Depends(get_db)
|
| ) -> Any:
|
| """
|
| Dependency to get the current authenticated API key.
|
|
|
| Args:
|
| api_key_header: The Authorization header value
|
| db: Database session
|
|
|
| Returns:
|
| The authenticated APIKeyModel
|
|
|
| Raises:
|
| HTTPException: If the API key is invalid or missing
|
| """
|
| if not api_key_header:
|
| logger.warning("Missing API key header")
|
| raise HTTPException(
|
| status_code=status.HTTP_401_UNAUTHORIZED,
|
| detail={
|
| "error": "authentication_failed",
|
| "message": "API key is required. Include it in the Authorization header: Bearer <API_KEY>"
|
| }
|
| )
|
|
|
|
|
| parts = api_key_header.split(" ")
|
| if len(parts) != 2 or parts[0].lower() != "bearer":
|
| logger.warning("Invalid Authorization header format")
|
| raise HTTPException(
|
| status_code=status.HTTP_401_UNAUTHORIZED,
|
| detail={
|
| "error": "authentication_failed",
|
| "message": "Invalid Authorization header format. Use: Bearer <API_KEY>"
|
| }
|
| )
|
|
|
| api_key = parts[1]
|
|
|
|
|
| verified_key = await verify_api_key(api_key, db)
|
|
|
| if not verified_key:
|
| logger.warning("Invalid API key attempted", metadata={"key_prefix": api_key[:8] + "..."})
|
| raise HTTPException(
|
| status_code=status.HTTP_401_UNAUTHORIZED,
|
| detail={
|
| "error": "authentication_failed",
|
| "message": "Invalid or inactive API key"
|
| }
|
| )
|
|
|
| return verified_key
|
|
|
|
|
|
|
|
|
|
|
|
|
| async def create_api_key(
|
| owner: str,
|
| rate_limit: int = 100,
|
| evaluation_mode_restriction: Optional[str] = None,
|
| mutation_enabled: bool = True,
|
| monitoring_only: bool = False,
|
| db: Optional[AsyncSession] = None
|
| ) -> tuple[str, APIKeyModel]:
|
| """
|
| Create a new API key.
|
|
|
| Args:
|
| owner: Owner identifier
|
| rate_limit: Requests per minute limit
|
| evaluation_mode_restriction: Optional evaluation mode restriction
|
| mutation_enabled: Whether mutation is enabled
|
| monitoring_only: Whether monitoring-only mode
|
| db: Database session (optional, for testing)
|
|
|
| Returns:
|
| Tuple of (raw_api_key, APIKeyModel)
|
| """
|
|
|
| raw_key = generate_api_key()
|
| key_hash = hash_api_key(raw_key)
|
|
|
|
|
| api_key = APIKeyModel(
|
| id=uuid.uuid4(),
|
| key_hash=key_hash,
|
| owner=owner,
|
| rate_limit=rate_limit,
|
| created_at=datetime.utcnow(),
|
| active=True,
|
| evaluation_mode_restriction=evaluation_mode_restriction,
|
| mutation_enabled=mutation_enabled,
|
| monitoring_only=monitoring_only
|
| )
|
|
|
| if db:
|
| db.add(api_key)
|
| await db.commit()
|
| await db.refresh(api_key)
|
|
|
| logger.info(
|
| "API key created",
|
| metadata={
|
| "api_key_id": str(api_key.id),
|
| "owner": owner,
|
| "rate_limit": rate_limit
|
| }
|
| )
|
|
|
| return raw_key, api_key
|
|
|
|
|
| async def revoke_api_key(
|
| api_key_id: uuid.UUID,
|
| db: AsyncSession
|
| ) -> bool:
|
| """
|
| Revoke an API key.
|
|
|
| Args:
|
| api_key_id: ID of the API key to revoke
|
| db: Database session
|
|
|
| Returns:
|
| True if revoked, False if not found
|
| """
|
| result = await db.execute(
|
| select(APIKeyModel).where(APIKeyModel.id == api_key_id)
|
| )
|
| api_key = result.scalar_one_or_none()
|
|
|
| if not api_key:
|
| return False
|
|
|
| api_key.active = False
|
| await db.commit()
|
|
|
| logger.info(
|
| "API key revoked",
|
| metadata={
|
| "api_key_id": str(api_key_id),
|
| "owner": api_key.owner
|
| }
|
| )
|
|
|
| return True
|
|
|
|
|
| async def get_api_key_info(
|
| api_key_id: uuid.UUID,
|
| db: AsyncSession
|
| ) -> Optional[APIKeyModel]:
|
| """
|
| Get information about an API key (without exposing the hash).
|
|
|
| Args:
|
| api_key_id: ID of the API key
|
| db: Database session
|
|
|
| Returns:
|
| APIKeyModel if found
|
| """
|
| result = await db.execute(
|
| select(APIKeyModel).where(APIKeyModel.id == api_key_id)
|
| )
|
| return result.scalar_one_or_none()
|
|
|