use std::path::Path;
use auths_keri::Prefix;
use auths_keri::witness::independence::{
Independence, IndependencePolicy, OperatorAttributes, WitnessOperatorInfo, spans_distinct,
};
use serde::{Deserialize, Serialize};
use url::Url;
#[derive(Debug, Clone, Copy)]
pub enum WitnessParams<'a> {
Enabled {
config: &'a WitnessConfig,
repo_path: &'a Path,
},
Disabled,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
pub struct WitnessRef {
pub url: Url,
pub aid: Prefix,
#[serde(default)]
pub operator_info: Option<WitnessOperatorInfo>,
}
impl WitnessRef {
pub fn operator_attributes(&self) -> Option<OperatorAttributes> {
self.operator_info
.as_ref()
.map(|info| info.to_attributes(self.aid.as_str()))
}
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct WitnessConfig {
#[serde(default = "default_version")]
pub version: u8,
#[serde(default)]
pub witnesses: Vec<WitnessRef>,
pub threshold: usize,
pub timeout_ms: u64,
pub policy: WitnessPolicy,
}
fn default_version() -> u8 {
2
}
impl Default for WitnessConfig {
fn default() -> Self {
Self {
version: 2,
witnesses: vec![],
threshold: 0,
timeout_ms: 5000,
policy: WitnessPolicy::Enforce,
}
}
}
impl WitnessConfig {
pub fn is_enabled(&self) -> bool {
!self.witnesses.is_empty() && self.threshold > 0 && self.policy != WitnessPolicy::Skip
}
pub fn urls(&self) -> impl Iterator<Item = &Url> {
self.witnesses.iter().map(|w| &w.url)
}
pub fn aids(&self) -> impl Iterator<Item = &Prefix> {
self.witnesses.iter().map(|w| &w.aid)
}
pub fn contains_aid(&self, aid: &Prefix) -> bool {
self.witnesses.iter().any(|w| &w.aid == aid)
}
pub fn pin(&mut self, witness: WitnessRef) -> bool {
if self.witnesses.iter().any(|w| w.aid == witness.aid) {
return false;
}
self.witnesses.push(witness);
true
}
pub fn remove_url(&mut self, url: &Url) -> bool {
let before = self.witnesses.len();
self.witnesses.retain(|w| &w.url != url);
self.witnesses.len() != before
}
pub fn roster_independence(&self, policy: &IndependencePolicy) -> Independence {
let mut attesters = Vec::with_capacity(self.witnesses.len());
for w in &self.witnesses {
match w.operator_attributes() {
Some(a) => attesters.push(a),
None => {
return Independence::cannot_prove(format!(
"witness {} is missing operator/organization/jurisdiction/infrastructure attributes",
w.aid.as_str()
));
}
}
}
spans_distinct(&attesters, policy)
}
}
#[derive(Debug, Clone, Serialize, Deserialize, Default, PartialEq, Eq)]
pub enum WitnessPolicy {
#[default]
Enforce,
Warn,
Skip,
}
#[cfg(test)]
mod tests {
use super::*;
use auths_keri::witness::independence::{
Infrastructure, Jurisdiction, OperatorId, Organization,
};
fn ref_for(url: &str, aid: &str) -> WitnessRef {
WitnessRef {
url: url.parse().unwrap(),
aid: Prefix::new_unchecked(aid.to_string()),
operator_info: None,
}
}
fn tagged_ref(aid: &str, operator: &str, org: &str, jur: &str, infra: &str) -> WitnessRef {
WitnessRef {
url: "http://w:3333".parse().unwrap(),
aid: Prefix::new_unchecked(aid.to_string()),
operator_info: Some(WitnessOperatorInfo {
operator: OperatorId::new(operator).unwrap(),
organization: Organization::new(org).unwrap(),
jurisdiction: Jurisdiction::new(jur).unwrap(),
infrastructure: Infrastructure::new(infra).unwrap(),
}),
}
}
fn config_with(witnesses: Vec<WitnessRef>) -> WitnessConfig {
WitnessConfig {
witnesses,
threshold: 2,
..Default::default()
}
}
#[test]
fn roster_independent_with_three_diverse_operators() {
let config = config_with(vec![
tagged_ref(
"BW_A0000000000000000000000000000000000000000",
"op-a",
"org-a",
"US",
"aws/us-east-1",
),
tagged_ref(
"BW_B0000000000000000000000000000000000000000",
"op-b",
"org-b",
"DE",
"gcp/eu-west-1",
),
tagged_ref(
"BW_C0000000000000000000000000000000000000000",
"op-c",
"org-c",
"JP",
"azure/jp-east",
),
]);
assert!(
config
.roster_independence(&IndependencePolicy::default())
.independent
);
}
#[test]
fn roster_fails_closed_when_attributes_missing() {
let config = config_with(vec![
tagged_ref(
"BW_A0000000000000000000000000000000000000000",
"op-a",
"org-a",
"US",
"aws/us-east-1",
),
ref_for(
"http://w2:3333",
"BW_B0000000000000000000000000000000000000000",
),
]);
let verdict = config.roster_independence(&IndependencePolicy::default());
assert!(!verdict.independent);
}
#[test]
fn roster_same_org_is_not_independent() {
let config = config_with(vec![
tagged_ref(
"BW_A0000000000000000000000000000000000000000",
"op-a",
"acme",
"US",
"aws/us-east-1",
),
tagged_ref(
"BW_B0000000000000000000000000000000000000000",
"op-b",
"acme",
"DE",
"gcp/eu-west-1",
),
tagged_ref(
"BW_C0000000000000000000000000000000000000000",
"op-c",
"acme",
"JP",
"azure/jp-east",
),
]);
assert!(
!config
.roster_independence(&IndependencePolicy::default())
.independent
);
}
#[test]
fn default_is_disabled() {
let config = WitnessConfig::default();
assert!(!config.is_enabled());
}
#[test]
fn enabled_with_witnesses_and_threshold() {
let config = WitnessConfig {
witnesses: vec![ref_for(
"http://w1:3333",
"BWitnessOne00000000000000000000000000000000",
)],
threshold: 1,
timeout_ms: 5000,
..Default::default()
};
assert!(config.is_enabled());
assert_eq!(config.urls().count(), 1);
assert!(config.contains_aid(&Prefix::new_unchecked(
"BWitnessOne00000000000000000000000000000000".to_string()
)));
}
#[test]
fn skip_policy_disables() {
let config = WitnessConfig {
witnesses: vec![ref_for(
"http://w1:3333",
"BWitnessOne00000000000000000000000000000000",
)],
threshold: 1,
timeout_ms: 5000,
policy: WitnessPolicy::Skip,
..Default::default()
};
assert!(!config.is_enabled());
}
#[test]
fn zero_threshold_disables() {
let config = WitnessConfig {
witnesses: vec![ref_for(
"http://w1:3333",
"BWitnessOne00000000000000000000000000000000",
)],
threshold: 0,
timeout_ms: 5000,
..Default::default()
};
assert!(!config.is_enabled());
}
#[test]
fn pin_dedupes_by_aid() {
let mut config = WitnessConfig::default();
assert!(config.pin(ref_for(
"http://w1:3333",
"BWitnessOne00000000000000000000000000000000"
)));
assert!(!config.pin(ref_for(
"http://other:3333",
"BWitnessOne00000000000000000000000000000000"
)));
assert_eq!(config.witnesses.len(), 1);
}
#[test]
fn remove_url_drops_entry() {
let mut config = WitnessConfig::default();
config.pin(ref_for(
"http://w1:3333",
"BWitnessOne00000000000000000000000000000000",
));
assert!(config.remove_url(&"http://w1:3333".parse().unwrap()));
assert!(config.witnesses.is_empty());
assert!(!config.remove_url(&"http://w1:3333".parse().unwrap()));
}
#[test]
fn deserializes_v2_shape() {
let json = serde_json::json!({
"version": 2,
"witnesses": [],
"threshold": 0,
"timeout_ms": 5000,
"policy": "Enforce"
});
let config: WitnessConfig = serde_json::from_value(json).unwrap();
assert!(!config.is_enabled());
}
}