use crate::core::error::CoreError;
use chrono::{DateTime, Utc};
use std::fmt;
pub fn parse_saml_datetime(s: &str) -> Result<DateTime<Utc>, CoreError> {
if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
return Ok(dt.with_timezone(&Utc));
}
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S") {
return Ok(dt.and_utc());
}
if let Ok(dt) = chrono::NaiveDateTime::parse_from_str(s, "%Y-%m-%dT%H:%M:%S%.f") {
return Ok(dt.and_utc());
}
Err(CoreError::InvalidDateTime(s.to_string()))
}
pub fn format_saml_datetime(dt: &DateTime<Utc>) -> String {
dt.format("%Y-%m-%dT%H:%M:%SZ").to_string()
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SamlDateTimeRef<'a> {
pub value: DateTime<Utc>,
pub raw: &'a str,
}
impl<'a> SamlDateTimeRef<'a> {
pub fn parse(s: &'a str) -> Result<Self, CoreError> {
let value = parse_saml_datetime(s)?;
Ok(SamlDateTimeRef { value, raw: s })
}
pub fn to_owned(&self) -> SamlDateTime {
SamlDateTime { value: self.value }
}
}
impl<'a> fmt::Display for SamlDateTimeRef<'a> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.raw)
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct SamlDateTime {
pub value: DateTime<Utc>,
}
impl SamlDateTime {
pub fn new(value: DateTime<Utc>) -> Self {
SamlDateTime { value }
}
pub fn now() -> Self {
SamlDateTime { value: Utc::now() }
}
pub fn to_saml_string(&self) -> String {
format_saml_datetime(&self.value)
}
}
impl fmt::Display for SamlDateTime {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{}", format_saml_datetime(&self.value))
}
}
pub fn is_within_validity_window(
now: DateTime<Utc>,
not_before: Option<DateTime<Utc>>,
not_on_or_after: Option<DateTime<Utc>>,
clock_skew_seconds: i64,
) -> bool {
let skew = chrono::Duration::seconds(clock_skew_seconds);
if let Some(nb) = not_before {
if now < nb - skew {
return false;
}
}
if let Some(noa) = not_on_or_after {
if now >= noa + skew {
return false;
}
}
true
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_parse_saml_datetime_rfc3339() {
let dt = parse_saml_datetime("2024-01-15T10:30:00Z").unwrap();
assert_eq!(dt.year(), 2024);
assert_eq!(dt.month(), 1);
assert_eq!(dt.day(), 15);
assert_eq!(dt.hour(), 10);
assert_eq!(dt.minute(), 30);
assert_eq!(dt.second(), 0);
}
#[test]
fn test_parse_saml_datetime_with_fractional() {
let dt = parse_saml_datetime("2024-01-15T10:30:00.123Z").unwrap();
assert_eq!(dt.year(), 2024);
}
#[test]
fn test_parse_saml_datetime_with_offset() {
let dt = parse_saml_datetime("2024-01-15T10:30:00+00:00").unwrap();
assert_eq!(dt.year(), 2024);
}
#[test]
fn test_parse_saml_datetime_invalid() {
assert!(parse_saml_datetime("not-a-date").is_err());
assert!(parse_saml_datetime("").is_err());
}
#[test]
fn test_format_saml_datetime() {
let dt = parse_saml_datetime("2024-01-15T10:30:00Z").unwrap();
let formatted = format_saml_datetime(&dt);
assert_eq!(formatted, "2024-01-15T10:30:00Z");
}
#[test]
fn test_saml_datetime_ref_roundtrip() {
let s = "2024-01-15T10:30:00Z";
let dt_ref = SamlDateTimeRef::parse(s).unwrap();
assert_eq!(dt_ref.raw, s);
let owned = dt_ref.to_owned();
assert_eq!(owned.value, dt_ref.value);
}
#[test]
fn test_saml_datetime_now() {
let now = SamlDateTime::now();
let s = now.to_saml_string();
assert!(s.ends_with('Z'));
parse_saml_datetime(&s).unwrap();
}
#[test]
fn test_is_within_validity_window() {
use chrono::TimeZone;
let now = Utc.with_ymd_and_hms(2024, 1, 15, 12, 0, 0).unwrap();
let not_before = Utc.with_ymd_and_hms(2024, 1, 15, 11, 0, 0).unwrap();
let not_on_or_after = Utc.with_ymd_and_hms(2024, 1, 15, 13, 0, 0).unwrap();
assert!(is_within_validity_window(
now,
Some(not_before),
Some(not_on_or_after),
0
));
let early = Utc.with_ymd_and_hms(2024, 1, 15, 10, 0, 0).unwrap();
assert!(!is_within_validity_window(
early,
Some(not_before),
Some(not_on_or_after),
0
));
let late = Utc.with_ymd_and_hms(2024, 1, 15, 14, 0, 0).unwrap();
assert!(!is_within_validity_window(
late,
Some(not_before),
Some(not_on_or_after),
0
));
assert!(is_within_validity_window(
early,
Some(not_before),
Some(not_on_or_after),
3600
));
assert!(is_within_validity_window(now, None, None, 0));
}
use chrono::Datelike;
use chrono::Timelike;
}