use std::collections::BTreeMap;
use std::fmt::Debug;
use std::sync::Mutex;
use std::time::Duration;
use async_trait::async_trait;
use crate::core::Timestamp;
use super::{PeerCredential, PeerId};
#[derive(Debug, thiserror::Error)]
pub enum CredentialError {
#[error("could not obtain a credential for '{audience}': {detail}")]
Unavailable { audience: PeerId, detail: String },
#[error(
"the issuer returned a credential for '{issued_for}' when '{audience}' was \
requested — an unbound token is one the recipient can replay"
)]
WrongAudience {
audience: PeerId,
issued_for: PeerId,
},
#[error(
"the issuer returned a credential for {} when '{subject}' was requested at \
'{audience}'",
issued_for.as_deref().map_or_else(|| "no subject".to_owned(), |s| format!("'{s}'"))
)]
WrongSubject {
audience: PeerId,
subject: String,
issued_for: Option<String>,
},
#[error("the issuer refused a credential for '{audience}': {detail}")]
Refused { audience: PeerId, detail: String },
#[error("the credential issued for '{audience}' is already spent")]
Stale { audience: PeerId },
}
#[async_trait]
pub trait TokenExchange: Send + Sync + Debug {
async fn exchange(
&self,
audience: &PeerId,
subject: &str,
) -> Result<PeerCredential, CredentialError>;
}
#[async_trait]
pub trait CredentialSource: Send + Sync + Debug {
async fn credential(
&self,
audience: &PeerId,
subject: &str,
now: Timestamp,
) -> Result<PeerCredential, CredentialError>;
fn forget(&self, _subject: &str) {}
}
#[allow(clippy::disallowed_methods)]
pub(super) fn now() -> Timestamp {
Timestamp::now_utc()
}
pub(super) fn bound_to(
credential: PeerCredential,
audience: &PeerId,
subject: &str,
) -> Result<PeerCredential, CredentialError> {
if credential.audience() != audience {
return Err(CredentialError::WrongAudience {
audience: audience.clone(),
issued_for: credential.audience().clone(),
});
}
if credential.subject() != Some(subject) {
return Err(CredentialError::WrongSubject {
audience: audience.clone(),
subject: subject.to_owned(),
issued_for: credential.subject().map(ToOwned::to_owned),
});
}
Ok(credential)
}
#[derive(Debug)]
pub struct Cached {
exchange: std::sync::Arc<dyn TokenExchange>,
skew: Duration,
held: Mutex<BTreeMap<(PeerId, String), PeerCredential>>,
}
impl Cached {
pub const DEFAULT_SKEW: Duration = Duration::from_mins(1);
#[must_use]
pub fn new(exchange: std::sync::Arc<dyn TokenExchange>) -> Self {
Self {
exchange,
skew: Self::DEFAULT_SKEW,
held: Mutex::new(BTreeMap::new()),
}
}
#[must_use]
pub const fn skew(mut self, skew: Duration) -> Self {
self.skew = skew;
self
}
}
#[async_trait]
impl CredentialSource for Cached {
async fn credential(
&self,
audience: &PeerId,
subject: &str,
now: Timestamp,
) -> Result<PeerCredential, CredentialError> {
let key = (audience.clone(), subject.to_owned());
{
let held = self.held.lock().expect("credential cache");
if let Some(c) = held.get(&key)
&& c.is_usable_at(now, self.skew)
{
return Ok(c.clone());
}
}
let fresh = bound_to(
self.exchange.exchange(audience, subject).await?,
audience,
subject,
)?;
if !fresh.is_usable_at(now, self.skew) {
return Err(CredentialError::Stale {
audience: audience.clone(),
});
}
self.held
.lock()
.expect("credential cache")
.insert(key, fresh.clone());
Ok(fresh)
}
fn forget(&self, subject: &str) {
self.held
.lock()
.expect("credential cache")
.retain(|(_, held_for), _| held_for != subject);
}
}