//! 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, /// `read`, `write`. Others, from a later version (`grant`), are kept and give /// nothing here. pub(crate) rights: BTreeSet, /// Who granted it: a user (`user:`), 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, limit: Option>, } impl Caps { pub(crate) fn new(caps: Vec) -> Self { Self { caps, limit: None } } /// These, only where `limit` allows too. pub(crate) fn within(mut self, limit: Vec) -> 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 { 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"))); } }