Wire nested nav pages for phases 5.1-5.11 to real list/create endpoints, fix COA import connectivity via same-origin proxy, and add operational migration. Co-authored-by: Cursor <cursoragent@cursor.com>
198 lines
5.9 KiB
Python
198 lines
5.9 KiB
Python
"""Phase 5.11 — Compliance, Audit & Governance API."""
|
|
from __future__ import annotations
|
|
|
|
from uuid import UUID
|
|
|
|
from fastapi import APIRouter, Depends, status
|
|
from pydantic import BaseModel
|
|
from sqlalchemy.ext.asyncio import AsyncSession
|
|
|
|
from app.api.deps import get_db, get_pagination, require_tenant
|
|
from app.core.security import get_current_user
|
|
from app.models.compliance import CompliancePolicy, GovernanceRule
|
|
from app.models.types import PolicyViolationSeverity, RiskLevel
|
|
from app.repositories.base import TenantBaseRepository
|
|
from app.services.compliance_service import AuditFramework, ComplianceEngine, GovernanceService
|
|
from shared.security import CurrentUser
|
|
|
|
router = APIRouter()
|
|
|
|
|
|
class AuditRecordCreate(BaseModel):
|
|
action: str
|
|
resource_type: str
|
|
resource_id: str
|
|
source_module: str | None = None
|
|
before_value: str | None = None
|
|
after_value: str | None = None
|
|
reason: str | None = None
|
|
|
|
|
|
class CompliancePolicyCreate(BaseModel):
|
|
code: str
|
|
name: str
|
|
policy_type: str
|
|
rules_config: str
|
|
country_code: str | None = None
|
|
|
|
|
|
class ApprovalRequestCreate(BaseModel):
|
|
workflow_id: UUID
|
|
resource_type: str
|
|
resource_id: str
|
|
|
|
|
|
class RejectRequest(BaseModel):
|
|
reason: str
|
|
|
|
|
|
class RiskCreate(BaseModel):
|
|
code: str
|
|
title: str
|
|
risk_category: str
|
|
risk_level: RiskLevel
|
|
risk_score: int = 0
|
|
description: str | None = None
|
|
|
|
|
|
class CompliancePolicyRepo(TenantBaseRepository[CompliancePolicy]):
|
|
model = CompliancePolicy
|
|
|
|
|
|
@router.get("/audit-records")
|
|
async def list_audit_records(
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
pagination=Depends(get_pagination),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
from app.services.compliance_service import AuditRecordRepo
|
|
|
|
repo = AuditRecordRepo(db)
|
|
records = await repo.list_by_tenant(tenant_id, offset=pagination.offset, limit=pagination.page_size)
|
|
return [
|
|
{
|
|
"id": str(r.id),
|
|
"action": r.action,
|
|
"resource_type": r.resource_type,
|
|
"actor_user_id": r.actor_user_id,
|
|
"created_at": r.created_at.isoformat(),
|
|
}
|
|
for r in records
|
|
]
|
|
|
|
|
|
@router.post("/audit-records", status_code=status.HTTP_201_CREATED)
|
|
async def create_audit_record(
|
|
body: AuditRecordCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
audit = AuditFramework(db)
|
|
record = await audit.record(
|
|
tenant_id,
|
|
actor_user_id=user.user_id,
|
|
action=body.action,
|
|
resource_type=body.resource_type,
|
|
resource_id=body.resource_id,
|
|
source_module=body.source_module,
|
|
before_value=body.before_value,
|
|
after_value=body.after_value,
|
|
reason=body.reason,
|
|
)
|
|
await db.commit()
|
|
return {"id": str(record.id)}
|
|
|
|
|
|
@router.get("/policies")
|
|
async def list_compliance_policies(
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
rows = await CompliancePolicyRepo(db).list_by_tenant(tenant_id, limit=200)
|
|
return [
|
|
{
|
|
"id": str(p.id),
|
|
"code": p.code,
|
|
"name": p.name,
|
|
"policy_type": p.policy_type,
|
|
"is_active": getattr(p, "is_active", True),
|
|
}
|
|
for p in rows
|
|
]
|
|
|
|
|
|
@router.post("/policies", status_code=status.HTTP_201_CREATED)
|
|
async def create_compliance_policy(
|
|
body: CompliancePolicyCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
repo = CompliancePolicyRepo(db)
|
|
entity = CompliancePolicy(tenant_id=tenant_id, **body.model_dump())
|
|
await repo.add(entity)
|
|
await db.commit()
|
|
return {"id": str(entity.id)}
|
|
|
|
|
|
@router.post("/approvals")
|
|
async def request_approval(
|
|
body: ApprovalRequestCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
gov = GovernanceService(db)
|
|
request = await gov.request_approval(
|
|
tenant_id,
|
|
workflow_id=body.workflow_id,
|
|
resource_type=body.resource_type,
|
|
resource_id=body.resource_id,
|
|
requested_by=user.user_id,
|
|
)
|
|
await db.commit()
|
|
return {"id": str(request.id), "status": request.status.value}
|
|
|
|
|
|
@router.post("/approvals/{request_id}/approve")
|
|
async def approve_request(
|
|
request_id: UUID,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
gov = GovernanceService(db)
|
|
request = await gov.approve(tenant_id, request_id, approver_user_id=user.user_id)
|
|
await db.commit()
|
|
return {"id": str(request.id), "status": request.status.value}
|
|
|
|
|
|
@router.post("/approvals/{request_id}/reject")
|
|
async def reject_request(
|
|
request_id: UUID,
|
|
body: RejectRequest,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
gov = GovernanceService(db)
|
|
request = await gov.reject(tenant_id, request_id, approver_user_id=user.user_id, reason=body.reason)
|
|
await db.commit()
|
|
return {"id": str(request.id), "status": request.status.value}
|
|
|
|
|
|
@router.post("/risks", status_code=status.HTTP_201_CREATED)
|
|
async def create_risk(
|
|
body: RiskCreate,
|
|
tenant_id: UUID = Depends(require_tenant),
|
|
db: AsyncSession = Depends(get_db),
|
|
_user: CurrentUser = Depends(get_current_user),
|
|
):
|
|
gov = GovernanceService(db)
|
|
risk = await gov.create_risk(tenant_id, **body.model_dump())
|
|
await db.commit()
|
|
return {"id": str(risk.id), "risk_level": risk.risk_level.value}
|