reinhardt_auth/sessions/
rotation.rs1#![allow(clippy::field_reassign_with_default)]
34use super::backends::{SessionBackend, SessionError};
35use super::session::Session;
36use chrono::{DateTime, Duration as ChronoDuration, Utc};
37use std::time::Duration;
38
39#[derive(Debug, Clone)]
59pub enum RotationPolicy {
60 OnLogin,
62 Periodic(Duration),
64 AfterRequests(usize),
66 OnPrivilegeEscalation,
68 Never,
70}
71
72impl Default for RotationPolicy {
73 fn default() -> Self {
84 Self::OnLogin
85 }
86}
87
88#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
90pub struct RotationMetadata {
91 pub last_rotation: DateTime<Utc>,
93 pub request_count: usize,
95}
96
97impl Default for RotationMetadata {
98 fn default() -> Self {
99 Self {
100 last_rotation: Utc::now(),
101 request_count: 0,
102 }
103 }
104}
105
106pub struct SessionRotator {
133 policy: RotationPolicy,
134}
135
136impl SessionRotator {
137 pub fn new(policy: RotationPolicy) -> Self {
148 Self { policy }
149 }
150
151 pub async fn rotate<B: SessionBackend>(
182 &self,
183 session: &mut Session<B>,
184 ) -> Result<(), SessionError> {
185 session.cycle_key().await?;
187
188 let metadata = RotationMetadata::default();
190 session
191 .set("_rotation_metadata", metadata)
192 .map_err(|e| SessionError::SerializationError(e.to_string()))?;
193
194 Ok(())
195 }
196
197 pub fn should_rotate(&self, metadata: &RotationMetadata) -> bool {
215 match &self.policy {
216 RotationPolicy::OnLogin => false, RotationPolicy::Periodic(duration) => {
218 let elapsed = Utc::now() - metadata.last_rotation;
219 elapsed > ChronoDuration::from_std(*duration).unwrap()
220 }
221 RotationPolicy::AfterRequests(max_requests) => metadata.request_count >= *max_requests,
222 RotationPolicy::OnPrivilegeEscalation => false, RotationPolicy::Never => false,
224 }
225 }
226
227 pub fn increment_request_count(&self, metadata: &mut RotationMetadata) {
242 metadata.request_count += 1;
243 }
244}
245
246impl Default for SessionRotator {
247 fn default() -> Self {
257 Self::new(RotationPolicy::default())
258 }
259}
260
261#[cfg(test)]
262mod tests {
263 use super::*;
264 use crate::sessions::InMemorySessionBackend;
265 use rstest::rstest;
266
267 #[rstest]
268 #[tokio::test]
269 async fn test_rotation_policy_default() {
270 let policy = RotationPolicy::default();
271 match policy {
272 RotationPolicy::OnLogin => {}
273 _ => panic!("Expected OnLogin policy"),
274 }
275 }
276
277 #[rstest]
278 #[tokio::test]
279 async fn test_rotation_metadata_default() {
280 let metadata = RotationMetadata::default();
281 assert_eq!(metadata.request_count, 0);
282 assert!(metadata.last_rotation <= Utc::now());
283 }
284
285 #[rstest]
286 #[tokio::test]
287 async fn test_session_rotator_creation() {
288 let _rotator = SessionRotator::new(RotationPolicy::OnLogin);
289 }
290
291 #[rstest]
292 #[tokio::test]
293 async fn test_session_rotator_default() {
294 let _rotator = SessionRotator::default();
295 }
296
297 #[rstest]
298 #[tokio::test]
299 async fn test_rotate_session() {
300 let backend = InMemorySessionBackend::new();
301 let mut session = Session::new(backend);
302
303 session.set("user_id", 123).unwrap();
304 let old_key = session.get_or_create_key().to_string();
305
306 let rotator = SessionRotator::new(RotationPolicy::OnLogin);
307 rotator.rotate(&mut session).await.unwrap();
308
309 assert_ne!(session.get_or_create_key(), old_key);
311
312 let user_id: i32 = session.get("user_id").unwrap().unwrap();
314 assert_eq!(user_id, 123);
315 }
316
317 #[rstest]
318 #[tokio::test]
319 async fn test_should_rotate_periodic() {
320 let rotator = SessionRotator::new(RotationPolicy::Periodic(Duration::from_secs(3600)));
321
322 let metadata = RotationMetadata::default();
323 assert!(!rotator.should_rotate(&metadata));
325
326 let old_metadata = RotationMetadata {
328 last_rotation: Utc::now() - ChronoDuration::hours(2),
329 request_count: 0,
330 };
331 assert!(rotator.should_rotate(&old_metadata));
332 }
333
334 #[rstest]
335 #[tokio::test]
336 async fn test_should_rotate_after_requests() {
337 let rotator = SessionRotator::new(RotationPolicy::AfterRequests(100));
338
339 let mut metadata = RotationMetadata::default();
340 metadata.request_count = 99;
341 assert!(!rotator.should_rotate(&metadata));
342
343 metadata.request_count = 100;
344 assert!(rotator.should_rotate(&metadata));
345
346 metadata.request_count = 150;
347 assert!(rotator.should_rotate(&metadata));
348 }
349
350 #[rstest]
351 #[tokio::test]
352 async fn test_should_rotate_never() {
353 let rotator = SessionRotator::new(RotationPolicy::Never);
354
355 let metadata = RotationMetadata::default();
356 assert!(!rotator.should_rotate(&metadata));
357
358 let old_metadata = RotationMetadata {
359 last_rotation: Utc::now() - ChronoDuration::days(365),
360 request_count: 1000000,
361 };
362 assert!(!rotator.should_rotate(&old_metadata));
363 }
364
365 #[rstest]
366 #[tokio::test]
367 async fn test_increment_request_count() {
368 let rotator = SessionRotator::default();
369 let mut metadata = RotationMetadata::default();
370
371 assert_eq!(metadata.request_count, 0);
372 rotator.increment_request_count(&mut metadata);
373 assert_eq!(metadata.request_count, 1);
374 rotator.increment_request_count(&mut metadata);
375 assert_eq!(metadata.request_count, 2);
376 }
377
378 #[rstest]
379 #[tokio::test]
380 async fn test_rotate_preserves_all_session_data() {
381 let backend = InMemorySessionBackend::new();
383 let mut session = Session::new(backend);
384 session.set("user_id", 42).unwrap();
385 session.set("role", "admin").unwrap();
386 session.set("theme", "dark").unwrap();
387 let old_key = session.get_or_create_key().to_string();
388 let rotator = SessionRotator::new(RotationPolicy::OnLogin);
389
390 rotator.rotate(&mut session).await.unwrap();
392
393 assert_ne!(session.get_or_create_key(), old_key);
395 let user_id: i32 = session.get("user_id").unwrap().unwrap();
396 assert_eq!(user_id, 42);
397 let role: String = session.get("role").unwrap().unwrap();
398 assert_eq!(role, "admin");
399 let theme: String = session.get("theme").unwrap().unwrap();
400 assert_eq!(theme, "dark");
401 }
402
403 #[rstest]
404 #[tokio::test]
405 async fn test_rotation_metadata_stored() {
406 let backend = InMemorySessionBackend::new();
408 let mut session = Session::new(backend);
409 session.set("user_id", 1).unwrap();
410 let rotator = SessionRotator::new(RotationPolicy::OnLogin);
411
412 rotator.rotate(&mut session).await.unwrap();
414
415 let metadata: RotationMetadata = session.get("_rotation_metadata").unwrap().unwrap();
417 assert_eq!(metadata.request_count, 0);
418 assert!(metadata.last_rotation <= Utc::now());
419 }
420
421 #[rstest]
422 #[tokio::test]
423 async fn test_never_policy_no_rotation() {
424 let rotator = SessionRotator::new(RotationPolicy::Never);
426 let metadata = RotationMetadata::default();
427
428 let should_rotate = rotator.should_rotate(&metadata);
430
431 assert!(!should_rotate);
433 }
434}