aboutsummaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--src/api/access.rs81
1 files changed, 62 insertions, 19 deletions
diff --git a/src/api/access.rs b/src/api/access.rs
index 8737ef8..11daf83 100644
--- a/src/api/access.rs
+++ b/src/api/access.rs
@@ -287,8 +287,21 @@ fn needed(method: &Method, path: &str, node: &str) -> Need {
}
}
-/// Finds what the request's user may do, refuses the request if its endpoint needs
-/// more (`needed`), and leaves `Access` on it for the store's handlers. Inside
+/// Whether `access` (a token's limit applied) covers what an endpoint needs.
+fn allowed(need: Need, access: &Access) -> bool {
+ match need {
+ Need::Store | Need::Auth => true,
+ Need::Observe => match access {
+ Access::Everything => true,
+ Access::Caps(caps) => !caps.is_empty(),
+ },
+ Need::Protocol(right, path) => access.allows(right, &path),
+ }
+}
+
+/// Finds what the request's user may do, a token's limit applied, refuses the request
+/// if its endpoint needs more (`needed`), and leaves `Access` on it for the store's
+/// handlers. Inside
/// `auth::basic_auth`, which found the user. A user the registry doesn't have may do
/// nothing: `403`.
pub(crate) async fn authorize(
@@ -325,23 +338,7 @@ pub(crate) async fn authorize(
}
}
};
- let allowed = match needed(request.method(), request.uri().path(), state.local_id()) {
- Need::Store | Need::Auth => true,
- Need::Observe => match &access {
- Access::Everything => true,
- Access::Caps(caps) => !caps.is_empty(),
- },
- Need::Protocol(right, path) => access.allows(right, &path),
- };
- if !allowed {
- debug!(user = %user, method = %request.method(), path = %request.uri().path(), "Refused a request the user's capabilities don't cover");
- return error_response(
- StatusCode::FORBIDDEN,
- format!("{user} may not do that here (docs/access.md)"),
- None,
- );
- }
- // A token that says less: only what it allows too.
+ // A token that says less: only what it allows too, for deciding as for the handlers.
let access = match request
.extensions()
.get::<super::login::Token>()
@@ -350,6 +347,17 @@ pub(crate) async fn authorize(
None => access,
Some(limit) => Access::Caps(access.caps(&state, &user).within(limit)),
};
+ if !allowed(
+ needed(request.method(), request.uri().path(), state.local_id()),
+ &access,
+ ) {
+ debug!(user = %user, method = %request.method(), path = %request.uri().path(), "Refused a request the user's capabilities don't cover");
+ return error_response(
+ StatusCode::FORBIDDEN,
+ format!("{user} may not do that here (docs/access.md)"),
+ None,
+ );
+ }
request.extensions_mut().insert(access);
next.run(request).await
}
@@ -367,6 +375,41 @@ mod tests {
.collect()
}
+ /// A token's limit narrows what an endpoint takes, not only what the store's handlers
+ /// read: the dashboard's hand-over token (the member list, read) of an operator may
+ /// look, and nothing else.
+ #[test]
+ fn a_limit_narrows_endpoints() {
+ let operator = Caps::new(vec![Capability {
+ path: path("_continuum"),
+ rights: ["read".to_owned(), "write".to_owned()].into(),
+ by: String::new(),
+ since: 0,
+ }]);
+ let members = vec![Capability {
+ path: path("_continuum/members"),
+ rights: ["read".to_owned()].into(),
+ by: String::new(),
+ since: 0,
+ }];
+ let handed = Access::Caps(operator.clone().within(members));
+ let need = |method: &Method, at: &str| needed(method, at, "n1");
+ assert!(allowed(need(&Method::GET, "/admin/stats"), &handed));
+ assert!(allowed(need(&Method::GET, "/peers"), &handed));
+ for (method, at) in [
+ (Method::DELETE, "/peers/continuum:00"),
+ (Method::POST, "/admin/remakes"),
+ (Method::GET, "/admin/document"),
+ (Method::PUT, "/admin/log"),
+ ] {
+ assert!(!allowed(need(&method, at), &handed), "{method} {at}");
+ assert!(
+ allowed(need(&method, at), &Access::Caps(operator.clone())),
+ "{method} {at}"
+ );
+ }
+ }
+
fn caps(list: &[(&str, &[&str])]) -> Access {
Access::Caps(Caps::new(
list.iter()