204000aabc
Resolve 3 merge conflicts and renumber the automation_hooks migration to follow main's migration chain (036_add_document_translation_fields). Conflicts resolved: - app/api/__init__.py: add automation_router alongside main's new routers - app/utils/settings_service.py: add automation_hooks_enabled alongside compliance_enabled - tests/conftest.py: add AutomationHook alongside AuditLog/ComplianceTemplate imports Migration renumbered: - 027_add_automation_hooks → 037_add_automation_hooks - down_revision: 026_add_scheduled_jobs → 036_add_document_translation_fields Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
184 lines
5.7 KiB
Python
184 lines
5.7 KiB
Python
"""API endpoints for managing compliance templates (GDPR, HIPAA, SOC2).
|
||
|
||
All endpoints require admin privileges.
|
||
|
||
Available routes:
|
||
GET /api/compliance/templates – list all compliance templates
|
||
GET /api/compliance/templates/{name} – get a single template with checks
|
||
POST /api/compliance/templates/{name}/apply – one-click apply a template
|
||
GET /api/compliance/templates/{name}/status – evaluate compliance status
|
||
GET /api/compliance/summary – overall compliance dashboard data
|
||
"""
|
||
|
||
import logging
|
||
from typing import Annotated, Any
|
||
|
||
from fastapi import APIRouter, Depends, HTTPException, Request, status
|
||
from pydantic import BaseModel
|
||
from sqlalchemy.orm import Session
|
||
|
||
from app.database import get_db
|
||
from app.utils.compliance_service import (
|
||
COMPLIANCE_TEMPLATES,
|
||
apply_template,
|
||
evaluate_template_status,
|
||
get_all_templates,
|
||
get_compliance_summary,
|
||
get_template_by_name,
|
||
)
|
||
|
||
logger = logging.getLogger(__name__)
|
||
router = APIRouter(prefix="/compliance", tags=["compliance"])
|
||
|
||
DbSession = Annotated[Session, Depends(get_db)]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Authorisation helper
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
def _require_admin(request: Request) -> dict:
|
||
"""Ensure the caller is an admin; raises HTTP 403 otherwise."""
|
||
user = request.session.get("user")
|
||
if not user or not user.get("is_admin"):
|
||
raise HTTPException(status_code=status.HTTP_403_FORBIDDEN, detail="Admin access required")
|
||
return user
|
||
|
||
|
||
AdminUser = Annotated[dict, Depends(_require_admin)]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Pydantic response models
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
class CheckResult(BaseModel):
|
||
"""Individual compliance check result."""
|
||
|
||
key: str
|
||
label: str
|
||
description: str
|
||
expected: str
|
||
actual: str
|
||
passing: bool
|
||
|
||
|
||
class TemplateStatusResponse(BaseModel):
|
||
"""Status evaluation for a compliance template."""
|
||
|
||
status: str
|
||
total: int
|
||
passed: int
|
||
failed: int
|
||
check_results: list[CheckResult]
|
||
|
||
|
||
class TemplateResponse(BaseModel):
|
||
"""Full compliance template representation."""
|
||
|
||
id: int
|
||
name: str
|
||
display_name: str
|
||
description: str | None
|
||
enabled: bool
|
||
status: str
|
||
applied_at: str | None
|
||
applied_by: str | None
|
||
settings: dict[str, str]
|
||
checks: list[dict[str, Any]]
|
||
check_count: int
|
||
|
||
|
||
class ApplyResponse(BaseModel):
|
||
"""Result of applying a compliance template."""
|
||
|
||
success: bool
|
||
template: str | None = None
|
||
applied_settings: dict[str, str] | None = None
|
||
errors: list[str] | None = None
|
||
error: str | None = None
|
||
status: TemplateStatusResponse | None = None
|
||
|
||
|
||
class SummaryTemplateResponse(BaseModel):
|
||
"""Per-template summary for the compliance dashboard."""
|
||
|
||
name: str
|
||
display_name: str
|
||
enabled: bool
|
||
status: str
|
||
total: int
|
||
passed: int
|
||
failed: int
|
||
applied_at: str | None
|
||
applied_by: str | None
|
||
|
||
|
||
class ComplianceSummaryResponse(BaseModel):
|
||
"""Overall compliance dashboard summary."""
|
||
|
||
overall_status: str
|
||
total_checks: int
|
||
total_passed: int
|
||
total_failed: int
|
||
templates: list[SummaryTemplateResponse]
|
||
|
||
|
||
# ---------------------------------------------------------------------------
|
||
# Endpoints
|
||
# ---------------------------------------------------------------------------
|
||
|
||
|
||
@router.get("/templates", response_model=list[TemplateResponse])
|
||
async def list_templates(db: DbSession, admin: AdminUser) -> list[dict[str, Any]]:
|
||
"""List all compliance templates with their current status."""
|
||
return get_all_templates(db)
|
||
|
||
|
||
@router.get("/templates/{name}", response_model=TemplateResponse)
|
||
async def get_template(name: str, db: DbSession, admin: AdminUser) -> dict[str, Any]:
|
||
"""Get a single compliance template by name."""
|
||
templates = get_all_templates(db)
|
||
for t in templates:
|
||
if t["name"] == name:
|
||
return t
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Template '{name}' not found")
|
||
|
||
|
||
@router.post("/templates/{name}/apply", response_model=ApplyResponse)
|
||
async def apply_compliance_template(name: str, db: DbSession, admin: AdminUser) -> dict[str, Any]:
|
||
"""Apply a compliance template (one-click).
|
||
|
||
Writes all template settings to the database and evaluates the resulting
|
||
compliance status.
|
||
"""
|
||
if name not in COMPLIANCE_TEMPLATES:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Template '{name}' not found")
|
||
|
||
template = get_template_by_name(db, name)
|
||
if template is None:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Template '{name}' not found")
|
||
|
||
admin_email = admin.get("email", "admin")
|
||
result = apply_template(db, name, applied_by=admin_email)
|
||
if not result.get("success") and result.get("error"):
|
||
raise HTTPException(status_code=status.HTTP_500_INTERNAL_SERVER_ERROR, detail=result["error"])
|
||
return result
|
||
|
||
|
||
@router.get("/templates/{name}/status", response_model=TemplateStatusResponse)
|
||
async def get_template_status(name: str, db: DbSession, admin: AdminUser) -> dict[str, Any]:
|
||
"""Evaluate the live compliance status of a template."""
|
||
template = get_template_by_name(db, name)
|
||
if template is None:
|
||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail=f"Template '{name}' not found")
|
||
return evaluate_template_status(db, name)
|
||
|
||
|
||
@router.get("/summary", response_model=ComplianceSummaryResponse)
|
||
async def compliance_summary(db: DbSession, admin: AdminUser) -> dict[str, Any]:
|
||
"""Overall compliance dashboard summary across all templates."""
|
||
return get_compliance_summary(db)
|