Files
Unraid-AppOps-Gateway-Public/backend/app/api/routes_approvals.py
T

151 lines
5.3 KiB
Python

from __future__ import annotations
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel, ConfigDict, Field
from ..db.session import get_session
from ..services.audit_logger import log_event
from ..services.action_execution import approval_execution_supported, execute_approved_action
from ..services.approval_service import (
ApprovalStateError,
approval_fingerprint,
create_approval,
dismiss_expired_approvals,
get_approval,
list_approvals,
mark_approval_executed,
update_approval_status,
)
router = APIRouter(prefix='/api/approvals')
class ApprovalCreateRequest(BaseModel):
model_config = ConfigDict(extra='forbid')
action: str = Field(min_length=1, max_length=160)
target: str = Field(min_length=1, max_length=240)
source: str = Field(default='manual', pattern='^(manual|voice|automation|system)$')
risk: str = Field(default='medium', pattern='^(low|medium|high|critical)$')
reason: str | None = Field(default=None, max_length=1000)
requester: str = Field(default='manual', max_length=120)
plan: list[str] = Field(default_factory=list, max_length=30)
class ApprovalDecisionRequest(BaseModel):
model_config = ConfigDict(extra='forbid')
status: str = Field(min_length=1, max_length=32)
@router.get('')
def list_all(include_dismissed: bool = Query(default=False), session=Depends(get_session)):
return [
{
**approval.model_dump(),
'executionSupported': approval_execution_supported(approval.action),
'executionReason': None if approval_execution_supported(approval.action) else 'Geen rollback-veilige executor geregistreerd.',
}
for approval in list_approvals(session, include_dismissed=include_dismissed)
]
@router.post('/cleanup')
def cleanup_expired(session=Depends(get_session)):
dismissed = dismiss_expired_approvals(session)
audit = log_event(
session,
source='manual',
identity='operator',
action_id='approval.cleanup',
target='approval-queue',
target_type='approval',
risk='low',
policy_decision='allowed',
reason=f'{len(dismissed)} expired approval(s) archived; active approvals were retained.',
result='success',
execution_plan={'steps': ['expire overdue pending approvals', 'archive expired approvals', 'retain approval and audit records']},
redacted_metadata={'dismissedCount': len(dismissed)},
)
return {
'success': True,
'message': f'{len(dismissed)} verlopen approval(s) opgeruimd.',
'dismissedCount': len(dismissed),
'auditId': audit.id,
}
@router.post('')
def create(payload: ApprovalCreateRequest, session=Depends(get_session)):
approval = create_approval(
session,
action=payload.action,
target=payload.target,
source=payload.source,
risk=payload.risk,
reason=payload.reason,
requester=payload.requester,
context={'plan': payload.plan},
)
return {'success': True, 'message': 'Approval created', 'id': approval.id}
@router.patch('/{approval_id}')
def update(approval_id: str, payload: ApprovalDecisionRequest, session=Depends(get_session)):
status = payload.status
try:
updated = update_approval_status(session, approval_id, status)
except ApprovalStateError as exc:
raise HTTPException(status_code=409, detail=str(exc)) from exc
if not updated:
return {'success': False, 'message': 'Approval not found', 'id': approval_id}
audit = log_event(
session,
source='manual',
identity='operator',
action_id=f'approval.{status}',
target=updated.target,
target_type='approval',
risk=updated.risk,
policy_decision='approval_required',
reason=updated.reason or f'Approval {approval_id} marked {status}.',
result='success',
execution_plan={'steps': ['load approval', f'mark {status}', 'write audit']},
redacted_metadata={'approvalId': updated.id, 'approvalStatus': updated.status, 'requestedAction': updated.action},
)
return {
'success': True,
'id': updated.id,
'status': updated.status,
'auditId': audit.id,
'message': f'Approval {updated.status}',
}
@router.post('/{approval_id}/execute')
def execute_approved(approval_id: str, session=Depends(get_session)):
approval = get_approval(session, approval_id)
if not approval:
raise HTTPException(status_code=404, detail='Approval not found.')
if approval.status != 'approved':
raise HTTPException(status_code=409, detail='Only an approved request can be executed.')
expected = approval_fingerprint(approval.action, approval.target, approval.source, approval.risk, approval.request_context_json)
if not approval.action_fingerprint or approval.action_fingerprint != expected:
raise HTTPException(status_code=409, detail='Approval action context changed after review.')
result = execute_approved_action(session, approval)
if not result.get('success'):
return result
mark_approval_executed(
session,
approval,
job_id=str(result['jobId']),
audit_event_id=str(result['auditId']),
)
return {
**result,
'approvalId': approval.id,
'approvalStatus': 'executed',
}