use crate::{
ApplicationVerifiedTenantBinding, DecisionAuditOccurrence, Locale, RequestId, SubjectRef,
SubjectSlot, TenantBinding, TenantBindingError, TenantId, TrustedServiceBinding,
};
use serde::Serialize;
use std::collections::BTreeMap;
use thiserror::Error;
pub trait Clock: Send + Sync {
fn now_utc(&self) -> time::OffsetDateTime;
}
impl<F> Clock for F
where
F: Fn() -> time::OffsetDateTime + Send + Sync,
{
fn now_utc(&self) -> time::OffsetDateTime {
self()
}
}
#[derive(Clone, Copy, Debug, Default)]
pub struct SystemClock;
impl Clock for SystemClock {
fn now_utc(&self) -> time::OffsetDateTime {
time::OffsetDateTime::now_utc()
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct Context {
tenant: TenantId,
binding: TenantBinding,
principal: SubjectRef,
subjects: BTreeMap<SubjectSlot, SubjectRef>,
locale: Locale,
request_id: Option<crate::RequestId>,
decision_audit_occurrence: Option<DecisionAuditOccurrence>,
}
impl Context {
pub fn new(
tenant: TenantId,
binding: TenantBinding,
principal: SubjectRef,
locale: Locale,
) -> Result<Self, ContextError> {
Self::new_at(
tenant,
binding,
principal,
locale,
time::OffsetDateTime::now_utc(),
)
}
pub fn new_at(
tenant: TenantId,
binding: TenantBinding,
principal: SubjectRef,
locale: Locale,
now: time::OffsetDateTime,
) -> Result<Self, ContextError> {
if tenant != *binding.tenant() {
return Err(ContextError::TenantMismatch {
expected: tenant,
bound: binding.tenant().clone(),
});
}
binding.validate_at(now).map_err(ContextError::Binding)?;
Ok(Self {
tenant,
binding,
principal,
subjects: BTreeMap::new(),
locale,
request_id: None,
decision_audit_occurrence: None,
})
}
pub fn from_application_verified(
binding: ApplicationVerifiedTenantBinding,
principal: SubjectRef,
locale: Locale,
) -> Result<Self, ContextError> {
let tenant = binding.tenant().clone();
Self::new(
tenant,
TenantBinding::ApplicationVerified(binding),
principal,
locale,
)
}
pub fn from_trusted_service(
binding: TrustedServiceBinding,
principal: SubjectRef,
locale: Locale,
) -> Result<Self, ContextError> {
let tenant = binding.tenant().clone();
Self::new(
tenant,
TenantBinding::TrustedService(binding),
principal,
locale,
)
}
pub fn validate_at(&self, now: time::OffsetDateTime) -> Result<(), ContextError> {
if self.tenant != *self.binding.tenant() {
return Err(ContextError::TenantMismatch {
expected: self.tenant.clone(),
bound: self.binding.tenant().clone(),
});
}
self.binding.validate_at(now).map_err(ContextError::Binding)
}
#[must_use]
pub const fn tenant(&self) -> &TenantId {
&self.tenant
}
#[must_use]
pub const fn binding(&self) -> &TenantBinding {
&self.binding
}
#[must_use]
pub const fn principal(&self) -> &SubjectRef {
&self.principal
}
#[must_use]
pub const fn subjects(&self) -> &BTreeMap<SubjectSlot, SubjectRef> {
&self.subjects
}
#[must_use]
pub const fn locale(&self) -> &Locale {
&self.locale
}
#[must_use]
pub const fn request_id(&self) -> Option<&RequestId> {
self.request_id.as_ref()
}
#[must_use]
pub fn with_subject(mut self, slot: SubjectSlot, subject: SubjectRef) -> Self {
self.subjects.insert(slot, subject);
self
}
#[must_use]
pub fn with_request_id(mut self, request_id: RequestId) -> Self {
self.request_id = Some(request_id);
self
}
#[must_use]
pub fn with_decision_audit_occurrence(mut self, occurrence: DecisionAuditOccurrence) -> Self {
self.decision_audit_occurrence = Some(occurrence);
self
}
#[must_use]
pub const fn decision_audit_occurrence(&self) -> Option<&DecisionAuditOccurrence> {
self.decision_audit_occurrence.as_ref()
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum ContextError {
#[error("tenant context does not match its binding: expected {expected}, bound {bound}")]
TenantMismatch {
expected: TenantId,
bound: TenantId,
},
#[error(transparent)]
Binding(#[from] TenantBindingError),
}