From 8a6ed7cc711e797542a531091b3bdbc73e5f61e4 Mon Sep 17 00:00:00 2001 From: tv Date: Mon, 9 Nov 2026 02:24:51 +0000 Subject: dashboard: keys edited, the last removed with a password, failures kept The credentials dialog listed a user's keys whole, to add one or remove one but the last, and any failure replaced the dialog with the error, the form lost. * A key is shown short: its type, the first and last four characters of what tells it apart (an Ed25519 key's base64 after the part all of them share), its comment; the whole line in its tooltip. * A key can be edited, its name and its line: saved under the name, a new name written first and the old one removed. * A user with a password on this node (`--htpasswd`) may remove its last key: it logs in by the password. GET /auth/keys says so (`password`), and DELETE /auth/keys/{id} takes it then; the rules no longer keep the last key on every node (no other node can tell who has a password where; under a strict policy the signed entry keeps one still). * What fails says why beside where it was asked (the add form, the edited row, the removed one), which stays as it was, to be set right and tried again. --- src/access/rules.rs | 31 ++++--------------------------- 1 file changed, 4 insertions(+), 27 deletions(-) (limited to 'src/access') diff --git a/src/access/rules.rs b/src/access/rules.rs index ac7377f..d8162a6 100644 --- a/src/access/rules.rs +++ b/src/access/rules.rs @@ -279,13 +279,14 @@ pub(crate) fn judge_reading( { true } - // A user's own keys (`/auth/keys`), whatever else it may do, one left at - // least. + // A user's own keys (`/auth/keys`), whatever else it may do; its last too + // (the node it asks keeps that from one without a password there, which no + // other node can tell). (Some(meta::USERS), Some(owner)) if path.get(3).map(String::as_str) == Some("keys") && change.user == Some(owner) => { - keys_left(&at, owner, change.ops) + true } // A node forgotten while alive cancels its own record (`swarm::peer::forget`). (Some(meta::FORGOTTEN), Some(owner)) => owner == node || user_may(path), @@ -531,30 +532,6 @@ fn made_the_epoch(at: &At<'_>, epoch: Epoch, id: &str, node: &str) -> bool { == Some(node) } -/// Whether `owner` has a key left after `ops`, its keys at `at` as they change them. -fn keys_left(at: &At<'_>, owner: &str, ops: &[Op]) -> bool { - let keys = meta::path(&[meta::USERS, owner, "keys"]); - let mut left: BTreeSet = store::keys_at(at, &keys) - .ok() - .flatten() - .unwrap_or_default() - .into_iter() - .collect(); - for op in ops.iter().filter(|op| op.path.starts_with(&keys)) { - match (op.path.get(keys.len()), &op.action) { - (None, _) => left.clear(), - (Some(id), Action::Delete) => { - left.remove(id); - } - (Some(id), _) if op.path.len() == keys.len() + 1 => { - left.insert(id.clone()); - } - _ => {} - } - } - !left.is_empty() -} - /// The coordinator remake `id`'s proposal names, at `at`. fn coordinator(at: &At<'_>, id: &str) -> Option { let proposal = store::read_path(at, &meta::path(&[meta::REMAKES, id, "proposal"])) -- cgit v1.3.1