1use std::{
4 collections::HashMap,
5 future::{Future, Ready, ready},
6 sync::{Mutex, MutexGuard, PoisonError},
7};
8
9use chrono::{DateTime, TimeDelta, Utc};
10use uuid::Uuid;
11
12use super::{ClaimScope, SessionRecord, SessionStore, SessionUpdate, Supersede, TokenLookup, TokenRecord};
13use crate::{
14 error::StoreError,
15 event::{EventKind, SessionEvent},
16 token::TokenHash,
17};
18
19#[derive(Debug, Default)]
22pub struct MemoryStore {
23 state: Mutex<State>,
24}
25
26#[derive(Debug, Default)]
27struct State {
28 sessions: HashMap<Uuid, Entry>,
29 token_index: HashMap<TokenHash, Uuid>,
30 events: Vec<SessionEvent>,
31 claims: HashMap<(ClaimKey, EventKind), DateTime<Utc>>,
32}
33
34#[derive(Debug, PartialEq, Eq, Hash)]
36enum ClaimKey {
37 Session(Uuid),
38 Subject(String),
39}
40
41#[derive(Debug)]
42struct Entry {
43 record: SessionRecord,
44 tokens: Vec<TokenRecord>,
46}
47
48impl State {
49 fn remove(&mut self, id: Uuid) -> bool {
50 let Some(entry) = self.sessions.remove(&id) else {
51 return false;
52 };
53 for token in entry.tokens {
54 self.token_index.remove(&token.hash);
55 }
56 true
57 }
58
59 fn apply(&mut self, update: &SessionUpdate) -> bool {
60 let Some(entry) = self.sessions.get_mut(&update.session_id) else {
61 return false;
62 };
63 let now = update.now;
64 let mut rotated = false;
65 if let Some(rotation) = &update.rotation
66 && let Some(current) = entry.tokens.last_mut()
67 && current.superseded_at.is_none()
68 {
69 let holds = match rotation.supersede {
70 Supersede::Exact(hash) => current.hash == hash,
71 Supersede::CurrentIssuedBefore(cutoff) => current.issued_at < cutoff,
72 Supersede::Current => true,
73 };
74 if holds && !self.token_index.contains_key(&rotation.new_token) {
75 current.superseded_at = Some(now);
76 entry.tokens.push(TokenRecord {
77 hash: rotation.new_token,
78 issued_at: now,
79 first_used_at: None,
80 superseded_at: None,
81 });
82 self.token_index.insert(rotation.new_token, update.session_id);
83 let cutoff = rotation.prune_superseded_before;
84 let index = &mut self.token_index;
85 entry.tokens.retain(|t| match t.superseded_at {
86 Some(at) if at < cutoff => {
87 index.remove(&t.hash);
88 false
89 }
90 _ => true,
91 });
92 rotated = true;
93 }
94 }
95 if let Some(hash) = update.mark_used
96 && let Some(token) = entry.tokens.iter_mut().find(|t| t.hash == hash)
97 && token.first_used_at.is_none()
98 {
99 token.first_used_at = Some(now);
100 }
101 let record = &mut entry.record;
102 record.last_seen_at = now;
103 if let Some(baseline) = &update.baseline {
104 record.baseline.clone_from(baseline);
105 }
106 if let Some(flag) = update.step_up_required {
107 record.step_up_required = flag;
108 }
109 if let Some(at) = update.authenticated_at {
110 record.authenticated_at = at;
111 }
112 rotated
113 }
114}
115
116impl MemoryStore {
117 pub fn new() -> Self {
119 Self::default()
120 }
121
122 pub fn events(&self) -> Vec<SessionEvent> {
124 self.lock().events.clone()
125 }
126
127 pub fn session_count(&self) -> usize {
129 self.lock().sessions.len()
130 }
131
132 fn lock(&self) -> MutexGuard<'_, State> {
133 self.state.lock().unwrap_or_else(PoisonError::into_inner)
136 }
137
138 fn with<T>(&self, f: impl FnOnce(&mut State) -> T) -> Ready<Result<T, StoreError>> {
139 ready(Ok(f(&mut self.lock())))
140 }
141}
142
143impl SessionStore for MemoryStore {
144 fn insert_session(
145 &self,
146 session: &SessionRecord,
147 token: &TokenHash,
148 ) -> impl Future<Output = Result<(), StoreError>> + Send {
149 let result = {
150 let mut state = self.lock();
151 if state.token_index.contains_key(token) || state.sessions.contains_key(&session.id) {
152 Err("duplicate session id or token".into())
153 } else {
154 state.token_index.insert(*token, session.id);
155 state.sessions.insert(
156 session.id,
157 Entry {
158 record: session.clone(),
159 tokens: vec![TokenRecord {
160 hash: *token,
161 issued_at: session.created_at,
162 first_used_at: None,
163 superseded_at: None,
164 }],
165 },
166 );
167 Ok(())
168 }
169 };
170 ready(result)
171 }
172
173 fn find_by_token(&self, token: &TokenHash) -> impl Future<Output = Result<Option<TokenLookup>, StoreError>> + Send {
174 self.with(|state| {
175 let id = state.token_index.get(token)?;
176 let entry = state.sessions.get(id)?;
177 let position = entry.tokens.iter().position(|t| t.hash == *token)?;
178 Some(TokenLookup {
179 session: entry.record.clone(),
180 token: entry.tokens[position],
181 newer_token_used: entry.tokens[position + 1..].iter().any(|t| t.first_used_at.is_some()),
182 })
183 })
184 }
185
186 fn apply(&self, update: &SessionUpdate) -> impl Future<Output = Result<bool, StoreError>> + Send {
187 self.with(|state| state.apply(update))
188 }
189
190 fn delete_session(&self, session_id: Uuid) -> impl Future<Output = Result<bool, StoreError>> + Send {
191 self.with(|state| state.remove(session_id))
192 }
193
194 fn delete_subject_sessions(&self, subject: &str) -> impl Future<Output = Result<Vec<Uuid>, StoreError>> + Send {
195 self.with(|state| {
196 let ids: Vec<Uuid> = state
197 .sessions
198 .values()
199 .filter(|e| e.record.subject == subject)
200 .map(|e| e.record.id)
201 .collect();
202 ids.into_iter().filter(|id| state.remove(*id)).collect()
203 })
204 }
205
206 fn list_subject_sessions(
207 &self,
208 subject: &str,
209 ) -> impl Future<Output = Result<Vec<SessionRecord>, StoreError>> + Send {
210 self.with(|state| {
211 state
212 .sessions
213 .values()
214 .filter(|e| e.record.subject == subject)
215 .map(|e| e.record.clone())
216 .collect()
217 })
218 }
219
220 fn delete_expired(
221 &self,
222 now: DateTime<Utc>,
223 idle_cutoff: DateTime<Utc>,
224 ) -> impl Future<Output = Result<u64, StoreError>> + Send {
225 self.with(|state| {
226 let ids: Vec<Uuid> = state
227 .sessions
228 .values()
229 .filter(|e| e.record.expires_at <= now || e.record.last_seen_at <= idle_cutoff)
230 .map(|e| e.record.id)
231 .collect();
232 ids.into_iter().filter(|id| state.remove(*id)).count() as u64
233 })
234 }
235
236 fn record_event(&self, event: &SessionEvent) -> impl Future<Output = Result<(), StoreError>> + Send {
237 self.with(|state| state.events.push(event.clone()))
238 }
239
240 fn delete_events_before(&self, cutoff: DateTime<Utc>) -> impl Future<Output = Result<u64, StoreError>> + Send {
241 self.with(|state| {
242 let before = state.events.len();
243 state.events.retain(|e| e.at >= cutoff);
244 state.claims.retain(|_, at| *at >= cutoff);
245 (before - state.events.len()) as u64
246 })
247 }
248
249 fn claim_event(
250 &self,
251 scope: ClaimScope<'_>,
252 kind: EventKind,
253 now: DateTime<Utc>,
254 window: TimeDelta,
255 ) -> impl Future<Output = Result<bool, StoreError>> + Send {
256 let key = match scope {
257 ClaimScope::Session(id) => (ClaimKey::Session(id), kind),
258 ClaimScope::Subject(subject) => (ClaimKey::Subject(subject.to_owned()), kind),
259 };
260 self.with(|state| match state.claims.get(&key) {
261 Some(at) if now - *at < window => false,
262 _ => {
263 state.claims.insert(key, now);
264 true
265 }
266 })
267 }
268}
269
270#[cfg(test)]
271mod tests {
272 use super::MemoryStore;
273
274 crate::store_conformance!(|| async { MemoryStore::new() });
275
276 #[tokio::test]
277 async fn duplicate_token_is_rejected() {
278 use crate::{store::SessionStore, testing};
279 let store = MemoryStore::new();
280 let now = testing::now_secs();
281 let token = testing::random_hash();
282 store
283 .insert_session(&testing::sample_session(now), &token)
284 .await
285 .unwrap();
286 assert!(
287 store
288 .insert_session(&testing::sample_session(now), &token)
289 .await
290 .is_err()
291 );
292 assert_eq!(store.session_count(), 1);
293 }
294}