666f739f4e
- Add ComplianceTemplate model in app/models.py - Create database migration 027_add_compliance_templates - Add compliance_enabled feature flag to config and settings metadata - Create compliance_service.py with pre-built template definitions and evaluation - Create compliance API endpoints (list, get, apply, status, summary) - Create compliance admin view and dashboard template - Add compliance link to admin navigation (desktop and mobile) - Seed compliance templates at application startup - Add comprehensive tests (29 passing) Co-authored-by: christianlouis <361235+christianlouis@users.noreply.github.com>
180 lines
5.6 KiB
Python
180 lines
5.6 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 (
|
||
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.
|
||
"""
|
||
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)
|