From 3a9d9da74f55395ead20f3915ed04639e37b2f03 Mon Sep 17 00:00:00 2001 From: tv Date: Sat, 7 Nov 2026 22:35:09 +0100 Subject: access: bootstrap users only The first users come only from --bootstrap-users; the genesis node warns of htpasswd users that aren't among them. Auth plan M1. --- docs/access.md | 9 ++++++--- docs/example.toml | 9 +++++---- src/access/registry.rs | 35 +++++++++-------------------------- src/config/mod.rs | 4 ++-- src/main.rs | 22 +++++++++++++++------- 5 files changed, 37 insertions(+), 42 deletions(-) diff --git a/docs/access.md b/docs/access.md index 7a85c94..519113c 100644 --- a/docs/access.md +++ b/docs/access.md @@ -34,9 +34,12 @@ password hashes: every member reads the document). A swarm gets a registry from its start if its genesis node is given its first users, `--bootstrap-users`: it records them, with the swarm ID, in its first change, each with -everything (`[]` and `["_continuum"]`), and the other users of its `--htpasswd` with -`[]`. Without it, the swarm has no registry, and is as swarms were before: users all -alike (An existing swarm, below), until someone gives it one. +everything (`[]` and `["_continuum"]`). Other users, of its `--htpasswd` or any node's, +may do nothing until added (the genesis node warns of those of its `--htpasswd`). +Without it, the swarm has no registry, and is as swarms were before: users all alike +(An existing swarm, below), until someone gives it one. `--protocol-writers` counts in +both: without a registry, as who may write protocol data; with one, to set it right +should nobody be left who may manage users (Keeping the swarm manageable, below). ## At the HTTP API diff --git a/docs/example.toml b/docs/example.toml index e48bd2c..b98752d 100644 --- a/docs/example.toml +++ b/docs/example.toml @@ -83,13 +83,14 @@ htpasswd = "/etc/continuum/htpasswd" # (remake records, members, forget records), e.g. to repair a stuck remake, in a swarm # without a user registry. Such writes act on the whole swarm and are logged. Epoch # metadata and the swarm ID are never writable. With a registry its capabilities decide -# instead (docs/access.md). +# instead (docs/access.md), but for a registry nobody may manage users of any more: then +# these may do everything on this node, to set it right. # protocol-writers = ["alice"] # With genesis: give the new swarm a user registry (docs/access.md), these its first -# users, each may do everything, protocol data and managing users included; htpasswd's -# other users may read and write the swarm's data. Unset, the swarm has no registry: its -# users are all alike, and only those in protocol-writers write protocol data. +# users, each may do everything, protocol data and managing users included; other users +# may do nothing until added. Unset, the swarm has no registry: its users are all alike, +# and only those in protocol-writers write protocol data. # bootstrap-users = ["alice"] # How log lines look: text (timestamped), journal (for systemd's journal: no timestamp or diff --git a/src/access/registry.rs b/src/access/registry.rs index 094e62c..3006b31 100644 --- a/src/access/registry.rs +++ b/src/access/registry.rs @@ -146,24 +146,12 @@ pub(crate) fn capability(by: &str, since: u64, path: &[String], rights: &[Right] /// included), not only read and write the swarm's data. pub(crate) type FirstUsers = Vec<(String, bool)>; -/// The first users of a new swarm, from the genesis node's options: those of -/// `bootstrap_users` may do everything, and the other users of its htpasswd file -/// (`htpasswd`) read and write the swarm's data. None without `bootstrap_users`: then -/// the swarm has no registry, and its users are all alike, as before there were -/// registries (`access.md`). -pub(crate) fn first_users(bootstrap_users: &[String], htpasswd: &[String]) -> FirstUsers { - if bootstrap_users.is_empty() { - return Vec::new(); - } - let mut users: BTreeMap = htpasswd - .iter() - .filter(|name| valid_name(name)) - .map(|name| (name.clone(), false)) - .collect(); - for name in bootstrap_users { - users.insert(name.clone(), true); - } - users.into_iter().collect() +/// The first users of a new swarm, from the genesis node's `--bootstrap-users`: each may +/// do everything. None without: then the swarm has no registry, and its users are all +/// alike, as before there were registries (`access.md`). +pub(crate) fn first_users(bootstrap_users: &[String]) -> FirstUsers { + let users: std::collections::BTreeSet<&String> = bootstrap_users.iter().collect(); + users.into_iter().map(|name| (name.clone(), true)).collect() } /// The capabilities of a first user (`first_users`): the swarm's data, and for one who @@ -261,16 +249,11 @@ mod tests { fn the_first_users() { let names = |list: &[&str]| list.iter().map(|n| (*n).to_owned()).collect::>(); assert_eq!( - first_users(&names(&["alice"]), &names(&["alice", "bob"])), - vec![("alice".to_owned(), true), ("bob".to_owned(), false)] - ); - // One the proxy names, not in htpasswd. - assert_eq!( - first_users(&names(&["carol"]), &names(&["bob"])), - vec![("bob".to_owned(), false), ("carol".to_owned(), true)] + first_users(&names(&["bob", "alice", "bob"])), + vec![("alice".to_owned(), true), ("bob".to_owned(), true)] ); // Without bootstrap users, no registry. - assert!(first_users(&[], &names(&["alice"])).is_empty()); + assert!(first_users(&[]).is_empty()); let admin = Caps::new( first_caps(true) .into_iter() diff --git a/src/config/mod.rs b/src/config/mod.rs index 320d2a8..5306c5e 100644 --- a/src/config/mod.rs +++ b/src/config/mod.rs @@ -121,14 +121,14 @@ pub(crate) const OPTIONS: &[OptionSpec] = &[ Kind::List, "USER,[USER...]", None, - "HTTP API users (from htpasswd or user-header) who may write protocol data under /store/_continuum (remake records, members, forget records), e.g. to repair a stuck remake, in a swarm without a user registry. Such writes act on the whole swarm and are logged. Epoch metadata and the swarm ID are never writable. With a registry its capabilities decide instead (docs/access.md).", + "HTTP API users (from htpasswd or user-header) who may write protocol data under /store/_continuum (remake records, members, forget records), e.g. to repair a stuck remake, in a swarm without a user registry. Such writes act on the whole swarm and are logged. Epoch metadata and the swarm ID are never writable. With a registry its capabilities decide instead (docs/access.md), but for a registry nobody may manage users of any more: then these may do everything on this node, to set it right.", ), option( "bootstrap-users", Kind::List, "USER,[USER...]", None, - "With genesis: give the new swarm a user registry (docs/access.md), these its first users, each may do everything, protocol data and managing users included; htpasswd's other users may read and write the swarm's data. Unset, the swarm has no registry: its users are all alike, and only those in protocol-writers write protocol data.", + "With genesis: give the new swarm a user registry (docs/access.md), these its first users, each may do everything, protocol data and managing users included; other users may do nothing until added. Unset, the swarm has no registry: its users are all alike, and only those in protocol-writers write protocol data.", ), option( "log-format", diff --git a/src/main.rs b/src/main.rs index 2b8c376..fb3c0ad 100644 --- a/src/main.rs +++ b/src/main.rs @@ -97,16 +97,24 @@ async fn main() -> Result<()> { // A broken htpasswd will abort startup. let auth_state = AuthState::from_settings(&args)?.context("htpasswd or user-header is required")?; - // A new swarm's first users, if it is to have a user registry. - let first_users = if args.genesis && !args.bootstrap_users.is_empty() { - let htpasswd = match args.htpasswd.as_deref() { - Some(path) => api::auth::htpasswd_names(path)?, - None => Vec::new(), - }; - access::registry::first_users(&args.bootstrap_users, &htpasswd) + // A new swarm's first users, if it is to have a user registry; the users of the + // htpasswd file that aren't among them may do nothing until they are added. + let first_users = if args.genesis { + access::registry::first_users(&args.bootstrap_users) } else { Vec::new() }; + if !first_users.is_empty() + && let Some(path) = args.htpasswd.as_deref() + { + let missing: Vec = api::auth::htpasswd_names(path)? + .into_iter() + .filter(|name| !args.bootstrap_users.contains(name)) + .collect(); + if !missing.is_empty() { + warn!(users = ?missing, "Users of htpasswd not among bootstrap-users: the new swarm's registry doesn't have them, so they may do nothing until added (docs/access.md)"); + } + } info!( bind_addr = ?args.bind_addr.iter().map(ToString::to_string).collect::>(), -- cgit v1.3.1