use std::time::{Duration, SystemTime};
use crate::lease::LeaseAcquisition;
use crate::session::{Session, SessionId};
use crate::tokens::IssuedTokenPair;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RenewalRequirement {
Proactive,
Required,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum AuthTokenState {
Valid {
expires_in: Duration,
},
NearExpiry {
expires_in: Duration,
},
Expired {
expired_for: Duration,
},
Invalid,
}
impl AuthTokenState {
#[must_use]
pub fn is_currently_usable(self) -> bool {
matches!(self, Self::Valid { .. } | Self::NearExpiry { .. })
}
#[must_use]
pub fn should_attempt_proactive_renewal(self) -> bool {
matches!(self, Self::NearExpiry { .. })
}
#[must_use]
pub fn requires_renewal(self) -> bool {
matches!(self, Self::Expired { .. })
}
#[must_use]
pub fn is_invalid(self) -> bool {
matches!(self, Self::Invalid)
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenewalRequest {
pub session_id: SessionId,
pub auth_token_state: AuthTokenState,
pub requirement: RenewalRequirement,
pub now: SystemTime,
}
impl RenewalRequest {
#[must_use]
pub fn new(
session_id: SessionId,
auth_token_state: AuthTokenState,
requirement: RenewalRequirement,
now: SystemTime,
) -> Self {
Self {
session_id,
auth_token_state,
requirement,
now,
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum RenewalDecision {
NoRenewal,
AttemptProactiveRenewal,
RequireRenewal,
Reject,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RenewalAttempt {
pub session: Session,
pub request: RenewalRequest,
}
impl RenewalAttempt {
#[must_use]
pub fn new(session: Session, request: RenewalRequest) -> Self {
Self { session, request }
}
#[must_use]
pub fn session_id(&self) -> SessionId {
self.request.session_id
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum RenewalOutcome {
NotNeeded,
LeaseUnavailable {
acquisition: LeaseAcquisition,
},
Renewed {
session: Session,
tokens: IssuedTokenPair,
},
ReplayDetected {
session_id: SessionId,
},
Rejected,
}
impl RenewalOutcome {
#[must_use]
pub fn issued_tokens(&self) -> Option<&IssuedTokenPair> {
match self {
Self::Renewed { tokens, .. } => Some(tokens),
Self::NotNeeded
| Self::LeaseUnavailable { .. }
| Self::ReplayDetected { .. }
| Self::Rejected => None,
}
}
#[must_use]
pub fn session(&self) -> Option<&Session> {
match self {
Self::Renewed { session, .. } => Some(session),
Self::NotNeeded
| Self::LeaseUnavailable { .. }
| Self::ReplayDetected { .. }
| Self::Rejected => None,
}
}
}
#[derive(Debug, Default, Clone, Copy)]
pub struct RenewalOrchestrator;
impl RenewalOrchestrator {
#[must_use]
pub fn new() -> Self {
Self
}
#[must_use]
pub fn decide_for_state(
&self,
auth_token_state: AuthTokenState,
requirement: RenewalRequirement,
) -> RenewalDecision {
match (auth_token_state, requirement) {
(AuthTokenState::Invalid, _) => RenewalDecision::Reject,
(AuthTokenState::Expired { .. }, _) => RenewalDecision::RequireRenewal,
(AuthTokenState::NearExpiry { .. }, RenewalRequirement::Proactive) => {
RenewalDecision::AttemptProactiveRenewal
}
(AuthTokenState::NearExpiry { .. }, RenewalRequirement::Required) => {
RenewalDecision::RequireRenewal
}
(AuthTokenState::Valid { .. }, _) => RenewalDecision::NoRenewal,
}
}
#[must_use]
pub fn decide(&self, request: &RenewalRequest) -> RenewalDecision {
self.decide_for_state(request.auth_token_state, request.requirement)
}
}
#[cfg(test)]
mod tests {
use super::*;
use uuid::Uuid;
fn session_id() -> SessionId {
SessionId::from_uuid(Uuid::now_v7())
}
#[test]
fn proactive_near_expiry_requests_attempt_renewal() {
let orchestrator = RenewalOrchestrator::new();
let request = RenewalRequest::new(
session_id(),
AuthTokenState::NearExpiry {
expires_in: Duration::from_secs(45),
},
RenewalRequirement::Proactive,
SystemTime::UNIX_EPOCH,
);
assert_eq!(
orchestrator.decide(&request),
RenewalDecision::AttemptProactiveRenewal
);
}
#[test]
fn required_near_expiry_requests_require_renewal() {
let orchestrator = RenewalOrchestrator::new();
let request = RenewalRequest::new(
session_id(),
AuthTokenState::NearExpiry {
expires_in: Duration::from_secs(10),
},
RenewalRequirement::Required,
SystemTime::UNIX_EPOCH,
);
assert_eq!(
orchestrator.decide(&request),
RenewalDecision::RequireRenewal
);
}
#[test]
fn expired_requests_require_renewal() {
let orchestrator = RenewalOrchestrator::new();
let request = RenewalRequest::new(
session_id(),
AuthTokenState::Expired {
expired_for: Duration::from_secs(30),
},
RenewalRequirement::Required,
SystemTime::UNIX_EPOCH,
);
assert_eq!(
orchestrator.decide(&request),
RenewalDecision::RequireRenewal
);
}
#[test]
fn valid_requests_do_not_trigger_renewal() {
let orchestrator = RenewalOrchestrator::new();
let request = RenewalRequest::new(
session_id(),
AuthTokenState::Valid {
expires_in: Duration::from_secs(600),
},
RenewalRequirement::Proactive,
SystemTime::UNIX_EPOCH,
);
assert_eq!(orchestrator.decide(&request), RenewalDecision::NoRenewal);
}
#[test]
fn invalid_requests_are_rejected() {
let orchestrator = RenewalOrchestrator::new();
let request = RenewalRequest::new(
session_id(),
AuthTokenState::Invalid,
RenewalRequirement::Proactive,
SystemTime::UNIX_EPOCH,
);
assert_eq!(orchestrator.decide(&request), RenewalDecision::Reject);
}
#[test]
fn auth_token_state_helpers_match_expected_behavior() {
let valid = AuthTokenState::Valid {
expires_in: Duration::from_secs(60),
};
let near_expiry = AuthTokenState::NearExpiry {
expires_in: Duration::from_secs(15),
};
let expired = AuthTokenState::Expired {
expired_for: Duration::from_secs(5),
};
let invalid = AuthTokenState::Invalid;
assert!(valid.is_currently_usable());
assert!(!valid.should_attempt_proactive_renewal());
assert!(!valid.requires_renewal());
assert!(!valid.is_invalid());
assert!(near_expiry.is_currently_usable());
assert!(near_expiry.should_attempt_proactive_renewal());
assert!(!near_expiry.requires_renewal());
assert!(!near_expiry.is_invalid());
assert!(!expired.is_currently_usable());
assert!(!expired.should_attempt_proactive_renewal());
assert!(expired.requires_renewal());
assert!(!expired.is_invalid());
assert!(!invalid.is_currently_usable());
assert!(!invalid.should_attempt_proactive_renewal());
assert!(!invalid.requires_renewal());
assert!(invalid.is_invalid());
}
}