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
|
|
@ -59,10 +59,7 @@ pub fn children_of(name: &str) -> Vec<String> {
|
|||
|
||||
/// Pure form of [`children_of`] for unit tests.
|
||||
#[must_use]
|
||||
pub fn children_of_in(
|
||||
topo: &BTreeMap<String, Option<String>>,
|
||||
name: &str,
|
||||
) -> Vec<String> {
|
||||
pub fn children_of_in(topo: &BTreeMap<String, Option<String>>, name: &str) -> Vec<String> {
|
||||
topo.iter()
|
||||
.filter_map(|(agent, parent)| {
|
||||
if parent.as_deref() == Some(name) {
|
||||
|
|
@ -90,7 +87,13 @@ pub fn top_level_agents() -> Vec<String> {
|
|||
#[must_use]
|
||||
pub fn top_level_agents_in(topo: &BTreeMap<String, Option<String>>) -> Vec<String> {
|
||||
topo.iter()
|
||||
.filter_map(|(name, parent)| if parent.is_none() { Some(name.clone()) } else { None })
|
||||
.filter_map(|(name, parent)| {
|
||||
if parent.is_none() {
|
||||
Some(name.clone())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
|
||||
|
|
@ -501,8 +504,12 @@ mod tests {
|
|||
// `alice` who lives under the manager) would close the loop.
|
||||
// The general cycle walk catches this; no separate manager
|
||||
// guard needed.
|
||||
let err = apply_set_parent(&topo_three_level(), crate::lifecycle::MANAGER_NAME, Some("bob"))
|
||||
.unwrap_err();
|
||||
let err = apply_set_parent(
|
||||
&topo_three_level(),
|
||||
crate::lifecycle::MANAGER_NAME,
|
||||
Some("bob"),
|
||||
)
|
||||
.unwrap_err();
|
||||
assert!(err.contains("cycle"), "err = {err}");
|
||||
}
|
||||
|
||||
|
|
@ -678,20 +685,32 @@ mod tests {
|
|||
"alice".to_owned(),
|
||||
vec![ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS.to_owned()],
|
||||
);
|
||||
assert!(has_role_in(&roles, "alice", ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS));
|
||||
assert!(has_role_in(
|
||||
&roles,
|
||||
"alice",
|
||||
ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_role_in_returns_false_for_absent_agent() {
|
||||
let roles: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
assert!(!has_role_in(&roles, "alice", ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS));
|
||||
assert!(!has_role_in(
|
||||
&roles,
|
||||
"alice",
|
||||
ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS
|
||||
));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn has_role_in_returns_false_for_empty_list() {
|
||||
let mut roles = BTreeMap::new();
|
||||
roles.insert("alice".to_owned(), vec![]);
|
||||
assert!(!has_role_in(&roles, "alice", ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS));
|
||||
assert!(!has_role_in(
|
||||
&roles,
|
||||
"alice",
|
||||
ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS
|
||||
));
|
||||
}
|
||||
|
||||
/// Revoking a role must leave the key present with an empty list so
|
||||
|
|
@ -701,7 +720,10 @@ mod tests {
|
|||
let mgr = crate::lifecycle::MANAGER_NAME;
|
||||
// Build an in-memory roles map as set_role would see it after granting.
|
||||
let mut roles: BTreeMap<String, Vec<String>> = BTreeMap::new();
|
||||
roles.insert(mgr.to_owned(), vec![ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS.to_owned()]);
|
||||
roles.insert(
|
||||
mgr.to_owned(),
|
||||
vec![ROLE_CAN_MANAGE_TOP_LEVEL_AGENTS.to_owned()],
|
||||
);
|
||||
|
||||
// Simulate the revoke path of set_role (in-memory, no disk).
|
||||
let list = roles.entry(mgr.to_owned()).or_default();
|
||||
|
|
@ -727,7 +749,10 @@ mod tests {
|
|||
let mgr_present = agent_names.iter().any(|n| n == mgr);
|
||||
let should_seed = mgr_present && !roles.contains_key(mgr);
|
||||
// should_seed must be false because manager key is present (tombstone).
|
||||
assert!(!should_seed, "reconcile_roles must not re-seed an explicit revoke");
|
||||
assert!(
|
||||
!should_seed,
|
||||
"reconcile_roles must not re-seed an explicit revoke"
|
||||
);
|
||||
}
|
||||
|
||||
/// `reconcile_roles` seeds the manager on first appearance (no prior entry).
|
||||
|
|
|
|||
Loading…
Reference in a new issue