feat(gateway): hivectl gateway user management + fix htpasswdFile assertion
Add `hivectl gateway {create-user,delete-user,list-users}` subcommands for
managing htpasswd files used by gateway Basic auth. Pure Rust bcrypt
(cost 12, $2y$ prefix nginx accepts). No external htpasswd binary required.
Also fix the NixOS module assertion: `cfg.auth ? htpasswdFile` is always
true in the module system (declared options always exist as keys); switch
to `nullOr path; default = null` + `!= null` check so the assertion
actually fires with a useful error when enable=true but no file is set.
Guard bind-mount and nginx config against null to prevent eval errors.
Update docs/gateway.md to show hivectl commands instead of raw htpasswd.
This commit is contained in:
parent
25d2951d1e
commit
4bff450343
61 changed files with 1084 additions and 547 deletions
|
|
@ -81,8 +81,7 @@ pub fn socket_path_for(name: &str) -> PathBuf {
|
|||
#[must_use]
|
||||
pub fn build_map(names: &[String]) -> BTreeMap<String, PathBuf> {
|
||||
build_map_with(names, |name| {
|
||||
ready_marker_for(name).exists()
|
||||
|| agent_dir_for(name).join(READY_MARKER_LEGACY).exists()
|
||||
ready_marker_for(name).exists() || agent_dir_for(name).join(READY_MARKER_LEGACY).exists()
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -146,12 +145,10 @@ pub fn write(names: &[String]) -> Result<()> {
|
|||
return Ok(());
|
||||
}
|
||||
if let Some(parent) = path.parent() {
|
||||
std::fs::create_dir_all(parent)
|
||||
.with_context(|| format!("create {}", parent.display()))?;
|
||||
std::fs::create_dir_all(parent).with_context(|| format!("create {}", parent.display()))?;
|
||||
}
|
||||
let tmp = path.with_extension("json.tmp");
|
||||
std::fs::write(&tmp, &body)
|
||||
.with_context(|| format!("write {}", tmp.display()))?;
|
||||
std::fs::write(&tmp, &body).with_context(|| format!("write {}", tmp.display()))?;
|
||||
std::fs::rename(&tmp, &path).with_context(|| {
|
||||
format!(
|
||||
"rename {} -> {} (atomic publish)",
|
||||
|
|
@ -305,24 +302,30 @@ mod tests {
|
|||
let marker = ready_marker_for("iris");
|
||||
let socket = socket_path_for("iris");
|
||||
assert_eq!(marker.parent(), socket.parent());
|
||||
assert_eq!(marker, Path::new("/run/hive-agent/iris/hyperhive-socket-bound"));
|
||||
assert_eq!(
|
||||
marker,
|
||||
Path::new("/run/hive-agent/iris/hyperhive-socket-bound")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn render_is_pretty_and_sorted() {
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert("zeta".to_owned(), PathBuf::from("/run/hive-agent/zeta/web.sock"));
|
||||
map.insert("alpha".to_owned(), PathBuf::from("/run/hive-agent/alpha/web.sock"));
|
||||
map.insert(
|
||||
"zeta".to_owned(),
|
||||
PathBuf::from("/run/hive-agent/zeta/web.sock"),
|
||||
);
|
||||
map.insert(
|
||||
"alpha".to_owned(),
|
||||
PathBuf::from("/run/hive-agent/alpha/web.sock"),
|
||||
);
|
||||
let body = render(&map);
|
||||
// Pretty-print = newlines between keys + indentation.
|
||||
assert!(body.contains('\n'));
|
||||
// BTreeMap sorts → alpha before zeta in output.
|
||||
let alpha_pos = body.find("alpha").expect("alpha in output");
|
||||
let zeta_pos = body.find("zeta").expect("zeta in output");
|
||||
assert!(
|
||||
alpha_pos < zeta_pos,
|
||||
"sorted order broken:\n{body}"
|
||||
);
|
||||
assert!(alpha_pos < zeta_pos, "sorted order broken:\n{body}");
|
||||
}
|
||||
|
||||
#[test]
|
||||
|
|
@ -332,10 +335,12 @@ mod tests {
|
|||
// gateway-side reader can deserialise into String values
|
||||
// without nested struct logic.
|
||||
let mut map = BTreeMap::new();
|
||||
map.insert("iris".to_owned(), PathBuf::from("/run/hive-agent/iris/web.sock"));
|
||||
map.insert(
|
||||
"iris".to_owned(),
|
||||
PathBuf::from("/run/hive-agent/iris/web.sock"),
|
||||
);
|
||||
let body = render(&map);
|
||||
assert!(body.contains("\"iris\""));
|
||||
assert!(body.contains("\"/run/hive-agent/iris/web.sock\""));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue