auth_framework/distributed/mod.rs
1//! Distributed session store abstraction.
2//!
3//! Provides a trait for querying the total number of active sessions across all
4//! instances in a distributed deployment. A no-op [`LocalOnlySessionStore`] is
5//! provided for single-node use; it always reports zero total sessions, which
6//! causes `AuthFramework`'s remote-session estimate logic to return `0`
7//! instead of an incorrect `local_count * 2` estimate.
8//!
9//! # Production integration
10//!
11//! To use Redis (or any key–value store) as the distributed session backend,
12//! implement this trait and inject it via
13//! [`AuthFramework::set_distributed_store`][crate::auth::AuthFramework]:
14//!
15//! ```rust,no_run
16//! use std::sync::Arc;
17//! use auth_framework::distributed::DistributedSessionStore;
18//! use auth_framework::errors::Result;
19//! use async_trait::async_trait;
20//!
21//! // Example: wrap your chosen backend (e.g., a Redis client) in this struct.
22//! struct MySessionStore {
23//! // inner: redis::Client, // fill in your backend type
24//! }
25//!
26//! #[async_trait]
27//! impl DistributedSessionStore for MySessionStore {
28//! async fn total_session_count(&self) -> Result<u64> {
29//! // Query DBSIZE or scan session keys in your backend.
30//! Ok(42)
31//! }
32//! }
33//!
34//! # async fn example() -> auth_framework::errors::Result<()> {
35//! let mut framework = auth_framework::AuthFramework::builder().build().await?;
36//! framework.set_distributed_store(Arc::new(MySessionStore {}));
37//! # Ok(())
38//! # }
39//! ```
40
41pub mod rate_limiting;
42
43use crate::errors::Result;
44use async_trait::async_trait;
45
46/// Abstraction over a distributed session backend.
47///
48/// Implement this trait to integrate with Redis Cluster, Valkey, Hazelcast,
49/// or any other distributed key–value store that tracks session state.
50#[async_trait]
51pub trait DistributedSessionStore: Send + Sync {
52 /// Return the **total** number of active sessions across *all* nodes,
53 /// including the current one.
54 ///
55 /// The caller subtracts the local session count to arrive at the remote
56 /// estimate; returning `0` from the default [`LocalOnlySessionStore`]
57 /// therefore means "no remote sessions".
58 async fn total_session_count(&self) -> Result<u64>;
59}
60
61/// No-op store used when no distributed backend is configured.
62///
63/// [`total_session_count`][DistributedSessionStore::total_session_count] always
64/// returns `0`, so the framework correctly reports zero remote sessions instead
65/// of a fabricated value.
66pub struct LocalOnlySessionStore;
67
68#[async_trait]
69impl DistributedSessionStore for LocalOnlySessionStore {
70 async fn total_session_count(&self) -> Result<u64> {
71 Ok(0)
72 }
73}
74
75#[cfg(test)]
76mod tests {
77 use super::*;
78 use std::sync::Arc;
79
80 /// Default local-only store always returns 0.
81 #[tokio::test]
82 async fn test_local_only_returns_zero() {
83 let store = LocalOnlySessionStore;
84 assert_eq!(store.total_session_count().await.unwrap(), 0);
85 }
86
87 /// The trait is usable via dynamic dispatch.
88 #[tokio::test]
89 async fn test_dyn_dispatch() {
90 let store: Arc<dyn DistributedSessionStore> = Arc::new(LocalOnlySessionStore);
91 assert_eq!(store.total_session_count().await.unwrap(), 0);
92 }
93
94 /// [`LocalOnlySessionStore`] satisfies Send + Sync (required for Arc).
95 #[test]
96 fn test_local_only_is_send_sync() {
97 fn assert_send_sync<T: Send + Sync>() {}
98 assert_send_sync::<LocalOnlySessionStore>();
99 }
100
101 /// A custom in-memory store can be injected via the trait.
102 struct FixedCountStore(u64);
103
104 #[async_trait]
105 impl DistributedSessionStore for FixedCountStore {
106 async fn total_session_count(&self) -> Result<u64> {
107 Ok(self.0)
108 }
109 }
110
111 #[tokio::test]
112 async fn test_custom_store_returns_fixed_count() {
113 let store: Arc<dyn DistributedSessionStore> = Arc::new(FixedCountStore(99));
114 assert_eq!(store.total_session_count().await.unwrap(), 99);
115 }
116
117 /// Two calls to the same store return consistent results.
118 #[tokio::test]
119 async fn test_multiple_calls_consistent() {
120 let store = LocalOnlySessionStore;
121 for _ in 0..5 {
122 assert_eq!(store.total_session_count().await.unwrap(), 0);
123 }
124 }
125}