use crate::authn::factor::FactorKind;
use crate::device::types::{Device, DeviceTrustLevel};
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct StepUpPolicy {
pub unknown: Vec<FactorKind>,
pub seen: Vec<FactorKind>,
pub trusted: Vec<FactorKind>,
pub revoked: Vec<FactorKind>,
}
impl Default for StepUpPolicy {
fn default() -> Self {
Self {
unknown: default_step_up_factors(),
seen: default_step_up_factors(),
trusted: Vec::new(),
revoked: Vec::new(),
}
}
}
impl StepUpPolicy {
pub fn builder() -> StepUpPolicyBuilder {
StepUpPolicyBuilder::default()
}
pub fn for_level(&self, level: DeviceTrustLevel) -> &[FactorKind] {
match level {
DeviceTrustLevel::Unknown => &self.unknown,
DeviceTrustLevel::Seen => &self.seen,
DeviceTrustLevel::Trusted => &self.trusted,
DeviceTrustLevel::Revoked => &self.revoked,
}
}
}
fn default_step_up_factors() -> Vec<FactorKind> {
vec![FactorKind::Totp]
}
#[derive(Debug, Default, Clone)]
pub struct StepUpPolicyBuilder {
unknown: Option<Vec<FactorKind>>,
seen: Option<Vec<FactorKind>>,
trusted: Option<Vec<FactorKind>>,
revoked: Option<Vec<FactorKind>>,
}
impl StepUpPolicyBuilder {
pub fn unknown(mut self, factors: Vec<FactorKind>) -> Self {
self.unknown = Some(factors);
self
}
pub fn seen(mut self, factors: Vec<FactorKind>) -> Self {
self.seen = Some(factors);
self
}
pub fn trusted(mut self, factors: Vec<FactorKind>) -> Self {
self.trusted = Some(factors);
self
}
pub fn revoked(mut self, factors: Vec<FactorKind>) -> Self {
self.revoked = Some(factors);
self
}
pub fn build(self) -> StepUpPolicy {
let d = StepUpPolicy::default();
StepUpPolicy {
unknown: self.unknown.unwrap_or(d.unknown),
seen: self.seen.unwrap_or(d.seen),
trusted: self.trusted.unwrap_or(d.trusted),
revoked: self.revoked.unwrap_or(d.revoked),
}
}
}
pub fn decide_step_up(device: &Device, policy: &StepUpPolicy) -> Option<Vec<FactorKind>> {
let factors = policy.for_level(device.trust_level);
if factors.is_empty() {
None
} else {
Some(factors.to_vec())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::device::types::FingerprintHash;
use chrono::{TimeZone, Utc};
fn build_device(level: DeviceTrustLevel) -> Device {
let now = Utc.with_ymd_and_hms(2026, 1, 1, 0, 0, 0).unwrap();
Device {
id: crate::authn::ids::testing::device("d"),
tenant_id: crate::authn::ids::testing::tenant("t"),
user_id: Some(crate::authn::ids::testing::user("u")),
trust_level: level,
fingerprint_hash: FingerprintHash::from_bytes([0u8; 32]),
first_seen_at: now,
last_seen_at: now,
revoked_at: None,
bindings: Vec::new(),
}
}
#[test]
fn default_policy_step_up_matrix() {
let p = StepUpPolicy::default();
assert!(decide_step_up(&build_device(DeviceTrustLevel::Unknown), &p).is_some());
assert!(decide_step_up(&build_device(DeviceTrustLevel::Seen), &p).is_some());
assert!(decide_step_up(&build_device(DeviceTrustLevel::Trusted), &p).is_none());
assert!(decide_step_up(&build_device(DeviceTrustLevel::Revoked), &p).is_none());
}
#[test]
fn builder_overrides_trusted_to_require_step_up() {
let p = StepUpPolicy::builder()
.trusted(vec![FactorKind::Totp])
.build();
let factors = decide_step_up(&build_device(DeviceTrustLevel::Trusted), &p);
assert_eq!(factors, Some(vec![FactorKind::Totp]));
}
#[test]
fn empty_factor_list_produces_none() {
let p = StepUpPolicy::builder().unknown(Vec::new()).build();
assert!(decide_step_up(&build_device(DeviceTrustLevel::Unknown), &p).is_none());
}
#[test]
fn for_level_returns_configured_slice() {
let p = StepUpPolicy::builder()
.seen(vec![FactorKind::EmailOtp])
.build();
assert_eq!(p.for_level(DeviceTrustLevel::Seen), &[FactorKind::EmailOtp]);
}
#[test]
fn unmodified_builder_equals_default() {
let built = StepUpPolicy::builder().build();
assert_eq!(built, StepUpPolicy::default());
}
#[test]
fn revoked_builder_setter_threads_through_to_build() {
let p = StepUpPolicy::builder()
.revoked(vec![FactorKind::Totp])
.build();
assert_eq!(p.revoked, vec![FactorKind::Totp]);
assert_eq!(p.unknown, default_step_up_factors());
assert_eq!(p.seen, default_step_up_factors());
assert!(p.trusted.is_empty());
}
}