authly_common/
id.rs

1//! Authly identifier types
2use std::{
3    fmt::{Debug, Display},
4    hash::Hash,
5    marker::PhantomData,
6    str::FromStr,
7};
8
9use anyhow::{anyhow, Context};
10use kind::{IdKind, Kind};
11use rand::Rng;
12use serde::{Deserialize, Serialize};
13use subset::{IdKindSubset, IdKindSupersetOf};
14
15use crate::FromStrVisitor;
16
17/// Authly generic 128-bit identifier
18pub struct Id128<K>([u8; 16], PhantomData<K>);
19
20impl<K> Id128<K> {
21    /// Construct a new identifier from a 128-bit unsigned int.
22    pub const fn from_uint(val: u128) -> Self {
23        Self(val.to_be_bytes(), PhantomData)
24    }
25
26    /// Construct a new identifier from a reference to a byte array, without type information
27    pub const fn from_raw_array(array: &[u8; 16]) -> Self {
28        Self(*array, PhantomData)
29    }
30
31    /// Get the byte-wise representation of the ID, without type information
32    pub const fn to_raw_array(self) -> [u8; 16] {
33        self.0
34    }
35
36    /// Try to deserialize from a raw byte representation, without type information
37    pub fn from_raw_bytes(bytes: &[u8]) -> Option<Self> {
38        Some(Self(bytes.try_into().ok()?, PhantomData))
39    }
40
41    /// Create a new random identifier.
42    pub fn random() -> Self {
43        loop {
44            let id: u128 = rand::rng().random();
45            // low IDs are reserved for builtin/fixed
46            if id > u16::MAX as u128 {
47                return Self(id.to_be_bytes(), PhantomData);
48            }
49        }
50    }
51
52    /// Convert to an unsigned integer
53    pub fn to_uint(&self) -> u128 {
54        u128::from_be_bytes(self.0)
55    }
56}
57
58impl<K: IdKind> Id128<K> {
59    /// Infallibly convert this into a [DynamicId]
60    pub fn upcast<KS: IdKindSubset + IdKindSupersetOf<K>>(self) -> DynamicId<KS> {
61        DynamicId {
62            id: self.0,
63            kind: K::kind(),
64            _subset: PhantomData,
65        }
66    }
67}
68
69impl<K> Clone for Id128<K> {
70    fn clone(&self) -> Self {
71        *self
72    }
73}
74
75impl<K> Copy for Id128<K> {}
76
77impl<K> PartialEq for Id128<K> {
78    fn eq(&self, other: &Self) -> bool {
79        self.0 == other.0
80    }
81}
82
83impl<K> Eq for Id128<K> {}
84
85impl<K> PartialOrd<Id128<K>> for Id128<K> {
86    fn partial_cmp(&self, other: &Id128<K>) -> Option<std::cmp::Ordering> {
87        Some(self.0.cmp(&other.0))
88    }
89}
90
91impl<K> Ord for Id128<K> {
92    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
93        self.0.cmp(&other.0)
94    }
95}
96
97impl<K> Hash for Id128<K> {
98    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
99        self.0.hash(state);
100    }
101}
102
103impl<K> From<[u8; 16]> for Id128<K> {
104    fn from(value: [u8; 16]) -> Self {
105        Self(value, PhantomData)
106    }
107}
108
109impl<K> From<&[u8; 16]> for Id128<K> {
110    fn from(value: &[u8; 16]) -> Self {
111        Self(*value, PhantomData)
112    }
113}
114
115impl<K> Debug for Id128<K> {
116    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
117        write!(f, "{}", hexhex::hex(&self.0))
118    }
119}
120
121impl<K: IdKind> Display for Id128<K> {
122    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
123        write!(f, "{}.{}", K::kind().str_prefix(), hexhex::hex(&self.0))
124    }
125}
126
127/// Conversion to and from byte arrays with Kind information.
128pub trait Id128DynamicArrayConv: Sized {
129    /// Convert a byte array into this type.
130    fn try_from_array_dynamic(array: &[u8; 17]) -> Option<Self>;
131
132    /// Convert a byte slice into this type.
133    fn try_from_bytes_dynamic(bytes: &[u8]) -> Option<Self> {
134        Self::try_from_array_dynamic(bytes.try_into().ok()?)
135    }
136
137    /// Convert this type into a byte array.
138    fn to_array_dynamic(&self) -> [u8; 17];
139}
140
141/// Types of Kinds of typed Ids.
142pub mod kind {
143    use int_enum::IntEnum;
144    use serde::{Deserialize, Serialize};
145
146    /// A dynamic kind of ID.
147    ///
148    /// It acts as a "namespace" for identifiers.
149    ///
150    /// NB: This enum is used in persisted postcard serializations, new variants should be added at the end!
151    #[derive(
152        Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, IntEnum, Serialize, Deserialize, Debug,
153    )]
154    #[repr(u8)]
155    pub enum Kind {
156        /// Persona Entity kind.
157        Persona = 0,
158        /// Group Entity kind.
159        Group = 1,
160        /// Service Entity kind.
161        Service = 2,
162        /// Domain kind.
163        Domain = 3,
164        /// Policy kind.
165        Policy = 4,
166        /// Property kind.
167        Property = 5,
168        /// Attribute kind.
169        Attribute = 6,
170        /// Directory kind.
171        Directory = 7,
172    }
173
174    impl Kind {
175        #[inline]
176        pub(super) const fn str_prefix(&self) -> &'static str {
177            match self {
178                Self::Persona => "p",
179                Self::Group => "g",
180                Self::Service => "s",
181                Self::Domain => "d",
182                Self::Property => "prp",
183                Self::Attribute => "atr",
184                Self::Directory => "dir",
185                Self::Policy => "pol",
186            }
187        }
188
189        pub(super) const fn name(&self) -> &'static str {
190            match self {
191                Kind::Persona => "persona ID",
192                Kind::Group => "group ID",
193                Kind::Service => "service ID",
194                Kind::Domain => "domain ID",
195                Kind::Policy => "policy ID",
196                Kind::Property => "property ID",
197                Kind::Attribute => "attribute ID",
198                Kind::Directory => "directory ID",
199            }
200        }
201
202        pub(super) const fn entries() -> &'static [Self] {
203            &[
204                Self::Persona,
205                Self::Group,
206                Self::Service,
207                Self::Domain,
208                Self::Policy,
209                Self::Property,
210                Self::Attribute,
211                Self::Directory,
212            ]
213        }
214    }
215
216    /// Trait for static kinds of Ids.
217    pub trait IdKind {
218        /// The runtime kind of static ID kind.
219        fn kind() -> Kind;
220    }
221
222    /// Persona ID kind.
223    pub struct Persona;
224
225    /// Group ID kind.
226    pub struct Group;
227
228    /// Service ID kind.
229    pub struct Service;
230
231    /// Domain ID kind.
232    pub struct Domain;
233
234    /// Policy ID kind.
235    pub struct Policy;
236
237    /// Attribute ID kind.
238    pub struct Property;
239
240    /// Attribute ID kind.
241    pub struct Attrbute;
242
243    /// Directory ID kind.
244    pub struct Directory;
245
246    impl IdKind for Persona {
247        fn kind() -> Kind {
248            Kind::Persona
249        }
250    }
251
252    impl IdKind for Group {
253        fn kind() -> Kind {
254            Kind::Group
255        }
256    }
257
258    impl IdKind for Service {
259        fn kind() -> Kind {
260            Kind::Service
261        }
262    }
263
264    impl IdKind for Domain {
265        fn kind() -> Kind {
266            Kind::Domain
267        }
268    }
269
270    impl IdKind for Policy {
271        fn kind() -> Kind {
272            Kind::Policy
273        }
274    }
275
276    impl IdKind for Property {
277        fn kind() -> Kind {
278            Kind::Property
279        }
280    }
281
282    impl IdKind for Attrbute {
283        fn kind() -> Kind {
284            Kind::Attribute
285        }
286    }
287
288    impl IdKind for Directory {
289        fn kind() -> Kind {
290            Kind::Directory
291        }
292    }
293}
294
295/// Id kind subsets.
296pub mod subset {
297    use super::kind::{Group, IdKind, Kind, Persona, Service};
298
299    /// Describes a specific subset of Authly ID kinds.
300    pub trait IdKindSubset {
301        /// Whether the subset contains the given [Kind].
302        fn contains(kind: Kind) -> bool;
303
304        /// The name of this subset.
305        fn name() -> &'static str;
306    }
307
308    /// Describes a specific _superset_ of K.
309    pub trait IdKindSupersetOf<K> {}
310
311    /// The Entity subset.
312    pub struct Entity;
313
314    /// The Any subset, which contains all kinds of Authly IDs.
315    pub struct Any;
316
317    impl IdKindSubset for Entity {
318        fn contains(kind: Kind) -> bool {
319            matches!(kind, Kind::Persona | Kind::Group | Kind::Service)
320        }
321
322        fn name() -> &'static str {
323            "Entity ID"
324        }
325    }
326
327    impl IdKindSupersetOf<Persona> for Entity {}
328    impl IdKindSupersetOf<Group> for Entity {}
329    impl IdKindSupersetOf<Service> for Entity {}
330
331    impl IdKindSubset for Any {
332        fn contains(_kind: Kind) -> bool {
333            true
334        }
335
336        fn name() -> &'static str {
337            "Any ID"
338        }
339    }
340
341    impl<K: IdKind> IdKindSupersetOf<K> for Any {}
342    impl IdKindSupersetOf<Entity> for Any {}
343}
344
345/// Authly Persona ID
346pub type PersonaId = Id128<kind::Persona>;
347
348/// Authly Group ID
349pub type GroupId = Id128<kind::Group>;
350
351/// Authly Service ID
352pub type ServiceId = Id128<kind::Service>;
353
354/// Authly Property ID
355pub type PropId = Id128<kind::Property>;
356
357/// Authly Attribute ID
358pub type AttrId = Id128<kind::Attrbute>;
359
360/// Authly Policy ID
361pub type PolicyId = Id128<kind::Policy>;
362
363/// Authly Domain ID
364pub type DomainId = Id128<kind::Domain>;
365
366/// Authly Directory ID
367pub type DirectoryId = Id128<kind::Directory>;
368
369/// Dynamically typed ID, can represent any kind "object" Id
370pub struct DynamicId<KS: IdKindSubset> {
371    pub(crate) id: [u8; 16],
372    kind: kind::Kind,
373    _subset: PhantomData<KS>,
374}
375
376impl<KS: IdKindSubset> DynamicId<KS> {
377    /// Construct a new dynamicId.
378    ///
379    /// Panics if [Kind] is not member of the KS subset.
380    pub fn new(kind: Kind, id: [u8; 16]) -> Self {
381        if !KS::contains(kind) {
382            panic!("Not in subset");
383        }
384        Self {
385            kind,
386            id,
387            _subset: PhantomData,
388        }
389    }
390
391    /// The dynamic kind of this dynamic id.
392    pub fn kind(&self) -> Kind {
393        self.kind
394    }
395
396    /// Infallibly upcast this into a superset [DynamicId].
397    pub fn upcast<KS2: IdKindSubset + IdKindSupersetOf<KS>>(&self) -> DynamicId<KS2> {
398        DynamicId {
399            id: self.id,
400            kind: self.kind,
401            _subset: PhantomData,
402        }
403    }
404
405    /// Get the byte-wise representation of the ID, without type information.
406    /// NB! This erases the dynamic tag!
407    pub const fn to_raw_array(self) -> [u8; 16] {
408        self.id
409    }
410}
411
412impl<KS: IdKindSubset> Clone for DynamicId<KS> {
413    fn clone(&self) -> Self {
414        *self
415    }
416}
417
418impl<KS: IdKindSubset> Copy for DynamicId<KS> {}
419
420impl<KS: IdKindSubset> Debug for DynamicId<KS> {
421    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
422        write!(f, "{}.{}", self.kind.str_prefix(), hexhex::hex(&self.id))
423    }
424}
425
426impl<KS: IdKindSubset> Display for DynamicId<KS> {
427    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
428        write!(f, "{}.{}", self.kind.str_prefix(), hexhex::hex(&self.id))
429    }
430}
431
432impl<KS: IdKindSubset> PartialEq for DynamicId<KS> {
433    fn eq(&self, other: &Self) -> bool {
434        self.kind == other.kind && self.id == other.id
435    }
436}
437
438impl<KS: IdKindSubset> Eq for DynamicId<KS> {}
439
440impl<KS: IdKindSubset> Hash for DynamicId<KS> {
441    fn hash<H: std::hash::Hasher>(&self, state: &mut H) {
442        self.id.hash(state);
443        self.kind.hash(state);
444    }
445}
446
447impl<KS: IdKindSubset> PartialOrd<DynamicId<KS>> for DynamicId<KS> {
448    fn partial_cmp(&self, other: &DynamicId<KS>) -> Option<std::cmp::Ordering> {
449        Some(self.cmp(other))
450    }
451}
452
453impl<KS: IdKindSubset> Ord for DynamicId<KS> {
454    fn cmp(&self, other: &Self) -> std::cmp::Ordering {
455        self.kind
456            .cmp(&other.kind)
457            .then_with(|| self.id.cmp(&other.id))
458    }
459}
460
461/// An Authly Entity ID.
462pub type EntityId = DynamicId<subset::Entity>;
463
464/// An Authly Any Id - the Id of any object, entities or other objects.
465pub type AnyId = DynamicId<subset::Any>;
466
467impl<K: IdKind> FromStr for Id128<K> {
468    type Err = anyhow::Error;
469
470    fn from_str(s: &str) -> Result<Self, Self::Err> {
471        let prefix = K::kind().str_prefix();
472        let Some(s) = s.strip_prefix(prefix) else {
473            return Err(anyhow!("unrecognized prefix, expected `{prefix}`"));
474        };
475        let s = s.strip_prefix('.').context("missing `.`")?;
476
477        let hex = hexhex::decode(s).context("invalid format")?;
478        let array: [u8; 16] = hex.try_into().map_err(|_| anyhow!("invalid length"))?;
479
480        let min = 32768_u128.to_be_bytes();
481
482        if array != [0; 16] && array < min {
483            return Err(anyhow!("invalid value, too small"));
484        }
485
486        Ok(Id128(array, PhantomData))
487    }
488}
489
490impl<S: IdKindSubset> FromStr for DynamicId<S> {
491    type Err = anyhow::Error;
492
493    fn from_str(s: &str) -> Result<Self, Self::Err> {
494        let mut segments = s.splitn(2, ".");
495        let prefix = segments.next().context("no prefix")?;
496        let s = segments.next().context("no hex code")?;
497
498        if segments.next().is_some() {
499            return Err(anyhow!("too many dots"));
500        }
501
502        let kind = Kind::entries()
503            .iter()
504            .copied()
505            .find(|kind| kind.str_prefix() == prefix)
506            .context("unrecognized prefix")?;
507
508        if !S::contains(kind) {
509            return Err(anyhow!("invalid subset"));
510        }
511
512        let hex = hexhex::decode(s).context("invalid format")?;
513        let array: [u8; 16] = hex.try_into().map_err(|_| anyhow!("invalid length"))?;
514
515        let min = 32768_u128.to_be_bytes();
516
517        if array != [0; 16] && array < min {
518            return Err(anyhow!("invalid value, too small"));
519        }
520
521        Ok(DynamicId {
522            id: array,
523            kind,
524            _subset: PhantomData,
525        })
526    }
527}
528
529impl<'de, K: IdKind> Deserialize<'de> for Id128<K> {
530    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
531    where
532        D: serde::Deserializer<'de>,
533    {
534        deserializer.deserialize_str(FromStrVisitor::new(K::kind().name()))
535    }
536}
537
538impl<K: IdKind> Serialize for Id128<K> {
539    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
540    where
541        S: serde::Serializer,
542    {
543        serializer.serialize_str(&format!(
544            "{}.{}",
545            K::kind().str_prefix(),
546            hexhex::hex(&self.0)
547        ))
548    }
549}
550
551impl<KS: IdKindSubset> Serialize for DynamicId<KS> {
552    fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
553    where
554        S: serde::Serializer,
555    {
556        serializer.serialize_str(&format!(
557            "{}.{}",
558            self.kind.str_prefix(),
559            hexhex::hex(&self.id)
560        ))
561    }
562}
563
564impl<'de, KS: IdKindSubset> Deserialize<'de> for DynamicId<KS> {
565    fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
566    where
567        D: serde::Deserializer<'de>,
568    {
569        deserializer.deserialize_str(FromStrVisitor::new(KS::name()))
570    }
571}
572
573impl<K: IdKind> Id128DynamicArrayConv for Id128<K> {
574    fn try_from_array_dynamic(array: &[u8; 17]) -> Option<Self> {
575        let kind_byte: u8 = K::kind().into();
576        if array[0] == kind_byte {
577            Self::from_raw_bytes(&array[1..])
578        } else {
579            None
580        }
581    }
582
583    fn to_array_dynamic(&self) -> [u8; 17] {
584        let mut output = [0u8; 17];
585        output[0] = K::kind().into();
586        output[1..].clone_from_slice(&self.to_raw_array());
587        output
588    }
589}
590
591impl<KS: IdKindSubset> Id128DynamicArrayConv for DynamicId<KS> {
592    fn try_from_array_dynamic(array: &[u8; 17]) -> Option<Self> {
593        let kind = Kind::try_from(array[0]).ok()?;
594        if !KS::contains(kind) {
595            return None;
596        }
597
598        let id = array[1..].try_into().ok()?;
599
600        Some(Self {
601            kind,
602            id,
603            _subset: PhantomData,
604        })
605    }
606
607    fn to_array_dynamic(&self) -> [u8; 17] {
608        let mut output = [0u8; 17];
609        output[0] = self.kind.into();
610        output[1..].clone_from_slice(&self.id);
611        output
612    }
613}
614
615impl<K: IdKind, KS: IdKindSubset> TryFrom<Id128<K>> for DynamicId<KS> {
616    type Error = ();
617
618    fn try_from(value: Id128<K>) -> Result<Self, Self::Error> {
619        if KS::contains(K::kind()) {
620            Ok(Self {
621                kind: K::kind(),
622                id: value.0,
623                _subset: PhantomData,
624            })
625        } else {
626            Err(())
627        }
628    }
629}
630
631impl<K: IdKind, KS: IdKindSubset> TryFrom<DynamicId<KS>> for Id128<K> {
632    type Error = ();
633
634    fn try_from(value: DynamicId<KS>) -> Result<Self, Self::Error> {
635        if value.kind != K::kind() {
636            return Err(());
637        }
638
639        Ok(Self(value.id, PhantomData))
640    }
641}
642
643#[test]
644fn from_hex_literal() {
645    let _ = PersonaId::from(hexhex::hex_literal!("1234abcd1234abcd1234abcd1234abcd"));
646}
647
648#[test]
649fn from_str() {
650    PersonaId::from_str("p.1234abcd1234abcd1234abcd1234abcd").unwrap();
651    ServiceId::from_str("s.1234abcd1234abcd1234abcd1234abcd").unwrap();
652    PersonaId::from_str("d.1234abcd1234abcd1234abcd1234abcd").unwrap_err();
653    DomainId::from_str("d.1234abcd1234abcd1234abcd1234abcd").unwrap();
654
655    AnyId::from_str("s.1234abcd1234abcd1234abcd1234abcd").unwrap();
656    AnyId::from_str("d.1234abcd1234abcd1234abcd1234abcd").unwrap();
657    EntityId::from_str("s.1234abcd1234abcd1234abcd1234abcd").unwrap();
658    EntityId::from_str("g.1234abcd1234abcd1234abcd1234abcd").unwrap();
659    EntityId::from_str("d.1234abcd1234abcd1234abcd1234abcd").unwrap_err();
660}
661
662#[test]
663fn serde() {
664    let before = PersonaId::from_str("p.1234abcd1234abcd1234abcd1234abcd").unwrap();
665    let json = serde_json::to_string(&before).unwrap();
666
667    assert_eq!("\"p.1234abcd1234abcd1234abcd1234abcd\"", json);
668
669    let after: PersonaId = serde_json::from_str(&json).unwrap();
670
671    assert_eq!(before, after);
672}