complianceos / api /auth.py
bhriguverma's picture
Upload api/auth.py with huggingface_hub
dd8ed55 verified
Raw
History Blame Contribute Delete
6.19 kB
"""
api/auth.py — API key authentication and role-based authorization.
Roles:
admin — all permissions
compliance_lead — query, view_results, manage_alerts
reader — query, view_results
auditor — view_audit only
API keys are loaded from the env var COMPLIANCE_API_KEYS in the format:
key1:role1:tenant1,key2:role2:tenant2
Default (for local dev) is set below.
"""
from __future__ import annotations
import logging
import os
from dataclasses import dataclass
from typing import Optional
from fastapi import HTTPException, status
from fastapi.security import APIKeyHeader
log = logging.getLogger(__name__)
# ─── Security scheme ──────────────────────────────────────────────────────────
API_KEY_HEADER = APIKeyHeader(name="X-API-Key", auto_error=False)
# ─── Role definitions ─────────────────────────────────────────────────────────
ROLE_PERMISSIONS: dict[str, set[str]] = {
"admin": {"query", "view_results", "manage_alerts", "view_audit", "admin"},
"compliance_lead": {"query", "view_results", "manage_alerts"},
"reader": {"query", "view_results"},
"auditor": {"view_audit"},
}
VALID_ROLES = set(ROLE_PERMISSIONS.keys())
# ─── User model ───────────────────────────────────────────────────────────────
@dataclass
class User:
api_key: str
role: str
tenant_id: str
@property
def permissions(self) -> set[str]:
return ROLE_PERMISSIONS.get(self.role, set())
def has_permission(self, perm: str) -> bool:
return perm in self.permissions
# ─── API key store ────────────────────────────────────────────────────────────
_DEFAULT_KEYS = (
"admin-key-001:admin:tenant-default,"
"reader-key-001:reader:tenant-default"
)
# Mutable runtime dict — admin can add keys at runtime via POST /admin/api-keys
_API_KEY_STORE: dict[str, tuple[str, str]] = {} # key -> (role, tenant_id)
def _load_api_keys() -> None:
"""Parse COMPLIANCE_API_KEYS env var and populate _API_KEY_STORE."""
raw = os.environ.get("COMPLIANCE_API_KEYS", _DEFAULT_KEYS)
for entry in raw.split(","):
entry = entry.strip()
if not entry:
continue
parts = entry.split(":")
if len(parts) != 3:
log.warning("Skipping malformed API key entry (expected key:role:tenant): %r", entry)
continue
key, role, tenant = parts[0].strip(), parts[1].strip(), parts[2].strip()
if role not in VALID_ROLES:
log.warning("Skipping API key with unknown role %r", role)
continue
_API_KEY_STORE[key] = (role, tenant)
log.info("Loaded %d API keys", len(_API_KEY_STORE))
# Load on module import
_load_api_keys()
def add_api_key(key: str, role: str, tenant_id: str) -> None:
"""Add or update an API key at runtime (admin action)."""
if role not in VALID_ROLES:
raise ValueError(f"Unknown role {role!r}. Valid roles: {sorted(VALID_ROLES)}")
_API_KEY_STORE[key] = (role, tenant_id)
log.info("API key added/updated: role=%s tenant=%s", role, tenant_id)
def list_api_keys() -> list[dict]:
"""Return sanitized list of keys (masked) for admin inspection."""
result = []
for key, (role, tenant) in _API_KEY_STORE.items():
masked = key[:4] + "****" + key[-4:] if len(key) >= 8 else "****"
result.append({"key_preview": masked, "role": role, "tenant_id": tenant})
return result
# ─── FastAPI dependency ───────────────────────────────────────────────────────
def get_current_user(api_key: Optional[str] = None) -> User:
"""
Validate X-API-Key header against _API_KEY_STORE.
Call this with Security(get_current_user) where api_key comes from
API_KEY_HEADER, or pass directly for testing.
"""
if not api_key:
raise HTTPException(
status_code=status.HTTP_401_UNAUTHORIZED,
detail="Missing X-API-Key header",
)
entry = _API_KEY_STORE.get(api_key)
if entry is None:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail="Invalid API key",
)
role, tenant_id = entry
return User(api_key=api_key, role=role, tenant_id=tenant_id)
def require_role(*roles: str):
"""
FastAPI dependency factory — raises 403 if the user's role is not in roles.
Usage:
@app.get("/admin/...", dependencies=[Depends(require_role("admin"))])
"""
from fastapi import Depends, Security
def _dependency(api_key: Optional[str] = Security(API_KEY_HEADER)) -> User:
user = get_current_user(api_key)
if user.role not in roles:
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Role '{user.role}' is not authorized. Required: {list(roles)}",
)
return user
return Depends(_dependency)
def require_permission(permission: str):
"""
FastAPI dependency factory — raises 403 if the user lacks the permission.
Usage:
@app.get("/audit", dependencies=[Depends(require_permission("view_audit"))])
"""
from fastapi import Depends, Security
def _dependency(api_key: Optional[str] = Security(API_KEY_HEADER)) -> User:
user = get_current_user(api_key)
if not user.has_permission(permission):
raise HTTPException(
status_code=status.HTTP_403_FORBIDDEN,
detail=f"Permission '{permission}' required. Your role '{user.role}' does not have it.",
)
return user
return Depends(_dependency)