From eed8f86912f53f549d9f9068f9bdf2c8e23aa0f7 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 7 Nov 2026 22:35:09 +0100 Subject: access: tokens ct1 tokens: who, how they logged in, a session, when they expire, and optionally a narrower limit; signed by the issuing node's key and taken by every member (a node ID is its key). Revocation by the registry as it stands when a request comes. Auth plan M2. --- src/access/mod.rs | 69 +++++++++++++++++++++++++++++++++++++++++++++++++------ 1 file changed, 62 insertions(+), 7 deletions(-) (limited to 'src/access/mod.rs') diff --git a/src/access/mod.rs b/src/access/mod.rs index 8961312..83a47b5 100644 --- a/src/access/mod.rs +++ b/src/access/mod.rs @@ -14,6 +14,7 @@ use serde::{Deserialize, Serialize}; use crate::document::meta; pub(crate) mod registry; +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)] @@ -75,29 +76,60 @@ impl Capability { } /// Everything a user may do: its capabilities, as its registry entry has them; none if -/// it has none, or is disabled. +/// 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(Vec); +pub(crate) struct Caps { + caps: Vec, + limit: Option>, +} impl Caps { pub(crate) fn new(caps: Vec) -> Self { - Self(caps) + 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`. + /// 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.0.iter().any(|cap| cap.covers(right, path)) + 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.0.iter().any(|cap| cap.reaches(right, path)) + 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.0.is_empty() + self.caps.is_empty() || self.limit.as_ref().is_some_and(Vec::is_empty) } } @@ -154,6 +186,29 @@ mod tests { 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"])]); -- cgit v1.3.1