diff options
Diffstat (limited to 'src')
| -rw-r--r-- | src/access/rules.rs | 2 | ||||
| -rw-r--r-- | src/api/access.rs | 86 | ||||
| -rw-r--r-- | src/api/store.rs | 11 | ||||
| -rw-r--r-- | src/document/meta.rs | 4 |
4 files changed, 76 insertions, 27 deletions
diff --git a/src/access/rules.rs b/src/access/rules.rs index d8162a6..d120961 100644 --- a/src/access/rules.rs +++ b/src/access/rules.rs @@ -265,6 +265,8 @@ pub(crate) fn judge_reading( } // Written by a root only. (Some(meta::EPOCHS | meta::SWARM), _) => false, + // Never stored: named for capabilities only (`api::access::needed`). + (Some(meta::LOCAL), _) => false, // Logging out: a user revokes its own tokens (`POST /auth/logout`); one who // manages users, anyone's (`DELETE /admin/users/{name}/tokens`). (Some(meta::LOGOUTS), Some(owner)) => { diff --git a/src/api/access.rs b/src/api/access.rs index 29d796a..8737ef8 100644 --- a/src/api/access.rs +++ b/src/api/access.rs @@ -247,10 +247,12 @@ enum Need { /// `_continuum/remakes`, forgetting a peer on `_continuum/forgotten`; everything else /// this node's operators do (its log filter, exports, quarantine, the changes) needs /// protocol data itself, reading it to look, writing it to act. -fn needed(method: &Method, path: &str) -> Need { +fn needed(method: &Method, path: &str, node: &str) -> Need { let looking = method == Method::GET || method == Method::HEAD; let protocol = |right, parts: &[&str]| Need::Protocol(right, meta::path(parts)); let under = |prefix: &str| path == prefix || path.starts_with(&format!("{prefix}/")); + // What this node holds alone: `_continuum/local/<node>/<name>`, never stored. + let local = |right, name: &str| protocol(right, &[meta::LOCAL, node, name]); if under("/store") || under("/stream") { Need::Store } else if under("/auth") { @@ -273,7 +275,12 @@ fn needed(method: &Method, path: &str) -> Need { } else { protocol(Right::Write, &[meta::REMAKES]) } - } else if looking && !under("/debug") { + } else if under("/admin/log") || under("/admin/config") { + let name = if under("/admin/log") { "log" } else { "config" }; + local(if looking { Right::Read } else { Right::Write }, name) + } else if under("/debug") { + local(Right::Write, "debug") + } else if looking { protocol(Right::Read, &[]) } else { protocol(Right::Write, &[]) @@ -318,7 +325,7 @@ pub(crate) async fn authorize( } } }; - let allowed = match needed(request.method(), request.uri().path()) { + let allowed = match needed(request.method(), request.uri().path(), state.local_id()) { Need::Store | Need::Auth => true, Need::Observe => match &access { Access::Everything => true, @@ -377,51 +384,82 @@ mod tests { fn endpoints_and_what_they_need() { let read = |parts: &[&str]| Need::Protocol(Right::Read, meta::path(parts)); let write = |parts: &[&str]| Need::Protocol(Right::Write, meta::path(parts)); - assert_eq!(needed(&Method::GET, "/store"), Need::Store); - assert_eq!(needed(&Method::POST, "/auth/login"), Need::Auth); - assert_eq!(needed(&Method::GET, "/auth/whoami"), Need::Auth); - assert_eq!(needed(&Method::PUT, "/store/a/b"), Need::Store); - assert_eq!(needed(&Method::GET, "/stream/a"), Need::Store); - assert_eq!(needed(&Method::GET, "/storehouse"), read(&[])); - assert_eq!(needed(&Method::GET, "/peers"), Need::Observe); - assert_eq!(needed(&Method::GET, "/admin/stats"), Need::Observe); + assert_eq!(needed(&Method::GET, "/store", "n1"), Need::Store); + assert_eq!(needed(&Method::POST, "/auth/login", "n1"), Need::Auth); + assert_eq!(needed(&Method::GET, "/auth/whoami", "n1"), Need::Auth); + assert_eq!(needed(&Method::PUT, "/store/a/b", "n1"), Need::Store); + assert_eq!(needed(&Method::GET, "/stream/a", "n1"), Need::Store); + assert_eq!(needed(&Method::GET, "/storehouse", "n1"), read(&[])); + assert_eq!(needed(&Method::GET, "/peers", "n1"), Need::Observe); + assert_eq!(needed(&Method::GET, "/admin/stats", "n1"), Need::Observe); assert_eq!( - needed(&Method::GET, "/admin/dashboard/state"), + needed(&Method::GET, "/admin/dashboard/state", "n1"), Need::Observe ); assert_eq!( - needed(&Method::POST, "/admin/dashboard/refresh"), + needed(&Method::POST, "/admin/dashboard/refresh", "n1"), Need::Observe ); assert_eq!( - needed(&Method::DELETE, "/peers/continuum:00"), + needed(&Method::DELETE, "/peers/continuum:00", "n1"), write(&[meta::FORGOTTEN]) ); assert_eq!( - needed(&Method::GET, "/admin/remakes/r1"), + needed(&Method::GET, "/admin/remakes/r1", "n1"), read(&[meta::REMAKES]) ); assert_eq!( - needed(&Method::POST, "/admin/remakes"), + needed(&Method::POST, "/admin/remakes", "n1"), write(&[meta::REMAKES]) ); assert_eq!( - needed(&Method::DELETE, "/admin/remakes/r1"), + needed(&Method::DELETE, "/admin/remakes/r1", "n1"), write(&[meta::REMAKES]) ); - assert_eq!(needed(&Method::GET, "/admin/config"), read(&[])); assert_eq!( - needed(&Method::GET, "/admin/users/alice"), + needed(&Method::GET, "/admin/config", "n1"), + read(&[meta::LOCAL, "n1", "config"]) + ); + assert_eq!( + needed(&Method::GET, "/admin/users/alice", "n1"), read(&[meta::USERS]) ); - assert_eq!(needed(&Method::POST, "/admin/users"), write(&[meta::USERS])); - assert_eq!(needed(&Method::GET, "/admin/quarantine/q"), read(&[])); assert_eq!( - needed(&Method::POST, "/admin/quarantine/q/replay"), + needed(&Method::POST, "/admin/users", "n1"), + write(&[meta::USERS]) + ); + assert_eq!(needed(&Method::GET, "/admin/quarantine/q", "n1"), read(&[])); + assert_eq!( + needed(&Method::POST, "/admin/quarantine/q/replay", "n1"), write(&[]) ); - assert_eq!(needed(&Method::PUT, "/admin/log"), write(&[])); - assert_eq!(needed(&Method::GET, "/debug/firewall"), write(&[])); + assert_eq!( + needed(&Method::GET, "/admin/log", "n1"), + read(&[meta::LOCAL, "n1", "log"]) + ); + assert_eq!( + needed(&Method::PUT, "/admin/log", "n1"), + write(&[meta::LOCAL, "n1", "log"]) + ); + assert_eq!( + needed(&Method::GET, "/debug/firewall", "n1"), + write(&[meta::LOCAL, "n1", "debug"]) + ); + // Every node's log filter, by one capability. + let logs = Caps::new(vec![Capability { + path: path("_continuum/local/*/log"), + rights: ["write".to_owned()].into(), + by: String::new(), + since: 0, + }]); + let Need::Protocol(right, at) = needed(&Method::PUT, "/admin/log", "n2") else { + unreachable!() + }; + assert!(logs.allows(right, &at)); + let Need::Protocol(right, at) = needed(&Method::GET, "/admin/config", "n2") else { + unreachable!() + }; + assert!(!logs.allows(right, &at)); } #[test] diff --git a/src/api/store.rs b/src/api/store.rs index e43d129..00ced48 100644 --- a/src/api/store.rs +++ b/src/api/store.rs @@ -662,7 +662,8 @@ fn parse_key_path(key: &str) -> Result<Vec<String>, StatusCode> { /// what every node acts on, so a write there acts on the whole swarm, and each is /// logged. Epoch metadata (and `_continuum` as a whole, which contains it) is never /// writable: the protocol reads it at the snapshot change, so an edit only misleads -/// whoever reads it later; nor is the swarm ID. A path inside a list or text is +/// whoever reads it later; nor is the swarm ID, nor `_continuum/local`, which names +/// what a node holds alone for capabilities and is never stored. A path inside a list or text is /// judged where the rules judge the write (`inspect::placed`): at the list. async fn parse_write_path( state: &AppState, @@ -674,8 +675,12 @@ async fn parse_write_path( let path = parse_key_path(key)?; let placed = crate::document::inspect::placed(&state.doc.read().await.doc, &path); let protocol = meta::is_protocol(&path); - // `_continuum` itself, epoch metadata and the swarm ID: nobody. - let fixed = protocol && (path.len() < 2 || path[1] == meta::EPOCHS || path[1] == meta::SWARM); + // `_continuum` itself, epoch metadata, the swarm ID and what is never stored: nobody. + let fixed = protocol + && (path.len() < 2 + || path[1] == meta::EPOCHS + || path[1] == meta::SWARM + || path[1] == meta::LOCAL); let allowed = match access { Access::Everything => !protocol || state.protocol_writers.contains(&user.0), Access::Caps(caps) => caps.allows(crate::access::Right::Write, &placed), diff --git a/src/document/meta.rs b/src/document/meta.rs index 691cf40..e040ba0 100644 --- a/src/document/meta.rs +++ b/src/document/meta.rs @@ -21,6 +21,9 @@ //! * `access/userSigned`: the paths whose values their users sign (`access::envelope`). //! * `banned/<pk>`: banned nodes (`access::bans`). //! * `logouts/<name>`: when a user's tokens were last revoked (`access::registry`). +//! * `local/<pk>/<name>`: never stored, and never writable: what a node holds alone +//! (its log filter, its configuration, its debug endpoints), named for capabilities +//! (`api::access::needed`). /// The key every protocol path starts with. pub(crate) const ROOT: &str = "_continuum"; @@ -38,6 +41,7 @@ pub(crate) const USERS: &str = "users"; pub(crate) const ACCESS: &str = "access"; pub(crate) const BANNED: &str = "banned"; pub(crate) const LOGOUTS: &str = "logouts"; +pub(crate) const LOCAL: &str = "local"; /// The store path `_continuum/<parts…>`. pub(crate) fn path(parts: &[&str]) -> Vec<String> { |
