use std::collections::BTreeMap;
use std::sync::Arc;
use arc_swap::ArcSwap;
use engenho_config::{AuthorizedClientConfig, RemoteTier};
use engenho_control_types::AuthorityTier;
use engenho_control_types::pin::{PinSet, Spki};
use engenho_control_types::types;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct Client {
pub name: String,
pub spki: Spki,
pub tier: AuthorityTier,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AuthorizedSet {
by_spki: BTreeMap<Spki, Client>,
}
impl AuthorizedSet {
pub fn from_config(clients: &[AuthorizedClientConfig]) -> Result<Self, String> {
let by_spki = clients
.iter()
.map(|c| {
let spki = Spki::parse(&c.spki_sha256).map_err(|e| e.to_string())?;
let tier = match c.tier {
RemoteTier::Observe => AuthorityTier::Observe,
RemoteTier::Mutate => AuthorityTier::Mutate,
RemoteTier::Destructive => AuthorityTier::Destructive,
};
Ok((
spki,
Client {
name: c.name.clone(),
spki,
tier,
},
))
})
.collect::<Result<_, String>>()?;
Ok(Self { by_spki })
}
#[must_use]
pub fn get(&self, spki: &Spki) -> Option<&Client> {
self.by_spki.get(spki)
}
#[must_use]
pub fn is_empty(&self) -> bool {
self.by_spki.is_empty()
}
#[must_use]
pub fn views(&self) -> Vec<types::AuthorizedClient> {
let mut views: Vec<types::AuthorizedClient> = self
.by_spki
.values()
.filter_map(|c| {
Some(types::AuthorizedClient {
name: c.name.clone(),
spki: types::SpkiSha256::try_from(c.spki.to_string()).ok()?,
tier: c.tier,
})
})
.collect();
views.sort_by(|a, b| a.name.cmp(&b.name));
views
}
}
#[derive(Debug, Clone, Default)]
pub struct Pins(Arc<ArcSwap<AuthorizedSet>>);
impl Pins {
#[must_use]
pub fn new(set: AuthorizedSet) -> Self {
Self(Arc::new(ArcSwap::from_pointee(set)))
}
#[must_use]
pub fn current(&self) -> Arc<AuthorizedSet> {
self.0.load_full()
}
pub fn replace(&self, set: AuthorizedSet) {
self.0.store(Arc::new(set));
}
#[must_use]
pub fn client(&self, spki: &Spki) -> Option<Client> {
self.0.load().get(spki).cloned()
}
}
impl PinSet for Pins {
fn admits(&self, spki: &Spki) -> bool {
self.0.load().get(spki).is_some()
}
}
#[cfg(test)]
mod tests {
use engenho_control_types::pin::KeyMaterial;
use super::*;
fn config(name: &str, spki: Spki, tier: RemoteTier) -> AuthorizedClientConfig {
AuthorizedClientConfig {
name: name.into(),
spki_sha256: spki.to_string(),
tier,
}
}
#[test]
fn a_pin_admits_its_client_at_the_declared_tier_and_revoking_it_takes_effect() {
let laptop = KeyMaterial::generate().unwrap().spki();
let ci = KeyMaterial::generate().unwrap().spki();
let pins = Pins::new(
AuthorizedSet::from_config(&[
config("laptop", laptop, RemoteTier::Destructive),
config("ci", ci, RemoteTier::Observe),
])
.unwrap(),
);
assert!(pins.admits(&laptop));
assert_eq!(pins.client(&ci).unwrap().tier, AuthorityTier::Observe);
let views = pins.current().views();
assert_eq!(
views.iter().map(|v| v.name.as_str()).collect::<Vec<_>>(),
["ci", "laptop"]
);
pins.replace(AuthorizedSet::from_config(&[config("ci", ci, RemoteTier::Observe)]).unwrap());
assert!(!pins.admits(&laptop), "revoked");
assert!(pins.admits(&ci));
}
#[test]
fn a_bad_pin_is_refused() {
let bad = AuthorizedClientConfig {
name: "x".into(),
spki_sha256: "sha256:nope".into(),
tier: RemoteTier::Observe,
};
assert!(AuthorizedSet::from_config(&[bad]).is_err());
}
}