aegislm / backend /api /public /auth.py
ACA050's picture
Upload 50 files
1a4aa87 verified
Raw
History Blame Contribute Delete
7.39 kB
"""
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
# Initialize logger
logger = get_logger("public_api_auth", component="api")
# =============================================================================
# API Key Header
# =============================================================================
API_KEY_HEADER = APIKeyHeader(
name="Authorization",
auto_error=False,
description="API Key in format: Bearer <API_KEY>"
)
# =============================================================================
# Helper Functions
# =============================================================================
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:
# Update last used timestamp
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>"
}
)
# Parse the header - expect "Bearer <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]
# Verify the key
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
# =============================================================================
# API Key Management Functions
# =============================================================================
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)
"""
# Generate the raw key (this is shown only once)
raw_key = generate_api_key()
key_hash = hash_api_key(raw_key)
# Create the model
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()