use std::collections::BTreeMap;
use thiserror::Error;
use time::OffsetDateTime;
use crate::{resource::ResourceRef, subject::Subject};
pub const VALID_FROM_ATTR: &str = "authz_valid_from";
pub const VALID_UNTIL_ATTR: &str = "authz_valid_until";
#[derive(Debug, Error, Clone, PartialEq, Eq)]
#[non_exhaustive]
pub enum TemporalError {
#[error("invalid permission timestamp for {field}: {value}")]
InvalidTimestamp {
field: &'static str,
value: String,
},
}
#[must_use]
#[derive(Debug, Clone, PartialEq, Eq, Default)]
pub struct PermissionWindow {
pub valid_from: Option<OffsetDateTime>,
pub valid_until: Option<OffsetDateTime>,
}
impl PermissionWindow {
pub fn new() -> Self {
Self::default()
}
pub fn starting_at(mut self, valid_from: OffsetDateTime) -> Self {
self.valid_from = Some(valid_from);
self
}
pub fn expiring_at(mut self, valid_until: OffsetDateTime) -> Self {
self.valid_until = Some(valid_until);
self
}
#[must_use]
pub fn is_active_at(&self, now: OffsetDateTime) -> bool {
if let Some(valid_from) = self.valid_from {
if now < valid_from {
return false;
}
}
if let Some(valid_until) = self.valid_until {
if now > valid_until {
return false;
}
}
true
}
pub fn apply_to_subject(&self, subject: &mut Subject) -> Result<(), TemporalError> {
Self::write_attributes(&mut subject.attributes, self);
Ok(())
}
pub fn apply_to_resource(&self, resource: &mut ResourceRef) -> Result<(), TemporalError> {
Self::write_attributes(&mut resource.attributes, self);
Ok(())
}
pub fn from_attributes(
attributes: &BTreeMap<String, String>,
) -> Result<Option<Self>, TemporalError> {
if !Self::has_constraints(attributes) {
return Ok(None);
}
let valid_from = Self::parse_timestamp(attributes, VALID_FROM_ATTR)?;
let valid_until = Self::parse_timestamp(attributes, VALID_UNTIL_ATTR)?;
Ok(Some(Self {
valid_from,
valid_until,
}))
}
#[must_use]
pub fn has_constraints(attributes: &BTreeMap<String, String>) -> bool {
attributes.contains_key(VALID_FROM_ATTR) || attributes.contains_key(VALID_UNTIL_ATTR)
}
fn write_attributes(attributes: &mut BTreeMap<String, String>, window: &Self) {
match window.valid_from {
Some(valid_from) => {
attributes.insert(
VALID_FROM_ATTR.to_string(),
valid_from.unix_timestamp().to_string(),
);
}
None => {
attributes.remove(VALID_FROM_ATTR);
}
}
match window.valid_until {
Some(valid_until) => {
attributes.insert(
VALID_UNTIL_ATTR.to_string(),
valid_until.unix_timestamp().to_string(),
);
}
None => {
attributes.remove(VALID_UNTIL_ATTR);
}
}
}
fn parse_timestamp(
attributes: &BTreeMap<String, String>,
field: &'static str,
) -> Result<Option<OffsetDateTime>, TemporalError> {
let Some(value) = attributes.get(field) else {
return Ok(None);
};
let parsed = value
.parse::<i64>()
.map_err(|_| TemporalError::InvalidTimestamp {
field,
value: value.clone(),
})?;
let timestamp = OffsetDateTime::from_unix_timestamp(parsed).map_err(|_| {
TemporalError::InvalidTimestamp {
field,
value: value.clone(),
}
})?;
Ok(Some(timestamp))
}
}