92 lines
3.3 KiB
Python
92 lines
3.3 KiB
Python
from __future__ import annotations
|
|
|
|
from fastapi import APIRouter, Depends, HTTPException
|
|
from pydantic import BaseModel
|
|
from sqlmodel import Session, select
|
|
|
|
from ..db.session import get_session
|
|
from ..db.models import Policy
|
|
from ..services.policy_engine import evaluate_policy
|
|
from ..services.action_registry import action_capability
|
|
from ..services.audit_logger import log_event
|
|
|
|
router = APIRouter(prefix='/api/policies')
|
|
DECISIONS = {'allowed', 'approval_required', 'blocked'}
|
|
RISKS = {'low', 'medium', 'high', 'critical'}
|
|
|
|
|
|
class PolicyUpdate(BaseModel):
|
|
voice: str
|
|
manual: str
|
|
risk: str
|
|
reason: str
|
|
|
|
|
|
@router.get('')
|
|
def list_policies(session: Session = Depends(get_session)):
|
|
return session.exec(select(Policy).order_by(Policy.action, Policy.target)).all()
|
|
|
|
|
|
@router.patch('/{policy_id}')
|
|
def update_policy(policy_id: str, payload: PolicyUpdate, session: Session = Depends(get_session)):
|
|
policy = session.get(Policy, policy_id)
|
|
if not policy:
|
|
raise HTTPException(status_code=404, detail='Policy not found.')
|
|
if payload.voice not in DECISIONS or payload.manual not in DECISIONS or payload.risk not in RISKS:
|
|
raise HTTPException(status_code=422, detail='Invalid policy decision or risk value.')
|
|
if not payload.reason.strip():
|
|
raise HTTPException(status_code=422, detail='A policy reason is required.')
|
|
|
|
capability = action_capability(policy.action)
|
|
if capability == 'destructive' and (payload.voice != 'blocked' or payload.manual != 'blocked'):
|
|
raise HTTPException(status_code=409, detail='Destructive actions remain blocked by the platform guardrail.')
|
|
|
|
before = {'voice': policy.voice, 'manual': policy.manual, 'risk': policy.risk, 'reason': policy.reason}
|
|
policy.voice = payload.voice
|
|
policy.manual = payload.manual
|
|
policy.risk = 'critical' if capability == 'destructive' else payload.risk
|
|
policy.reason = payload.reason.strip()
|
|
session.add(policy)
|
|
session.commit()
|
|
session.refresh(policy)
|
|
after = {'voice': policy.voice, 'manual': policy.manual, 'risk': policy.risk, 'reason': policy.reason}
|
|
log_event(
|
|
session,
|
|
source='manual',
|
|
identity='operator',
|
|
action_id='policy.update',
|
|
target=policy.id,
|
|
target_type='policy',
|
|
risk='medium',
|
|
policy_decision='allowed',
|
|
reason='Operator updated policy decisions within enforced platform guardrails.',
|
|
result='success',
|
|
before_state=before,
|
|
after_state=after,
|
|
redacted_metadata={'protectedGuardrail': capability == 'destructive', 'policyAction': policy.action},
|
|
)
|
|
return {
|
|
'id': policy.id,
|
|
'action': policy.action,
|
|
'target': policy.target,
|
|
'voice': policy.voice,
|
|
'manual': policy.manual,
|
|
'risk': policy.risk,
|
|
'reason': policy.reason,
|
|
}
|
|
|
|
|
|
@router.post('/evaluate')
|
|
def evaluate(payload: dict, session: Session = Depends(get_session)):
|
|
action = str(payload.get('action', '')).strip()
|
|
target = str(payload.get('target', '')).strip()
|
|
source = str(payload.get('source', 'manual')).strip() or 'manual'
|
|
result = evaluate_policy(session, action=action, target=target, source=source)
|
|
|
|
return {
|
|
'decision': result['decision'],
|
|
'reason': result['reason'],
|
|
'risk': result['risk'],
|
|
'policyId': result['policy_id'],
|
|
}
|