Skip to main content

nntp_proxy/session/
auth_state.rs

1//! Authentication state management for client sessions
2//!
3//! This module provides a type-safe wrapper around authentication state,
4//! ensuring proper initialization and access patterns.
5
6use std::sync::atomic::{AtomicBool, Ordering};
7use std::sync::{Arc, OnceLock};
8
9/// Represents the authentication state of a client session
10///
11/// This type encapsulates the authentication status and username in a
12/// thread-safe manner using atomic operations and write-once semantics.
13///
14/// # Design
15///
16/// - **Status**: `AtomicBool` for lock-free concurrent access
17/// - **Username**: `OnceLock<Arc<str>>` for write-once, cheap-clone reads
18/// - Both fields are private and accessed through controlled methods
19///
20/// # Examples
21///
22/// ```
23/// use nntp_proxy::session::AuthState;
24///
25/// let auth_state = AuthState::new();
26/// assert!(!auth_state.is_authenticated());
27///
28/// // After authentication
29/// auth_state.mark_authenticated("user@example.com");
30/// assert!(auth_state.is_authenticated());
31/// assert_eq!(auth_state.username().unwrap(), "user@example.com");
32/// ```
33#[derive(Debug)]
34pub struct AuthState {
35    /// Whether the client has successfully authenticated
36    ///
37    /// Starts as `false` and is set to `true` after successful authentication.
38    /// Uses `Relaxed` ordering since authentication is a one-way transition
39    /// and doesn't require synchronization with other memory operations.
40    authenticated: AtomicBool,
41
42    /// The authenticated username (if any)
43    ///
44    /// Write-once field that stores the username after successful authentication.
45    /// Uses `Arc<str>` for cheap clones when reading the username.
46    username: OnceLock<Arc<str>>,
47}
48
49impl AuthState {
50    /// Create a new unauthenticated state
51    ///
52    /// # Examples
53    ///
54    /// ```
55    /// use nntp_proxy::session::AuthState;
56    ///
57    /// let auth_state = AuthState::new();
58    /// assert!(!auth_state.is_authenticated());
59    /// assert!(auth_state.username().is_none());
60    /// ```
61    #[inline]
62    #[must_use]
63    pub const fn new() -> Self {
64        Self {
65            authenticated: AtomicBool::new(false),
66            username: OnceLock::new(),
67        }
68    }
69
70    /// Check if the client has authenticated
71    ///
72    /// This is a cheap operation (single atomic load) that can be called
73    /// frequently without performance concerns.
74    ///
75    /// # Examples
76    ///
77    /// ```
78    /// use nntp_proxy::session::AuthState;
79    ///
80    /// let auth_state = AuthState::new();
81    /// assert!(!auth_state.is_authenticated());
82    ///
83    /// auth_state.mark_authenticated("alice");
84    /// assert!(auth_state.is_authenticated());
85    /// ```
86    #[inline]
87    #[must_use]
88    pub fn is_authenticated(&self) -> bool {
89        self.authenticated.load(Ordering::Relaxed)
90    }
91
92    /// Mark the client as authenticated with the given username
93    ///
94    /// This is a one-way operation - once authenticated, the state cannot
95    /// be reverted. The username is stored in a write-once field.
96    ///
97    /// # Arguments
98    ///
99    /// * `username` - The authenticated username
100    ///
101    /// # Panics
102    ///
103    /// Panics if called multiple times with different usernames (implementation
104    /// detail of `OnceLock::set`).
105    ///
106    /// # Examples
107    ///
108    /// ```
109    /// use nntp_proxy::session::AuthState;
110    ///
111    /// let auth_state = AuthState::new();
112    /// auth_state.mark_authenticated("bob");
113    ///
114    /// assert!(auth_state.is_authenticated());
115    /// assert_eq!(auth_state.username().unwrap(), "bob");
116    /// ```
117    #[inline]
118    pub fn mark_authenticated(&self, username: impl Into<Arc<str>>) {
119        let username_arc: Arc<str> = username.into();
120        // Set username first (write-once, safe to call multiple times with same value)
121        let _ = self.username.set(username_arc);
122        // Then mark as authenticated (one-way transition)
123        self.authenticated.store(true, Ordering::Relaxed);
124    }
125
126    /// Get the authenticated username if available
127    ///
128    /// Returns a cheap-to-clone `Arc<str>` reference to the username.
129    /// Returns `None` if the client has not authenticated yet.
130    ///
131    /// # Examples
132    ///
133    /// ```
134    /// use nntp_proxy::session::AuthState;
135    ///
136    /// let auth_state = AuthState::new();
137    /// assert!(auth_state.username().is_none());
138    ///
139    /// auth_state.mark_authenticated("charlie");
140    /// let username = auth_state.username().unwrap();
141    /// assert_eq!(username, "charlie");
142    ///
143    /// // Cloning is cheap (Arc reference count bump)
144    /// let username2 = username.clone();
145    /// assert_eq!(username2, "charlie");
146    /// ```
147    #[inline]
148    #[must_use]
149    pub fn username(&self) -> Option<&str> {
150        self.username.get().map(|arc| &**arc)
151    }
152
153    /// Check if authenticated, optionally bypassing the check
154    ///
155    /// This method is useful when authentication checks can be skipped
156    /// (e.g., when the backend doesn't require authentication).
157    ///
158    /// # Arguments
159    ///
160    /// * `skip_check` - If `true`, always returns `true`. Otherwise, returns actual auth state.
161    ///
162    /// # Examples
163    ///
164    /// ```
165    /// use nntp_proxy::session::AuthState;
166    ///
167    /// let auth_state = AuthState::new();
168    /// assert!(!auth_state.is_authenticated_or_skipped(false));
169    /// assert!(auth_state.is_authenticated_or_skipped(true)); // Skips check
170    ///
171    /// auth_state.mark_authenticated("dave");
172    /// assert!(auth_state.is_authenticated_or_skipped(false));
173    /// assert!(auth_state.is_authenticated_or_skipped(true));
174    /// ```
175    #[inline]
176    #[must_use]
177    pub fn is_authenticated_or_skipped(&self, skip_check: bool) -> bool {
178        skip_check || self.is_authenticated()
179    }
180}
181
182impl Default for AuthState {
183    #[inline]
184    fn default() -> Self {
185        Self::new()
186    }
187}
188
189#[cfg(test)]
190mod tests {
191    use super::*;
192
193    #[test]
194    fn test_new_unauthenticated() {
195        let state = AuthState::new();
196        assert!(!state.is_authenticated());
197        assert!(state.username().is_none());
198    }
199
200    #[test]
201    fn test_mark_authenticated() {
202        let state = AuthState::new();
203        state.mark_authenticated("testuser");
204
205        assert!(state.is_authenticated());
206        assert_eq!(state.username().unwrap(), "testuser");
207    }
208
209    #[test]
210    fn test_mark_authenticated_with_arc() {
211        let state = AuthState::new();
212        let username: Arc<str> = Arc::from("arcuser");
213        state.mark_authenticated(username);
214
215        assert!(state.is_authenticated());
216        assert_eq!(state.username().unwrap(), "arcuser");
217    }
218
219    #[test]
220    fn test_is_authenticated_or_skipped() {
221        let state = AuthState::new();
222
223        // Not authenticated, skip=false
224        assert!(!state.is_authenticated_or_skipped(false));
225
226        // Not authenticated, skip=true
227        assert!(state.is_authenticated_or_skipped(true));
228
229        state.mark_authenticated("skiptest");
230
231        // Authenticated, skip=false
232        assert!(state.is_authenticated_or_skipped(false));
233
234        // Authenticated, skip=true
235        assert!(state.is_authenticated_or_skipped(true));
236    }
237
238    #[test]
239    fn test_default() {
240        let state = AuthState::default();
241        assert!(!state.is_authenticated());
242        assert!(state.username().is_none());
243    }
244
245    #[test]
246    fn test_multiple_mark_same_username() {
247        let state = AuthState::new();
248        state.mark_authenticated("same");
249        state.mark_authenticated("same"); // Should not panic
250
251        assert!(state.is_authenticated());
252        assert_eq!(state.username().unwrap(), "same");
253    }
254}