aboutsummaryrefslogtreecommitdiffstats
path: root/src
diff options
context:
space:
mode:
Diffstat (limited to 'src')
-rw-r--r--src/access/envelope.rs10
-rw-r--r--src/access/mod.rs74
-rw-r--r--src/access/sealed.rs9
-rw-r--r--src/api/login.rs8
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.