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.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 = character as u32;
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)
}
#[allow(dead_code)]
pub(crate) fn from_trusted(value: impl Into<String>) -> Self {
Self(value.into())
}
#[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(serde::de::Error::custom)
}
}
};
}
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 bytes = value.as_bytes();
assert!(!bytes.is_empty(), "static identity must not be empty");
let mut index = 0;
let mut has_non_whitespace = false;
while index < bytes.len() {
let byte = bytes[index];
if !(byte == b' ' || byte == b'\n' || byte == b'\r' || byte == b'\t') {
has_non_whitespace = true;
}
index += 1;
}
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 bytes = value.as_bytes();
let mut index = 0;
while index < bytes.len() {
let first = bytes[index];
let (code_point, width) = match first {
0x00..=0x7F => (first as u32, 1),
0x80..=0xDF => {
let code_point = ((first & 0x1F) as u32) << 6 | (bytes[index + 1] & 0x3F) as u32;
(code_point, 2)
}
0xE0..=0xEF => {
let code_point = ((first & 0x0F) as u32) << 12
| ((bytes[index + 1] & 0x3F) as u32) << 6
| (bytes[index + 2] & 0x3F) as u32;
(code_point, 3)
}
_ => {
let code_point = ((first & 0x07) as u32) << 18
| ((bytes[index + 1] & 0x3F) as u32) << 12
| ((bytes[index + 2] & 0x3F) as u32) << 6
| (bytes[index + 3] & 0x3F) as u32;
(code_point, 4)
}
};
assert!(
!(code_point <= 0x1F || code_point == 0x7F),
"static tenant identity contains a control character"
);
assert!(
!((code_point >= 0xFDD0 && code_point <= 0xFDEF)
|| code_point & 0xFFFF == 0xFFFF
|| code_point & 0xFFFF == 0xFFFE),
"static tenant identity contains a Unicode noncharacter"
);
index += width;
}
}
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)
}
#[allow(dead_code)]
pub(crate) fn from_trusted(value: impl Into<String>) -> Self {
Self(value.into())
}
#[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(serde::de::Error::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, Deserialize)]
pub struct DecisionAuditOccurrence {
pub decision_audit_id: DecisionAuditId,
pub 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 - (nanosecond % 1_000);
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),
})
}
}
#[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,
}
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(serde::de::Error::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(serde::de::Error::custom)
}
}