Skip to main content

loonfs_api/
access.rs

1//! Types for access grants on inodes.
2//!
3//! A principal is an application-assigned identity such as a user, a group,
4//! or a public audience. LoonFS stores principal ids as opaque strings and
5//! never resolves them.
6
7use 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;
14/// Most principals one access row may name.
15pub const MAX_ACCESS_GRANT_ENTRIES: usize = 1_000;
16/// Most bytes of principal ids one access row may hold, summed over its entries.
17pub 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    /// The stable identity a request acts as, used for upload ownership
31    /// and commit replay.
32    ///
33    /// Subject IDs contain 1 to 256 visible ASCII characters (0x21 through 0x7E)
34    /// other than the comma, which separates ids on the wire.
35    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/// Who a request acts as: a principal scope, stable id, and applicable principals.
46#[derive(Debug, Clone, PartialEq, Eq)]
47pub struct Subject {
48    /// Identity domain for the subject's principals.
49    ///
50    /// Commit fingerprints and upload ownership use the subject id alone because a namespace
51    /// has one scope and refuses subjects from any other scope.
52    pub principal_scope: PrincipalScope,
53    /// Stable identity for upload ownership and commit replay.
54    pub subject_id: SubjectId,
55    /// Principals whose grants apply to the request.
56    pub principals: PrincipalSet,
57}
58
59validation_error!(
60    PrincipalScopeValidationError,
61    "invalid principal_scope {value:?}: {reason}"
62);
63
64string_id! {
65    /// A validated principal identifier supplied by the application.
66    ///
67    /// Principal IDs contain 1 to 256 visible ASCII characters (0x21 through 0x7E)
68    /// other than the comma, which separates ids on the wire.
69    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    /// The identity domain a namespace's principal ids belong to.
81    ///
82    /// A deployment that did not issue a namespace's grants refuses to
83    /// interpret them by comparing this value. Same grammar as a principal id.
84    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    /// Monotonic per-inode access revision.
96    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/// One right a grant can carry.
136#[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 the item: names and kinds of a directory's entries, or a file's current bytes.
141    Read,
142    /// Read older revisions and snapshots.
143    History,
144    /// Change the item: new revisions and attributes.
145    Write,
146    /// Add an entry to a directory.
147    Create,
148    /// Remove an entry from a directory.
149    Remove,
150    /// Grant a subset of one's own rights to others.
151    Share,
152    /// Change any grant or the boundary.
153    Manage,
154    /// Every right on every inode. Valid only on the root inode's row.
155    Admin,
156}
157
158impl AccessRight {
159    /// Every right, in the order the durable encoding lists them.
160    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    /// Returns the serialized name.
172    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/// A set of rights. Encoded as distinct names in `read`, `history`, `write`,
191/// `create`, `remove`, `share`, `manage`, `admin` order. Decoding accepts any
192/// order and rejects repeated names.
193#[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    /// The set with no rights.
200    pub const EMPTY: Self = Self(0);
201
202    /// Every right.
203    pub const ALL: Self = Self(((1_u16 << AccessRight::ALL.len()) - 1) as u8);
204    /// The set holding only `admin`.
205    pub const ADMIN: Self = Self(AccessRight::Admin.bit());
206
207    /// Whether the set holds `right`.
208    pub fn contains(self, right: AccessRight) -> bool {
209        self.0 & right.bit() != 0
210    }
211
212    /// Adds `right` to the set.
213    pub fn insert(&mut self, right: AccessRight) {
214        self.0 |= right.bit();
215    }
216
217    /// Rights in either set.
218    pub fn union(self, other: Self) -> Self {
219        Self(self.0 | other.0)
220    }
221
222    /// Rights in `self` that `other` lacks.
223    pub fn difference(self, other: Self) -> Self {
224        Self(self.0 & !other.0)
225    }
226
227    /// Whether every right in `self` is also in `other`.
228    pub fn is_subset_of(self, other: Self) -> bool {
229        self.0 & !other.0 == 0
230    }
231
232    /// Whether the set holds no rights.
233    pub fn is_empty(self) -> bool {
234        self.0 == 0
235    }
236
237    /// The rights in encoding order.
238    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/// A validated map from principal to rights, limited to
278/// [`MAX_ACCESS_GRANT_ENTRIES`] entries and [`MAX_ACCESS_GRANTS_PRINCIPAL_BYTES`]
279/// bytes of principal ids. No entry has an empty set of rights. Decoding
280/// rejects repeated principals.
281#[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    /// Validates `entries` against the caps and the no-empty-rights rule.
289    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    /// The rights granted to `principal_id`, empty when it has no entry.
308    pub fn get(&self, principal_id: &PrincipalId) -> AccessRights {
309        self.0.get(principal_id).copied().unwrap_or_default()
310    }
311
312    /// The entries in principal order.
313    pub fn iter(&self) -> impl Iterator<Item = (&PrincipalId, AccessRights)> {
314        self.0.iter().map(|(id, rights)| (id, *rights))
315    }
316
317    /// Number of principals named.
318    pub fn len(&self) -> usize {
319        self.0.len()
320    }
321
322    /// Whether no principal is named.
323    pub fn is_empty(&self) -> bool {
324        self.0.is_empty()
325    }
326
327    /// The underlying map.
328    pub fn as_map(&self) -> &BTreeMap<PrincipalId, AccessRights> {
329        &self.0
330    }
331
332    /// Bytes this map accounts for when decoded: every principal id plus one
333    /// byte per right.
334    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/// Why a grant map was rejected.
389#[derive(Debug, Clone, PartialEq, Eq, Error)]
390pub enum AccessGrantsError {
391    /// A principal appeared more than once.
392    #[error("access grants name principal `{principal_id}` more than once")]
393    DuplicatePrincipal {
394        /// The repeated principal.
395        principal_id: PrincipalId,
396    },
397    /// More principals than [`MAX_ACCESS_GRANT_ENTRIES`].
398    #[error("access grants name {entries} principals, which exceeds the maximum of {MAX_ACCESS_GRANT_ENTRIES}")]
399    TooManyEntries {
400        /// Principals the map named.
401        entries: usize,
402    },
403    /// More principal id bytes than [`MAX_ACCESS_GRANTS_PRINCIPAL_BYTES`].
404    #[error("access grants hold {principal_bytes} bytes of principal ids, which exceeds the maximum of {MAX_ACCESS_GRANTS_PRINCIPAL_BYTES} bytes")]
405    TooManyPrincipalBytes {
406        /// Bytes of principal ids the map held.
407        principal_bytes: usize,
408    },
409    /// An entry granted nothing.
410    #[error("access grant for `{principal_id}` carries no rights")]
411    EmptyRights {
412        /// The principal with the empty entry.
413        principal_id: PrincipalId,
414    },
415}
416
417/// Most principals one request may act as.
418pub const MAX_SUBJECT_PRINCIPALS: usize = 64;
419
420/// The principals a request acts as: distinct ids, at most
421/// [`MAX_SUBJECT_PRINCIPALS`] of them.
422#[derive(Debug, Clone, PartialEq, Eq, Default)]
423pub struct PrincipalSet(BTreeSet<PrincipalId>);
424
425impl PrincipalSet {
426    /// Rejects sets larger than [`MAX_SUBJECT_PRINCIPALS`].
427    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    /// The ids in principal order.
437    pub fn iter(&self) -> impl Iterator<Item = &PrincipalId> {
438        self.0.iter()
439    }
440
441    /// Whether the request acts as `principal_id`.
442    pub fn contains(&self, principal_id: &PrincipalId) -> bool {
443        self.0.contains(principal_id)
444    }
445
446    /// Number of distinct identities the request acts as.
447    pub fn len(&self) -> usize {
448        self.0.len()
449    }
450
451    /// Whether the request acts as no principal.
452    pub fn is_empty(&self) -> bool {
453        self.0.is_empty()
454    }
455}
456
457/// Why a request's principal set was rejected.
458#[derive(Debug, Clone, PartialEq, Eq, Error)]
459pub enum PrincipalSetError {
460    /// More identities than one request may act as.
461    #[error("principal set names {principals} principals, which exceeds the maximum of {MAX_SUBJECT_PRINCIPALS}")]
462    TooManyPrincipals {
463        /// Number of identities the rejected set held.
464        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}