Skip to main content

reifydb_value/value/identity/
mod.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright (c) 2026 ReifyDB
3
4use std::{fmt, ops::Deref, str::FromStr};
5
6use serde::{Deserialize, Deserializer, Serialize, Serializer, de, de::Visitor};
7use uuid::Uuid;
8
9use crate::{
10	clock::{ClockNow, RandomBytes},
11	value::uuid::Uuid7,
12};
13
14#[repr(transparent)]
15#[derive(Debug, Copy, Clone, PartialOrd, PartialEq, Ord, Eq, Hash, Default)]
16pub struct IdentityId(pub Uuid7);
17
18impl IdentityId {
19	pub fn generate<C: ClockNow, R: RandomBytes>(clock: &C, rng: &R) -> Self {
20		IdentityId(Uuid7::generate(clock, rng))
21	}
22
23	pub fn new(id: Uuid7) -> Self {
24		IdentityId(id)
25	}
26
27	pub fn value(&self) -> Uuid7 {
28		self.0
29	}
30
31	pub fn anonymous() -> Self {
32		let bytes = [
33			0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x70, 0x00, 0x80, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
34		];
35		IdentityId(Uuid7(Uuid::from_bytes(bytes)))
36	}
37
38	pub fn root() -> Self {
39		let bytes = [
40			0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0x7F, 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
41		];
42		IdentityId(Uuid7(Uuid::from_bytes(bytes)))
43	}
44
45	pub fn system() -> Self {
46		let bytes = [
47			0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFE, 0x7F, 0xFF, 0xBF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF,
48		];
49		IdentityId(Uuid7(Uuid::from_bytes(bytes)))
50	}
51
52	pub fn is_anonymous(&self) -> bool {
53		*self == Self::anonymous()
54	}
55
56	pub fn is_root(&self) -> bool {
57		*self == Self::root()
58	}
59
60	pub fn is_system(&self) -> bool {
61		*self == Self::system()
62	}
63
64	pub fn sentinel_kind(&self) -> Option<IdentityKind> {
65		if self.is_root() {
66			Some(IdentityKind::Root)
67		} else if self.is_system() {
68			Some(IdentityKind::System)
69		} else if self.is_anonymous() {
70			Some(IdentityKind::Anonymous)
71		} else {
72			None
73		}
74	}
75
76	pub fn is_privileged(&self) -> bool {
77		matches!(self.sentinel_kind(), Some(IdentityKind::Root | IdentityKind::System))
78	}
79}
80
81#[repr(u8)]
82#[derive(Debug, Copy, Clone, PartialEq, Eq, Hash, Serialize, Deserialize)]
83pub enum IdentityKind {
84	User = 0,
85	Service = 1,
86	Root = 2,
87	System = 3,
88	Anonymous = 4,
89	Guest = 5,
90}
91
92impl IdentityKind {
93	pub fn to_u8(self) -> u8 {
94		match self {
95			IdentityKind::User => 0,
96			IdentityKind::Service => 1,
97			IdentityKind::Root => 2,
98			IdentityKind::System => 3,
99			IdentityKind::Anonymous => 4,
100			IdentityKind::Guest => 5,
101		}
102	}
103
104	pub fn from_u8(value: u8) -> Self {
105		match value {
106			0 => IdentityKind::User,
107			1 => IdentityKind::Service,
108			2 => IdentityKind::Root,
109			3 => IdentityKind::System,
110			4 => IdentityKind::Anonymous,
111			5 => IdentityKind::Guest,
112			_ => IdentityKind::User,
113		}
114	}
115
116	pub fn as_str(self) -> &'static str {
117		match self {
118			IdentityKind::User => "user",
119			IdentityKind::Service => "service",
120			IdentityKind::Root => "root",
121			IdentityKind::System => "system",
122			IdentityKind::Anonymous => "anonymous",
123			IdentityKind::Guest => "guest",
124		}
125	}
126
127	pub fn is_builtin(self) -> bool {
128		matches!(self, IdentityKind::Root | IdentityKind::System | IdentityKind::Anonymous)
129	}
130}
131
132impl fmt::Display for IdentityKind {
133	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
134		f.write_str(self.as_str())
135	}
136}
137
138impl Deref for IdentityId {
139	type Target = Uuid7;
140
141	fn deref(&self) -> &Self::Target {
142		&self.0
143	}
144}
145
146impl PartialEq<Uuid7> for IdentityId {
147	fn eq(&self, other: &Uuid7) -> bool {
148		self.0.eq(other)
149	}
150}
151
152impl From<Uuid7> for IdentityId {
153	fn from(id: Uuid7) -> Self {
154		IdentityId(id)
155	}
156}
157
158impl From<IdentityId> for Uuid7 {
159	fn from(identity_id: IdentityId) -> Self {
160		identity_id.0
161	}
162}
163
164impl fmt::Display for IdentityId {
165	fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
166		write!(f, "{}", self.0)
167	}
168}
169
170impl Serialize for IdentityId {
171	fn serialize<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
172	where
173		S: Serializer,
174	{
175		Serialize::serialize(&self.0, serializer)
176	}
177}
178
179impl<'de> Deserialize<'de> for IdentityId {
180	fn deserialize<D>(deserializer: D) -> Result<IdentityId, D::Error>
181	where
182		D: Deserializer<'de>,
183	{
184		struct Uuid7Visitor;
185
186		impl<'de> Visitor<'de> for Uuid7Visitor {
187			type Value = IdentityId;
188
189			fn expecting(&self, formatter: &mut fmt::Formatter) -> fmt::Result {
190				formatter.write_str("a UUID version 7")
191			}
192
193			fn visit_str<E>(self, value: &str) -> Result<Self::Value, E>
194			where
195				E: de::Error,
196			{
197				let uuid =
198					Uuid::from_str(value).map_err(|e| E::custom(format!("invalid UUID: {}", e)))?;
199
200				if uuid.get_version_num() != 7 {
201					return Err(E::custom(format!(
202						"expected UUID v7, got v{}",
203						uuid.get_version_num()
204					)));
205				}
206
207				Ok(IdentityId(Uuid7::from(uuid)))
208			}
209
210			fn visit_bytes<E>(self, value: &[u8]) -> Result<Self::Value, E>
211			where
212				E: de::Error,
213			{
214				let uuid = Uuid::from_slice(value)
215					.map_err(|e| E::custom(format!("invalid UUID bytes: {}", e)))?;
216
217				if uuid.get_version_num() != 7 {
218					return Err(E::custom(format!(
219						"expected UUID v7, got v{}",
220						uuid.get_version_num()
221					)));
222				}
223
224				Ok(IdentityId(Uuid7::from(uuid)))
225			}
226		}
227
228		if deserializer.is_human_readable() {
229			deserializer.deserialize_str(Uuid7Visitor)
230		} else {
231			deserializer.deserialize_bytes(Uuid7Visitor)
232		}
233	}
234}
235
236#[cfg(test)]
237pub mod tests {
238	use postcard::{from_bytes, to_allocvec};
239	use serde_json::{from_str, to_string};
240
241	use super::*;
242	use crate::clock::testing::{TestClock, TestRng};
243
244	fn test_clock_and_rng() -> (TestClock, TestClock, TestRng) {
245		let clock = TestClock::from_millis(1000);
246		(clock.clone(), clock, TestRng)
247	}
248
249	#[test]
250	fn test_identity_id_creation() {
251		let (_, clock, rng) = test_clock_and_rng();
252		let id = IdentityId::generate(&clock, &rng);
253		assert_ne!(id, IdentityId::default());
254	}
255
256	#[test]
257	fn test_identity_id_from_uuid7() {
258		let (_, clock, rng) = test_clock_and_rng();
259		let uuid = Uuid7::generate(&clock, &rng);
260		let id = IdentityId::from(uuid);
261		assert_eq!(id.value(), uuid);
262	}
263
264	#[test]
265	fn test_identity_id_display() {
266		let (_, clock, rng) = test_clock_and_rng();
267		let id = IdentityId::generate(&clock, &rng);
268		let display = format!("{}", id);
269		assert!(!display.is_empty());
270	}
271
272	#[test]
273	fn test_identity_id_equality() {
274		let (_, clock, rng) = test_clock_and_rng();
275		let uuid = Uuid7::generate(&clock, &rng);
276		let id1 = IdentityId::from(uuid);
277		let id2 = IdentityId::from(uuid);
278		assert_eq!(id1, id2);
279	}
280
281	#[test]
282	fn test_identity_id_postcard_roundtrip() {
283		let (_, clock, rng) = test_clock_and_rng();
284		let id = IdentityId::generate(&clock, &rng);
285		let bytes = to_allocvec(&id).expect("postcard serialize");
286		let decoded: IdentityId = from_bytes(&bytes).expect("postcard deserialize");
287		assert_eq!(id, decoded);
288	}
289
290	#[test]
291	fn test_identity_id_postcard_roundtrip_root() {
292		let id = IdentityId::root();
293		let bytes = to_allocvec(&id).expect("postcard serialize root");
294		let decoded: IdentityId = from_bytes(&bytes).expect("postcard deserialize root");
295		assert_eq!(id, decoded);
296	}
297
298	#[test]
299	fn test_identity_id_json_roundtrip() {
300		let (_, clock, rng) = test_clock_and_rng();
301		let id = IdentityId::generate(&clock, &rng);
302		let s = to_string(&id).expect("json serialize");
303		let decoded: IdentityId = from_str(&s).expect("json deserialize");
304		assert_eq!(id, decoded);
305	}
306
307	#[test]
308	fn test_sentinel_kind_covers_all_three_sentinels() {
309		// The sentinels have no catalog row, so their kind can only come from
310		// the id itself. A None here would make the resolution rule
311		// sentinel_kind().unwrap_or(stored) fall through to a stored kind that
312		// does not exist.
313		assert_eq!(IdentityId::root().sentinel_kind(), Some(IdentityKind::Root));
314		assert_eq!(IdentityId::system().sentinel_kind(), Some(IdentityKind::System));
315		assert_eq!(IdentityId::anonymous().sentinel_kind(), Some(IdentityKind::Anonymous));
316	}
317
318	#[test]
319	fn test_sentinel_kind_is_none_for_a_regular_identity() {
320		// A generated id must defer to its stored kind, otherwise every
321		// identity would be forced into a builtin kind.
322		let (_, clock, rng) = test_clock_and_rng();
323		assert_eq!(IdentityId::generate(&clock, &rng).sentinel_kind(), None);
324	}
325
326	#[test]
327	fn test_default_identity_id_is_not_anonymous() {
328		// IdentityId derives Default (all-zero Uuid7), which is a distinct
329		// value from the anonymous sentinel (that one carries version and
330		// variant bits). Conflating them would hand a default id the
331		// anonymous kind.
332		assert_ne!(IdentityId::default(), IdentityId::anonymous());
333		assert_eq!(IdentityId::default().sentinel_kind(), None);
334	}
335
336	#[test]
337	fn test_is_privileged_is_root_and_system_only() {
338		// is_privileged gates all five policy bypass sites. Anonymous must
339		// never be privileged.
340		assert!(IdentityId::root().is_privileged());
341		assert!(IdentityId::system().is_privileged());
342		assert!(!IdentityId::anonymous().is_privileged());
343		let (_, clock, rng) = test_clock_and_rng();
344		assert!(!IdentityId::generate(&clock, &rng).is_privileged());
345	}
346
347	#[test]
348	fn test_identity_kind_u8_roundtrip() {
349		// The u8 is the on-disk representation; a mismatch silently
350		// reinterprets stored identities as a different kind.
351		for kind in [
352			IdentityKind::User,
353			IdentityKind::Service,
354			IdentityKind::Root,
355			IdentityKind::System,
356			IdentityKind::Anonymous,
357			IdentityKind::Guest,
358		] {
359			assert_eq!(IdentityKind::from_u8(kind.to_u8()), kind);
360		}
361	}
362
363	#[test]
364	fn test_identity_kind_discriminants_are_stable() {
365		// The discriminants are the on-disk encoding. Renumbering an existing
366		// kind reinterprets every stored identity row, so a new kind must only
367		// ever be appended.
368		assert_eq!(IdentityKind::User.to_u8(), 0);
369		assert_eq!(IdentityKind::Service.to_u8(), 1);
370		assert_eq!(IdentityKind::Root.to_u8(), 2);
371		assert_eq!(IdentityKind::System.to_u8(), 3);
372		assert_eq!(IdentityKind::Anonymous.to_u8(), 4);
373		assert_eq!(IdentityKind::Guest.to_u8(), 5);
374	}
375
376	#[test]
377	fn test_identity_kind_names_are_distinct() {
378		// as_str is what `$identity.kind` compares against in policy
379		// predicates, so two kinds sharing a name would silently widen a
380		// policy written for one of them.
381		let names = [
382			IdentityKind::User.as_str(),
383			IdentityKind::Service.as_str(),
384			IdentityKind::Root.as_str(),
385			IdentityKind::System.as_str(),
386			IdentityKind::Anonymous.as_str(),
387			IdentityKind::Guest.as_str(),
388		];
389		for (i, a) in names.iter().enumerate() {
390			for b in &names[i + 1..] {
391				assert_ne!(a, b);
392			}
393		}
394		assert_eq!(IdentityKind::Guest.as_str(), "guest");
395	}
396
397	#[test]
398	fn test_identity_kind_user_is_zero() {
399		// User must be 0 so that a row written before the kind field existed
400		// decodes from zeroed padding as User rather than as a builtin kind.
401		assert_eq!(IdentityKind::User.to_u8(), 0);
402	}
403
404	#[test]
405	fn test_identity_kind_from_unknown_u8_falls_back_to_user() {
406		// from_u8 is total by house convention (see FlowStatus). An unknown
407		// byte must not panic, and must not decode as a builtin kind, which
408		// would grant it the DROP/ALTER/GRANT immunity builtins get.
409		let kind = IdentityKind::from_u8(200);
410		assert_eq!(kind, IdentityKind::User);
411		assert!(!kind.is_builtin());
412	}
413
414	#[test]
415	fn test_is_builtin_matches_the_unstorable_kinds() {
416		// is_builtin gates DROP/ALTER/GRANT. It must cover exactly the kinds
417		// that are never stored, so User and Service stay reachable by DDL.
418		assert!(IdentityKind::Root.is_builtin());
419		assert!(IdentityKind::System.is_builtin());
420		assert!(IdentityKind::Anonymous.is_builtin());
421		assert!(!IdentityKind::User.is_builtin());
422		assert!(!IdentityKind::Service.is_builtin());
423		// Guest is a stored kind: a guest identity has to stay renamable and
424		// alterable, because registering promotes it in place.
425		assert!(!IdentityKind::Guest.is_builtin());
426	}
427}