use serde::{Deserialize, Deserializer, Serialize};
use thiserror::Error;
use time::{Duration, OffsetDateTime};
use crate::TenantId;
const MAX_PROVENANCE_CHARS: usize = 128;
const EVIDENCE_DIGEST_BYTES: usize = 32;
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize)]
pub struct BindingProvenance(String);
impl BindingProvenance {
pub fn new(value: impl Into<String>) -> Result<Self, TenantBindingError> {
let value = value.into();
if value.trim().is_empty()
|| value.chars().any(char::is_control)
|| value.chars().count() > MAX_PROVENANCE_CHARS
{
return Err(TenantBindingError::InvalidProvenance { value });
}
Ok(Self(value))
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl<'de> Deserialize<'de> for BindingProvenance {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(serde::de::Error::custom)
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct EvidenceDigest([u8; EVIDENCE_DIGEST_BYTES]);
impl EvidenceDigest {
#[must_use]
pub const fn new(bytes: [u8; EVIDENCE_DIGEST_BYTES]) -> Self {
Self(bytes)
}
#[must_use]
pub const fn as_bytes(&self) -> &[u8; EVIDENCE_DIGEST_BYTES] {
&self.0
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum BindingAuthority {
Issuer {
issuer: BindingProvenance,
key_id: Option<BindingProvenance>,
},
Provider {
provider: BindingProvenance,
key_id: Option<BindingProvenance>,
},
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TenantBindingEvidence {
authority: BindingAuthority,
authenticated_at: OffsetDateTime,
claims_digest: EvidenceDigest,
}
impl TenantBindingEvidence {
#[must_use]
pub const fn new(
authority: BindingAuthority,
authenticated_at: OffsetDateTime,
claims_digest: EvidenceDigest,
) -> Self {
Self {
authority,
authenticated_at,
claims_digest,
}
}
#[must_use]
pub const fn authority(&self) -> &BindingAuthority {
&self.authority
}
#[must_use]
pub const fn authenticated_at(&self) -> OffsetDateTime {
self.authenticated_at
}
#[must_use]
pub const fn claims_digest(&self) -> &EvidenceDigest {
&self.claims_digest
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct ApplicationVerifiedTenantBinding {
tenant: TenantId,
evidence: TenantBindingEvidence,
valid_from: OffsetDateTime,
valid_until: OffsetDateTime,
}
#[derive(Deserialize)]
struct ApplicationVerifiedTenantBindingFields {
tenant: TenantId,
evidence: TenantBindingEvidence,
valid_from: OffsetDateTime,
valid_until: OffsetDateTime,
}
impl<'de> Deserialize<'de> for ApplicationVerifiedTenantBinding {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let fields = ApplicationVerifiedTenantBindingFields::deserialize(deserializer)?;
Self::new(
fields.tenant,
fields.evidence,
fields.valid_from,
fields.valid_until,
)
.map_err(serde::de::Error::custom)
}
}
impl ApplicationVerifiedTenantBinding {
pub fn new(
tenant: TenantId,
evidence: TenantBindingEvidence,
valid_from: OffsetDateTime,
valid_until: OffsetDateTime,
) -> Result<Self, TenantBindingError> {
let lifetime = valid_until - valid_from;
if lifetime <= Duration::ZERO || evidence.authenticated_at() > valid_until {
return Err(TenantBindingError::InvalidWindow);
}
Ok(Self {
tenant,
evidence,
valid_from,
valid_until,
})
}
#[must_use]
pub const fn tenant(&self) -> &TenantId {
&self.tenant
}
#[must_use]
pub const fn evidence(&self) -> &TenantBindingEvidence {
&self.evidence
}
#[must_use]
pub const fn valid_from(&self) -> OffsetDateTime {
self.valid_from
}
#[must_use]
pub const fn valid_until(&self) -> OffsetDateTime {
self.valid_until
}
pub fn validate_at(&self, now: OffsetDateTime) -> Result<(), TenantBindingError> {
if self.evidence.authenticated_at() > now {
return Err(TenantBindingError::AuthenticatedInFuture {
authenticated_at: self.evidence.authenticated_at(),
now,
});
}
if now < self.valid_from {
return Err(TenantBindingError::NotYetValid {
valid_from: self.valid_from,
now,
});
}
if now >= self.valid_until {
return Err(TenantBindingError::Stale {
valid_until: self.valid_until,
now,
});
}
Ok(())
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub struct TrustedServiceBinding {
tenant: TenantId,
service: BindingProvenance,
}
impl TrustedServiceBinding {
pub fn new(tenant: TenantId, service: impl Into<String>) -> Result<Self, TenantBindingError> {
Ok(Self {
tenant,
service: BindingProvenance::new(service)?,
})
}
#[must_use]
pub const fn tenant(&self) -> &TenantId {
&self.tenant
}
#[must_use]
pub const fn service(&self) -> &BindingProvenance {
&self.service
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub enum TenantBinding {
ApplicationVerified(ApplicationVerifiedTenantBinding),
TrustedService(TrustedServiceBinding),
}
impl TenantBinding {
#[must_use]
pub const fn tenant(&self) -> &TenantId {
match self {
Self::ApplicationVerified(binding) => binding.tenant(),
Self::TrustedService(binding) => binding.tenant(),
}
}
pub fn validate_at(&self, now: OffsetDateTime) -> Result<(), TenantBindingError> {
match self {
Self::ApplicationVerified(binding) => binding.validate_at(now),
Self::TrustedService(_) => Ok(()),
}
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
#[non_exhaustive]
pub enum TenantBindingError {
#[error(
"tenant binding provenance must be non-empty, printable, and at most 128 characters: {value:?}"
)]
InvalidProvenance {
value: String,
},
#[error("tenant binding validity window is invalid")]
InvalidWindow,
#[error("tenant binding is not yet valid at {now}; it starts at {valid_from}")]
NotYetValid {
valid_from: OffsetDateTime,
now: OffsetDateTime,
},
#[error("tenant binding is stale at {now}; it expired at {valid_until}")]
Stale {
valid_until: OffsetDateTime,
now: OffsetDateTime,
},
#[error(
"tenant binding authentication is from the future at {now}; it occurred at {authenticated_at}"
)]
AuthenticatedInFuture {
authenticated_at: OffsetDateTime,
now: OffsetDateTime,
},
}