#![allow(clippy::field_reassign_with_default)]
use super::backends::{SessionBackend, SessionError};
use super::session::Session;
use chrono::{DateTime, Duration as ChronoDuration, Utc};
use std::time::Duration;
#[derive(Debug, Clone)]
pub enum RotationPolicy {
OnLogin,
Periodic(Duration),
AfterRequests(usize),
OnPrivilegeEscalation,
Never,
}
impl Default for RotationPolicy {
fn default() -> Self {
Self::OnLogin
}
}
#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)]
pub struct RotationMetadata {
pub last_rotation: DateTime<Utc>,
pub request_count: usize,
}
impl Default for RotationMetadata {
fn default() -> Self {
Self {
last_rotation: Utc::now(),
request_count: 0,
}
}
}
pub struct SessionRotator {
policy: RotationPolicy,
}
impl SessionRotator {
pub fn new(policy: RotationPolicy) -> Self {
Self { policy }
}
pub async fn rotate<B: SessionBackend>(
&self,
session: &mut Session<B>,
) -> Result<(), SessionError> {
session.cycle_key().await?;
let metadata = RotationMetadata::default();
session
.set("_rotation_metadata", metadata)
.map_err(|e| SessionError::SerializationError(e.to_string()))?;
Ok(())
}
pub fn should_rotate(&self, metadata: &RotationMetadata) -> bool {
match &self.policy {
RotationPolicy::OnLogin => false, RotationPolicy::Periodic(duration) => {
let elapsed = Utc::now() - metadata.last_rotation;
elapsed > ChronoDuration::from_std(*duration).unwrap()
}
RotationPolicy::AfterRequests(max_requests) => metadata.request_count >= *max_requests,
RotationPolicy::OnPrivilegeEscalation => false, RotationPolicy::Never => false,
}
}
pub fn increment_request_count(&self, metadata: &mut RotationMetadata) {
metadata.request_count += 1;
}
}
impl Default for SessionRotator {
fn default() -> Self {
Self::new(RotationPolicy::default())
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::sessions::InMemorySessionBackend;
use rstest::rstest;
#[rstest]
#[tokio::test]
async fn test_rotation_policy_default() {
let policy = RotationPolicy::default();
match policy {
RotationPolicy::OnLogin => {}
_ => panic!("Expected OnLogin policy"),
}
}
#[rstest]
#[tokio::test]
async fn test_rotation_metadata_default() {
let metadata = RotationMetadata::default();
assert_eq!(metadata.request_count, 0);
assert!(metadata.last_rotation <= Utc::now());
}
#[rstest]
#[tokio::test]
async fn test_session_rotator_creation() {
let _rotator = SessionRotator::new(RotationPolicy::OnLogin);
}
#[rstest]
#[tokio::test]
async fn test_session_rotator_default() {
let _rotator = SessionRotator::default();
}
#[rstest]
#[tokio::test]
async fn test_rotate_session() {
let backend = InMemorySessionBackend::new();
let mut session = Session::new(backend);
session.set("user_id", 123).unwrap();
let old_key = session.get_or_create_key().to_string();
let rotator = SessionRotator::new(RotationPolicy::OnLogin);
rotator.rotate(&mut session).await.unwrap();
assert_ne!(session.get_or_create_key(), old_key);
let user_id: i32 = session.get("user_id").unwrap().unwrap();
assert_eq!(user_id, 123);
}
#[rstest]
#[tokio::test]
async fn test_should_rotate_periodic() {
let rotator = SessionRotator::new(RotationPolicy::Periodic(Duration::from_secs(3600)));
let metadata = RotationMetadata::default();
assert!(!rotator.should_rotate(&metadata));
let old_metadata = RotationMetadata {
last_rotation: Utc::now() - ChronoDuration::hours(2),
request_count: 0,
};
assert!(rotator.should_rotate(&old_metadata));
}
#[rstest]
#[tokio::test]
async fn test_should_rotate_after_requests() {
let rotator = SessionRotator::new(RotationPolicy::AfterRequests(100));
let mut metadata = RotationMetadata::default();
metadata.request_count = 99;
assert!(!rotator.should_rotate(&metadata));
metadata.request_count = 100;
assert!(rotator.should_rotate(&metadata));
metadata.request_count = 150;
assert!(rotator.should_rotate(&metadata));
}
#[rstest]
#[tokio::test]
async fn test_should_rotate_never() {
let rotator = SessionRotator::new(RotationPolicy::Never);
let metadata = RotationMetadata::default();
assert!(!rotator.should_rotate(&metadata));
let old_metadata = RotationMetadata {
last_rotation: Utc::now() - ChronoDuration::days(365),
request_count: 1000000,
};
assert!(!rotator.should_rotate(&old_metadata));
}
#[rstest]
#[tokio::test]
async fn test_increment_request_count() {
let rotator = SessionRotator::default();
let mut metadata = RotationMetadata::default();
assert_eq!(metadata.request_count, 0);
rotator.increment_request_count(&mut metadata);
assert_eq!(metadata.request_count, 1);
rotator.increment_request_count(&mut metadata);
assert_eq!(metadata.request_count, 2);
}
#[rstest]
#[tokio::test]
async fn test_rotate_preserves_all_session_data() {
let backend = InMemorySessionBackend::new();
let mut session = Session::new(backend);
session.set("user_id", 42).unwrap();
session.set("role", "admin").unwrap();
session.set("theme", "dark").unwrap();
let old_key = session.get_or_create_key().to_string();
let rotator = SessionRotator::new(RotationPolicy::OnLogin);
rotator.rotate(&mut session).await.unwrap();
assert_ne!(session.get_or_create_key(), old_key);
let user_id: i32 = session.get("user_id").unwrap().unwrap();
assert_eq!(user_id, 42);
let role: String = session.get("role").unwrap().unwrap();
assert_eq!(role, "admin");
let theme: String = session.get("theme").unwrap().unwrap();
assert_eq!(theme, "dark");
}
#[rstest]
#[tokio::test]
async fn test_rotation_metadata_stored() {
let backend = InMemorySessionBackend::new();
let mut session = Session::new(backend);
session.set("user_id", 1).unwrap();
let rotator = SessionRotator::new(RotationPolicy::OnLogin);
rotator.rotate(&mut session).await.unwrap();
let metadata: RotationMetadata = session.get("_rotation_metadata").unwrap().unwrap();
assert_eq!(metadata.request_count, 0);
assert!(metadata.last_rotation <= Utc::now());
}
#[rstest]
#[tokio::test]
async fn test_never_policy_no_rotation() {
let rotator = SessionRotator::new(RotationPolicy::Never);
let metadata = RotationMetadata::default();
let should_rotate = rotator.should_rotate(&metadata);
assert!(!should_rotate);
}
}