hivectl matrix create-user: --password / --password-stdin for operator accounts (#663)
This commit is contained in:
parent
18253bf2f5
commit
5d1909bb5c
2 changed files with 115 additions and 20 deletions
|
|
@ -92,10 +92,28 @@ enum MatrixCmd {
|
||||||
/// the freshly-minted access token to stdout — no
|
/// the freshly-minted access token to stdout — no
|
||||||
/// `/var/lib/hyperhive/agents/` directory is created for the user
|
/// `/var/lib/hyperhive/agents/` directory is created for the user
|
||||||
/// (#662).
|
/// (#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 {
|
CreateUser {
|
||||||
/// Matrix localpart. For agents: the container/agent name.
|
/// Matrix localpart. For agents: the container/agent name.
|
||||||
/// For humans: any matrix localpart — `mara`, `damocles`, etc.
|
/// For humans: any matrix localpart — `mara`, `damocles`, etc.
|
||||||
name: String,
|
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,
|
ForgeCmd::CreateUser { name } => forge_create_user(&name).await,
|
||||||
},
|
},
|
||||||
Cmd::Matrix { cmd } => match cmd {
|
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(())
|
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 {
|
if !hive_c0re::matrix::is_present().await {
|
||||||
bail!(
|
bail!(
|
||||||
"hive-matrix container not running — start it (services.hyperhive.matrix.enable = true) before provisioning matrix users"
|
"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))
|
.timeout(std::time::Duration::from_secs(30))
|
||||||
.build()
|
.build()
|
||||||
.context("build reqwest client")?;
|
.context("build reqwest client")?;
|
||||||
|
let user_password = resolve_password(password, password_stdin)?;
|
||||||
if is_agent(name) {
|
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, ®ister_token)
|
hive_c0re::matrix::ensure_user_for(&client, name, ®ister_token)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("matrix create-user {name}"))?;
|
.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!("matrix: provisioned agent user '{name}'");
|
||||||
println!("token persisted at: {}", path.display());
|
println!("token persisted at: {}", path.display());
|
||||||
} else {
|
} else {
|
||||||
let token = hive_c0re::matrix::provision_user_token(&client, name, ®ister_token)
|
let effective_password = match user_password {
|
||||||
.await
|
Some(p) => p,
|
||||||
.with_context(|| format!("matrix create-user {name}"))?;
|
None => hive_c0re::matrix::random_password()
|
||||||
|
.context("generate random matrix password")?,
|
||||||
|
};
|
||||||
|
let token =
|
||||||
|
hive_c0re::matrix::provision_user_token(&client, name, ®ister_token, &effective_password)
|
||||||
|
.await
|
||||||
|
.with_context(|| format!("matrix create-user {name}"))?;
|
||||||
println!("matrix: provisioned user '{name}' (not an agent — token not persisted)");
|
println!("matrix: provisioned user '{name}' (not an agent — token not persisted)");
|
||||||
println!("token: {token}");
|
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(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -169,19 +169,33 @@ async fn register_post(
|
||||||
Ok((status, json))
|
Ok((status, json))
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the matrix-spec UIAA flow to register `agent` and return the
|
/// Generate a throwaway random password for matrix UIAA registration.
|
||||||
/// resulting access token. Two round-trips: first POST elicits the
|
/// `PASSWORD_BYTES` raw bytes ⇒ 64-char hex string. Agents authenticate
|
||||||
/// 401 + session id, second POST supplies the registration token in
|
/// by `access_token` so the password is protocol overhead we never
|
||||||
/// the `auth` block. If the homeserver returns 200 on the first POST
|
/// persist; the operator path in `hivectl` (#663) lets the caller
|
||||||
/// (no flow stages required — `allow_registration` with no token), we
|
/// supply a real password instead so they can log into a matrix web
|
||||||
/// take the access token directly.
|
/// client (`m.login.password`).
|
||||||
|
pub fn random_password() -> Result<String> {
|
||||||
|
random_hex(PASSWORD_BYTES)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Run the matrix-spec UIAA flow to register `agent` with the given
|
||||||
|
/// `password` and return the resulting access token. Two round-trips:
|
||||||
|
/// first POST elicits the 401 + session id, second POST supplies the
|
||||||
|
/// registration token in the `auth` block. If the homeserver returns
|
||||||
|
/// 200 on the first POST (no flow stages required — `allow_registration`
|
||||||
|
/// with no token), we take the access token directly.
|
||||||
|
///
|
||||||
|
/// Caller picks the password: agents use [`random_password`] (throwaway
|
||||||
|
/// — they auth by `access_token`), operators on the `hivectl` path
|
||||||
|
/// supply their own so they can log into matrix web clients (#663).
|
||||||
async fn register_user(
|
async fn register_user(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
agent: &str,
|
agent: &str,
|
||||||
register_token: &str,
|
register_token: &str,
|
||||||
|
password: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let localpart = user_localpart(agent);
|
let localpart = user_localpart(agent);
|
||||||
let password = random_hex(PASSWORD_BYTES)?;
|
|
||||||
let initial = serde_json::json!({
|
let initial = serde_json::json!({
|
||||||
"username": localpart,
|
"username": localpart,
|
||||||
"password": password,
|
"password": password,
|
||||||
|
|
@ -252,7 +266,8 @@ pub async fn ensure_user_for(
|
||||||
tracing::debug!(%name, "matrix: token already present");
|
tracing::debug!(%name, "matrix: token already present");
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let access_token = register_user(client, name, register_token).await?;
|
let password = random_password()?;
|
||||||
|
let access_token = register_user(client, name, register_token, &password).await?;
|
||||||
if let Some(parent) = path.parent() {
|
if let Some(parent) = path.parent() {
|
||||||
std::fs::create_dir_all(parent).ok();
|
std::fs::create_dir_all(parent).ok();
|
||||||
}
|
}
|
||||||
|
|
@ -263,18 +278,24 @@ pub async fn ensure_user_for(
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Register a matrix account for `name` and return the freshly-minted
|
/// Register a matrix account for `name` with the supplied `password`
|
||||||
/// access token. Unlike [`ensure_user_for`], the token is **not**
|
/// and return the freshly-minted access token. Unlike [`ensure_user_for`],
|
||||||
/// persisted to disk — the caller is responsible for storing it. Used
|
/// the token is **not** persisted to disk — the caller is responsible
|
||||||
/// by `hivectl matrix create-user` for human (non-agent) accounts so
|
/// for storing it. Used by `hivectl matrix create-user` for human
|
||||||
/// we don't create stray `/var/lib/hyperhive/agents/<name>/` directories
|
/// (non-agent) accounts so we don't create stray
|
||||||
/// for users that aren't agents (#662).
|
/// `/var/lib/hyperhive/agents/<name>/` directories for users that
|
||||||
|
/// aren't agents (#662). For operator accounts the caller passes a
|
||||||
|
/// real password so the operator can `m.login.password` into matrix
|
||||||
|
/// web clients afterwards (#663); for headless agent re-provisioning
|
||||||
|
/// the caller can pass [`random_password`] to keep the existing
|
||||||
|
/// throwaway behaviour.
|
||||||
pub async fn provision_user_token(
|
pub async fn provision_user_token(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
name: &str,
|
name: &str,
|
||||||
register_token: &str,
|
register_token: &str,
|
||||||
|
password: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
register_user(client, name, register_token).await
|
register_user(client, name, register_token, password).await
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Per-agent matrix sync: ensure the agent has a matrix account + token.
|
/// Per-agent matrix sync: ensure the agent has a matrix account + token.
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue