aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--docs/access.md6
-rw-r--r--docs/openapi.yaml2
-rw-r--r--src/access/mod.rs71
-rw-r--r--src/api/login.rs13
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,
}))