aboutsummaryrefslogtreecommitdiffstats
path: root/src/access/mod.rs
blob: feddff0555a646fd59f2b146de90bf52cf598e0e (plain)
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
//! 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`). 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;

/// 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).
    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 so that granting on, by delegation, can come later
    /// without a new format (`future-work.md`, Authentication across nodes).
    #[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) and it gives `right`. One that isn't within protocol data
    /// (`_continuum`) never covers it: operators are those given protocol data itself.
    pub(crate) fn covers(&self, right: Right, path: &[String]) -> bool {
        self.gives(right)
            && path.starts_with(&self.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)
            && (self.path.starts_with(path) || self.covers(right, path))
            && (meta::is_protocol(&self.path) || !meta::is_protocol(path))
    }

    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| {
                        cap.path.starts_with(&outer.path) && cap.rights.is_subset(&outer.rights)
                    })
                });
                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")));
    }
}