Skip to main content

haematite_cli/serve/
access.rs

1//! Request-scoped authority resolved by a source, never supplied by a record.
2
3use std::collections::{BTreeMap, BTreeSet};
4use std::sync::Arc;
5
6use crate::serve::storage::Failure;
7
8type DeniedDocuments = BTreeMap<String, BTreeSet<String>>;
9
10/// A source's answer for one request. Its fields cannot be set by a reader.
11///
12/// What it holds is private and is whatever its source resolved; a reader
13/// asks it a question (`can_read_document`) and never looks inside.
14///
15/// The control for the two refusals below: the same path, used as intended,
16/// compiles and runs. Without it a wrong path would also "fail to compile".
17///
18/// ```
19/// use haematite_cli::serve::access::{Access, Standalone, request};
20/// let access: Result<Access, _> = request(&Standalone);
21/// assert!(access.is_ok());
22/// ```
23///
24/// ```compile_fail,E0451
25/// use haematite_cli::serve::access::Access;
26/// let access = Access { denied_documents: None };
27/// ```
28///
29/// ```compile_fail,E0277
30/// use haematite_cli::serve::access::Access;
31/// let access: Access = Default::default();
32/// ```
33pub struct Access {
34    denied_documents: Option<Arc<DeniedDocuments>>,
35}
36
37impl Access {
38    pub(in crate::serve) fn can_read_collection(&self, collection: &str) -> bool {
39        self.denied_documents
40            .as_ref()
41            .and_then(|denied| denied.get(collection))
42            .is_none_or(BTreeSet::is_empty)
43    }
44
45    pub(in crate::serve) fn can_read_store(&self) -> bool {
46        self.denied_documents
47            .as_ref()
48            .is_none_or(|denied| denied.values().all(BTreeSet::is_empty))
49    }
50
51    pub(in crate::serve) fn can_read_document(&self, collection: &str, id: &str) -> bool {
52        self.denied_documents
53            .as_ref()
54            .and_then(|denied| denied.get(collection))
55            .is_none_or(|documents| !documents.contains(id))
56    }
57}
58
59mod sealed {
60    pub trait Source {}
61}
62
63/// Resolves authority without allowing a consumer to implement a granting source.
64pub trait Source: sealed::Source + Send + Sync {
65    /// Resolve the authority for one request.
66    ///
67    /// # Errors
68    /// Returns the source's named refusal when authority cannot be resolved.
69    fn access(&self) -> Result<Access, Failure>;
70}
71
72/// The authority of the owner who runs the service without a directory.
73pub struct Standalone;
74
75impl sealed::Source for Standalone {}
76
77impl Source for Standalone {
78    fn access(&self) -> Result<Access, Failure> {
79        Ok(Access {
80            denied_documents: None,
81        })
82    }
83}
84
85/// Asks the source once; source failures are returned without another source.
86///
87/// # Errors
88/// Propagates the source's refusal without substituting another source.
89pub fn request(source: &dyn Source) -> Result<Access, Failure> {
90    source.access()
91}
92
93#[cfg(test)]
94pub(super) struct TestSource {
95    denied_documents: Arc<DeniedDocuments>,
96    calls: std::sync::atomic::AtomicUsize,
97    unavailable: bool,
98}
99
100#[cfg(test)]
101impl TestSource {
102    pub(super) fn denying(documents: &[(&str, &str)]) -> Self {
103        let mut denied: DeniedDocuments = BTreeMap::new();
104        for (collection, id) in documents {
105            denied
106                .entry((*collection).to_owned())
107                .or_default()
108                .insert((*id).to_owned());
109        }
110        Self {
111            denied_documents: Arc::new(denied),
112            calls: std::sync::atomic::AtomicUsize::new(0),
113            unavailable: false,
114        }
115    }
116
117    pub(super) fn allowing_collection(mut self, collection: &str) -> Self {
118        Arc::make_mut(&mut self.denied_documents).insert(collection.to_owned(), BTreeSet::new());
119        self
120    }
121
122    pub(super) fn unavailable() -> Self {
123        Self {
124            unavailable: true,
125            ..Self::denying(&[])
126        }
127    }
128
129    pub(super) fn calls(&self) -> usize {
130        self.calls.load(std::sync::atomic::Ordering::Relaxed)
131    }
132}
133
134#[cfg(test)]
135impl sealed::Source for TestSource {}
136
137#[cfg(test)]
138impl Source for TestSource {
139    fn access(&self) -> Result<Access, Failure> {
140        self.calls
141            .fetch_add(1, std::sync::atomic::Ordering::Relaxed);
142        if self.unavailable {
143            return Err(Failure::new(
144                "access_unavailable",
145                "request authority could not be resolved",
146            ));
147        }
148        Ok(Access {
149            denied_documents: Some(Arc::clone(&self.denied_documents)),
150        })
151    }
152}