diff options
| -rw-r--r-- | docs/access.md | 8 | ||||
| -rw-r--r-- | src/access/mod.rs | 5 | ||||
| -rw-r--r-- | src/api/access.rs | 24 |
3 files changed, 33 insertions, 4 deletions
diff --git a/docs/access.md b/docs/access.md index 571034e..5a1e04b 100644 --- a/docs/access.md +++ b/docs/access.md @@ -35,7 +35,8 @@ password hashes: every member reads the document). `authorized_keys` line has it (`"laptop": "ssh-ed25519 AAAA… alice@laptop"`; Ed25519 only; the comment, 256 bytes at most, is kept and checks nothing): what it signs values with (Values signed by their users, below) and logs in with. A user manages its own, whatever else it may do: `GET /auth/keys`, `PUT /auth/keys/<name>` - with the line, `DELETE /auth/keys/<name>`; under a strict policy, by signing its + with the line, `DELETE /auth/keys/<name>` (not with a token that has a `limit`); under a + strict policy, by signing its entry with only its keys changed (`continuum-signed key-add laptop ~/.ssh/id_ed25519.pub`, `key-remove laptop`), with a key it holds already. Every node takes that from the user itself, as from one who may manage users. Its last key @@ -124,10 +125,11 @@ curl -H "Authorization: Bearer ct1.…" http://other-node:3000/store refreshes it past its end. A dashboard opening another member's hands it one reading the member list only, for five minutes ([`dashboard.md`](dashboard.md)). * **Revoking**: a user disabled or removed is refused at once; all of a user's tokens, - by the user (`POST /auth/logout`) or by one who may manage users (`DELETE + by the user (`POST /auth/logout`, not with a token that has a `limit`) or by one who may manage users (`DELETE /admin/users/{name}/tokens`), which sets `_continuum/logouts/<name>`: tokens issued before aren't taken any more. One session's (`DELETE /auth/session`, the dashboard's - "log out"): `_continuum/sessions/<name>/<session>`, until when the session would have + "log out", with any token of the session, a narrowed one too, as a dashboard opened + from another member's holds): `_continuum/sessions/<name>/<session>`, until when the session would have lasted, written by the user or one who manages users; a remake's snapshot leaves it out once that has passed. Without a registry, the node ending it refuses the session's tokens itself, until it restarts; others take them until they expire. diff --git a/src/access/mod.rs b/src/access/mod.rs index e04be02..ad50c72 100644 --- a/src/access/mod.rs +++ b/src/access/mod.rs @@ -192,6 +192,11 @@ impl Caps { .is_none_or(|limit| limit.iter().any(|cap| cap.reaches(right, path))) } + /// Whether a token's limit narrows them (`within`). + pub(crate) fn limited(&self) -> bool { + self.limit.is_some() + } + pub(crate) fn is_empty(&self) -> bool { self.caps.is_empty() || self.limit.as_ref().is_some_and(Vec::is_empty) } diff --git a/src/api/access.rs b/src/api/access.rs index 6d26139..6977a0a 100644 --- a/src/api/access.rs +++ b/src/api/access.rs @@ -252,8 +252,13 @@ enum Need { Store, /// Looking at this node (its figures, peers, dashboard): any capability. Observe, - /// Logging in, refreshing, asking who one is: any registered user. + /// Logging in, refreshing, asking who one is, ending one's session: any registered + /// user. Auth, + /// Changing what one logs in with (one's keys), or ending every token one has: a + /// user not narrowed by a token's limit. A token handed over to read the member list + /// for five minutes adds no key that logs in as its user. + Credentials, /// A right on protocol data. Protocol(Right, Vec<String>), } @@ -271,6 +276,8 @@ fn needed(method: &Method, path: &str, node: &str) -> Need { let local = |right, name: &str| protocol(right, &[meta::LOCAL, node, name]); if under("/store") || under("/stream") { Need::Store + } else if (under("/auth/keys") && !looking) || path == "/auth/logout" { + Need::Credentials } else if under("/auth") { Need::Auth } else if (path == "/peers" && looking) || path == "/admin/stats" || under("/admin/dashboard") { @@ -307,6 +314,10 @@ fn needed(method: &Method, path: &str, node: &str) -> Need { fn allowed(need: Need, access: &Access) -> bool { match need { Need::Store | Need::Auth => true, + Need::Credentials => match access { + Access::Everything => true, + Access::Caps(caps) => !caps.limited(), + }, Need::Observe => match access { Access::Everything => true, Access::Caps(caps) => !caps.is_empty(), @@ -413,6 +424,17 @@ mod tests { assert!(allowed(need(&Method::GET, "/admin/stats"), &handed)); assert!(allowed(need(&Method::GET, "/peers"), &handed)); for (method, at) in [ + (Method::POST, "/auth/refresh"), + (Method::DELETE, "/auth/session"), + (Method::GET, "/auth/whoami"), + (Method::GET, "/auth/keys"), + ] { + assert!(allowed(need(&method, at), &handed), "{method} {at}"); + } + for (method, at) in [ + (Method::PUT, "/auth/keys/laptop"), + (Method::DELETE, "/auth/keys/laptop"), + (Method::POST, "/auth/logout"), (Method::DELETE, "/peers/continuum:00"), (Method::POST, "/admin/remakes"), (Method::GET, "/admin/document"), |
