informed-decision/informed_decision/review.py
tegwick 83849b75d4 Connect policy-gated browser review and audit runtime
Assistant: codex
Assistant-Model: gpt-6-astra
Assistant-Session: 01a07ff8-19d0-7820-b4d0-1353833cb7fc
2026-09-11 00:31:03 +02:00

176 lines
8.4 KiB
Python

"""Protected review orchestration. Policy comes only from the injected PDP client."""
from dataclasses import dataclass
import time
from .approval_client import ApprovalEngineError
from .disposition import Actor, ActorKind, Verb
from .memo import BindingLevel, StepKind
from .oidc import HumanSession
from .policy import PolicyError, build_request
from .provenance import Route, assert_human_control_dischargeable
from .store import Conflict
UI_RELEASE = "informed-decision@0.2.0"
class ReviewError(RuntimeError):
def __init__(self, status, code):
super().__init__(code)
self.status, self.code = status, code
@dataclass(frozen=True)
class ReviewPage:
memo: object
presentation: object
binding: dict
documents: dict
intent: dict | None
stale: bool
engine_status: str
dispositions: tuple = ()
class ReviewController:
def __init__(self, store, policy, approval_factory, *, clock=time.time):
self.store, self.policy, self.approval_factory = store, policy, approval_factory
self.clock = clock
def _session(self, session):
if not isinstance(session, HumanSession) or session.expires_at <= self.clock():
raise ReviewError(401, "session_expired")
assert_human_control_dischargeable(session.principal_type)
if session.tenant.value != "tenant:platform" or session.tenant.route not in (Route.DIRECTORY, Route.REGISTRATION):
raise ReviewError(403, "wrong_identity")
def _memo(self, session, memo):
self._session(session)
# Stage 1 is a named approver, not a mandate discovery mechanism. This
# structural match never grants entitlement; a PDP allow is still owed.
if memo.binding.principal.id != session.subject or memo.binding.principal.kind != "person":
raise ReviewError(403, "wrong_recipient")
if not memo.approval_id or not memo.approval_binding_digest:
raise ReviewError(409, "missing_act_binding")
if memo.ui_release != UI_RELEASE:
raise ReviewError(409, "renderer_changed")
if (memo.locale != "en" or memo.binding_level is not BindingLevel.ORGANIZATIONAL
or memo.step_kind is not StepKind.APPROVE):
raise ReviewError(409, "unsupported_review_profile")
def _authorize(self, session, memo, action):
self._memo(session, memo)
request = build_request(session, memo, action, self.policy.version)
observation = self.policy.check(request)
policy_id = self.store.observe_policy(observation)
try:
observation.require_current(self.clock())
except PolicyError:
if observation.outcome == "policy_unavailable":
self.store.record_unreachable(memo.id, "access-engine")
raise ReviewError(403 if observation.outcome == "policy_denied" else 503, observation.outcome) from None
self._session(session)
return policy_id, observation
def _approval(self, session, memo):
current = self.approval_factory(session).get_approval(memo.approval_id)
if current["binding"]["digest"] != memo.approval_binding_digest:
raise ReviewError(409, "binding_changed")
return current
def open(self, session, memo_id):
self._session(session)
memo = self.store.memo(memo_id)
policy_id, observation = self._authorize(session, memo, "read")
current = self._approval(session, memo)
observation.require_current(self.clock())
self._session(session)
intent = self.store.intent_for(memo.approval_id, session.subject)
if intent is not None:
# Recover the original view instead of assigning an existing entry
# or uncertain attempt to a freshly rendered presentation.
return self.load(session, intent["presentation_id"])
p = self.store.present(memo.id, principal_sub=session.subject, tenant=session.tenant,
principal_type=session.principal_type, expected_version=memo.version,
policy_id=policy_id, approval_binding=current["binding"])
saved, p, docs = self.store.retrieve_presentation(p.id)
return ReviewPage(saved, p, current["binding"], docs, None, False, current["status"])
def _presentation(self, session, presentation_id, *, current=False):
self._session(session)
p = self.store.presentation(presentation_id)
if p.principal_sub != session.subject:
raise ReviewError(403, "wrong_recipient")
memo = self.store.memo(p.memo_id, p.memo_version)
self._memo(session, memo)
if current and self.store.memo(memo.id).version != memo.version:
raise ReviewError(409, "stale_presentation")
return memo, p
def load(self, session, presentation_id):
memo, p = self._presentation(session, presentation_id)
_, observation = self._authorize(session, memo, "read")
current = self._approval(session, memo)
content = self.store.presentation_content(p.id)
binding = content.get("approval_binding")
if not isinstance(binding, dict) or binding.get("digest") != memo.approval_binding_digest:
raise ReviewError(409, "missing_act_binding")
observation.require_current(self.clock())
self._session(session)
saved, p, docs = self.store.retrieve_presentation(p.id)
return ReviewPage(saved, p, binding, docs,
self.store.intent_for(memo.approval_id, session.subject),
self.store.memo(memo.id).version != memo.version, current["status"],
tuple(self.store.dispositions_for(p.id)))
def acknowledge(self, session, presentation_id, highlight_ids):
memo, p = self._presentation(session, presentation_id, current=True)
policy_id, observation = self._authorize(session, memo, "acknowledge")
observation.require_current(self.clock())
return self.store.acknowledge(p.id, Actor(session.subject, ActorKind.PERSON),
highlight_ids, policy_id=policy_id)
def act(self, session, presentation_id, verb, *, operation_id, reasons=(), note=None):
if verb not in (Verb.ACCEPT, Verb.RETURN, Verb.DISCUSS, Verb.DECLINE):
raise ReviewError(400, "unsupported_action")
memo, p = self._presentation(session, presentation_id, current=True)
policy_id, observation = self._authorize(session, memo, verb.value)
if verb is Verb.ACCEPT:
original = self.store.intent_for(memo.approval_id, session.subject)
if original is not None and (original["presentation_id"] != p.id or original["state"] != "prepared"):
return original["presentation_id"]
current = self._approval(session, memo)
entries = current.get("entries", [])
if not isinstance(entries, list) or any(not isinstance(e, dict) for e in entries):
raise ReviewError(503, "invalid_approval_response")
if any(e.get("subject_id") == session.subject for e in entries):
raise ReviewError(409, "existing_entry_unlinked")
if current["status"] not in ("requested", "approved"):
raise ReviewError(409, "act_unavailable")
observation.require_current(self.clock())
self._session(session)
d = self.store.record_disposition(p.id, Actor(session.subject, ActorKind.PERSON), verb,
operation_id=operation_id, reasons=reasons, note=note, policy_id=policy_id)
if verb is not Verb.ACCEPT:
return p.id
attempt = None
def before_post():
nonlocal attempt
observation.require_current(self.clock())
self._session(session)
attempt = self.store.begin_submission(d.id, policy_id=policy_id)
try:
result = self.approval_factory(session).add_entry(memo.approval_id,
expected_binding_digest=memo.approval_binding_digest, before_post=before_post)
except ApprovalEngineError:
if attempt is None:
raise
self.store.finish_submission(d.id, attempt) # May have committed; never POST again.
except Conflict:
if attempt is not None or self.store.submission(d.id)["state"] == "prepared":
raise
# Another request won the durable reservation. Its original view
# now exposes the in-flight/result state; no second POST occurs.
else:
self.store.finish_submission(d.id, attempt, result)
return p.id