//! 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 envelope; pub(crate) mod policy; pub(crate) mod registry; pub(crate) mod rules; pub(crate) mod screen; pub(crate) mod sshsig; 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, /// `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 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, 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| { 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 { 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"))); } }