hackamore_control/
tokens.rs1use hackamore_models::control::MintResponse;
11use hackamore_models::policy::Policy;
12use parking_lot::RwLock;
13use std::collections::HashMap;
14use uuid::Uuid;
15
16struct Entry {
17 policy: Policy,
18 expires_at_ms: u64,
19 secret: Option<String>,
22}
23
24pub struct SigV4Mint {
28 pub access_key_id: String,
29 pub secret_access_key: String,
30 pub expires_at_ms: u64,
31}
32
33#[derive(Default)]
36pub struct Tokens {
37 entries: RwLock<HashMap<String, Entry>>,
38}
39
40impl Tokens {
41 pub fn new() -> Self {
42 Self::default()
43 }
44
45 pub fn mint(&self, policy: Policy, ttl_seconds: u64, now_ms: u64) -> MintResponse {
47 let token = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
49 let expires_at_ms = now_ms.saturating_add(ttl_seconds.saturating_mul(1000));
50 self.entries.write().insert(
51 token.clone(),
52 Entry {
53 policy,
54 expires_at_ms,
55 secret: None,
56 },
57 );
58 MintResponse {
59 token,
60 expires_at_ms,
61 }
62 }
63
64 pub fn mint_sigv4(&self, policy: Policy, ttl_seconds: u64, now_ms: u64) -> SigV4Mint {
67 let access_key_id = format!(
68 "AKIAHACKAMORE{}",
69 &Uuid::new_v4().simple().to_string()[..10]
70 )
71 .to_ascii_uppercase();
72 let secret_access_key = format!("{}{}", Uuid::new_v4().simple(), Uuid::new_v4().simple());
73 let expires_at_ms = now_ms.saturating_add(ttl_seconds.saturating_mul(1000));
74 self.entries.write().insert(
75 access_key_id.clone(),
76 Entry {
77 policy,
78 expires_at_ms,
79 secret: Some(secret_access_key.clone()),
80 },
81 );
82 SigV4Mint {
83 access_key_id,
84 secret_access_key,
85 expires_at_ms,
86 }
87 }
88
89 pub fn resolve_sigv4(&self, access_key_id: &str, now_ms: u64) -> Option<(Policy, String)> {
92 let entries = self.entries.read();
93 let entry = entries.get(access_key_id)?;
94 if entry.expires_at_ms <= now_ms {
95 return None;
96 }
97 let secret = entry.secret.clone()?;
98 Some((entry.policy.clone(), secret))
99 }
100
101 pub fn resolve(&self, token: &str, now_ms: u64) -> Option<Policy> {
103 self.resolve_full(token, now_ms).map(|(policy, _)| policy)
104 }
105
106 pub fn resolve_full(&self, token: &str, now_ms: u64) -> Option<(Policy, u64)> {
109 let entries = self.entries.read();
110 let entry = entries.get(token)?;
111 if entry.expires_at_ms <= now_ms {
112 return None;
113 }
114 Some((entry.policy.clone(), entry.expires_at_ms))
115 }
116
117 pub fn revoke(&self, token: &str) -> bool {
119 self.entries.write().remove(token).is_some()
120 }
121
122 pub fn sweep(&self, now_ms: u64) -> usize {
127 let mut entries = self.entries.write();
128 let before = entries.len();
129 entries.retain(|_, e| e.expires_at_ms > now_ms);
130 before - entries.len()
131 }
132
133 pub fn len(&self) -> usize {
135 self.entries.read().len()
136 }
137
138 pub fn is_empty(&self) -> bool {
140 self.entries.read().is_empty()
141 }
142}
143
144#[cfg(test)]
145#[allow(clippy::unwrap_used, clippy::expect_used, clippy::panic)]
146mod tests {
147 use super::*;
148
149 fn empty_policy() -> Policy {
150 Policy { rules: vec![] }
151 }
152
153 #[test]
154 fn mint_then_resolve_within_ttl() {
155 let tokens = Tokens::new();
156 let minted = tokens.mint(empty_policy(), 60, 1_000);
157 assert!(tokens.resolve(&minted.token, 1_000).is_some());
158 assert!(tokens.resolve(&minted.token, 60_999).is_some());
160 }
161
162 #[test]
163 fn token_expires() {
164 let tokens = Tokens::new();
165 let minted = tokens.mint(empty_policy(), 60, 1_000);
166 assert!(tokens.resolve(&minted.token, 61_000).is_none());
168 }
169
170 #[test]
171 fn unknown_token_resolves_none() {
172 let tokens = Tokens::new();
173 assert!(tokens.resolve("bogus", 1).is_none());
174 }
175
176 #[test]
177 fn sigv4_mint_resolves_by_access_key_id() {
178 let tokens = Tokens::new();
179 let m = tokens.mint_sigv4(empty_policy(), 60, 1_000);
180 assert!(m.access_key_id.starts_with("AKIAHACKAMORE"));
181 let (_policy, secret) = tokens.resolve_sigv4(&m.access_key_id, 1_000).unwrap();
182 assert_eq!(secret, m.secret_access_key);
183 assert!(tokens.resolve_sigv4(&m.access_key_id, 61_000).is_none());
185 assert!(tokens.resolve_sigv4("AKIAUNKNOWN", 1_000).is_none());
186 let bearer = tokens.mint(empty_policy(), 60, 1_000);
188 assert!(tokens.resolve_sigv4(&bearer.token, 1_000).is_none());
189 }
190
191 #[test]
192 fn revoke_invalidates() {
193 let tokens = Tokens::new();
194 let minted = tokens.mint(empty_policy(), 60, 0);
195 assert!(tokens.revoke(&minted.token));
196 assert!(tokens.resolve(&minted.token, 1).is_none());
197 assert!(!tokens.revoke(&minted.token));
198 }
199
200 #[test]
201 fn sweep_evicts_only_expired_entries() {
202 let tokens = Tokens::new();
203 let short = tokens.mint(empty_policy(), 60, 1_000);
205 let long = tokens.mint(empty_policy(), 3600, 1_000);
206 let dummy = tokens.mint_sigv4(empty_policy(), 60, 1_000);
207 assert_eq!(tokens.len(), 3);
208
209 assert_eq!(tokens.sweep(61_000), 2);
212 assert_eq!(tokens.len(), 1);
213 assert!(tokens.resolve(&short.token, 61_000).is_none());
214 assert!(tokens.resolve_sigv4(&dummy.access_key_id, 61_000).is_none());
215 assert!(tokens.resolve(&long.token, 61_000).is_some());
216
217 assert_eq!(tokens.sweep(61_000), 0);
219 }
220}