From de12c862b3f4e69290c8e93b3b4520024ba458f6 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 7 Nov 2026 22:35:09 +0100 Subject: access: the users endpoints /admin/users: list, add (one or several), remove, disable, grant and revoke capabilities; each one change, refused if it would lock its writer out. Capabilities as the store's paths name them. Auth plan M1. --- src/api/access.rs | 79 ++++++++++++++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 78 insertions(+), 1 deletion(-) (limited to 'src/api/access.rs') diff --git a/src/api/access.rs b/src/api/access.rs index ade6542..63aa87b 100644 --- a/src/api/access.rs +++ b/src/api/access.rs @@ -20,7 +20,7 @@ use crate::state::AppState; /// What the user of a request may do (a request extension, set by `authorize`). #[derive(Clone, Debug)] pub(crate) enum Access { - /// The swarm has no registry (one from before it, `access.md`): every user the + /// The swarm has no registry (`access.md`, A swarm without a registry): every user the /// node lets in may do what all could before; protocol data only those in /// `--protocol-writers` (`store::parse_write_path`). Everything, @@ -138,6 +138,70 @@ pub(crate) fn prune_shallow(mut value: JsonValue, path: &[String], access: &Acce value } +/// The registry as a write found it (`registry_kept`): whether it existed, and whether +/// the writing user could manage users (`registry::managers`). +#[derive(Clone, Copy)] +pub(crate) struct Before { + existed: bool, + managed: bool, +} + +impl Before { + pub(crate) fn of(doc: &impl crate::document::ReadDoc, user: &str) -> Self { + use crate::access::registry; + let existed = registry::exists(doc); + Self { + existed, + managed: existed + && registry::caps_of(doc, user) + .allows(crate::access::Right::Write, ®istry::users_dir()), + } + } +} + +/// `409` for a write to `path` by `user` that would lock that user out of managing +/// users: one that could and no longer may (deleting, disabling or narrowing its own +/// entry), and the write that makes the registry, if it doesn't make `user` one who may +/// (from then on only registered users are served). And for one that would remove +/// the registry, back to a swarm without one. Checked in the write's transaction, +/// after it: the write is dropped whole. Others are not kept from removing the last who +/// may manage users: no node could keep two such writes at once on two nodes from +/// leaving nobody once they merge; then the users in `--protocol-writers` may do +/// everything again (`access::authorize`). +pub(crate) fn registry_kept( + doc: &impl crate::document::ReadDoc, + path: &[String], + before: Before, + user: &str, +) -> Result<(), super::ApiError> { + use crate::access::registry; + if !crate::document::store::paths_related(path, ®istry::users_dir()) { + return Ok(()); + } + let refuse = |why: &str| { + Err(super::ApiError( + StatusCode::CONFLICT, + Some(format!("{why} (docs/access.md)")), + )) + }; + if !registry::exists(doc) { + return if before.existed { + refuse("this would remove the user registry") + } else { + Ok(()) + }; + } + let manages = + registry::caps_of(doc, user).allows(crate::access::Right::Write, ®istry::users_dir()); + if !before.existed && !manages { + return refuse("the registry's first users must include you, with write on _continuum"); + } + if before.managed && !manages { + return refuse("this would keep you from managing users"); + } + Ok(()) +} + /// What an endpoint needs (`needed`). #[derive(Debug, PartialEq, Eq)] enum Need { @@ -164,6 +228,14 @@ fn needed(method: &Method, path: &str) -> Need { Need::Observe } else if under("/peers") { protocol(Right::Write, &[meta::FORGOTTEN]) + } else if under("/admin/users") { + // Changing them is checked by their handlers too (`users::may_manage`): in a + // swarm without a registry, `--protocol-writers`. + if looking { + protocol(Right::Read, &[meta::USERS]) + } else { + protocol(Right::Write, &[meta::USERS]) + } } else if under("/admin/remakes") { if looking { protocol(Right::Read, &[meta::REMAKES]) @@ -296,6 +368,11 @@ mod tests { write(&[meta::REMAKES]) ); assert_eq!(needed(&Method::GET, "/admin/config"), read(&[])); + assert_eq!( + needed(&Method::GET, "/admin/users/alice"), + read(&[meta::USERS]) + ); + assert_eq!(needed(&Method::POST, "/admin/users"), write(&[meta::USERS])); assert_eq!(needed(&Method::GET, "/admin/quarantine/q"), read(&[])); assert_eq!( needed(&Method::POST, "/admin/quarantine/q/replay"), -- cgit v1.3.1