1use std::sync::Arc;
39use std::time::Duration;
40
41use issuerd_cluster::cache_keys;
42use issuerd_core::{DistributedCache, RealmId, SessionId, UserId};
43use tracing::{debug, warn};
44
45use crate::state::ServerState;
46
47const NEGATIVE_TTL: Duration = Duration::from_secs(5);
50
51#[derive(Debug, Clone, PartialEq, Eq)]
53pub struct SessionSnapshot {
54 pub user_id: UserId,
55}
56
57#[derive(Debug, serde::Serialize, serde::Deserialize)]
59#[serde(untagged)]
60enum CacheEntry {
61 Negative { x: u8 },
62 Positive { u: String, v: u64 },
63}
64
65fn parse_entry(bytes: &[u8]) -> Option<CacheEntry> {
66 serde_json::from_slice(bytes).ok()
67}
68
69async fn current_version(
72 cache: &Arc<dyn DistributedCache>,
73 realm_id: &RealmId,
74 user_id: &UserId,
75) -> Option<u64> {
76 let key = cache_keys::session_version(realm_id.as_ref(), user_id.as_ref());
77 match cache.get(&key).await {
78 Ok(Some(bytes)) => match std::str::from_utf8(&bytes).ok()?.parse::<u64>() {
79 Ok(v) => Some(v),
80 Err(_) => {
81 warn!(realm = %realm_id, "session version counter unparsable; treating as 0");
82 Some(0)
83 }
84 },
85 Ok(None) => Some(0),
86 Err(e) => {
87 warn!(realm = %realm_id, error = %e, "session version read failed; falling back to storage");
88 None
89 }
90 }
91}
92
93pub async fn session_snapshot(
101 state: &Arc<ServerState>,
102 realm_id: &RealmId,
103 sid: &SessionId,
104) -> Option<SessionSnapshot> {
105 let ttl_secs = state.config.cache.read_cache_ttl_secs;
106 if ttl_secs == 0 {
107 return match state.storage.get_user_session(realm_id, sid).await {
109 Ok(Some(session)) => Some(SessionSnapshot {
110 user_id: session.user_id,
111 }),
112 Ok(None) => None,
113 Err(e) => {
114 debug!(realm = %realm_id, error = %e, "session read failed");
115 None
116 }
117 };
118 }
119
120 let key = cache_keys::session(realm_id.as_ref(), sid.as_ref());
121 match state.cache.get(&key).await {
122 Ok(Some(bytes)) => match parse_entry(&bytes) {
123 Some(CacheEntry::Negative { .. }) => {
124 debug!(realm = %realm_id, "session cache: negative hit");
125 None
126 }
127 Some(CacheEntry::Positive { u, v }) => {
128 let Ok(user_id) = UserId::new(&u) else {
129 debug!(realm = %realm_id, "session cache: malformed user id; re-reading");
130 return load_and_cache(state, realm_id, sid, ttl_secs).await;
131 };
132 match current_version(&state.cache, realm_id, &user_id).await {
133 Some(current) if current == v => {
134 debug!(realm = %realm_id, "session cache: hit");
135 Some(SessionSnapshot { user_id })
136 }
137 _ => load_and_cache(state, realm_id, sid, ttl_secs).await,
140 }
141 }
142 None => {
143 debug!(realm = %realm_id, "session cache: malformed entry; re-reading");
144 load_and_cache(state, realm_id, sid, ttl_secs).await
145 }
146 },
147 Ok(None) => load_and_cache(state, realm_id, sid, ttl_secs).await,
148 Err(e) => {
149 warn!(realm = %realm_id, error = %e, "session cache read failed; falling back to storage");
150 load_and_cache(state, realm_id, sid, ttl_secs).await
151 }
152 }
153}
154
155async fn load_and_cache(
157 state: &Arc<ServerState>,
158 realm_id: &RealmId,
159 sid: &SessionId,
160 ttl_secs: u64,
161) -> Option<SessionSnapshot> {
162 let key = cache_keys::session(realm_id.as_ref(), sid.as_ref());
163 match state.storage.get_user_session(realm_id, sid).await {
164 Ok(Some(session)) => {
165 let version =
169 current_version(&state.cache, realm_id, &session.user_id).await.unwrap_or(0);
170 let entry = CacheEntry::Positive {
171 u: session.user_id.to_string(),
172 v: version,
173 };
174 if let Ok(bytes) = serde_json::to_vec(&entry) {
175 if let Err(e) =
176 state.cache.set(&key, bytes, Some(Duration::from_secs(ttl_secs))).await
177 {
178 warn!(realm = %realm_id, error = %e, "session cache write failed");
179 }
180 }
181 Some(SessionSnapshot {
182 user_id: session.user_id,
183 })
184 }
185 Ok(None) => {
186 if let Ok(bytes) = serde_json::to_vec(&CacheEntry::Negative { x: 1 }) {
187 if let Err(e) = state.cache.set(&key, bytes, Some(NEGATIVE_TTL)).await {
188 warn!(realm = %realm_id, error = %e, "session cache write failed");
189 }
190 }
191 None
192 }
193 Err(e) => {
194 debug!(realm = %realm_id, error = %e, "session read failed");
195 None
196 }
197 }
198}
199
200pub async fn invalidate_session(state: &Arc<ServerState>, realm_id: &RealmId, sid: &SessionId) {
204 invalidate_session_entry(state.cache.as_ref(), realm_id, sid).await;
205}
206
207pub async fn invalidate_session_entry(
210 cache: &dyn DistributedCache,
211 realm_id: &RealmId,
212 sid: &SessionId,
213) {
214 let key = cache_keys::session(realm_id.as_ref(), sid.as_ref());
215 if let Err(e) = cache.delete(&key).await {
216 warn!(realm = %realm_id, error = %e, "session cache invalidation failed");
217 }
218}
219
220pub async fn bump_user_session_version(
226 cache: &dyn DistributedCache,
227 realm_id: &RealmId,
228 user_id: &UserId,
229) {
230 let key = cache_keys::session_version(realm_id.as_ref(), user_id.as_ref());
231 if let Err(e) = cache.increment(&key, None).await {
232 warn!(realm = %realm_id, error = %e, "session version bump failed");
233 }
234}
235
236#[cfg(test)]
237mod tests {
238 use super::*;
239 use issuerd_core::{AuthMethod, UserSession, Username};
240
241 async fn setup() -> (Arc<ServerState>, issuerd_core::RealmId, UserId, SessionId) {
242 let state = Arc::new(
243 ServerState::from_config(&crate::config::ServerConfig::default()).await.unwrap(),
244 );
245 let realm_id = RealmId::new("master").unwrap();
246 let user_id = UserId::new("admin").unwrap();
247 let sid = SessionId::new(issuerd_core::utils::generate_id()).unwrap();
248 (state, realm_id, user_id, sid)
249 }
250
251 fn session(realm_id: &RealmId, user_id: &UserId, sid: &SessionId) -> UserSession {
252 UserSession {
253 id: sid.clone(),
254 realm_id: realm_id.clone(),
255 user_id: user_id.clone(),
256 login_username: Username::new("admin").unwrap(),
257 ip_address: "127.0.0.1".parse().unwrap(),
258 auth_method: AuthMethod::Password,
259 remember_me: false,
260 offline: false,
261 started: chrono::Utc::now(),
262 last_session_refresh: chrono::Utc::now(),
263 auth_time: chrono::Utc::now(),
264 impersonator: None,
265 clients: vec![],
266 }
267 }
268
269 fn cache_key(realm_id: &RealmId, sid: &SessionId) -> String {
270 cache_keys::session(realm_id.as_ref(), sid.as_ref())
271 }
272
273 #[tokio::test]
274 async fn snapshot_caches_positive_entry() {
275 let (state, realm_id, user_id, sid) = setup().await;
276 state
277 .storage
278 .create_user_session(&realm_id, &session(&realm_id, &user_id, &sid))
279 .await
280 .unwrap();
281
282 let snap = session_snapshot(&state, &realm_id, &sid).await.expect("snapshot");
283 assert_eq!(snap.user_id, user_id);
284
285 state.storage.delete_user_session(&realm_id, &sid).await.unwrap();
289 let snap = session_snapshot(&state, &realm_id, &sid).await;
290 assert!(snap.is_some(), "stale positive entry served within TTL");
291
292 state
294 .storage
295 .create_user_session(&realm_id, &session(&realm_id, &user_id, &sid))
296 .await
297 .unwrap();
298 invalidate_session(&state, &realm_id, &sid).await;
299 state.storage.delete_user_session(&realm_id, &sid).await.unwrap();
300 assert!(session_snapshot(&state, &realm_id, &sid).await.is_none());
301 }
302
303 #[tokio::test]
304 async fn snapshot_negative_entry_absorbs_replays() {
305 let (state, realm_id, _, sid) = setup().await;
306
307 assert!(session_snapshot(&state, &realm_id, &sid).await.is_none());
309 let raw = state
310 .cache
311 .get(&cache_key(&realm_id, &sid))
312 .await
313 .unwrap()
314 .expect("negative marker cached");
315 assert_eq!(std::str::from_utf8(&raw).unwrap(), r#"{"x":1}"#);
316
317 let user_id = UserId::new("admin").unwrap();
321 state
322 .storage
323 .create_user_session(&realm_id, &session(&realm_id, &user_id, &sid))
324 .await
325 .unwrap();
326 assert!(session_snapshot(&state, &realm_id, &sid).await.is_none());
327 state.cache.delete(&cache_key(&realm_id, &sid)).await.unwrap();
328 assert!(session_snapshot(&state, &realm_id, &sid).await.is_some());
329 }
330
331 #[tokio::test]
332 async fn version_bump_invalidates_cached_snapshot() {
333 let (state, realm_id, user_id, sid) = setup().await;
334 state
335 .storage
336 .create_user_session(&realm_id, &session(&realm_id, &user_id, &sid))
337 .await
338 .unwrap();
339 assert!(session_snapshot(&state, &realm_id, &sid).await.is_some());
340
341 state.storage.delete_user(&realm_id, &user_id).await.unwrap();
344 bump_user_session_version(state.cache.as_ref(), &realm_id, &user_id).await;
345 assert!(session_snapshot(&state, &realm_id, &sid).await.is_none());
346
347 let v = state
349 .cache
350 .get(&cache_keys::session_version(realm_id.as_ref(), user_id.as_ref()))
351 .await
352 .unwrap()
353 .expect("version counter present");
354 assert_eq!(std::str::from_utf8(&v).unwrap(), "1");
355 }
356
357 #[tokio::test]
358 async fn version_bump_alone_does_not_hide_live_session() {
359 let (state, realm_id, user_id, sid) = setup().await;
360 state
361 .storage
362 .create_user_session(&realm_id, &session(&realm_id, &user_id, &sid))
363 .await
364 .unwrap();
365 assert!(session_snapshot(&state, &realm_id, &sid).await.is_some());
366
367 bump_user_session_version(state.cache.as_ref(), &realm_id, &user_id).await;
370 assert!(session_snapshot(&state, &realm_id, &sid).await.is_some());
371 assert!(session_snapshot(&state, &realm_id, &sid).await.is_some());
372 }
373
374 #[tokio::test]
375 async fn disabled_cache_is_pure_db() {
376 let mut cfg = crate::config::ServerConfig::default();
377 cfg.cache.read_cache_ttl_secs = 0;
378 let state = Arc::new(ServerState::from_config(&cfg).await.unwrap());
379 let realm_id = RealmId::new("master").unwrap();
380 let user_id = UserId::new("admin").unwrap();
381 let sid = SessionId::new(issuerd_core::utils::generate_id()).unwrap();
382 state
383 .storage
384 .create_user_session(&realm_id, &session(&realm_id, &user_id, &sid))
385 .await
386 .unwrap();
387
388 assert!(session_snapshot(&state, &realm_id, &sid).await.is_some());
389 assert!(state.cache.get(&cache_key(&realm_id, &sid)).await.unwrap().is_none());
391 state.storage.delete_user_session(&realm_id, &sid).await.unwrap();
393 assert!(session_snapshot(&state, &realm_id, &sid).await.is_none());
394 assert!(state.cache.get(&cache_key(&realm_id, &sid)).await.unwrap().is_none());
395 }
396
397 #[tokio::test]
398 async fn cache_outage_falls_back_to_storage() {
399 #[derive(Debug)]
400 struct DeadCache;
401 #[async_trait::async_trait]
402 impl DistributedCache for DeadCache {
403 async fn get(&self, _: &str) -> Result<Option<Vec<u8>>, issuerd_core::IssuerdError> {
404 Err(issuerd_core::IssuerdError::ServerError("cache down".into()))
405 }
406 async fn set(
407 &self,
408 _: &str,
409 _: Vec<u8>,
410 _: Option<Duration>,
411 ) -> Result<(), issuerd_core::IssuerdError> {
412 Err(issuerd_core::IssuerdError::ServerError("cache down".into()))
413 }
414 async fn delete(&self, _: &str) -> Result<(), issuerd_core::IssuerdError> {
415 Err(issuerd_core::IssuerdError::ServerError("cache down".into()))
416 }
417 async fn compare_and_swap(
418 &self,
419 _: &str,
420 _: Option<Vec<u8>>,
421 _: Vec<u8>,
422 ) -> Result<bool, issuerd_core::IssuerdError> {
423 Err(issuerd_core::IssuerdError::ServerError("cache down".into()))
424 }
425 async fn publish(&self, _: &str, _: Vec<u8>) -> Result<(), issuerd_core::IssuerdError> {
426 Err(issuerd_core::IssuerdError::ServerError("cache down".into()))
427 }
428 async fn subscribe(
429 &self,
430 _: &str,
431 _: Box<dyn Fn(Vec<u8>) + Send + Sync>,
432 ) -> Result<(), issuerd_core::IssuerdError> {
433 Err(issuerd_core::IssuerdError::ServerError("cache down".into()))
434 }
435 }
436
437 let cfg = crate::config::ServerConfig::default();
438 let storage: Arc<dyn issuerd_core::Storage> =
439 Arc::new(issuerd_storage::InMemoryStorage::new());
440 let state = Arc::new(
441 ServerState::from_components(&cfg, storage, Arc::new(DeadCache)).await.unwrap(),
442 );
443 let realm_id = RealmId::new("master").unwrap();
444 let user_id = UserId::new("admin").unwrap();
445 let sid = SessionId::new(issuerd_core::utils::generate_id()).unwrap();
446 state
447 .storage
448 .create_user_session(&realm_id, &session(&realm_id, &user_id, &sid))
449 .await
450 .unwrap();
451
452 assert!(session_snapshot(&state, &realm_id, &sid).await.is_some());
454 state.storage.delete_user_session(&realm_id, &sid).await.unwrap();
455 assert!(session_snapshot(&state, &realm_id, &sid).await.is_none());
456 }
457}