aboutsummaryrefslogtreecommitdiffstats
path: root/src/access/registry.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/access/registry.rs')
-rw-r--r--src/access/registry.rs87
1 files changed, 87 insertions, 0 deletions
diff --git a/src/access/registry.rs b/src/access/registry.rs
index c4ad0e6..e863ce5 100644
--- a/src/access/registry.rs
+++ b/src/access/registry.rs
@@ -133,6 +133,53 @@ pub(crate) fn capability(by: &str, since: u64, path: &[String], rights: &[Right]
})
}
+/// The first users of a new swarm, as its genesis node records them (`first_users`):
+/// each name, and whether it may do everything (protocol data and managing users
+/// 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`, each may do everything; unset, the users of its htpasswd file
+/// (`htpasswd`) may read and write the swarm's data, and those of them, or of the proxy's
+/// (`--user-header`), in `protocol_writers` everything. Refused if nobody could manage
+/// the swarm: a registry nobody may change would lock everyone out for good.
+pub(crate) fn first_users(
+ bootstrap_users: &[String],
+ htpasswd: &[String],
+ protocol_writers: &[String],
+) -> anyhow::Result<FirstUsers> {
+ let mut users: BTreeMap<String, bool> = BTreeMap::new();
+ if bootstrap_users.is_empty() {
+ for name in htpasswd.iter().filter(|name| valid_name(name)) {
+ users.insert(name.clone(), false);
+ }
+ for name in protocol_writers.iter().filter(|name| valid_name(name)) {
+ users.insert(name.clone(), true);
+ }
+ } else {
+ for name in bootstrap_users {
+ users.insert(name.clone(), true);
+ }
+ }
+ if !users.values().any(|admin| *admin) {
+ anyhow::bail!(
+ "genesis: nobody could manage the new swarm's users; name one with bootstrap-users, or put one of htpasswd's users in protocol-writers (docs/access.md)"
+ );
+ }
+ Ok(users.into_iter().collect())
+}
+
+/// The capabilities of a first user (`first_users`): the swarm's data, and for one who
+/// may do everything protocol data too.
+pub(crate) fn first_caps(admin: bool) -> Vec<(Vec<String>, &'static [Right])> {
+ const BOTH: &[Right] = &[Right::Read, Right::Write];
+ let mut caps = vec![(Vec::new(), BOTH)];
+ if admin {
+ caps.push((meta::path(&[]), BOTH));
+ }
+ caps
+}
+
/// A random ID for a capability (8 bytes, hex): concurrent grants don't collide.
pub(crate) fn new_id() -> String {
let mut bytes = [0u8; 8];
@@ -214,6 +261,46 @@ mod tests {
}
#[test]
+ fn the_first_users() {
+ let names = |list: &[&str]| list.iter().map(|n| (*n).to_owned()).collect::<Vec<_>>();
+ assert_eq!(
+ first_users(&[], &names(&["alice", "bob"]), &names(&["alice"])).unwrap(),
+ vec![("alice".to_owned(), true), ("bob".to_owned(), false)]
+ );
+ // A protocol writer the proxy names, not in htpasswd.
+ assert_eq!(
+ first_users(&[], &names(&["bob"]), &names(&["carol"])).unwrap(),
+ vec![("bob".to_owned(), false), ("carol".to_owned(), true)]
+ );
+ assert_eq!(
+ first_users(&names(&["root"]), &names(&["alice"]), &names(&["alice"])).unwrap(),
+ vec![("root".to_owned(), true)]
+ );
+ assert!(first_users(&[], &names(&["alice"]), &[]).is_err());
+ assert!(first_users(&[], &[], &[]).is_err());
+ let admin = Caps::new(
+ first_caps(true)
+ .into_iter()
+ .map(|(path, rights)| {
+ serde_json::from_value(capability("x", 0, &path, rights)).unwrap()
+ })
+ .collect(),
+ );
+ assert!(admin.allows(Right::Write, &path("_continuum/users/x")));
+ assert!(admin.allows(Right::Write, &path("accounts")));
+ let user = Caps::new(
+ first_caps(false)
+ .into_iter()
+ .map(|(path, rights)| {
+ serde_json::from_value(capability("x", 0, &path, rights)).unwrap()
+ })
+ .collect(),
+ );
+ assert!(user.allows(Right::Write, &path("accounts")));
+ assert!(!user.allows(Right::Read, &path("_continuum")));
+ }
+
+ #[test]
fn concurrent_grants_both_hold() {
let mut ours = Document::new();
let entry = entry("x", 0, &[]);