fix(hivectl): ask the daemon whether an agent exists
The agents root is 0700 and owned by the daemon's user, so hivectl's client-side existence guard hit EACCES on traversal for anyone not root. It reported that as "this command needs root; re-run with sudo", which turned three verbs' pre-flight check into a permission error about the wrong thing: `choom`, `subvol upgrade` and `subvol snapshot create` all failed at the guard rather than at whatever they actually needed. The daemon runs as the owning user and already answers this question for its own provisioning paths, so expose it on the host socket as `AgentExists` and have hivectl ask. Operators reach that socket through the `hive-admin` group, so the guard now works without sudo. `choom` still needs root for `machinectl shell` — we ship no polkit rule granting those actions — so it now checks the effective uid and says so directly instead of failing later inside systemd's authorisation.
This commit is contained in:
parent
1bbc09e9dd
commit
170fd817ea
9 changed files with 119 additions and 36 deletions
|
|
@ -117,32 +117,34 @@ pub(crate) async fn query_hive_urls(socket: &Path) -> Result<Option<hive_host_so
|
|||
}
|
||||
|
||||
/// True when `name` matches an existing hyperhive agent — i.e. it has a
|
||||
/// persistent state dir under `/var/lib/hyperhive/agents/`. We use the
|
||||
/// state dir (not the live container list) so kept-state tombstones
|
||||
/// still resolve as agents — re-provisioning a destroyed-but-kept agent
|
||||
/// should still drop its token in the existing state tree.
|
||||
/// persistent state dir under the agents root. The state dir (not the
|
||||
/// live container list) is the test, so kept-state tombstones still
|
||||
/// resolve as agents: re-provisioning a destroyed-but-kept agent should
|
||||
/// drop its token into the existing state tree.
|
||||
///
|
||||
/// Uses `try_exists()` rather than `Path::exists()` so a permission
|
||||
/// error reaching the agents root is surfaced, not collapsed into
|
||||
/// `false`. The agents root is `0700 hive-core`, so running hivectl
|
||||
/// without root yields EACCES on traversal — `Path::exists()` would
|
||||
/// silently report `false`, which callers turn into a misleading "no
|
||||
/// such agent" (or, for the create-user paths, a silent misclassify of
|
||||
/// a real agent as a non-agent account). Mapping EACCES to an explicit
|
||||
/// "needs root" error fixes that first-run footgun, where running a
|
||||
/// privileged verb without sudo reported as a missing agent.
|
||||
pub(crate) fn agent_exists(name: &str) -> Result<bool> {
|
||||
let Ok(name) = hive_types::Ident::parse(name) else {
|
||||
bail!("invalid agent name {name:?}");
|
||||
};
|
||||
let root = hive_host_sock::agent_state_dir(&name);
|
||||
match root.try_exists() {
|
||||
Ok(found) => Ok(found),
|
||||
Err(e) if e.kind() == std::io::ErrorKind::PermissionDenied => bail!(
|
||||
"cannot read the agents root {} (permission denied) - this command needs root; \
|
||||
re-run with sudo",
|
||||
root.parent().unwrap_or(&root).display()
|
||||
),
|
||||
Err(e) => Err(e).with_context(|| format!("check agent state dir {}", root.display())),
|
||||
/// Asks the daemon rather than stat-ing the path. hivectl used to check
|
||||
/// locally, but the agents root is `0700` and owned by the daemon's
|
||||
/// user, so an operator without root got EACCES on traversal — which
|
||||
/// this function then had to report as "needs root", turning every
|
||||
/// caller's existence guard into a permission error. The daemon owns
|
||||
/// that directory and answers the same question over the socket, which
|
||||
/// operators can already reach via the `hive-admin` group, so the guard
|
||||
/// works sudoless and the remaining root requirements (if any) surface
|
||||
/// where they actually are.
|
||||
pub(crate) async fn agent_exists(socket: &Path, name: &str) -> Result<bool> {
|
||||
let resp = crate::client::request(
|
||||
socket,
|
||||
hive_host_sock::HostRequest::AgentExists {
|
||||
name: parse_ident(name)?,
|
||||
},
|
||||
)
|
||||
.await?;
|
||||
if !resp.ok {
|
||||
bail!(
|
||||
"check whether {name:?} is an agent: {}",
|
||||
resp.error.as_deref().unwrap_or("unknown error")
|
||||
);
|
||||
}
|
||||
resp.agent_exists
|
||||
.context("daemon answered the agent-exists check without a result")
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in a new issue