diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/access/envelope.rs | 10 | ||||
| -rw-r--r-- | src/access/mod.rs | 74 | ||||
| -rw-r--r-- | src/access/sealed.rs | 9 | ||||
| -rw-r--r-- | src/api/login.rs | 8 |
4 files changed, 68 insertions, 33 deletions
diff --git a/src/access/envelope.rs b/src/access/envelope.rs index e99a06e..5b1b368 100644 --- a/src/access/envelope.rs +++ b/src/access/envelope.rs @@ -18,7 +18,7 @@ use serde::{Deserialize, Serialize}; use serde_json::Value as JsonValue; use sha2::{Digest, Sha256}; -use super::{Right, registry, sshsig}; +use super::{Right, agree, registry, sshsig}; use crate::document::{ReadDoc, meta, store}; /// What a signature over a payload is for (`sshsig`). @@ -97,14 +97,6 @@ pub(crate) fn reaches_signed(signed: &[Vec<String>], path: &[String]) -> bool { signed.iter().any(|pattern| agree(pattern, path)) } -/// Whether `pattern` and `path` agree as far as both go (`*`: any key). -fn agree(pattern: &[String], path: &[String]) -> bool { - pattern - .iter() - .zip(path) - .all(|(segment, key)| segment == "*" || segment == key) -} - /// A signed value's payload. #[derive(Debug, Clone, PartialEq, Serialize, Deserialize)] pub(crate) struct Payload { 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"))); + } } diff --git a/src/access/sealed.rs b/src/access/sealed.rs index ed5024c..7e48e34 100644 --- a/src/access/sealed.rs +++ b/src/access/sealed.rs @@ -24,7 +24,7 @@ use base64::{ use serde_json::Value as JsonValue; use sha2::{Digest, Sha256}; -use super::{envelope, registry, sshsig}; +use super::{agree, envelope, registry, sshsig}; use crate::document::{ReadDoc, meta, store}; /// What every sealed value starts with. @@ -90,13 +90,6 @@ fn reaches(declared: &[Declared], path: &[String]) -> bool { declared.iter().any(|declared| agree(&declared.path, path)) } -fn agree(pattern: &[String], path: &[String]) -> bool { - pattern - .iter() - .zip(path) - .all(|(segment, key)| segment == "*" || segment == key) -} - /// How an age file names `key` as a recipient: the first four bytes of the SHA-256 of /// its SSH encoding, base64, unpadded. pub(crate) fn tag(key: &sshsig::PublicKey) -> String { diff --git a/src/api/login.rs b/src/api/login.rs index e3f8d02..cc70c18 100644 --- a/src/api/login.rs +++ b/src/api/login.rs @@ -163,11 +163,9 @@ impl LimitRequest { /// `limit` as a token may say it, within `earlier`'s (a refreshed one says no more): each /// of its capabilities at or below one of `earlier`'s, with some of its rights. fn within(limit: &[Capability], earlier: &[Capability]) -> bool { - limit.iter().all(|cap| { - earlier - .iter() - .any(|outer| cap.path.starts_with(&outer.path) && cap.rights.is_subset(&outer.rights)) - }) + limit + .iter() + .all(|cap| earlier.iter().any(|outer| outer.holds(cap))) } /// `claims` (issued now, by this node), signed, as the answer to a login or refresh. |
