aboutsummaryrefslogtreecommitdiffstats
path: root/src/access/mod.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 22:35:09 +0100
committertv <tv@krebsco.de>2026-11-07 22:35:09 +0100
commiteed8f86912f53f549d9f9068f9bdf2c8e23aa0f7 (patch)
treee60c309a16f479bdb684f9307b148604c9ec45dd /src/access/mod.rs
parentde12c862b3f4e69290c8e93b3b4520024ba458f6 (diff)
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.
Diffstat (limited to 'src/access/mod.rs')
-rw-r--r--src/access/mod.rs69
1 files changed, 62 insertions, 7 deletions
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<Capability>);
+pub(crate) struct Caps {
+ caps: Vec<Capability>,
+ limit: Option<Vec<Capability>>,
+}
impl Caps {
pub(crate) fn new(caps: Vec<Capability>) -> Self {
- Self(caps)
+ Self { caps, limit: None }
+ }
+
+ /// These, only where `limit` allows too.
+ pub(crate) fn within(mut self, limit: Vec<Capability>) -> 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)
}
}
@@ -155,6 +187,29 @@ mod tests {
}
#[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.