use chrono::{DateTime, TimeDelta, Utc};
use trust_tasks_rs::FreshnessPolicy;
use trust_tasks_rs::freshness::DEFAULT_SKEW;
use trust_tasks_rs::specs::trust_task_discovery::v0_3 as wire;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct AcceptanceWindow {
pub max_age: TimeDelta,
pub clock_skew: TimeDelta,
}
pub const VTI_ACCEPTANCE_WINDOW: AcceptanceWindow = AcceptanceWindow {
max_age: super::ACCEPTANCE_WINDOW,
clock_skew: DEFAULT_SKEW,
};
impl AcceptanceWindow {
pub fn freshness_policy(&self) -> FreshnessPolicy {
FreshnessPolicy::default()
.with_max_age(self.max_age)
.with_skew(self.clock_skew)
}
pub fn advertised(&self) -> Option<wire::AcceptanceWindow> {
let max_age = u64::try_from(self.max_age.num_seconds()).ok()?;
let clock_skew_seconds = u64::try_from(self.clock_skew.num_seconds()).ok()?;
wire::AcceptanceWindow::builder()
.clock_skew_seconds(clock_skew_seconds)
.max_age_seconds(std::num::NonZeroU64::new(max_age)?)
.try_into()
.ok()
}
pub fn from_advertised(window: &wire::AcceptanceWindow) -> Self {
let secs = |s: u64| {
i64::try_from(s)
.ok()
.and_then(TimeDelta::try_seconds)
.unwrap_or(TimeDelta::MAX)
};
Self {
max_age: secs(window.max_age_seconds.get()),
clock_skew: secs(window.clock_skew_seconds),
}
}
pub fn past_max_age(&self, issued_at: DateTime<Utc>, now: DateTime<Utc>) -> bool {
issued_at
.checked_add_signed(self.max_age)
.is_some_and(|edge| now >= edge)
}
pub fn refuses(&self, issued_at: DateTime<Utc>, now: DateTime<Utc>) -> bool {
issued_at
.checked_add_signed(self.max_age)
.and_then(|t| t.checked_add_signed(self.clock_skew))
.is_some_and(|edge| now > edge)
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn vti_trn_047_the_vti_window_is_advertised_exactly() {
let wire = VTI_ACCEPTANCE_WINDOW.advertised().expect("advertisable");
assert_eq!(wire.max_age_seconds.get(), 600);
assert_eq!(wire.clock_skew_seconds, 60);
assert_eq!(
AcceptanceWindow::from_advertised(&wire),
VTI_ACCEPTANCE_WINDOW
);
}
#[test]
fn vti_trn_047_a_fractional_window_is_never_advertised_wider() {
let w = AcceptanceWindow {
max_age: TimeDelta::milliseconds(90_999),
clock_skew: TimeDelta::milliseconds(1_500),
};
let wire = w.advertised().expect("advertisable");
assert_eq!(wire.max_age_seconds.get(), 90);
assert_eq!(wire.clock_skew_seconds, 1);
let back = AcceptanceWindow::from_advertised(&wire);
assert!(back.max_age <= w.max_age && back.clock_skew <= w.clock_skew);
}
#[test]
fn an_unstatable_window_is_not_advertised() {
let sub_second = AcceptanceWindow {
max_age: TimeDelta::milliseconds(500),
clock_skew: TimeDelta::zero(),
};
assert!(sub_second.advertised().is_none());
let negative_skew = AcceptanceWindow {
max_age: TimeDelta::minutes(1),
clock_skew: TimeDelta::seconds(-1),
};
assert!(negative_skew.advertised().is_none());
}
#[test]
fn vti_trn_045_the_producer_predicates_agree_with_the_consumer_policy() {
let w = VTI_ACCEPTANCE_WINDOW;
let policy = w.freshness_policy();
assert_eq!(policy.max_age, Some(w.max_age));
assert_eq!(policy.skew, w.clock_skew);
let now = chrono::SubsecRound::trunc_subsecs(Utc::now(), 0);
let at_edge = now - w.max_age - w.clock_skew;
let past_edge = at_edge - TimeDelta::seconds(1);
let doc = |issued: DateTime<Utc>| {
let mut d = trust_tasks_rs::TrustTask::new(
"urn:uuid:00000000-0000-4000-8000-000000000000".to_string(),
"https://trusttasks.org/spec/acl/list/0.1".parse().unwrap(),
serde_json::json!({}),
);
d.issued_at = Some(issued);
d
};
assert!(doc(at_edge).validate_freshness(now, &policy).is_ok());
assert!(!w.refuses(at_edge, now));
assert!(doc(past_edge).validate_freshness(now, &policy).is_err());
assert!(w.refuses(past_edge, now));
assert!(w.past_max_age(now - w.max_age, now));
assert!(!w.past_max_age(now - w.max_age + TimeDelta::seconds(1), now));
}
}