use crate::auth::ApiKey;
use crate::error::Result;
use std::fmt;
use std::time::Duration;
#[derive(Clone)]
pub struct MintedToken {
pub value: String,
pub lifetime: Duration,
}
impl MintedToken {
pub fn new(value: impl Into<String>, lifetime_secs: u64) -> Self {
Self {
value: value.into(),
lifetime: Duration::from_secs(lifetime_secs),
}
}
}
impl fmt::Debug for MintedToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("MintedToken")
.field("value", &"<redacted>")
.field("lifetime", &self.lifetime)
.finish()
}
}
pub trait TokenSource: Send + Sync {
fn mint(&self) -> Result<MintedToken>;
fn describe(&self) -> String {
"token source".to_string()
}
}
impl fmt::Debug for dyn TokenSource {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("TokenSource")
.field("describe", &self.describe())
.finish()
}
}
impl TokenSource for ApiKey {
fn mint(&self) -> Result<MintedToken> {
Ok(MintedToken {
value: self.token()?,
lifetime: Duration::from_secs(self.lifetime_secs()),
})
}
fn describe(&self) -> String {
format!("api key {}", self.key_id())
}
}
#[derive(Clone)]
pub struct StaticToken {
value: String,
lifetime: Duration,
}
impl StaticToken {
pub fn new(value: impl Into<String>) -> Self {
Self {
value: value.into(),
lifetime: Duration::from_secs(20 * 60),
}
}
pub fn with_lifetime_secs(mut self, secs: u64) -> Self {
self.lifetime = Duration::from_secs(secs);
self
}
}
impl fmt::Debug for StaticToken {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.debug_struct("StaticToken")
.field("value", &"<redacted>")
.field("lifetime", &self.lifetime)
.finish()
}
}
impl TokenSource for StaticToken {
fn mint(&self) -> Result<MintedToken> {
Ok(MintedToken {
value: self.value.clone(),
lifetime: self.lifetime,
})
}
fn describe(&self) -> String {
"static token".to_string()
}
}
pub trait IntoTokenSource {
fn into_token_source(self) -> Box<dyn TokenSource>;
}
impl<T: TokenSource + 'static> IntoTokenSource for T {
fn into_token_source(self) -> Box<dyn TokenSource> {
Box::new(self)
}
}
impl IntoTokenSource for Box<dyn TokenSource> {
fn into_token_source(self) -> Box<dyn TokenSource> {
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn a_static_token_mints_its_value_with_apples_twenty_minute_ceiling() {
let minted = StaticToken::new("eyJ.static").mint().unwrap();
assert_eq!(minted.value, "eyJ.static");
assert_eq!(minted.lifetime, Duration::from_secs(20 * 60));
}
#[test]
fn a_static_token_lifetime_can_be_overridden() {
let minted = StaticToken::new("eyJ.static")
.with_lifetime_secs(90)
.mint()
.unwrap();
assert_eq!(minted.lifetime, Duration::from_secs(90));
}
#[test]
fn debug_output_never_contains_the_token() {
let token = StaticToken::new("eyJ.secret");
let minted = token.mint().unwrap();
let boxed: Box<dyn TokenSource> = Box::new(token.clone());
for rendered in [
format!("{token:?}"),
format!("{minted:?}"),
format!("{boxed:?}"),
] {
assert!(!rendered.contains("eyJ.secret"), "leaked: {rendered}");
}
assert_eq!(boxed.describe(), "static token");
}
#[test]
fn a_boxed_source_converts_into_a_token_source() {
let boxed: Box<dyn TokenSource> = Box::new(StaticToken::new("eyJ.boxed"));
let source = boxed.into_token_source();
assert_eq!(source.mint().unwrap().value, "eyJ.boxed");
}
}