use serde::de::Error as SerdeError;
use std::fmt;
use serde::{Deserialize, Deserializer, Serialize, Serializer};
use thiserror::Error;
use time::{OffsetDateTime, UtcOffset};
pub type GatekeepResult<T> = Result<T, GatekeepError>;
pub const MAX_TENANT_ID_BYTES: usize = 255;
#[derive(Debug, Error, Clone, PartialEq, Eq)]
pub enum GatekeepError {
#[error("{field} must not be empty")]
EmptyIdentifier {
field: &'static str,
},
#[error("{field} uses the reserved prefix {prefix:?}")]
ReservedIdentifierPrefix {
field: &'static str,
prefix: &'static str,
},
#[error("invalid locale tag: {value}")]
InvalidLocale {
value: String,
},
#[error("invalid imported legacy decision audit id: {value}")]
InvalidLegacyIdentifier {
value: String,
},
#[error("policy record is invalid: {reason}")]
InvalidPolicyRecord {
reason: &'static str,
},
#[error("tenant_id exceeds {max_bytes} UTF-8 bytes (was {actual_bytes})")]
TenantIdTooLong {
max_bytes: usize,
actual_bytes: usize,
},
#[error("tenant_id contains forbidden control character U+{code_point:04X}")]
TenantIdControlCharacter {
code_point: u32,
},
#[error("tenant_id contains forbidden Unicode noncharacter U+{code_point:04X}")]
TenantIdNoncharacter {
code_point: u32,
},
}
fn validate_identifier(field: &'static str, value: impl Into<String>) -> GatekeepResult<String> {
let value = value.into();
if value.trim().is_empty() {
Err(GatekeepError::EmptyIdentifier { field })
} else if field == "decision_audit_id" && value.starts_with("legacy-") {
Err(GatekeepError::ReservedIdentifierPrefix {
field,
prefix: "legacy-",
})
} else {
Ok(value)
}
}
fn validate_tenant_id(value: impl Into<String>) -> GatekeepResult<String> {
let value = value.into();
if value.trim().is_empty() {
return Err(GatekeepError::EmptyIdentifier { field: "tenant_id" });
}
if value.len() > MAX_TENANT_ID_BYTES {
return Err(GatekeepError::TenantIdTooLong {
max_bytes: MAX_TENANT_ID_BYTES,
actual_bytes: value.len(),
});
}
for character in value.chars() {
let code_point = u32::from(character);
if character.is_control() {
return Err(GatekeepError::TenantIdControlCharacter { code_point });
}
if (0xFDD0..=0xFDEF).contains(&code_point)
|| code_point & 0xFFFF == 0xFFFF
|| code_point & 0xFFFF == 0xFFFE
{
return Err(GatekeepError::TenantIdNoncharacter { code_point });
}
}
Ok(value)
}
fn validate_locale(value: impl Into<String>) -> GatekeepResult<String> {
let value = value.into();
let valid = !value.trim().is_empty()
&& value
.bytes()
.all(|byte| byte.is_ascii_alphanumeric() || byte == b'-');
if valid {
Ok(value)
} else {
Err(GatekeepError::InvalidLocale { value })
}
}
macro_rules! owned_id {
($name:ident, $field:literal) => {
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name(String);
impl $name {
pub fn new(value: impl Into<String>) -> GatekeepResult<Self> {
validate_identifier($field, value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for $name {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Serialize for $name {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for $name {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(SerdeError::custom)
}
}
};
}
macro_rules! trusted_id {
($name:ident) => {
impl $name {
pub(crate) fn from_trusted(value: impl Into<String>) -> Self {
Self(value.into())
}
}
};
}
trusted_id!(FactId);
trusted_id!(ObligationId);
trusted_id!(PolicyHash);
trusted_id!(DecisionAuditId);
macro_rules! static_id {
($name:ident, $owned:ident, $validator:path) => {
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct $name(&'static str);
impl $name {
#[must_use]
pub const fn new(value: &'static str) -> Self {
$validator(value);
Self(value)
}
#[must_use]
pub const fn as_str(self) -> &'static str {
self.0
}
pub fn to_owned_id(self) -> GatekeepResult<$owned> {
$owned::new(self.0)
}
}
};
($name:ident, $owned:ident) => {
static_id!($name, $owned, assert_valid_static_id);
};
}
const fn assert_valid_static_id(value: &str) {
let mut remaining = value.as_bytes();
assert!(!remaining.is_empty(), "static identity must not be empty");
let mut has_non_whitespace = false;
while !remaining.is_empty() {
remaining = match remaining {
[b' ' | 0x09..=0x0D, rest @ ..]
| [0xC2, 0x85 | 0xA0, rest @ ..]
| [0xE1, 0x9A, 0x80, rest @ ..]
| [0xE2, 0x80, 0x80..=0x8A | 0xA8 | 0xA9 | 0xAF, rest @ ..]
| [0xE2, 0x81, 0x9F, rest @ ..]
| [0xE3, 0x80, 0x80, rest @ ..] => rest,
_ => {
has_non_whitespace = true;
break;
}
};
}
assert!(has_non_whitespace, "static identity must not be whitespace");
}
const fn assert_valid_static_tenant_id(value: &str) {
assert_valid_static_id(value);
assert!(
value.len() <= MAX_TENANT_ID_BYTES,
"static tenant identity exceeds 255 UTF-8 bytes"
);
let mut remaining = value.as_bytes();
while let [first, rest @ ..] = remaining {
let control =
matches!(first, 0x00..=0x1F | 0x7F) || matches!(remaining, [0xC2, 0x80..=0x9F, ..]);
assert!(
!control,
"static tenant identity contains a control character"
);
let noncharacter = matches!(
remaining,
[0xEF, 0xB7, 0x90..=0xAF, ..] | [0xEF, 0xBF, 0xBE | 0xBF, ..]
) || matches!(remaining, [0xF0..=0xF4, second, 0xBF, 0xBE | 0xBF, ..] if *second & 0x0F == 0x0F);
assert!(
!noncharacter,
"static tenant identity contains a Unicode noncharacter"
);
remaining = rest;
}
}
owned_id!(FactId, "fact_id");
owned_id!(ClauseLabel, "clause_label");
owned_id!(ObligationId, "obligation_id");
owned_id!(ParamKey, "param_key");
owned_id!(PolicyHash, "policy_hash");
owned_id!(PolicyId, "policy_id");
owned_id!(ReasonCode, "reason_code");
owned_id!(RequestId, "request_id");
owned_id!(DecisionAuditId, "decision_audit_id");
owned_id!(SubjectSlot, "subject_slot");
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct TenantId(String);
impl TenantId {
pub fn new(value: impl Into<String>) -> GatekeepResult<Self> {
validate_tenant_id(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl fmt::Display for TenantId {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.0)
}
}
impl Serialize for TenantId {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for TenantId {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(SerdeError::custom)
}
}
impl DecisionAuditId {
#[must_use]
pub fn generate() -> Self {
Self::from_trusted(uuid::Uuid::now_v7().to_string())
}
pub fn from_legacy_import(value: impl Into<String>) -> GatekeepResult<Self> {
let value = value.into();
if value
.strip_prefix("legacy-")
.is_some_and(|suffix| !suffix.is_empty())
{
Ok(Self::from_trusted(value))
} else {
Err(GatekeepError::InvalidLegacyIdentifier { value })
}
}
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct DecisionAuditOccurrence {
decision_audit_id: DecisionAuditId,
occurred_at: OffsetDateTime,
}
impl DecisionAuditOccurrence {
pub fn new(
decision_audit_id: DecisionAuditId,
occurred_at: OffsetDateTime,
) -> Result<Self, DecisionAuditOccurrenceError> {
const MAX_PORTABLE_UNIX_SECONDS: i64 = 253_402_300_799;
if decision_audit_id.as_str().starts_with("legacy-") {
return Err(DecisionAuditOccurrenceError::ReservedLegacyIdentity);
}
let seconds = occurred_at.unix_timestamp();
if !(0..=MAX_PORTABLE_UNIX_SECONDS).contains(&seconds) {
return Err(DecisionAuditOccurrenceError::OutOfRange);
}
let nanosecond = occurred_at.nanosecond();
let normalized_nanosecond = nanosecond
.checked_sub(nanosecond % 1_000)
.ok_or(DecisionAuditOccurrenceError::OutOfRange)?;
if seconds == MAX_PORTABLE_UNIX_SECONDS && normalized_nanosecond > 999_999_000 {
return Err(DecisionAuditOccurrenceError::OutOfRange);
}
let normalized = occurred_at
.replace_nanosecond(normalized_nanosecond)
.map_err(|_| DecisionAuditOccurrenceError::OutOfRange)?;
Ok(Self {
decision_audit_id,
occurred_at: normalized.to_offset(UtcOffset::UTC),
})
}
#[must_use]
pub const fn decision_audit_id(&self) -> &DecisionAuditId {
&self.decision_audit_id
}
#[must_use]
pub const fn occurred_at(&self) -> OffsetDateTime {
self.occurred_at
}
pub fn validate(&self) -> Result<(), DecisionAuditOccurrenceError> {
Self::new(self.decision_audit_id.clone(), self.occurred_at)?;
if self.occurred_at.offset() != UtcOffset::UTC
|| !self.occurred_at.nanosecond().is_multiple_of(1_000)
{
return Err(DecisionAuditOccurrenceError::NonCanonical);
}
Ok(())
}
pub(crate) fn into_parts(self) -> (DecisionAuditId, OffsetDateTime) {
(self.decision_audit_id, self.occurred_at)
}
pub(crate) const fn from_validated_parts(
decision_audit_id: DecisionAuditId,
occurred_at: OffsetDateTime,
) -> Self {
Self {
decision_audit_id,
occurred_at,
}
}
}
impl<'de> Deserialize<'de> for DecisionAuditOccurrence {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct Wire {
decision_audit_id: DecisionAuditId,
occurred_at: OffsetDateTime,
}
let wire = Wire::deserialize(deserializer)?;
Self::new(wire.decision_audit_id, wire.occurred_at).map_err(SerdeError::custom)
}
}
#[derive(Clone, Debug, Error, PartialEq, Eq)]
pub enum DecisionAuditOccurrenceError {
#[error("legacy decision identities are valid only while importing history")]
ReservedLegacyIdentity,
#[error("decision occurrence is outside Dovecote's portable instant range")]
OutOfRange,
#[error("decision occurrence must use UTC at exact microsecond precision")]
NonCanonical,
}
static_id!(StaticFactId, FactId);
static_id!(StaticClauseLabel, ClauseLabel);
static_id!(StaticObligationId, ObligationId);
static_id!(StaticParamKey, ParamKey);
static_id!(StaticReasonCode, ReasonCode);
static_id!(StaticRequestId, RequestId);
static_id!(StaticSubjectSlot, SubjectSlot);
static_id!(StaticTenantId, TenantId, assert_valid_static_tenant_id);
#[derive(Clone, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub struct Locale(String);
impl Locale {
pub fn new(value: impl Into<String>) -> GatekeepResult<Self> {
validate_locale(value).map(Self)
}
#[must_use]
pub fn as_str(&self) -> &str {
&self.0
}
}
impl Serialize for Locale {
fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
self.0.serialize(serializer)
}
}
impl<'de> Deserialize<'de> for Locale {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
let value = String::deserialize(deserializer)?;
Self::new(value).map_err(SerdeError::custom)
}
}
pub trait Fact {
const ID: StaticFactId;
}
pub trait ObligationSpec {
const ID: StaticObligationId;
}
#[derive(Clone, Debug, PartialEq, Eq, Serialize)]
pub struct SubjectRef {
kind: String,
id: String,
}
impl SubjectRef {
pub fn new(kind: impl Into<String>, id: impl Into<String>) -> GatekeepResult<Self> {
Ok(Self {
kind: validate_identifier("subject_kind", kind)?,
id: validate_identifier("subject_id", id)?,
})
}
#[must_use]
pub fn kind(&self) -> &str {
&self.kind
}
#[must_use]
pub fn id(&self) -> &str {
&self.id
}
}
impl<'de> Deserialize<'de> for SubjectRef {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
where
D: Deserializer<'de>,
{
#[derive(Deserialize)]
struct SubjectRefRecord {
kind: String,
id: String,
}
let record = SubjectRefRecord::deserialize(deserializer)?;
Self::new(record.kind, record.id).map_err(SerdeError::custom)
}
}