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