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}