"""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