aboutsummaryrefslogtreecommitdiffstats
path: root/src/api/access.rs
diff options
context:
space:
mode:
authortv <tv@krebsco.de>2026-11-07 22:35:09 +0100
committertv <tv@krebsco.de>2026-11-07 22:35:09 +0100
commitec70060ae5fa665953741da921e28031b56bc5e0 (patch)
treecd1f6f5ae90d3ad87b9667da20d6e85b62394ab1 /src/api/access.rs
parenteed8f86912f53f549d9f9068f9bdf2c8e23aa0f7 (diff)
auth: logging in, and tokens as bearers
POST /auth/login (by htpasswd or the proxy), /auth/refresh, /auth/logout, GET /auth/whoami; every request may bring Authorization: Bearer instead of a password. --token-lifetime and --session-lifetime. Auth plan M2.
Diffstat (limited to 'src/api/access.rs')
-rw-r--r--src/api/access.rs44
1 files changed, 43 insertions, 1 deletions
diff --git a/src/api/access.rs b/src/api/access.rs
index 63aa87b..29d796a 100644
--- a/src/api/access.rs
+++ b/src/api/access.rs
@@ -47,6 +47,33 @@ impl Access {
}
}
+ /// As capabilities: `Caps` as they are; in a swarm without a registry, what every
+ /// user may do there (the swarm's data, protocol data to read, and to write for
+ /// `--protocol-writers`), to narrow by a token's limit.
+ pub(crate) fn caps(&self, state: &AppState, user: &str) -> Caps {
+ use crate::access::Capability;
+ match self {
+ Self::Caps(caps) => caps.clone(),
+ Self::Everything => {
+ let protocol: &[&str] = if state.protocol_writers.contains(user) {
+ &["read", "write"]
+ } else {
+ &["read"]
+ };
+ let cap = |path: Vec<String>, rights: &[&str]| Capability {
+ path,
+ rights: rights.iter().map(|right| (*right).to_owned()).collect(),
+ by: String::new(),
+ since: 0,
+ };
+ Caps::new(vec![
+ cap(Vec::new(), &["read", "write"]),
+ cap(meta::path(&[]), protocol),
+ ])
+ }
+ }
+ }
+
pub(crate) fn visible(&self, path: &[String]) -> Visible {
match self {
Self::Everything => Visible::All,
@@ -209,6 +236,8 @@ enum Need {
Store,
/// Looking at this node (its figures, peers, dashboard): any capability.
Observe,
+ /// Logging in, refreshing, asking who one is: any registered user.
+ Auth,
/// A right on protocol data.
Protocol(Right, Vec<String>),
}
@@ -224,6 +253,8 @@ fn needed(method: &Method, path: &str) -> Need {
let under = |prefix: &str| path == prefix || path.starts_with(&format!("{prefix}/"));
if under("/store") || under("/stream") {
Need::Store
+ } else if under("/auth") {
+ Need::Auth
} else if (path == "/peers" && looking) || path == "/admin/stats" || under("/admin/dashboard") {
Need::Observe
} else if under("/peers") {
@@ -288,7 +319,7 @@ pub(crate) async fn authorize(
}
};
let allowed = match needed(request.method(), request.uri().path()) {
- Need::Store => true,
+ Need::Store | Need::Auth => true,
Need::Observe => match &access {
Access::Everything => true,
Access::Caps(caps) => !caps.is_empty(),
@@ -303,6 +334,15 @@ pub(crate) async fn authorize(
None,
);
}
+ // A token that says less: only what it allows too.
+ let access = match request
+ .extensions()
+ .get::<super::login::Token>()
+ .and_then(|token| token.claims.limit.clone())
+ {
+ None => access,
+ Some(limit) => Access::Caps(access.caps(&state, &user).within(limit)),
+ };
request.extensions_mut().insert(access);
next.run(request).await
}
@@ -338,6 +378,8 @@ mod tests {
let read = |parts: &[&str]| Need::Protocol(Right::Read, meta::path(parts));
let write = |parts: &[&str]| Need::Protocol(Right::Write, meta::path(parts));
assert_eq!(needed(&Method::GET, "/store"), Need::Store);
+ assert_eq!(needed(&Method::POST, "/auth/login"), Need::Auth);
+ assert_eq!(needed(&Method::GET, "/auth/whoami"), Need::Auth);
assert_eq!(needed(&Method::PUT, "/store/a/b"), Need::Store);
assert_eq!(needed(&Method::GET, "/stream/a"), Need::Store);
assert_eq!(needed(&Method::GET, "/storehouse"), read(&[]));