use hackamore_models::control::MintResponse;
use hackamore_models::policy::Policy;
use parking_lot::RwLock;
use std::collections::HashMap;
use uuid::Uuid;
struct Entry {
policy: Policy,
expires_at_ms: u64,
secret: Option<String>,
}
pub struct SigV4Mint {
pub access_key_id: String,
pub secret_access_key: String,
pub expires_at_ms: u64,
}
#[derive(Default)]
pub struct Tokens {
entries: RwLock<HashMap<String, Entry>>,
}
impl Tokens {
pub fn new() -> Self {
Self::default()
}
pub fn mint(&self, policy: Policy, ttl_seconds: u64, now_ms: u64) -> MintResponse {
let token = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
let expires_at_ms = now_ms.saturating_add(ttl_seconds.saturating_mul(1000));
self.entries.write().insert(
token.clone(),
Entry {
policy,
expires_at_ms,
secret: None,
},
);
MintResponse {
token,
expires_at_ms,
}
}
pub fn mint_sigv4(&self, policy: Policy, ttl_seconds: u64, now_ms: u64) -> SigV4Mint {
let access_key_id = format!(
"AKIAHACKAMORE{}",
&Uuid::new_v4().simple().to_string()[..10]
)
.to_ascii_uppercase();
let secret_access_key = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
let expires_at_ms = now_ms.saturating_add(ttl_seconds.saturating_mul(1000));
self.entries.write().insert(
access_key_id.clone(),
Entry {
policy,
expires_at_ms,
secret: Some(secret_access_key.clone()),
},
);
SigV4Mint {
access_key_id,
secret_access_key,
expires_at_ms,
}
}
pub fn resolve_sigv4(&self, access_key_id: &str, now_ms: u64) -> Option<(Policy, String)> {
let entries = self.entries.read();
let entry = entries.get(access_key_id)?;
if entry.expires_at_ms <= now_ms {
return None;
}
let secret = entry.secret.clone()?;
Some((entry.policy.clone(), secret))
}
pub fn resolve(&self, token: &str, now_ms: u64) -> Option<Policy> {
self.resolve_full(token, now_ms).map(|(policy, _)| policy)
}
pub fn resolve_full(&self, token: &str, now_ms: u64) -> Option<(Policy, u64)> {
let entries = self.entries.read();
let entry = entries.get(token)?;
if entry.expires_at_ms <= now_ms {
return None;
}
Some((entry.policy.clone(), entry.expires_at_ms))
}
pub fn revoke(&self, token: &str) -> bool {
self.entries.write().remove(token).is_some()
}
pub fn sweep(&self, now_ms: u64) -> usize {
let mut entries = self.entries.write();
let before = entries.len();
entries.retain(|_, e| e.expires_at_ms > now_ms);
before - entries.len()
}
pub fn len(&self) -> usize {
self.entries.read().len()
}
pub fn is_empty(&self) -> bool {
self.entries.read().is_empty()
}
}
#[cfg(test)]
#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
mod tests {
use super::*;
fn empty_policy() -> Policy {
Policy { rules: vec![] }
}
#[test]
fn mint_then_resolve_within_ttl() {
let tokens = Tokens::new();
let minted = tokens.mint(empty_policy(), 60, 1_000);
assert!(tokens.resolve(&minted.token, 1_000).is_some());
assert!(tokens.resolve(&minted.token, 60_999).is_some());
}
#[test]
fn token_expires() {
let tokens = Tokens::new();
let minted = tokens.mint(empty_policy(), 60, 1_000);
assert!(tokens.resolve(&minted.token, 61_000).is_none());
}
#[test]
fn unknown_token_resolves_none() {
let tokens = Tokens::new();
assert!(tokens.resolve("bogus", 1).is_none());
}
#[test]
fn sigv4_mint_resolves_by_access_key_id() {
let tokens = Tokens::new();
let m = tokens.mint_sigv4(empty_policy(), 60, 1_000);
assert!(m.access_key_id.starts_with("AKIAHACKAMORE"));
let (_policy, secret) = tokens.resolve_sigv4(&m.access_key_id, 1_000).unwrap();
assert_eq!(secret, m.secret_access_key);
assert!(tokens.resolve_sigv4(&m.access_key_id, 61_000).is_none());
assert!(tokens.resolve_sigv4("AKIAUNKNOWN", 1_000).is_none());
let bearer = tokens.mint(empty_policy(), 60, 1_000);
assert!(tokens.resolve_sigv4(&bearer.token, 1_000).is_none());
}
#[test]
fn revoke_invalidates() {
let tokens = Tokens::new();
let minted = tokens.mint(empty_policy(), 60, 0);
assert!(tokens.revoke(&minted.token));
assert!(tokens.resolve(&minted.token, 1).is_none());
assert!(!tokens.revoke(&minted.token));
}
#[test]
fn sweep_evicts_only_expired_entries() {
let tokens = Tokens::new();
let short = tokens.mint(empty_policy(), 60, 1_000);
let long = tokens.mint(empty_policy(), 3600, 1_000);
let dummy = tokens.mint_sigv4(empty_policy(), 60, 1_000);
assert_eq!(tokens.len(), 3);
assert_eq!(tokens.sweep(61_000), 2);
assert_eq!(tokens.len(), 1);
assert!(tokens.resolve(&short.token, 61_000).is_none());
assert!(tokens.resolve_sigv4(&dummy.access_key_id, 61_000).is_none());
assert!(tokens.resolve(&long.token, 61_000).is_some());
assert_eq!(tokens.sweep(61_000), 0);
}
}