149 lines
5.1 KiB
Python
149 lines
5.1 KiB
Python
# Copyright (c) 2025 Patrick Motsch
|
|
# All rights reserved.
|
|
"""Compliance & Audit API endpoints.
|
|
|
|
Provides three views:
|
|
- AI Data-Flow Log (Tab A)
|
|
- Security/GDPR Audit Log (Tab B)
|
|
- Aggregated Audit Statistics (Tab C)
|
|
|
|
RBAC: mandate-admin or compliance-viewer role required.
|
|
"""
|
|
|
|
import logging
|
|
from typing import Optional
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException, Query, Path, status
|
|
from starlette.requests import Request
|
|
|
|
from modules.auth import limiter, getCurrentUser, getRequestContext, RequestContext
|
|
from modules.datamodels.datamodelUam import User
|
|
from modules.shared.i18nRegistry import apiRouteContext
|
|
|
|
logger = logging.getLogger(__name__)
|
|
routeApiMsg = apiRouteContext("routeAudit")
|
|
|
|
router = APIRouter(prefix="/api/audit", tags=["Audit"])
|
|
|
|
|
|
def _requireAuditAccess(context: RequestContext):
|
|
"""Raise 403 unless user has mandate-admin or compliance-viewer access."""
|
|
from modules.auth.authentication import _hasSysAdminRole
|
|
if _hasSysAdminRole(str(context.user.id)):
|
|
return
|
|
|
|
from modules.interfaces.interfaceDbApp import getInterface
|
|
appIf = getInterface(context.user, mandateId=str(context.mandateId) if context.mandateId else None)
|
|
mandateId = str(context.mandateId) if context.mandateId else None
|
|
if not mandateId:
|
|
raise HTTPException(status_code=403, detail=routeApiMsg("Mandanten-Kontext erforderlich"))
|
|
|
|
from modules.datamodels.datamodelRbac import AccessRuleContext
|
|
permissions = appIf.rbac.getUserPermissions(
|
|
context.user, AccessRuleContext.UI, "ui.system.complianceAudit",
|
|
mandateId=mandateId,
|
|
)
|
|
if not permissions or not permissions.view:
|
|
raise HTTPException(status_code=403, detail=routeApiMsg("Kein Zugriff auf Audit-Daten"))
|
|
|
|
|
|
# ── Tab A: AI Data-Flow Log ──
|
|
|
|
@router.get("/ai-log")
|
|
@limiter.limit("120/minute")
|
|
async def getAiAuditLog(
|
|
request: Request,
|
|
context: RequestContext = Depends(getRequestContext),
|
|
userId: Optional[str] = Query(None),
|
|
featureInstanceId: Optional[str] = Query(None),
|
|
aiModel: Optional[str] = Query(None),
|
|
dateFrom: Optional[float] = Query(None, description="UTC epoch seconds"),
|
|
dateTo: Optional[float] = Query(None, description="UTC epoch seconds"),
|
|
limit: int = Query(50, ge=1, le=500),
|
|
offset: int = Query(0, ge=0),
|
|
):
|
|
_requireAuditAccess(context)
|
|
mandateId = str(context.mandateId) if context.mandateId else ""
|
|
if not mandateId:
|
|
raise HTTPException(status_code=400, detail=routeApiMsg("Mandanten-ID erforderlich"))
|
|
|
|
from modules.shared.aiAuditLogger import aiAuditLogger
|
|
return aiAuditLogger.getAiAuditLogs(
|
|
mandateId,
|
|
userId=userId,
|
|
featureInstanceId=featureInstanceId,
|
|
aiModel=aiModel,
|
|
fromTimestamp=dateFrom,
|
|
toTimestamp=dateTo,
|
|
limit=limit,
|
|
offset=offset,
|
|
)
|
|
|
|
|
|
@router.get("/ai-log/{entryId}/content")
|
|
@limiter.limit("60/minute")
|
|
async def getAiAuditEntryContent(
|
|
request: Request,
|
|
entryId: str = Path(...),
|
|
context: RequestContext = Depends(getRequestContext),
|
|
):
|
|
_requireAuditAccess(context)
|
|
mandateId = str(context.mandateId) if context.mandateId else ""
|
|
|
|
from modules.shared.aiAuditLogger import aiAuditLogger
|
|
result = aiAuditLogger.getAiAuditEntryContent(entryId, mandateId)
|
|
if not result:
|
|
raise HTTPException(status_code=404, detail=routeApiMsg("Audit-Eintrag nicht gefunden"))
|
|
return result
|
|
|
|
|
|
# ── Tab B: Security / GDPR Audit Log ──
|
|
|
|
@router.get("/log")
|
|
@limiter.limit("120/minute")
|
|
async def getAuditLog(
|
|
request: Request,
|
|
context: RequestContext = Depends(getRequestContext),
|
|
userId: Optional[str] = Query(None),
|
|
category: Optional[str] = Query(None),
|
|
action: Optional[str] = Query(None),
|
|
dateFrom: Optional[float] = Query(None),
|
|
dateTo: Optional[float] = Query(None),
|
|
limit: int = Query(100, ge=1, le=500),
|
|
offset: int = Query(0, ge=0),
|
|
):
|
|
_requireAuditAccess(context)
|
|
mandateId = str(context.mandateId) if context.mandateId else None
|
|
|
|
from modules.shared.auditLogger import audit_logger
|
|
records = audit_logger.getAuditLogs(
|
|
userId=userId,
|
|
mandateId=mandateId,
|
|
category=category,
|
|
action=action,
|
|
fromTimestamp=dateFrom,
|
|
toTimestamp=dateTo,
|
|
limit=limit + offset + 1,
|
|
)
|
|
totalItems = len(records)
|
|
page = records[offset: offset + limit]
|
|
return {"items": page, "totalItems": totalItems}
|
|
|
|
|
|
# ── Tab C: Audit Statistics ──
|
|
|
|
@router.get("/stats")
|
|
@limiter.limit("60/minute")
|
|
async def getAuditStats(
|
|
request: Request,
|
|
context: RequestContext = Depends(getRequestContext),
|
|
timeRange: int = Query(30, ge=1, le=365, description="Days to aggregate"),
|
|
groupBy: str = Query("model", description="Grouping: model, user, feature, day"),
|
|
):
|
|
_requireAuditAccess(context)
|
|
mandateId = str(context.mandateId) if context.mandateId else ""
|
|
if not mandateId:
|
|
raise HTTPException(status_code=400, detail=routeApiMsg("Mandanten-ID erforderlich"))
|
|
|
|
from modules.shared.aiAuditLogger import aiAuditLogger
|
|
return aiAuditLogger.getAiAuditStats(mandateId, timeRangeDays=timeRange, groupBy=groupBy)
|