diff options
| author | tv <tv@krebsco.de> | 2026-11-09 17:35:12 +0000 |
|---|---|---|
| committer | tv <tv@krebsco.de> | 2026-11-09 19:10:26 +0100 |
| commit | 5f7e29cec4333715346b4bf3195633db93449d9d (patch) | |
| tree | 3574cb120e01c4272452666aeb518ac0d24e8e90 /src/access/mod.rs | |
| parent | 3bf93bc192bf362b58b2161ac05da0bb8323bf43 (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/mod.rs')
| -rw-r--r-- | src/access/mod.rs | 71 |
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() { |
