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
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
|
//! Who may do what. The users and what each may do are the swarm's: a registry in the
//! document (`_continuum/users`, `registry`), each user with capabilities, each a path
//! and the rights it gives there and below (`Capability::covers`), `*` in it standing
//! for any one key (`agree`). How a user proves who
//! it is stays each node's (`--htpasswd`, `--user-header`: `api::auth`). Enforced at
//! the HTTP API ([`access.md`](../../docs/access.md)).
// Used by the genesis node and the API from the next patches on.
#![cfg_attr(not(test), allow(dead_code))]
use std::collections::BTreeSet;
use serde::{Deserialize, Serialize};
use crate::document::meta;
pub(crate) mod bans;
pub(crate) mod challenge;
pub(crate) mod envelope;
pub(crate) mod policy;
pub(crate) mod registry;
pub(crate) mod rules;
pub(crate) mod screen;
pub(crate) mod sealed;
pub(crate) mod sshsig;
pub(crate) mod strict;
pub(crate) mod token;
/// Whether `pattern` and `path` agree on the segments both have: each the same, or `*`
/// in `pattern`, for any key. Of capabilities, signed paths and sealed places alike.
pub(crate) fn agree(pattern: &[String], path: &[String]) -> bool {
pattern
.iter()
.zip(path)
.all(|(segment, key)| segment == "*" || segment == key)
}
/// Whether the pattern `inner` lies at or below the pattern `outer`: everything `inner`
/// covers, `outer` does (a `*` of `inner` only under one of `outer`).
fn at_or_below(inner: &[String], outer: &[String]) -> bool {
inner.len() >= outer.len() && agree(outer, inner)
}
/// What a capability lets its holder do, at its path and below.
#[derive(Clone, Copy, Debug, PartialEq, Eq, PartialOrd, Ord, Hash)]
pub(crate) enum Right {
Read,
Write,
}
impl Right {
pub(crate) fn as_str(self) -> &'static str {
match self {
Self::Read => "read",
Self::Write => "write",
}
}
}
/// A path, and what it lets its holder do there and below: as the registry records it
/// (`registry::User::caps`).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Capability {
/// Segments, as the store's API takes them (`[]`: everything), `*` for any one key
/// (`agree`): a key that is `*` itself can't be named alone.
pub(crate) path: Vec<String>,
/// `read`, `write`. Others, from a later version (`grant`), are kept and give
/// nothing here.
pub(crate) rights: BTreeSet<String>,
/// Who granted it: a user (`user:<name>`), or for those the genesis node records, the
/// node (its node ID), kept with the capability.
#[serde(default)]
pub(crate) by: String,
/// When it was granted (Unix ms).
#[serde(default)]
pub(crate) since: u64,
}
impl Capability {
/// Whether it lets its holder do `right` at `path`: its path is a prefix of `path`
/// (segment by segment, `*` for any key) and it gives `right`. One that isn't within
/// protocol data (`_continuum`) never covers it, a `*` first neither: operators are
/// those given protocol data itself.
pub(crate) fn covers(&self, right: Right, path: &[String]) -> bool {
self.gives(right)
&& path.len() >= self.path.len()
&& agree(&self.path, path)
&& (meta::is_protocol(&self.path) || !meta::is_protocol(path))
}
/// Whether `path` leads to a place where it lets its holder do `right`: its path
/// lies below `path` (or is it, or above it). What a listing of `path` shows of a
/// tree its holder may only partly read (`Caps::reaches`).
pub(crate) fn reaches(&self, right: Right, path: &[String]) -> bool {
self.gives(right)
&& agree(&self.path, path)
&& (meta::is_protocol(&self.path) || !meta::is_protocol(path))
}
/// Whether `inner` gives nothing it doesn't: at or below its path (a `*` of `inner`
/// only under one of its own), with some of its rights. Of limits narrowing again.
pub(crate) fn holds(&self, inner: &Capability) -> bool {
at_or_below(&inner.path, &self.path) && inner.rights.is_subset(&self.rights)
}
fn gives(&self, right: Right) -> bool {
self.rights.contains(right.as_str())
}
}
/// Everything a user may do: its capabilities, as its registry entry has them; none if
/// it has none, or is disabled. Narrowed by a token that says less (`limit`,
/// `token::Claims::limit`): then only what both allow.
#[derive(Clone, Debug, Default, PartialEq, Eq)]
pub(crate) struct Caps {
caps: Vec<Capability>,
limit: Option<Vec<Capability>>,
}
impl Caps {
pub(crate) fn new(caps: Vec<Capability>) -> Self {
Self { caps, limit: None }
}
/// These, only where `limit` allows too.
pub(crate) fn within(mut self, limit: Vec<Capability>) -> Self {
self.limit = Some(match self.limit.take() {
// Narrowed twice: the earlier limit's capabilities that lie within the new
// one's (less than what both allow, where one only partly covers the
// other: never more).
Some(mut earlier) => {
earlier.retain(|cap| limit.iter().any(|outer| outer.holds(cap)));
earlier
}
None => limit,
});
self
}
/// Whether one of them lets its holder do `right` at `path` (and the limit too).
pub(crate) fn allows(&self, right: Right, path: &[String]) -> bool {
self.caps.iter().any(|cap| cap.covers(right, path))
&& self
.limit
.as_ref()
.is_none_or(|limit| limit.iter().any(|cap| cap.covers(right, path)))
}
/// Whether `path` leads to a place where one of them lets its holder do `right`: a
/// listing of `path` shows the way there, as a directory one may pass through but not
/// list does (`Capability::reaches`).
pub(crate) fn reaches(&self, right: Right, path: &[String]) -> bool {
self.caps.iter().any(|cap| cap.reaches(right, path))
&& self
.limit
.as_ref()
.is_none_or(|limit| limit.iter().any(|cap| cap.reaches(right, path)))
}
pub(crate) fn is_empty(&self) -> bool {
self.caps.is_empty() || self.limit.as_ref().is_some_and(Vec::is_empty)
}
}
#[cfg(test)]
mod tests {
use super::*;
fn path(text: &str) -> Vec<String> {
text.split('/')
.filter(|segment| !segment.is_empty())
.map(str::to_owned)
.collect()
}
fn cap(at: &str, rights: &[&str]) -> Capability {
Capability {
path: path(at),
rights: rights.iter().map(|right| (*right).to_owned()).collect(),
by: String::new(),
since: 0,
}
}
#[test]
fn a_capability_covers_its_path_and_below() {
let servers = cap("servers/eu", &["read"]);
assert!(servers.covers(Right::Read, &path("servers/eu")));
assert!(servers.covers(Right::Read, &path("servers/eu/0/name")));
// By segment, not by text.
assert!(!servers.covers(Right::Read, &path("servers/europe")));
assert!(!servers.covers(Right::Read, &path("servers")));
assert!(!servers.covers(Right::Write, &path("servers/eu")));
}
#[test]
fn protocol_data_only_by_capabilities_within_it() {
let everything = cap("", &["read", "write"]);
assert!(everything.covers(Right::Write, &path("accounts/1")));
assert!(everything.covers(Right::Read, &[]));
assert!(!everything.covers(Right::Read, &path("_continuum")));
assert!(!everything.covers(Right::Write, &path("_continuum/users/alice")));
let operator = cap("_continuum", &["read", "write"]);
assert!(operator.covers(Right::Write, &path("_continuum/users/alice")));
assert!(!operator.covers(Right::Read, &path("accounts")));
let remakes = cap("_continuum/remakes", &["write"]);
assert!(remakes.covers(Right::Write, &path("_continuum/remakes/r1")));
assert!(!remakes.covers(Right::Write, &path("_continuum/users")));
}
#[test]
fn rights_from_later_versions_give_nothing() {
let later = cap("", &["grant"]);
assert!(!later.covers(Right::Read, &path("x")));
assert!(!later.covers(Right::Write, &path("x")));
}
#[test]
fn a_limit_narrows() {
let caps =
Caps::new(vec![cap("", &["read", "write"])]).within(vec![cap("notes", &["read"])]);
assert!(caps.allows(Right::Read, &path("notes/a")));
assert!(!caps.allows(Right::Write, &path("notes/a")));
assert!(!caps.allows(Right::Read, &path("logs")));
assert!(caps.reaches(Right::Read, &[]));
// Never wider than what the user has.
let narrow =
Caps::new(vec![cap("notes", &["read"])]).within(vec![cap("", &["read", "write"])]);
assert!(!narrow.allows(Right::Write, &path("notes")));
assert!(!narrow.allows(Right::Read, &path("logs")));
// Narrowed again: within both.
let twice = caps.within(vec![cap("notes/a", &["read", "write"])]);
assert!(!twice.allows(Right::Read, &path("notes/a")));
assert!(
Caps::new(vec![cap("", &["read"])])
.within(Vec::new())
.is_empty()
);
}
#[test]
fn a_listing_shows_the_way_to_what_may_be_read() {
let caps = Caps::new(vec![cap("servers/eu", &["read"])]);
// The way there, as a directory passed through.
assert!(caps.reaches(Right::Read, &[]));
assert!(caps.reaches(Right::Read, &path("servers")));
// There and below.
assert!(caps.reaches(Right::Read, &path("servers/eu")));
assert!(caps.reaches(Right::Read, &path("servers/eu/0")));
// Elsewhere, nothing.
assert!(!caps.reaches(Right::Read, &path("servers/us")));
assert!(!caps.reaches(Right::Read, &path("accounts")));
assert!(!caps.allows(Right::Read, &path("servers")));
// A capability on everything doesn't lead into protocol data.
let everything = Caps::new(vec![cap("", &["read"])]);
assert!(everything.reaches(Right::Read, &[]));
assert!(!everything.reaches(Right::Read, &path("_continuum")));
let members = Caps::new(vec![cap("_continuum/members", &["read"])]);
assert!(members.reaches(Right::Read, &[]));
assert!(members.reaches(Right::Read, &path("_continuum")));
assert!(!members.reaches(Right::Read, &path("_continuum/users")));
}
/// `*` stands for any one key: in covering, in listings, and in narrowing.
#[test]
fn a_star_is_any_key() {
let logs = cap("_continuum/local/*/log", &["write"]);
assert!(logs.covers(Right::Write, &path("_continuum/local/n1/log")));
assert!(logs.covers(Right::Write, &path("_continuum/local/n2/log/x")));
assert!(!logs.covers(Right::Write, &path("_continuum/local/n1/config")));
assert!(!logs.covers(Right::Write, &path("_continuum/local/n1")));
// Listings: every key on the way, nothing past where the pattern differs.
assert!(logs.reaches(Right::Write, &path("_continuum/local")));
assert!(logs.reaches(Right::Write, &path("_continuum/local/n1")));
assert!(!logs.reaches(Right::Write, &path("_continuum/local/n1/config")));
// A `*` first never covers protocol data, as `[]` doesn't.
let any = cap("*", &["read"]);
assert!(any.covers(Right::Read, &path("accounts/1")));
assert!(!any.covers(Right::Read, &path("_continuum/users")));
assert!(!any.reaches(Right::Read, &path("_continuum")));
// Narrowing: a limit with a `*` takes any key there; a key never takes a `*`.
let all = cap("_continuum/local/*", &["read", "write"]);
assert!(all.holds(&cap("_continuum/local/n1/log", &["write"])));
assert!(all.holds(&cap("_continuum/local/*/log", &["write"])));
let one = cap("_continuum/local/n1", &["read", "write"]);
assert!(!one.holds(&cap("_continuum/local/*/log", &["write"])));
assert!(one.holds(&cap("_continuum/local/n1/log", &["write"])));
let caps = Caps::new(vec![all.clone()]).within(vec![one.clone()]);
assert!(caps.allows(Right::Write, &path("_continuum/local/n1/log")));
assert!(!caps.allows(Right::Write, &path("_continuum/local/n2/log")));
let twice = caps.within(vec![cap("_continuum/local/*/log", &["write"])]);
assert!(!twice.allows(Right::Write, &path("_continuum/local/n1/log")));
}
}
|