use anyhow::Result;
use chrono::{DateTime, Utc};
use uuid::Uuid;
use async_trait::async_trait;
use crate::models::identity::Identity;
#[async_trait]
pub trait TemporaryIdentityProvider: Send + Sync {
async fn create_temporary(&self, expires_at: DateTime<Utc>) -> Result<Identity>;
async fn get_temporary(&self, id: Uuid) -> Result<Option<Identity>>;
async fn is_expired(&self, id: Uuid) -> Result<bool>;
async fn revoke_temporary(&self, id: Uuid) -> Result<()>;
async fn is_revoked(&self, id: Uuid) -> Result<bool>;
}
#[cfg(test)]
mod tests {
use std::collections::HashSet;
use chrono::Duration;
use tokio::sync::RwLock;
use super::*;
struct InMemoryTemporaryProvider {
identities: RwLock<Vec<Identity>>,
revoked: RwLock<HashSet<Uuid>>,
}
impl InMemoryTemporaryProvider {
fn new() -> Self {
Self {
identities: RwLock::new(Vec::new()),
revoked: RwLock::new(HashSet::new()),
}
}
}
fn temporary_id(identity: &Identity) -> Uuid {
let Identity::Temporary { id, .. } = identity else {
panic!("expected a temporary identity");
};
*id
}
#[async_trait]
impl TemporaryIdentityProvider for InMemoryTemporaryProvider {
async fn create_temporary(&self, expires_at: DateTime<Utc>) -> Result<Identity> {
let identity = Identity::Temporary {
id: Uuid::now_v7(),
expires_at,
};
self.identities.write().await.push(identity.clone());
Ok(identity)
}
async fn get_temporary(&self, id: Uuid) -> Result<Option<Identity>> {
Ok(self
.identities
.read()
.await
.iter()
.find(|i| temporary_id(i) == id)
.cloned())
}
async fn is_expired(&self, id: Uuid) -> Result<bool> {
Ok(self
.get_temporary(id)
.await?
.is_some_and(|identity| match identity {
Identity::Temporary { expires_at, .. } => expires_at < Utc::now(),
_ => false,
}))
}
async fn revoke_temporary(&self, id: Uuid) -> Result<()> {
self.revoked.write().await.insert(id);
Ok(())
}
async fn is_revoked(&self, id: Uuid) -> Result<bool> {
Ok(self.revoked.read().await.contains(&id))
}
}
#[tokio::test]
async fn revoke_is_idempotent_and_permanent() {
let provider = InMemoryTemporaryProvider::new();
let identity = provider
.create_temporary(Utc::now() + Duration::hours(1))
.await
.unwrap();
let id = temporary_id(&identity);
assert!(!provider.is_expired(id).await.unwrap());
assert!(!provider.is_revoked(id).await.unwrap());
provider.revoke_temporary(id).await.unwrap();
assert!(provider.is_revoked(id).await.unwrap());
provider.revoke_temporary(id).await.unwrap();
assert!(provider.is_revoked(id).await.unwrap());
let unknown = Uuid::now_v7();
provider.revoke_temporary(unknown).await.unwrap();
assert!(provider.is_revoked(unknown).await.unwrap());
}
#[tokio::test]
async fn is_revoked_stays_orthogonal_to_expiry() {
let provider = InMemoryTemporaryProvider::new();
let identity = provider
.create_temporary(Utc::now() - Duration::hours(1))
.await
.unwrap();
let id = temporary_id(&identity);
assert!(provider.is_expired(id).await.unwrap());
assert!(!provider.is_revoked(id).await.unwrap());
}
}