aboutsummaryrefslogtreecommitdiffstats
path: root/src/access/mod.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/access/mod.rs')
-rw-r--r--src/access/mod.rs74
1 files changed, 63 insertions, 11 deletions
diff --git a/src/access/mod.rs b/src/access/mod.rs
index feddff0..f3e5f80 100644
--- a/src/access/mod.rs
+++ b/src/access/mod.rs
@@ -1,6 +1,7 @@
//! 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
+//! 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)).
@@ -25,6 +26,21 @@ 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 {
@@ -45,7 +61,8 @@ impl Right {
/// (`registry::User::caps`).
#[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)]
pub(crate) struct Capability {
- /// Segments, as the store's API takes them (`[]`: everything).
+ /// 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.
@@ -62,11 +79,13 @@ pub(crate) struct Capability {
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.
+ /// (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.starts_with(&self.path)
+ && path.len() >= self.path.len()
+ && agree(&self.path, path)
&& (meta::is_protocol(&self.path) || !meta::is_protocol(path))
}
@@ -75,10 +94,16 @@ impl Capability {
/// 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))
+ && 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())
}
@@ -105,11 +130,7 @@ impl Caps {
// 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.retain(|cap| limit.iter().any(|outer| outer.holds(cap)));
earlier
}
None => limit,
@@ -240,4 +261,35 @@ mod tests {
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")));
+ }
}