Skip to main content

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}