//! 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 registry; /// 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. #[derive(Clone, Debug, Default, PartialEq, Eq)] pub(crate) struct Caps(Vec); impl Caps { pub(crate) fn new(caps: Vec) -> Self { Self(caps) } /// Whether one of them lets its holder do `right` at `path`. pub(crate) fn allows(&self, right: Right, path: &[String]) -> bool { self.0.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.0.iter().any(|cap| cap.reaches(right, path)) } pub(crate) fn is_empty(&self) -> bool { self.0.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_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"))); } }