diff options
| -rw-r--r-- | docs/access.md | 6 | ||||
| -rw-r--r-- | docs/openapi.yaml | 2 | ||||
| -rw-r--r-- | src/access/mod.rs | 71 | ||||
| -rw-r--r-- | src/api/login.rs | 13 |
4 files changed, 89 insertions, 3 deletions
diff --git a/docs/access.md b/docs/access.md index 1397656..571034e 100644 --- a/docs/access.md +++ b/docs/access.md @@ -177,8 +177,10 @@ forgets it. The login page token pasted, from a login by key, say; the dashboard renews it while open ([`dashboard.md`](dashboard.md)). -`GET /auth/whoami` says the swarm, who a request's user is, how it proved it, and what -it may do. +`GET /auth/whoami` says the swarm, who a request's user is, how it proved it, its +capabilities (`caps`), and what this request may do (`may`): those, or with a token +that says less (`limit`), what both give, each capability's path the deeper of the two +and its rights those both name. Pages of other origins, the dashboards of other members, may call a node with a token: its own links (`/admin/dashboard/links/own`, and its events) and messages diff --git a/docs/openapi.yaml b/docs/openapi.yaml index adee4a6..1b4a199 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -1031,7 +1031,7 @@ paths: get: tags: [Auth] summary: Who you are here - description: The swarm, the user, how it proved it (`htpasswd`, `header`, `token`), its registry entry's capabilities, and its token's claims if it came with one. + description: The swarm, the user, how it proved it (`htpasswd`, `header`, `token`), its registry entry's capabilities (`caps`), what this request may do (`may`, a list of capabilities; with a token that says less, what both give), and its token's claims if it came with one. responses: '200': description: Who. 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() { diff --git a/src/api/login.rs b/src/api/login.rs index 0dc74e5..694ad0a 100644 --- a/src/api/login.rs +++ b/src/api/login.rs @@ -715,6 +715,18 @@ pub(crate) async fn whoami( let swarm = state.swarm_id().await; let replica = state.doc.read().await; let entry = registry::user(&replica.doc, &user.0); + // What the request may do: the user's capabilities, and with a token saying less, + // what both give (`Capability::meet`), as `api::access::authorize` decides. + let may = entry.as_ref().map(|entry| { + match token.as_ref().and_then(|token| token.claims.limit.as_ref()) { + None => entry.caps.values().cloned().collect::<Vec<_>>(), + Some(limit) => entry + .caps + .values() + .flat_map(|cap| limit.iter().filter_map(|outer| cap.meet(outer))) + .collect(), + } + }); let token = token.map(|Extension(token)| { json!({ "issuer": token.claims.issuer, @@ -732,6 +744,7 @@ pub(crate) async fn whoami( "via": via.0, "registry": registry::exists(&replica.doc), "caps": entry.as_ref().map(|entry| &entry.caps), + "may": may, "disabled": entry.as_ref().map(|entry| entry.disabled), "token": token, })) |
