aboutsummaryrefslogtreecommitdiffstats
path: root/src/access
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-09 17:35:12 +0000
committertv <tv@krebsco.de>2026-11-09 19:10:26 +0100
commit5f7e29cec4333715346b4bf3195633db93449d9d (patch)
tree3574cb120e01c4272452666aeb518ac0d24e8e90 /src/access
parent3bf93bc192bf362b58b2161ac05da0bb8323bf43 (diff)
auth: whoami says what the request may do
`GET /auth/whoami` lists the user's capabilities (`caps`) and its token's `limit`, if any, side by side; what the request may do is what both give, as endpoints decide it. It says so now (`may`): the capabilities, or with a limit each capability met with each of the limit's (`Capability::meet`): at the deeper of the two paths, a `*` of one taking the other's key there, with the rights both name; nothing where the paths part, no right is shared, or the path is protocol data one of them doesn't name.
Diffstat (limited to 'src/access')
-rw-r--r--src/access/mod.rs71
1 files changed, 71 insertions, 0 deletions
diff --git a/src/access/mod.rs b/src/access/mod.rs
index e223756..e04be02 100644
--- a/src/access/mod.rs
+++ b/src/access/mod.rs
@@ -97,6 +97,41 @@ impl Capability {
&& (meta::is_protocol(&self.path) || !meta::is_protocol(path))
}
+ /// What both `self` and `other` give, as one capability, if anything: at the path
+ /// both cover (the longer, a `*` of one taking the other's key there), with the
+ /// rights both give. Nothing where the paths part, no right is shared, or the path
+ /// is protocol data that one of them doesn't name (`covers`'s carve-out).
+ pub(crate) fn meet(&self, other: &Capability) -> Option<Capability> {
+ if !agree(&self.path, &other.path) {
+ return None;
+ }
+ let (longer, shorter) = if self.path.len() >= other.path.len() {
+ (&self.path, &other.path)
+ } else {
+ (&other.path, &self.path)
+ };
+ let path: Vec<String> = longer
+ .iter()
+ .enumerate()
+ .map(|(at, segment)| match shorter.get(at) {
+ Some(theirs) if segment == "*" => theirs.clone(),
+ _ => segment.clone(),
+ })
+ .collect();
+ if meta::is_protocol(&path)
+ && !(meta::is_protocol(&self.path) && meta::is_protocol(&other.path))
+ {
+ return None;
+ }
+ let rights: BTreeSet<String> = self.rights.intersection(&other.rights).cloned().collect();
+ (!rights.is_empty()).then(|| Capability {
+ path,
+ rights,
+ by: self.by.clone(),
+ since: self.since,
+ })
+ }
+
/// Whether `inner` gives nothing it doesn't: at or below its path (a `*` of `inner`
/// only under one of its own), with some of its rights. Of limits narrowing again.
pub(crate) fn holds(&self, inner: &Capability) -> bool {
@@ -261,6 +296,42 @@ mod tests {
assert!(!members.reaches(Right::Read, &path("_continuum/users")));
}
+ /// What two capabilities both give: the deeper path, shared rights, protocol data
+ /// only where both name it.
+ #[test]
+ fn what_both_give() {
+ let both = cap("data/*/x", &["read", "write"])
+ .meet(&cap("data/eu", &["read"]))
+ .unwrap();
+ assert_eq!(
+ (both.path, both.rights),
+ (path("data/eu/x"), ["read".to_owned()].into())
+ );
+ assert!(
+ cap("data/us", &["read"])
+ .meet(&cap("data/eu", &["read"]))
+ .is_none()
+ );
+ assert!(
+ cap("data", &["write"])
+ .meet(&cap("data/eu", &["read"]))
+ .is_none()
+ );
+ // `[]` never covers protocol data; `_continuum` meets `_continuum/members`.
+ assert!(
+ cap("", &["read"])
+ .meet(&cap("_continuum/members", &["read"]))
+ .is_none()
+ );
+ assert_eq!(
+ cap("_continuum", &["read"])
+ .meet(&cap("_continuum/members", &["read"]))
+ .unwrap()
+ .path,
+ path("_continuum/members")
+ );
+ }
+
/// `*` stands for any one key: in covering, in listings, and in narrowing.
#[test]
fn a_star_is_any_key() {