Skip to main content

simple_zanzibar/
domain.rs

1//! Validated domain primitives for the local Zanzibar engine.
2
3use std::{fmt, str::FromStr};
4
5use crate::model::{Object, Relation, RelationTuple, User};
6
7const MAX_TYPE_BYTES: usize = 64;
8const MAX_RELATION_BYTES: usize = 64;
9const MAX_ID_BYTES: usize = 256;
10const MAX_RELATIONSHIP_BYTES: usize = 768;
11const LEGACY_USER_SUBJECT_TYPE: &str = "user";
12
13/// Identifies a kind of validated domain identifier.
14#[derive(Debug, Clone, Copy, PartialEq, Eq)]
15pub enum IdentifierKind {
16    /// Object type, also called namespace in the legacy model.
17    ObjectType,
18    /// Object identifier inside an object type.
19    ObjectId,
20    /// Relation or permission name.
21    RelationName,
22    /// Subject type for direct users.
23    SubjectType,
24    /// Subject identifier inside a subject type.
25    SubjectId,
26}
27
28impl fmt::Display for IdentifierKind {
29    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
30        match self {
31            Self::ObjectType => formatter.write_str("object type"),
32            Self::ObjectId => formatter.write_str("object id"),
33            Self::RelationName => formatter.write_str("relation name"),
34            Self::SubjectType => formatter.write_str("subject type"),
35            Self::SubjectId => formatter.write_str("subject id"),
36        }
37    }
38}
39
40/// Errors produced while validating domain primitives.
41#[derive(Debug, Clone, PartialEq, Eq, thiserror::Error)]
42pub enum DomainError {
43    /// An identifier exceeds the configured byte cap.
44    #[error("identifier {kind} exceeds {max_bytes} bytes")]
45    IdentifierTooLong {
46        /// Identifier category.
47        kind: IdentifierKind,
48        /// Maximum accepted byte length.
49        max_bytes: usize,
50    },
51
52    /// An identifier is empty.
53    #[error("identifier {kind} must not be empty")]
54    EmptyIdentifier {
55        /// Identifier category.
56        kind: IdentifierKind,
57    },
58
59    /// An identifier contains a byte outside its allowlist.
60    #[error("identifier {kind} contains invalid byte at offset {offset}")]
61    InvalidIdentifierByte {
62        /// Identifier category.
63        kind: IdentifierKind,
64        /// Byte offset of the rejected input byte.
65        offset: usize,
66    },
67
68    /// A relationship string does not match the accepted grammar.
69    #[error("relationship is malformed: {reason}")]
70    MalformedRelationship {
71        /// Static parse failure reason.
72        reason: &'static str,
73    },
74}
75
76macro_rules! validated_identifier {
77    ($name:ident, $kind:expr, $max:expr, $validator:ident) => {
78        #[doc = concat!("Validated ", stringify!($name), " domain primitive.")]
79        #[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
80        pub struct $name(String);
81
82        impl $name {
83            #[doc = concat!("Creates a validated ", stringify!($name), ".")]
84            ///
85            /// # Errors
86            ///
87            /// Returns [`DomainError`] when the value is empty, too long, or contains bytes outside
88            /// the identifier allowlist.
89            pub fn new(value: impl Into<String>) -> Result<Self, DomainError> {
90                let value = value.into();
91                $validator($kind, &value, $max)?;
92                Ok(Self(value))
93            }
94
95            /// Returns the validated identifier as a string slice.
96            #[must_use]
97            pub fn as_str(&self) -> &str {
98                &self.0
99            }
100        }
101
102        impl fmt::Display for $name {
103            fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
104                formatter.write_str(&self.0)
105            }
106        }
107
108        impl FromStr for $name {
109            type Err = DomainError;
110
111            fn from_str(value: &str) -> Result<Self, Self::Err> {
112                Self::new(value)
113            }
114        }
115
116        impl TryFrom<&str> for $name {
117            type Error = DomainError;
118
119            fn try_from(value: &str) -> Result<Self, Self::Error> {
120                Self::new(value)
121            }
122        }
123
124        impl TryFrom<String> for $name {
125            type Error = DomainError;
126
127            fn try_from(value: String) -> Result<Self, Self::Error> {
128                Self::new(value)
129            }
130        }
131
132        #[cfg(feature = "serde")]
133        impl serde::Serialize for $name {
134            fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
135            where
136                S: serde::Serializer,
137            {
138                serializer.serialize_str(self.as_str())
139            }
140        }
141
142        #[cfg(feature = "serde")]
143        impl<'de> serde::Deserialize<'de> for $name {
144            fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
145            where
146                D: serde::Deserializer<'de>,
147            {
148                let value = <String as serde::Deserialize>::deserialize(deserializer)?;
149                Self::try_from(value).map_err(serde::de::Error::custom)
150            }
151        }
152    };
153}
154
155validated_identifier!(
156    ObjectType,
157    IdentifierKind::ObjectType,
158    MAX_TYPE_BYTES,
159    validate_type_identifier
160);
161validated_identifier!(
162    ObjectId,
163    IdentifierKind::ObjectId,
164    MAX_ID_BYTES,
165    validate_id_identifier
166);
167validated_identifier!(
168    RelationName,
169    IdentifierKind::RelationName,
170    MAX_RELATION_BYTES,
171    validate_type_identifier
172);
173validated_identifier!(
174    SubjectType,
175    IdentifierKind::SubjectType,
176    MAX_TYPE_BYTES,
177    validate_type_identifier
178);
179validated_identifier!(
180    SubjectId,
181    IdentifierKind::SubjectId,
182    MAX_ID_BYTES,
183    validate_id_identifier
184);
185
186/// A validated object reference.
187#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
188pub struct ObjectRef {
189    object_type: ObjectType,
190    object_id: ObjectId,
191}
192
193impl ObjectRef {
194    /// Creates a validated object reference.
195    #[must_use]
196    pub fn new(object_type: ObjectType, object_id: ObjectId) -> Self {
197        Self {
198            object_type,
199            object_id,
200        }
201    }
202
203    /// Returns the object type.
204    #[must_use]
205    pub fn object_type(&self) -> &ObjectType {
206        &self.object_type
207    }
208
209    /// Returns the object identifier.
210    #[must_use]
211    pub fn object_id(&self) -> &ObjectId {
212        &self.object_id
213    }
214}
215
216impl fmt::Display for ObjectRef {
217    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
218        write!(formatter, "{}:{}", self.object_type, self.object_id)
219    }
220}
221
222impl FromStr for ObjectRef {
223    type Err = DomainError;
224
225    fn from_str(value: &str) -> Result<Self, Self::Err> {
226        let (object_type, object_id) = split_once(value, ':', "object reference must contain ':'")?;
227        Ok(Self::new(
228            ObjectType::try_from(object_type)?,
229            ObjectId::try_from(object_id)?,
230        ))
231    }
232}
233
234#[cfg(feature = "serde")]
235impl serde::Serialize for ObjectRef {
236    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
237    where
238        S: serde::Serializer,
239    {
240        serializer.serialize_str(&self.to_string())
241    }
242}
243
244#[cfg(feature = "serde")]
245impl<'de> serde::Deserialize<'de> for ObjectRef {
246    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
247    where
248        D: serde::Deserializer<'de>,
249    {
250        let value = <String as serde::Deserialize>::deserialize(deserializer)?;
251        Self::from_str(&value).map_err(serde::de::Error::custom)
252    }
253}
254
255impl TryFrom<&Object> for ObjectRef {
256    type Error = DomainError;
257
258    fn try_from(value: &Object) -> Result<Self, Self::Error> {
259        Ok(Self::new(
260            ObjectType::try_from(value.namespace.as_str())?,
261            ObjectId::try_from(value.id.as_str())?,
262        ))
263    }
264}
265
266/// A validated subject reference.
267#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord)]
268pub enum SubjectRef {
269    /// A direct subject object, usually `user:<id>`.
270    Object(ObjectRef),
271    /// A userset subject such as `group:eng#member`.
272    Userset {
273        /// Userset object.
274        object: ObjectRef,
275        /// Userset relation.
276        relation: RelationName,
277    },
278}
279
280impl fmt::Display for SubjectRef {
281    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
282        match self {
283            Self::Object(object) => write!(formatter, "{object}"),
284            Self::Userset { object, relation } => write!(formatter, "{object}#{relation}"),
285        }
286    }
287}
288
289impl FromStr for SubjectRef {
290    type Err = DomainError;
291
292    fn from_str(value: &str) -> Result<Self, Self::Err> {
293        match value.split_once('#') {
294            Some((object, relation)) => Ok(Self::Userset {
295                object: object.parse()?,
296                relation: RelationName::try_from(relation)?,
297            }),
298            None => Ok(Self::Object(parse_subject_object(value)?)),
299        }
300    }
301}
302
303#[cfg(feature = "serde")]
304impl serde::Serialize for SubjectRef {
305    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
306    where
307        S: serde::Serializer,
308    {
309        serializer.serialize_str(&self.to_string())
310    }
311}
312
313#[cfg(feature = "serde")]
314impl<'de> serde::Deserialize<'de> for SubjectRef {
315    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
316    where
317        D: serde::Deserializer<'de>,
318    {
319        let value = <String as serde::Deserialize>::deserialize(deserializer)?;
320        Self::from_str(&value).map_err(serde::de::Error::custom)
321    }
322}
323
324impl TryFrom<&User> for SubjectRef {
325    type Error = DomainError;
326
327    fn try_from(value: &User) -> Result<Self, Self::Error> {
328        match value {
329            User::UserId(id) => Ok(Self::Object(ObjectRef::new(
330                ObjectType::try_from(LEGACY_USER_SUBJECT_TYPE)?,
331                ObjectId::try_from(id.as_str())?,
332            ))),
333            User::Userset(object, relation) => Ok(Self::Userset {
334                object: ObjectRef::try_from(object)?,
335                relation: RelationName::try_from(relation.0.as_str())?,
336            }),
337        }
338    }
339}
340
341/// A validated relationship tuple.
342#[derive(Debug, Clone, PartialEq, Eq, Hash)]
343pub struct Relationship {
344    resource: ObjectRef,
345    relation: RelationName,
346    subject: SubjectRef,
347}
348
349impl Relationship {
350    /// Creates a validated relationship from already validated parts.
351    #[must_use]
352    pub fn new(resource: ObjectRef, relation: RelationName, subject: SubjectRef) -> Self {
353        Self {
354            resource,
355            relation,
356            subject,
357        }
358    }
359
360    /// Returns the relationship resource object.
361    #[must_use]
362    pub fn resource(&self) -> &ObjectRef {
363        &self.resource
364    }
365
366    /// Returns the relationship relation.
367    #[must_use]
368    pub fn relation(&self) -> &RelationName {
369        &self.relation
370    }
371
372    /// Returns the relationship subject.
373    #[must_use]
374    pub fn subject(&self) -> &SubjectRef {
375        &self.subject
376    }
377}
378
379impl fmt::Display for Relationship {
380    fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
381        write!(
382            formatter,
383            "{}#{}@{}",
384            self.resource, self.relation, self.subject
385        )
386    }
387}
388
389impl FromStr for Relationship {
390    type Err = DomainError;
391
392    fn from_str(value: &str) -> Result<Self, Self::Err> {
393        if value.len() > MAX_RELATIONSHIP_BYTES {
394            return Err(DomainError::IdentifierTooLong {
395                kind: IdentifierKind::ObjectId,
396                max_bytes: MAX_RELATIONSHIP_BYTES,
397            });
398        }
399
400        let (resource, subject) =
401            split_once(value, '@', "relationship must contain one '@' separator")?;
402        if subject.contains('@') {
403            return Err(DomainError::MalformedRelationship {
404                reason: "relationship must contain one '@' separator",
405            });
406        }
407
408        let (object, relation) =
409            split_once(resource, '#', "relationship resource must contain '#'")?;
410        if relation.contains('#') {
411            return Err(DomainError::MalformedRelationship {
412                reason: "relationship resource must contain one '#'",
413            });
414        }
415
416        Ok(Self::new(
417            object.parse()?,
418            RelationName::try_from(relation)?,
419            subject.parse()?,
420        ))
421    }
422}
423
424#[cfg(feature = "serde")]
425impl serde::Serialize for Relationship {
426    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
427    where
428        S: serde::Serializer,
429    {
430        serializer.serialize_str(&self.to_string())
431    }
432}
433
434#[cfg(feature = "serde")]
435impl<'de> serde::Deserialize<'de> for Relationship {
436    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
437    where
438        D: serde::Deserializer<'de>,
439    {
440        let value = <String as serde::Deserialize>::deserialize(deserializer)?;
441        Self::from_str(&value).map_err(serde::de::Error::custom)
442    }
443}
444
445impl TryFrom<&RelationTuple> for Relationship {
446    type Error = DomainError;
447
448    fn try_from(value: &RelationTuple) -> Result<Self, Self::Error> {
449        Ok(Self::new(
450            ObjectRef::try_from(&value.object)?,
451            RelationName::try_from(value.relation.0.as_str())?,
452            SubjectRef::try_from(&value.user)?,
453        ))
454    }
455}
456
457fn parse_subject_object(value: &str) -> Result<ObjectRef, DomainError> {
458    let (subject_type, subject_id) = split_once(value, ':', "subject reference must contain ':'")?;
459    Ok(ObjectRef::new(
460        ObjectType::new(SubjectType::try_from(subject_type)?.as_str())?,
461        ObjectId::new(SubjectId::try_from(subject_id)?.as_str())?,
462    ))
463}
464
465fn split_once<'a>(
466    value: &'a str,
467    delimiter: char,
468    missing_reason: &'static str,
469) -> Result<(&'a str, &'a str), DomainError> {
470    let (left, right) = value
471        .split_once(delimiter)
472        .ok_or(DomainError::MalformedRelationship {
473            reason: missing_reason,
474        })?;
475    if left.is_empty() || right.is_empty() {
476        return Err(DomainError::MalformedRelationship {
477            reason: missing_reason,
478        });
479    }
480    Ok((left, right))
481}
482
483fn validate_type_identifier(
484    kind: IdentifierKind,
485    value: &str,
486    max_bytes: usize,
487) -> Result<(), DomainError> {
488    if value.is_empty() {
489        return Err(DomainError::EmptyIdentifier { kind });
490    }
491    if value.len() > max_bytes {
492        return Err(DomainError::IdentifierTooLong { kind, max_bytes });
493    }
494
495    for (offset, byte) in value.bytes().enumerate() {
496        let valid = if offset == 0 {
497            byte.is_ascii_alphabetic()
498        } else {
499            byte.is_ascii_alphanumeric() || byte == b'_'
500        };
501        if !valid {
502            return Err(DomainError::InvalidIdentifierByte { kind, offset });
503        }
504    }
505
506    Ok(())
507}
508
509fn validate_id_identifier(
510    kind: IdentifierKind,
511    value: &str,
512    max_bytes: usize,
513) -> Result<(), DomainError> {
514    if value.is_empty() {
515        return Err(DomainError::EmptyIdentifier { kind });
516    }
517    if value.len() > max_bytes {
518        return Err(DomainError::IdentifierTooLong { kind, max_bytes });
519    }
520
521    for (offset, byte) in value.bytes().enumerate() {
522        let valid = byte.is_ascii_graphic()
523            && !matches!(byte, b'#' | b'@' | b':' | b'/' | b'\\')
524            && !byte.is_ascii_whitespace();
525        if !valid {
526            return Err(DomainError::InvalidIdentifierByte { kind, offset });
527        }
528    }
529
530    Ok(())
531}
532
533impl TryFrom<&Relation> for RelationName {
534    type Error = DomainError;
535
536    fn try_from(value: &Relation) -> Result<Self, Self::Error> {
537        Self::try_from(value.0.as_str())
538    }
539}
540
541impl From<&ObjectType> for SubjectType {
542    fn from(value: &ObjectType) -> Self {
543        Self(value.0.clone())
544    }
545}
546
547impl From<&ObjectId> for SubjectId {
548    fn from(value: &ObjectId) -> Self {
549        Self(value.0.clone())
550    }
551}