hivectl matrix create-user: --password / --password-stdin for operator accounts (#663)

This commit is contained in:
damocles 2026-05-30 21:49:04 +02:00 committed by Mara
commit 5d1909bb5c
2 changed files with 115 additions and 20 deletions

View file

@ -92,10 +92,28 @@ enum MatrixCmd {
/// the freshly-minted access token to stdout — no
/// `/var/lib/hyperhive/agents/` directory is created for the user
/// (#662).
///
/// Without `--password` / `--password-stdin` a random throwaway is
/// used (fine for agents — they auth by `access_token`, never by
/// password). Set a password to log into a matrix web client
/// afterwards (#663).
CreateUser {
/// Matrix localpart. For agents: the container/agent name.
/// For humans: any matrix localpart — `mara`, `damocles`, etc.
name: String,
/// Set the account password to this string instead of a random
/// throwaway. Use this for operator accounts that need to log
/// into matrix web clients via `m.login.password` (#663).
/// Mutually exclusive with `--password-stdin`. WARNING: the
/// password is visible in shell history + process listings;
/// prefer `--password-stdin` for anything sensitive.
#[arg(long)]
password: Option<String>,
/// Read the password from stdin (single line, trailing newline
/// stripped) instead of an inline flag. Mutually exclusive with
/// `--password`.
#[arg(long, conflicts_with = "password")]
password_stdin: bool,
},
}
@ -113,7 +131,11 @@ async fn main() -> Result<()> {
ForgeCmd::CreateUser { name } => forge_create_user(&name).await,
},
Cmd::Matrix { cmd } => match cmd {
MatrixCmd::CreateUser { name } => matrix_create_user(&name).await,
MatrixCmd::CreateUser {
name,
password,
password_stdin,
} => matrix_create_user(&name, password.as_deref(), password_stdin).await,
},
}
}
@ -150,7 +172,37 @@ async fn forge_create_user(name: &str) -> Result<()> {
Ok(())
}
async fn matrix_create_user(name: &str) -> Result<()> {
/// Resolve the password the caller asked for, or return `None` to fall
/// back to a random throwaway. `--password <PW>` wins outright;
/// `--password-stdin` reads one line off stdin (trailing newline
/// stripped). Empty stdin is treated as an error so the caller doesn't
/// silently provision an empty-password account.
fn resolve_password(password: Option<&str>, password_stdin: bool) -> Result<Option<String>> {
if let Some(p) = password {
return Ok(Some(p.to_owned()));
}
if password_stdin {
use std::io::BufRead as _;
let stdin = std::io::stdin();
let mut line = String::new();
stdin
.lock()
.read_line(&mut line)
.context("read password from stdin")?;
let trimmed = line.trim_end_matches(['\r', '\n']).to_owned();
if trimmed.is_empty() {
bail!("--password-stdin: empty input");
}
return Ok(Some(trimmed));
}
Ok(None)
}
async fn matrix_create_user(
name: &str,
password: Option<&str>,
password_stdin: bool,
) -> Result<()> {
if !hive_c0re::matrix::is_present().await {
bail!(
"hive-matrix container not running — start it (services.hyperhive.matrix.enable = true) before provisioning matrix users"
@ -162,7 +214,16 @@ async fn matrix_create_user(name: &str) -> Result<()> {
.timeout(std::time::Duration::from_secs(30))
.build()
.context("build reqwest client")?;
let user_password = resolve_password(password, password_stdin)?;
if is_agent(name) {
if user_password.is_some() {
// The boot-sweep / approval-time agent provisioning path
// doesn't accept a password — agents auth by access_token,
// never by password. Refuse rather than silently dropping it.
bail!(
"matrix create-user: --password is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via access_token"
);
}
hive_c0re::matrix::ensure_user_for(&client, name, &register_token)
.await
.with_context(|| format!("matrix create-user {name}"))?;
@ -170,11 +231,24 @@ async fn matrix_create_user(name: &str) -> Result<()> {
println!("matrix: provisioned agent user '{name}'");
println!("token persisted at: {}", path.display());
} else {
let token = hive_c0re::matrix::provision_user_token(&client, name, &register_token)
.await
.with_context(|| format!("matrix create-user {name}"))?;
let effective_password = match user_password {
Some(p) => p,
None => hive_c0re::matrix::random_password()
.context("generate random matrix password")?,
};
let token =
hive_c0re::matrix::provision_user_token(&client, name, &register_token, &effective_password)
.await
.with_context(|| format!("matrix create-user {name}"))?;
println!("matrix: provisioned user '{name}' (not an agent — token not persisted)");
println!("token: {token}");
if password.is_some() || password_stdin {
println!("password: set as supplied — use it to log into a matrix web client");
} else {
println!(
"password: random throwaway (not surfaced — pass --password or --password-stdin to set one you can use)"
);
}
}
Ok(())
}