Oregami
Repositories/oxedyne/ore

oxedyne/ore/relay/src/acl.rs

9.2 KiB, 1 run

created by r2848102244:141, which is this file's identity for as long as the history lasts, whatever it is later renamed to

download · who wrote it · its history

1//! Who may read and write one hosted repository.
2//!
3//! The list lives beside the repository in the relay's own store and never in
4//! the history. Two reasons, one principled and one forced.
5//!
6//! Principled: the list answers "who may consume this relay's disk and
7//! bandwidth", which is a fact about the relay's resources and not about the
8//! history. Putting it in the operation graph would make the log an authority
9//! over the relay and would entangle every change of access with the operation
10//! vocabulary. GitHub's collaborators are a forge fact and not a git fact, and
11//! that precedent is the honest one to follow.
12//!
13//! Forced: a repository the relay cannot read -- the private form, once entries
14//! carry an encrypted body -- would have an unenforceable list if the list were
15//! inside it, and the relay is the party that must enforce it.
16//!
17//! The owner is the key that created the repository, and holds every role. A
18//! public repository grants [`Role::Pull`] to anyone, signed request or not;
19//! everything else is explicit.
20
21use ore_store::keys::{
22 bytes_of,
23 text_of,
24};
25
26use oxedyne_fe2o3_core::prelude::*;
27use oxedyne_fe2o3_jdat::prelude::*;
28
29use std::fs;
30use std::path::{
31 Path,
32 PathBuf,
33};
34
35
36/// Name of the access list, within a hosted repository's directory.
37pub const ACL_FILE: &str = "acl";
38
39/// Version of the access list this relay writes.
40pub const ACL_VERSION: u64 = 1;
41
42
43/// What one key may do to one hosted repository.
44#[derive(Clone, Copy, Debug, Eq, Ord, PartialEq, PartialOrd)]
45pub enum Role {
46 /// May take operations from the relay.
47 Pull,
48 /// May give operations to the relay, and take them. A push without a pull
49 /// would be a peer that can fill a disk and never converge.
50 Push,
51 /// May edit this list and the repository's relay policy, and everything a
52 /// push may do.
53 Admin,
54}
55
56impl Role {
57
58 /// Returns the name the list is written with.
59 pub fn name(&self) -> &'static str {
60 match self {
61 Self::Pull => "pull",
62 Self::Push => "push",
63 Self::Admin => "admin",
64 }
65 }
66
67 /// Reads a role by name.
68 pub fn of(name: &str)
69 -> Outcome<Self>
70 {
71 match name {
72 "pull" => Ok(Self::Pull),
73 "push" => Ok(Self::Push),
74 "admin" => Ok(Self::Admin),
75 other => Err(err!(
76 "There is no role {:?}. There are three: pull, push, admin.", other;
77 Invalid, Input)),
78 }
79 }
80
81 /// Reports whether holding this role is enough to do what `want` asks.
82 pub fn covers(&self, want: Role) -> bool {
83 match self {
84 Self::Admin => true,
85 Self::Push => want != Role::Admin,
86 Self::Pull => want == Role::Pull,
87 }
88 }
89}
90
91
92/// Who may do what to one hosted repository.
93#[derive(Clone, Debug)]
94pub struct Acl {
95 /// Version of the list format.
96 pub format: u64,
97 /// The public key that created the repository, which holds every role.
98 pub owner: Vec<u8>,
99 /// Whether anybody at all may pull, request signed or not.
100 pub public: bool,
101 /// Public key to the role it was granted.
102 pub grants: Vec<(Vec<u8>, Role)>,
103}
104
105impl Acl {
106
107 /// Constructs the list a fresh repository is created with.
108 pub fn new(owner: Vec<u8>, public: bool) -> Self {
109 Self {
110 format: ACL_VERSION,
111 owner,
112 public,
113 grants: Vec::new(),
114 }
115 }
116
117 /// Grants a role to a key, replacing whatever it held before.
118 pub fn grant(&mut self, public: Vec<u8>, role: Role) {
119 self.grants.retain(|(k, _)| k != &public);
120 self.grants.push((public, role));
121 self.grants.sort();
122 }
123
124 /// Reports whether a key may do what `want` asks.
125 ///
126 /// `key` is `None` where the request carried no signature, which only a
127 /// public repository's pull can survive.
128 pub fn may(&self, key: Option<&[u8]>, want: Role) -> bool {
129 if self.public && want == Role::Pull {
130 return true;
131 }
132 let key = match key {
133 Some(k) => k,
134 None => return false,
135 };
136 if key == self.owner.as_slice() {
137 return true;
138 }
139 self.grants.iter().any(|(k, role)| k == key && role.covers(want))
140 }
141
142 /// Returns the path of the list within a repository's directory.
143 pub fn path_of(dir: &Path) -> PathBuf {
144 dir.join(ACL_FILE)
145 }
146
147 /// Serialises the list to a [`Dat`].
148 pub fn to_dat(&self) -> Dat {
149 let grants: Vec<Dat> = self.grants.iter().map(|(key, role)| Dat::List(vec![
150 Dat::Str(text_of(key)),
151 Dat::Str(fmt!("{}", role.name())),
152 ])).collect();
153 let mut map = DaticleMap::new();
154 map.insert(Dat::Str(fmt!("format")), Dat::U64(self.format));
155 map.insert(Dat::Str(fmt!("owner")), Dat::Str(text_of(&self.owner)));
156 map.insert(Dat::Str(fmt!("public")), Dat::Bool(self.public));
157 map.insert(Dat::Str(fmt!("grants")), Dat::List(grants));
158 Dat::Map(map)
159 }
160
161 /// Reconstructs the list from a [`Dat`].
162 pub fn from_dat(dat: &Dat)
163 -> Outcome<Self>
164 {
165 let map = match dat {
166 Dat::Map(m) => m,
167 other => return Err(err!(
168 "An access list expects a map, got {:?}.", other;
169 Decode, Input, Mismatch)),
170 };
171 let format = match map.get(&Dat::Str(fmt!("format"))) {
172 Some(Dat::U64(n)) => *n,
173 Some(Dat::U32(n)) => *n as u64,
174 Some(Dat::U8(n)) => *n as u64,
175 other => return Err(err!(
176 "An access list's format expects a number, got {:?}.", other;
177 Decode, Input, Mismatch)),
178 };
179 if format != ACL_VERSION {
180 return Err(err!(
181 "An access list declares format version {}, and this relay knows only \
182 version {}.", format, ACL_VERSION;
183 Decode, Input, Version, Mismatch));
184 }
185 let owner = match map.get(&Dat::Str(fmt!("owner"))) {
186 Some(Dat::Str(s)) => res!(bytes_of(s)),
187 other => return Err(err!(
188 "An access list's owner expects a key, got {:?}.", other;
189 Decode, Input, Mismatch)),
190 };
191 let public = match map.get(&Dat::Str(fmt!("public"))) {
192 Some(Dat::Bool(b)) => *b,
193 other => return Err(err!(
194 "An access list's \"public\" expects true or false, got {:?}.", other;
195 Decode, Input, Mismatch)),
196 };
197 let listed = match map.get(&Dat::Str(fmt!("grants"))) {
198 Some(Dat::List(l)) => l,
199 other => return Err(err!(
200 "An access list's grants expect a list, got {:?}.", other;
201 Decode, Input, Mismatch)),
202 };
203 let mut grants = Vec::new();
204 for item in listed {
205 let pair = match item {
206 Dat::List(l) if l.len() == 2 => l,
207 other => return Err(err!(
208 "A grant expects a key and a role, got {:?}.", other;
209 Decode, Input, Mismatch)),
210 };
211 let key = match &pair[0] {
212 Dat::Str(s) => res!(bytes_of(s)),
213 other => return Err(err!(
214 "A grant's key expects a string, got {:?}.", other;
215 Decode, Input, Mismatch)),
216 };
217 let role = match &pair[1] {
218 Dat::Str(s) => res!(Role::of(s)),
219 other => return Err(err!(
220 "A grant's role expects a string, got {:?}.", other;
221 Decode, Input, Mismatch)),
222 };
223 grants.push((key, role));
224 }
225 grants.sort();
226 Ok(Self { format, owner, public, grants })
227 }
228
229 /// Reads the list from a repository's directory.
230 pub fn read(dir: &Path)
231 -> Outcome<Self>
232 {
233 let path = Self::path_of(dir);
234 let text = match fs::read_to_string(&path) {
235 Ok(t) => t,
236 Err(e) => return Err(err!(e,
237 "The access list {:?} could not be read.", path;
238 IO, File, Read)),
239 };
240 let dat = match Dat::decode_string(text) {
241 Ok(d) => d,
242 Err(e) => return Err(err!(e,
243 "The access list {:?} is not readable JDAT.", path;
244 Decode, Input)),
245 };
246 Self::from_dat(&dat)
247 }
248
249 /// Writes the list to a repository's directory.
250 pub fn write(&self, dir: &Path)
251 -> Outcome<()>
252 {
253 let text = res!(self.to_dat().jdat_to_lines(" "));
254 let path = Self::path_of(dir);
255 match fs::write(&path, fmt!("{}\n", text)) {
256 Ok(()) => Ok(()),
257 Err(e) => Err(err!(e,
258 "The access list {:?} could not be written.", path;
259 IO, File, Write)),
260 }
261 }
262}
263
264
265#[cfg(test)]
266mod tests {
267 use super::*;
268
269 /// A role covers what it implies and nothing above it.
270 #[test]
271 fn a_role_covers_what_it_implies() -> Outcome<()> {
272 assert!(Role::Push.covers(Role::Pull), "a push may take as well as give");
273 assert!(!Role::Pull.covers(Role::Push), "a pull may not give");
274 assert!(!Role::Push.covers(Role::Admin), "a push may not grant");
275 assert!(Role::Admin.covers(Role::Push) && Role::Admin.covers(Role::Pull));
276 Ok(())
277 }
278
279 /// The owner holds everything, a grant holds what it says, a stranger holds
280 /// nothing, and a public repository lets anyone pull and nobody push.
281 #[test]
282 fn the_list_answers_who_may_do_what() -> Outcome<()> {
283 let owner = b"owner-key".to_vec();
284 let friend = b"friend-key".to_vec();
285 let stranger = b"stranger-key".to_vec();
286 let mut acl = Acl::new(owner.clone(), false);
287 acl.grant(friend.clone(), Role::Pull);
288 assert!(acl.may(Some(&owner), Role::Admin));
289 assert!(acl.may(Some(&friend), Role::Pull));
290 assert!(!acl.may(Some(&friend), Role::Push));
291 assert!(!acl.may(Some(&stranger), Role::Pull));
292 assert!(!acl.may(None, Role::Pull), "a private repository answers nobody");
293
294 // Raising the grant replaces it rather than adding a second.
295 acl.grant(friend.clone(), Role::Push);
296 assert_eq!(acl.grants.len(), 1);
297 assert!(acl.may(Some(&friend), Role::Push));
298
299 let open = Acl::new(owner.clone(), true);
300 assert!(open.may(None, Role::Pull), "a public repository answers anyone");
301 assert!(!open.may(None, Role::Push), "and still takes nothing from a stranger");
302 assert!(!open.may(Some(&stranger), Role::Push));
303
304 // And it survives the round trip through its file form.
305 let back = res!(Acl::from_dat(&acl.to_dat()));
306 assert_eq!(back.owner, acl.owner);
307 assert_eq!(back.public, acl.public);
308 assert_eq!(back.grants, acl.grants);
309 Ok(())
310 }
311}