use serde::{Deserialize, Serialize};
use super::{next, text, Result};
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
pub struct Action {
pub operation: String,
pub account: String,
pub recipient_or_target: String,
pub payload: String,
}
impl Action {
pub fn validate(&self) -> Result<()> {
text(&self.operation)?;
text(&self.account)?;
text(&self.recipient_or_target)?;
text(&self.payload)
}
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
pub struct Proposal {
pub id: u64,
pub action: Action,
pub expires_at_ms: u64,
pub policy_generation: u64,
}
#[derive(Debug)]
struct Pending {
proposal: Proposal,
created_at_ms: u64,
approved: bool,
}
#[derive(Debug, Default)]
pub struct ApprovalGate {
sequence: u64,
pending: Option<Pending>,
}
impl ApprovalGate {
pub fn pending_review(&self) -> Option<Proposal> {
self.pending.as_ref().map(|p| p.proposal.clone())
}
pub fn propose(
&mut self,
action: Action,
now_ms: u64,
ttl_ms: u64,
policy_generation: u64,
) -> Result<Proposal> {
action.validate()?;
if ttl_ms == 0 || ttl_ms > 5 * 60 * 1000 {
return Err("approval expires within five minutes".into());
}
let expires_at_ms = now_ms.checked_add(ttl_ms).ok_or("clock overflow")?;
self.sequence = next(self.sequence)?;
let proposal = Proposal {
id: self.sequence,
action,
expires_at_ms,
policy_generation,
};
self.pending = Some(Pending {
proposal: proposal.clone(),
created_at_ms: now_ms,
approved: false,
});
Ok(proposal)
}
pub fn approve_from_host(
&mut self,
reviewed: &Proposal,
now_ms: u64,
policy_generation: u64,
) -> Result<()> {
let pending = self.pending.as_mut().ok_or("no pending review")?;
if pending.proposal != *reviewed {
return Err("review no longer matches the pending action".into());
}
check_fresh(pending, now_ms, policy_generation)?;
pending.approved = true;
Ok(())
}
pub fn reject_from_host(&mut self) {
self.pending = None;
}
pub fn take_approved(
&mut self,
id: u64,
now_ms: u64,
policy_generation: u64,
) -> Result<Action> {
let pending = self.pending.as_ref().ok_or("no pending review")?;
check_fresh(pending, now_ms, policy_generation)?;
if pending.proposal.id != id || !pending.approved {
return Err("action has not been approved by the host".into());
}
self.pending
.take()
.map(|p| p.proposal.action)
.ok_or_else(|| "no pending review".into())
}
}
fn check_fresh(pending: &Pending, now_ms: u64, policy_generation: u64) -> Result<()> {
if now_ms < pending.created_at_ms
|| now_ms >= pending.proposal.expires_at_ms
|| policy_generation != pending.proposal.policy_generation
{
return Err("approval expired or policy changed".into());
}
Ok(())
}