1use crate::ids::{numeric_id, string_id, validation_error};
8use serde::{Deserialize, Serialize};
9use std::collections::{BTreeMap, BTreeSet};
10use std::fmt;
11use thiserror::Error;
12
13const MAX_PRINCIPAL_ID_BYTES: usize = 256;
14pub const MAX_ACCESS_GRANT_ENTRIES: usize = 1_000;
16pub const MAX_ACCESS_GRANTS_PRINCIPAL_BYTES: usize = 65_536;
18
19validation_error!(
20 PrincipalIdValidationError,
21 "invalid principal_id {value:?}: {reason}"
22);
23
24validation_error!(
25 SubjectIdValidationError,
26 "invalid subject_id {value:?}: {reason}"
27);
28
29string_id! {
30 SubjectId,
36 error = SubjectIdValidationError,
37 validate = validate_subject_id,
38 schema(
39 description = "Stable opaque subject id containing 1 to 256 visible ASCII characters other than the comma.",
40 pattern = r"^[\x21-\x2B\x2D-\x7E]{1,256}$",
41 example = "usr_8f3c"
42 )
43}
44
45#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Subject {
48 pub principal_scope: PrincipalScope,
53 pub subject_id: SubjectId,
55 pub principals: PrincipalSet,
57}
58
59validation_error!(
60 PrincipalScopeValidationError,
61 "invalid principal_scope {value:?}: {reason}"
62);
63
64string_id! {
65 PrincipalId,
70 error = PrincipalIdValidationError,
71 validate = validate_principal_id,
72 schema(
73 description = "Stable opaque principal id containing 1 to 256 visible ASCII characters other than the comma.",
74 pattern = r"^[\x21-\x2B\x2D-\x7E]{1,256}$",
75 example = "prn_8f3c"
76 )
77}
78
79string_id! {
80 PrincipalScope,
85 error = PrincipalScopeValidationError,
86 validate = validate_principal_scope,
87 schema(
88 description = "Opaque identity-domain id containing 1 to 256 visible ASCII characters other than the comma.",
89 pattern = r"^[\x21-\x2B\x2D-\x7E]{1,256}$",
90 example = "org_acme"
91 )
92}
93
94numeric_id! {
95 AccessRevisionNo,
97 public_ordinal,
98 schema_description = "Monotonic per-inode access revision. It increases with every accepted access update."
99}
100
101fn visible_ascii_reason(value: &str) -> Option<&'static str> {
102 if value.is_empty() {
103 return Some("must not be empty");
104 }
105 if value.len() > MAX_PRINCIPAL_ID_BYTES {
106 return Some("must be 256 bytes or fewer");
107 }
108 if !value.bytes().all(|byte| (0x21..=0x7e).contains(&byte)) {
109 return Some("must contain only visible ASCII characters");
110 }
111 if value.contains(',') {
112 return Some("must not contain a comma, which separates ids on the wire");
113 }
114 None
115}
116
117fn validate_principal_id(value: &str) -> Result<(), PrincipalIdValidationError> {
118 visible_ascii_reason(value).map_or(Ok(()), |reason| {
119 Err(PrincipalIdValidationError::new(value, reason))
120 })
121}
122
123fn validate_subject_id(value: &str) -> Result<(), SubjectIdValidationError> {
124 visible_ascii_reason(value).map_or(Ok(()), |reason| {
125 Err(SubjectIdValidationError::new(value, reason))
126 })
127}
128
129fn validate_principal_scope(value: &str) -> Result<(), PrincipalScopeValidationError> {
130 visible_ascii_reason(value).map_or(Ok(()), |reason| {
131 Err(PrincipalScopeValidationError::new(value, reason))
132 })
133}
134
135#[derive(Debug, Clone, Copy, PartialEq, Eq, PartialOrd, Ord, Hash, Serialize, Deserialize)]
137#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
138#[serde(rename_all = "snake_case")]
139pub enum AccessRight {
140 Read,
142 History,
144 Write,
146 Create,
148 Remove,
150 Share,
152 Manage,
154 Admin,
156}
157
158impl AccessRight {
159 pub const ALL: [Self; 8] = [
161 Self::Read,
162 Self::History,
163 Self::Write,
164 Self::Create,
165 Self::Remove,
166 Self::Share,
167 Self::Manage,
168 Self::Admin,
169 ];
170
171 pub const fn as_str(self) -> &'static str {
173 match self {
174 Self::Read => "read",
175 Self::History => "history",
176 Self::Write => "write",
177 Self::Create => "create",
178 Self::Remove => "remove",
179 Self::Share => "share",
180 Self::Manage => "manage",
181 Self::Admin => "admin",
182 }
183 }
184
185 const fn bit(self) -> u8 {
186 1 << (self as u8)
187 }
188}
189
190#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Default)]
194#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
195#[cfg_attr(feature = "openapi", schema(value_type = Vec<AccessRight>))]
196pub struct AccessRights(u8);
197
198impl AccessRights {
199 pub const EMPTY: Self = Self(0);
201
202 pub const ALL: Self = Self(((1_u16 << AccessRight::ALL.len()) - 1) as u8);
204 pub const ADMIN: Self = Self(AccessRight::Admin.bit());
206
207 pub fn contains(self, right: AccessRight) -> bool {
209 self.0 & right.bit() != 0
210 }
211
212 pub fn insert(&mut self, right: AccessRight) {
214 self.0 |= right.bit();
215 }
216
217 pub fn union(self, other: Self) -> Self {
219 Self(self.0 | other.0)
220 }
221
222 pub fn difference(self, other: Self) -> Self {
224 Self(self.0 & !other.0)
225 }
226
227 pub fn is_subset_of(self, other: Self) -> bool {
229 self.0 & !other.0 == 0
230 }
231
232 pub fn is_empty(self) -> bool {
234 self.0 == 0
235 }
236
237 pub fn iter(self) -> impl Iterator<Item = AccessRight> {
239 AccessRight::ALL
240 .into_iter()
241 .filter(move |right| self.contains(*right))
242 }
243}
244
245impl FromIterator<AccessRight> for AccessRights {
246 fn from_iter<I: IntoIterator<Item = AccessRight>>(rights: I) -> Self {
247 rights.into_iter().fold(Self::EMPTY, |mut set, right| {
248 set.insert(right);
249 set
250 })
251 }
252}
253
254impl Serialize for AccessRights {
255 fn serialize<S: serde::Serializer>(&self, serializer: S) -> Result<S::Ok, S::Error> {
256 serializer.collect_seq(self.iter())
257 }
258}
259
260impl<'de> Deserialize<'de> for AccessRights {
261 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
262 let names = Vec::<AccessRight>::deserialize(deserializer)?;
263 let mut rights = Self::EMPTY;
264 for right in names {
265 if rights.contains(right) {
266 return Err(serde::de::Error::custom(format!(
267 "duplicate right `{}`",
268 right.as_str()
269 )));
270 }
271 rights.insert(right);
272 }
273 Ok(rights)
274 }
275}
276
277#[derive(Debug, Clone, PartialEq, Eq, Default, Serialize)]
282#[cfg_attr(feature = "openapi", derive(utoipa::ToSchema))]
283#[cfg_attr(feature = "openapi", schema(value_type = std::collections::BTreeMap<String, AccessRights>))]
284#[serde(transparent)]
285pub struct AccessGrants(BTreeMap<PrincipalId, AccessRights>);
286
287impl AccessGrants {
288 pub fn new(entries: BTreeMap<PrincipalId, AccessRights>) -> Result<Self, AccessGrantsError> {
290 if entries.len() > MAX_ACCESS_GRANT_ENTRIES {
291 return Err(AccessGrantsError::TooManyEntries {
292 entries: entries.len(),
293 });
294 }
295 if let Some((principal_id, _)) = entries.iter().find(|(_, rights)| rights.is_empty()) {
296 return Err(AccessGrantsError::EmptyRights {
297 principal_id: principal_id.clone(),
298 });
299 }
300 let principal_bytes = entries.keys().map(|id| id.as_str().len()).sum::<usize>();
301 if principal_bytes > MAX_ACCESS_GRANTS_PRINCIPAL_BYTES {
302 return Err(AccessGrantsError::TooManyPrincipalBytes { principal_bytes });
303 }
304 Ok(Self(entries))
305 }
306
307 pub fn get(&self, principal_id: &PrincipalId) -> AccessRights {
309 self.0.get(principal_id).copied().unwrap_or_default()
310 }
311
312 pub fn iter(&self) -> impl Iterator<Item = (&PrincipalId, AccessRights)> {
314 self.0.iter().map(|(id, rights)| (id, *rights))
315 }
316
317 pub fn len(&self) -> usize {
319 self.0.len()
320 }
321
322 pub fn is_empty(&self) -> bool {
324 self.0.is_empty()
325 }
326
327 pub fn as_map(&self) -> &BTreeMap<PrincipalId, AccessRights> {
329 &self.0
330 }
331
332 pub fn logical_bytes(&self) -> usize {
335 self.0
336 .iter()
337 .map(|(id, rights)| id.as_str().len() + rights.iter().count())
338 .sum()
339 }
340}
341
342impl TryFrom<BTreeMap<PrincipalId, AccessRights>> for AccessGrants {
343 type Error = AccessGrantsError;
344
345 fn try_from(entries: BTreeMap<PrincipalId, AccessRights>) -> Result<Self, Self::Error> {
346 Self::new(entries)
347 }
348}
349
350impl<'de> Deserialize<'de> for AccessGrants {
351 fn deserialize<D: serde::Deserializer<'de>>(deserializer: D) -> Result<Self, D::Error> {
352 struct AccessGrantsVisitor;
353
354 impl<'de> serde::de::Visitor<'de> for AccessGrantsVisitor {
355 type Value = AccessGrants;
356
357 fn expecting(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
358 formatter.write_str("a map from principal ids to access rights")
359 }
360
361 fn visit_map<A: serde::de::MapAccess<'de>>(
362 self,
363 mut map: A,
364 ) -> Result<Self::Value, A::Error> {
365 let mut entries: BTreeMap<PrincipalId, AccessRights> = BTreeMap::new();
366 while let Some((principal_id, rights)) = map.next_entry()? {
367 match entries.entry(principal_id) {
368 std::collections::btree_map::Entry::Vacant(entry) => {
369 entry.insert(rights);
370 }
371 std::collections::btree_map::Entry::Occupied(entry) => {
372 return Err(serde::de::Error::custom(
373 AccessGrantsError::DuplicatePrincipal {
374 principal_id: entry.key().clone(),
375 },
376 ));
377 }
378 }
379 }
380 AccessGrants::new(entries).map_err(serde::de::Error::custom)
381 }
382 }
383
384 deserializer.deserialize_map(AccessGrantsVisitor)
385 }
386}
387
388#[derive(Debug, Clone, PartialEq, Eq, Error)]
390pub enum AccessGrantsError {
391 #[error("access grants name principal `{principal_id}` more than once")]
393 DuplicatePrincipal {
394 principal_id: PrincipalId,
396 },
397 #[error("access grants name {entries} principals, which exceeds the maximum of {MAX_ACCESS_GRANT_ENTRIES}")]
399 TooManyEntries {
400 entries: usize,
402 },
403 #[error("access grants hold {principal_bytes} bytes of principal ids, which exceeds the maximum of {MAX_ACCESS_GRANTS_PRINCIPAL_BYTES} bytes")]
405 TooManyPrincipalBytes {
406 principal_bytes: usize,
408 },
409 #[error("access grant for `{principal_id}` carries no rights")]
411 EmptyRights {
412 principal_id: PrincipalId,
414 },
415}
416
417pub const MAX_SUBJECT_PRINCIPALS: usize = 64;
419
420#[derive(Debug, Clone, PartialEq, Eq, Default)]
423pub struct PrincipalSet(BTreeSet<PrincipalId>);
424
425impl PrincipalSet {
426 pub fn new(principals: BTreeSet<PrincipalId>) -> Result<Self, PrincipalSetError> {
428 if principals.len() > MAX_SUBJECT_PRINCIPALS {
429 return Err(PrincipalSetError::TooManyPrincipals {
430 principals: principals.len(),
431 });
432 }
433 Ok(Self(principals))
434 }
435
436 pub fn iter(&self) -> impl Iterator<Item = &PrincipalId> {
438 self.0.iter()
439 }
440
441 pub fn contains(&self, principal_id: &PrincipalId) -> bool {
443 self.0.contains(principal_id)
444 }
445
446 pub fn len(&self) -> usize {
448 self.0.len()
449 }
450
451 pub fn is_empty(&self) -> bool {
453 self.0.is_empty()
454 }
455}
456
457#[derive(Debug, Clone, PartialEq, Eq, Error)]
459pub enum PrincipalSetError {
460 #[error("principal set names {principals} principals, which exceeds the maximum of {MAX_SUBJECT_PRINCIPALS}")]
462 TooManyPrincipals {
463 principals: usize,
465 },
466}
467
468#[cfg(test)]
469mod tests {
470 use super::{
471 AccessGrants, AccessGrantsError, AccessRight, AccessRights, PrincipalId,
472 MAX_ACCESS_GRANTS_PRINCIPAL_BYTES, MAX_ACCESS_GRANT_ENTRIES,
473 };
474
475 #[test]
476 fn ids_never_contain_the_wire_separator() {
477 assert!(PrincipalId::parse("visitor,prn_root").is_err());
478 assert!(super::SubjectId::parse("usr,ada").is_err());
479 assert!(super::PrincipalScope::parse("org,demo").is_err());
480 assert!(PrincipalId::parse("visitor").is_ok());
481 }
482 use std::collections::BTreeMap;
483
484 #[test]
485 fn access_rights_encode_in_declaration_order_and_decode_any_order() {
486 let rights: AccessRights = AccessRight::ALL.into_iter().rev().collect();
487 let encoded = serde_json::to_string(&rights).expect("serialize rights");
488 assert_eq!(
489 encoded,
490 r#"["read","history","write","create","remove","share","manage","admin"]"#
491 );
492 assert_eq!(
493 serde_json::from_str::<AccessRights>(&encoded).expect("decode encoded rights"),
494 AccessRights::ALL
495 );
496 assert_eq!(
497 serde_json::from_str::<AccessRights>(r#"["manage","read"]"#)
498 .expect("decode rights in another order"),
499 [AccessRight::Read, AccessRight::Manage]
500 .into_iter()
501 .collect()
502 );
503 assert!(serde_json::from_str::<AccessRights>(r#"["read","read"]"#).is_err());
504 assert!(serde_json::from_str::<AccessRights>(r#"["owner"]"#).is_err());
505 let empty = serde_json::to_string(&AccessRights::EMPTY).expect("serialize empty rights");
506 assert_eq!(empty, "[]");
507 assert_eq!(
508 serde_json::from_str::<AccessRights>(&empty).expect("empty rights"),
509 AccessRights::EMPTY
510 );
511 }
512
513 #[test]
514 fn access_grants_json_rejects_a_repeated_principal() {
515 let error =
516 serde_json::from_str::<AccessGrants>(r#"{"viewer":["read"],"viewer":["manage"]}"#)
517 .expect_err("repeated principal");
518 assert!(
519 error
520 .to_string()
521 .contains("access grants name principal `viewer` more than once"),
522 "{error}"
523 );
524 }
525
526 #[test]
527 fn access_grants_cbor_rejects_a_repeated_principal() {
528 let mut encoded = Vec::new();
529 ciborium::ser::into_writer(
530 &ciborium::Value::Map(vec![
531 (
532 ciborium::Value::Text("viewer".to_owned()),
533 ciborium::Value::Array(vec![ciborium::Value::Text("read".to_owned())]),
534 ),
535 (
536 ciborium::Value::Text("viewer".to_owned()),
537 ciborium::Value::Array(vec![ciborium::Value::Text("manage".to_owned())]),
538 ),
539 ]),
540 &mut encoded,
541 )
542 .expect("encode repeated principal");
543 let error = ciborium::de::from_reader::<AccessGrants, _>(encoded.as_slice())
544 .expect_err("repeated principal");
545 assert!(
546 error
547 .to_string()
548 .contains("access grants name principal `viewer` more than once"),
549 "{error}"
550 );
551 }
552
553 #[test]
554 fn access_grants_reject_empty_rights_and_oversized_maps() {
555 let principal = PrincipalId::parse("prn_ada").expect("principal");
556 let read: AccessRights = [AccessRight::Read].into_iter().collect();
557 let long_id_count = MAX_ACCESS_GRANTS_PRINCIPAL_BYTES / 256 + 1;
558 assert!(long_id_count < MAX_ACCESS_GRANT_ENTRIES);
559 for (entries, expected) in [
560 (
561 BTreeMap::from([(principal.clone(), AccessRights::EMPTY)]),
562 AccessGrantsError::EmptyRights {
563 principal_id: principal,
564 },
565 ),
566 (
567 (0..=MAX_ACCESS_GRANT_ENTRIES)
568 .map(|index| {
569 (
570 PrincipalId::parse(format!("prn_{index}")).expect("principal"),
571 read,
572 )
573 })
574 .collect(),
575 AccessGrantsError::TooManyEntries {
576 entries: MAX_ACCESS_GRANT_ENTRIES + 1,
577 },
578 ),
579 (
580 (0..long_id_count)
581 .map(|index| {
582 (
583 PrincipalId::parse(format!("{index:0256}")).expect("principal"),
584 read,
585 )
586 })
587 .collect(),
588 AccessGrantsError::TooManyPrincipalBytes {
589 principal_bytes: long_id_count * 256,
590 },
591 ),
592 ] {
593 assert_eq!(
594 AccessGrants::new(entries).expect_err("invalid grants"),
595 expected
596 );
597 }
598 }
599}