rustlavel_auth/tokens/
store.rs1use super::Token;
16use crate::guard::Identity;
17use rustlavel_core::Result;
18use std::collections::HashMap;
19use std::future::Future;
20use std::pin::Pin;
21use std::sync::{Arc, Mutex};
22
23pub type TokenFuture<'a, T> = Pin<Box<dyn Future<Output = Result<T>> + Send + 'a>>;
30
31pub trait TokenStore: Send + Sync + 'static {
32 fn create(&self, token: Token) -> TokenFuture<'_, Token>;
39
40 fn find<'a>(&'a self, id: &'a str) -> TokenFuture<'a, Option<Token>>;
45
46 fn delete<'a>(&'a self, id: &'a str) -> TokenFuture<'a, ()>;
48
49 fn delete_for_identity<'a>(&'a self, identity: &'a Identity) -> TokenFuture<'a, ()>;
55
56 fn touch<'a>(&'a self, id: &'a str, at: u64) -> TokenFuture<'a, ()>;
63}
64
65pub type SharedTokenStore = Arc<dyn TokenStore>;
67
68pub struct MemoryTokenStore {
74 tokens: Mutex<HashMap<String, Token>>,
75}
76
77impl MemoryTokenStore {
78 pub fn new() -> Self {
79 MemoryTokenStore { tokens: Mutex::new(HashMap::new()) }
80 }
81
82 pub fn len(&self) -> usize {
84 self.lock().len()
85 }
86
87 pub fn is_empty(&self) -> bool {
88 self.len() == 0
89 }
90
91 fn lock(&self) -> std::sync::MutexGuard<'_, HashMap<String, Token>> {
92 self.tokens.lock().expect("token store lock poisoned")
93 }
94
95 #[cfg(test)]
101 pub(crate) fn dump(&self) -> String {
102 let mut out = String::new();
103 for (id, token) in self.lock().iter() {
104 let digest: String = token.digest().iter().map(|byte| format!("{byte:02x}")).collect();
105 out.push_str(&format!("{id} {token:?} {digest}\n"));
106 }
107 out
108 }
109}
110
111impl Default for MemoryTokenStore {
112 fn default() -> Self {
113 MemoryTokenStore::new()
114 }
115}
116
117impl std::fmt::Debug for MemoryTokenStore {
122 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123 f.debug_struct("MemoryTokenStore").field("tokens", &self.len()).finish()
124 }
125}
126
127impl TokenStore for MemoryTokenStore {
128 fn create(&self, token: Token) -> TokenFuture<'_, Token> {
129 Box::pin(async move {
130 self.lock().insert(token.id().to_string(), token.clone());
131 Ok(token)
132 })
133 }
134
135 fn find<'a>(&'a self, id: &'a str) -> TokenFuture<'a, Option<Token>> {
136 Box::pin(async move { Ok(self.lock().get(id).cloned()) })
137 }
138
139 fn delete<'a>(&'a self, id: &'a str) -> TokenFuture<'a, ()> {
140 Box::pin(async move {
141 self.lock().remove(id);
142 Ok(())
143 })
144 }
145
146 fn delete_for_identity<'a>(&'a self, identity: &'a Identity) -> TokenFuture<'a, ()> {
147 Box::pin(async move {
148 self.lock().retain(|_, token| token.identity() != identity);
149 Ok(())
150 })
151 }
152
153 fn touch<'a>(&'a self, id: &'a str, at: u64) -> TokenFuture<'a, ()> {
154 Box::pin(async move {
155 if let Some(token) = self.lock().get_mut(id) {
156 token.set_last_used_at(at);
157 }
158 Ok(())
159 })
160 }
161}
162
163#[cfg(test)]
164mod tests {
165 use super::*;
166 use crate::tokens::{NewToken, Scopes, digest};
167
168 #[tokio::test]
169 async fn the_memory_store_round_trips_a_token() {
170 let store = MemoryTokenStore::new();
171 assert!(store.is_empty());
172
173 let token = Token::new("7", Identity::new("41"), "iPhone", digest("s"), Scopes::any());
174 let stored = store.create(token).await.unwrap();
175
176 assert_eq!(store.len(), 1);
177
178 let found = store.find("7").await.unwrap().expect("it was just created");
179 assert_eq!(found.id(), stored.id());
180 assert_eq!(found.name(), "iPhone");
181 assert_eq!(found.digest(), &digest("s"));
182 }
183
184 #[tokio::test]
185 async fn an_unknown_id_is_not_found_rather_than_an_error() {
186 let store = MemoryTokenStore::new();
187
188 assert!(store.find("nobody").await.unwrap().is_none());
189 assert!(store.find("").await.unwrap().is_none());
190
191 store.delete("nobody").await.unwrap();
193 store.touch("nobody", 1).await.unwrap();
194 }
195
196 #[tokio::test]
197 async fn deleting_for_one_identity_leaves_the_others() {
198 let store = MemoryTokenStore::new();
199 for (id, owner) in [("a", "41"), ("b", "41"), ("c", "99")] {
200 let token = Token::new(id, Identity::new(owner), id, digest(id), Scopes::any());
201 store.create(token).await.unwrap();
202 }
203
204 store.delete_for_identity(&Identity::new("41")).await.unwrap();
205
206 assert!(store.find("a").await.unwrap().is_none());
207 assert!(store.find("b").await.unwrap().is_none());
208 assert!(store.find("c").await.unwrap().is_some());
209 assert_eq!(store.len(), 1);
210 }
211
212 #[tokio::test]
213 async fn touching_records_the_moment_and_nothing_else() {
214 let store = MemoryTokenStore::new();
215 let token = Token::new("7", Identity::new("41"), "iPhone", digest("s"), Scopes::any());
216 store.create(token).await.unwrap();
217
218 store.touch("7", 1_700_000_000).await.unwrap();
219
220 let found = store.find("7").await.unwrap().unwrap();
221 assert_eq!(found.last_used_at(), Some(1_700_000_000));
222 assert_eq!(found.name(), "iPhone");
223 assert_eq!(found.digest(), &digest("s"));
224 }
225
226 #[tokio::test]
227 async fn a_store_works_behind_a_trait_object() {
228 let store: SharedTokenStore = Arc::new(MemoryTokenStore::new());
229
230 let issued =
231 NewToken::new(Identity::new("41"), "iPhone").any().issue(&*store).await.unwrap();
232
233 assert!(store.find(issued.token().id()).await.unwrap().is_some());
234 store.delete(issued.token().id()).await.unwrap();
235 assert!(store.find(issued.token().id()).await.unwrap().is_none());
236 }
237
238 #[test]
239 fn the_stores_debug_output_is_a_count() {
240 let printed = format!("{:?}", MemoryTokenStore::new());
241
242 assert!(printed.contains("MemoryTokenStore"), "printed {printed}");
243 assert!(printed.contains('0'));
244 }
245}