use crate::contacts::{ContactStore, IdentityType, TrustLevel};
use crate::identity::{AgentId, MachineId};
#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize)]
#[serde(rename_all = "snake_case")]
pub enum TrustDecision {
Accept,
AcceptWithFlag,
RejectMachineMismatch,
RejectBlocked,
Unknown,
}
impl std::fmt::Display for TrustDecision {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
Self::Accept => write!(f, "accept"),
Self::AcceptWithFlag => write!(f, "accept_with_flag"),
Self::RejectMachineMismatch => write!(f, "reject_machine_mismatch"),
Self::RejectBlocked => write!(f, "reject_blocked"),
Self::Unknown => write!(f, "unknown"),
}
}
}
#[derive(Debug, Clone, Copy)]
pub struct TrustContext<'a> {
pub agent_id: &'a AgentId,
pub machine_id: &'a MachineId,
}
pub struct TrustEvaluator<'a> {
store: &'a ContactStore,
}
impl<'a> TrustEvaluator<'a> {
#[must_use]
pub fn new(store: &'a ContactStore) -> Self {
Self { store }
}
pub fn evaluate(&self, ctx: &TrustContext<'_>) -> TrustDecision {
let contact = match self.store.get(ctx.agent_id) {
Some(c) => c,
None => return TrustDecision::Unknown,
};
if contact.trust_level == TrustLevel::Blocked {
return TrustDecision::RejectBlocked;
}
if contact.identity_type == IdentityType::Pinned {
let is_pinned_machine = contact
.machines
.iter()
.any(|m| m.machine_id == *ctx.machine_id && m.pinned);
if is_pinned_machine {
return TrustDecision::Accept;
} else {
return TrustDecision::RejectMachineMismatch;
}
}
if contact.trust_level == TrustLevel::Trusted {
return TrustDecision::Accept;
}
if contact.trust_level == TrustLevel::Known {
return TrustDecision::AcceptWithFlag;
}
TrustDecision::Unknown
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::contacts::{Contact, ContactStore, IdentityType, MachineRecord, TrustLevel};
use crate::identity::{AgentKeypair, MachineKeypair};
fn agent_id() -> AgentId {
AgentKeypair::generate().expect("keygen").agent_id()
}
fn machine_id() -> MachineId {
MachineKeypair::generate().expect("keygen").machine_id()
}
fn store_with_contact(trust: TrustLevel, id_type: IdentityType) -> (ContactStore, AgentId) {
let dir = tempfile::tempdir().expect("tmpdir");
let mut store = ContactStore::new(dir.path().join("contacts.json"));
let aid = agent_id();
store.add(Contact {
agent_id: aid,
trust_level: trust,
label: None,
added_at: 0,
last_seen: None,
identity_type: id_type,
machines: Vec::new(),
});
(store, aid)
}
#[test]
fn unknown_agent_returns_unknown() {
let dir = tempfile::tempdir().expect("tmpdir");
let store = ContactStore::new(dir.path().join("contacts.json"));
let evaluator = TrustEvaluator::new(&store);
let aid = agent_id();
let mid = machine_id();
let decision = evaluator.evaluate(&TrustContext {
agent_id: &aid,
machine_id: &mid,
});
assert_eq!(decision, TrustDecision::Unknown);
}
#[test]
fn blocked_agent_returns_reject_blocked() {
let (store, aid) = store_with_contact(TrustLevel::Blocked, IdentityType::Anonymous);
let evaluator = TrustEvaluator::new(&store);
let mid = machine_id();
let decision = evaluator.evaluate(&TrustContext {
agent_id: &aid,
machine_id: &mid,
});
assert_eq!(decision, TrustDecision::RejectBlocked);
}
#[test]
fn trusted_non_pinned_returns_accept() {
let (store, aid) = store_with_contact(TrustLevel::Trusted, IdentityType::Trusted);
let evaluator = TrustEvaluator::new(&store);
let mid = machine_id();
let decision = evaluator.evaluate(&TrustContext {
agent_id: &aid,
machine_id: &mid,
});
assert_eq!(decision, TrustDecision::Accept);
}
#[test]
fn known_agent_returns_accept_with_flag() {
let (store, aid) = store_with_contact(TrustLevel::Known, IdentityType::Known);
let evaluator = TrustEvaluator::new(&store);
let mid = machine_id();
let decision = evaluator.evaluate(&TrustContext {
agent_id: &aid,
machine_id: &mid,
});
assert_eq!(decision, TrustDecision::AcceptWithFlag);
}
#[test]
fn unknown_trust_level_returns_unknown() {
let (store, aid) = store_with_contact(TrustLevel::Unknown, IdentityType::Anonymous);
let evaluator = TrustEvaluator::new(&store);
let mid = machine_id();
let decision = evaluator.evaluate(&TrustContext {
agent_id: &aid,
machine_id: &mid,
});
assert_eq!(decision, TrustDecision::Unknown);
}
#[test]
fn pinned_correct_machine_returns_accept() {
let dir = tempfile::tempdir().expect("tmpdir");
let mut store = ContactStore::new(dir.path().join("contacts.json"));
let aid = agent_id();
let mid = machine_id();
store.add(Contact {
agent_id: aid,
trust_level: TrustLevel::Trusted,
label: None,
added_at: 0,
last_seen: None,
identity_type: IdentityType::Anonymous,
machines: Vec::new(),
});
store.add_machine(&aid, MachineRecord::new(mid, None));
store.pin_machine(&aid, &mid);
let evaluator = TrustEvaluator::new(&store);
let decision = evaluator.evaluate(&TrustContext {
agent_id: &aid,
machine_id: &mid,
});
assert_eq!(decision, TrustDecision::Accept);
}
#[test]
fn pinned_wrong_machine_returns_reject_mismatch() {
let dir = tempfile::tempdir().expect("tmpdir");
let mut store = ContactStore::new(dir.path().join("contacts.json"));
let aid = agent_id();
let mid = machine_id();
let other_mid = machine_id();
store.add(Contact {
agent_id: aid,
trust_level: TrustLevel::Trusted,
label: None,
added_at: 0,
last_seen: None,
identity_type: IdentityType::Anonymous,
machines: Vec::new(),
});
store.add_machine(&aid, MachineRecord::new(mid, None));
store.pin_machine(&aid, &mid);
let evaluator = TrustEvaluator::new(&store);
let decision = evaluator.evaluate(&TrustContext {
agent_id: &aid,
machine_id: &other_mid,
});
assert_eq!(decision, TrustDecision::RejectMachineMismatch);
}
#[test]
fn blocked_pinned_agent_returns_reject_blocked_not_machine_mismatch() {
let dir = tempfile::tempdir().expect("tmpdir");
let mut store = ContactStore::new(dir.path().join("contacts.json"));
let aid = agent_id();
let mid = machine_id();
let other_mid = machine_id();
store.add(Contact {
agent_id: aid,
trust_level: TrustLevel::Blocked,
label: None,
added_at: 0,
last_seen: None,
identity_type: IdentityType::Anonymous,
machines: Vec::new(),
});
store.add_machine(&aid, MachineRecord::new(mid, None));
store.pin_machine(&aid, &mid);
let evaluator = TrustEvaluator::new(&store);
let decision = evaluator.evaluate(&TrustContext {
agent_id: &aid,
machine_id: &other_mid,
});
assert_eq!(decision, TrustDecision::RejectBlocked);
}
#[test]
fn full_trust_round_trip() {
let dir = tempfile::tempdir().expect("tmpdir");
let mut store = ContactStore::new(dir.path().join("contacts.json"));
let aid = agent_id();
let mid = machine_id();
let other_mid = machine_id();
store.set_trust(&aid, TrustLevel::Trusted);
store.add_machine(&aid, MachineRecord::new(mid, Some("laptop".into())));
store.pin_machine(&aid, &mid);
let evaluator = TrustEvaluator::new(&store);
assert_eq!(
evaluator.evaluate(&TrustContext {
agent_id: &aid,
machine_id: &mid,
}),
TrustDecision::Accept
);
assert_eq!(
evaluator.evaluate(&TrustContext {
agent_id: &aid,
machine_id: &other_mid,
}),
TrustDecision::RejectMachineMismatch
);
store.set_trust(&aid, TrustLevel::Blocked);
let evaluator = TrustEvaluator::new(&store);
assert_eq!(
evaluator.evaluate(&TrustContext {
agent_id: &aid,
machine_id: &mid,
}),
TrustDecision::RejectBlocked
);
let unknown_aid = agent_id();
let unknown_mid = machine_id();
let evaluator = TrustEvaluator::new(&store);
assert_eq!(
evaluator.evaluate(&TrustContext {
agent_id: &unknown_aid,
machine_id: &unknown_mid,
}),
TrustDecision::Unknown
);
}
#[test]
fn trust_decision_display() {
assert_eq!(TrustDecision::Accept.to_string(), "accept");
assert_eq!(
TrustDecision::AcceptWithFlag.to_string(),
"accept_with_flag"
);
assert_eq!(
TrustDecision::RejectMachineMismatch.to_string(),
"reject_machine_mismatch"
);
assert_eq!(TrustDecision::RejectBlocked.to_string(), "reject_blocked");
assert_eq!(TrustDecision::Unknown.to_string(), "unknown");
}
}