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