matrix: create accounts as the appservice, and promote the admin explicitly
Account creation stops presenting a shared registration token in a UIAA
flow and starts acting as the hive's appservice: one POST, typed
`m.login.application_service`, authorised by the `as_token` the
registration file names. The account that comes out is an ordinary user
with its own device and its own access token — nothing about what an agent
holds changes.
Three things get better than "one fewer round-trip":
- An account whose token file was lost is re-tokened by an appservice
login, which needs neither its password nor admin rights. That was
previously a stored-password login, and failing that an admin-room
password reset. Both are kept behind it, for accounts created before
this existed or named outside the appservice's namespace.
- The hive admin no longer has to be the first account ever registered.
It could not be, in fact: tuwunel excludes appservice-created users from
the automatic first-user grant, and on a homeserver that already had
users the rule never fired anyway. Rights now come from an explicit
`make_user_admin` — performed by `admin_execute` at homeserver startup,
and verified here each sweep by reading the account's own joined-rooms
list. Absent rights are reported with the one command that grants them,
and are not fatal: agent accounts, the Space and the chat room all work
without them.
- hive-c0re reads the appservice token and never mints it. The old token
was the whole agreement, so whoever wrote it first was right; this one
is also named by a registration file that only the nix side writes, and
a token minted here would be one the homeserver has never heard of.
Also fixes the `make-user-admin` reply matcher, which recognised neither
spelling tuwunel v1.9.0 uses ("<user> has been granted admin
privileges.") — a promotion that had already taken effect was reported as
a 15-second timeout.
Refs #4402
This commit is contained in:
parent
5809077924
commit
43cd8607ba
3 changed files with 379 additions and 153 deletions
|
|
@ -1,12 +1,19 @@
|
||||||
//! Optional matrix-tuwunel wiring: shared registration token (host) +
|
//! Optional matrix-tuwunel wiring: the hive's appservice identity (host) +
|
||||||
//! per-agent UIAA registration → `<agent-state>/matrix-token`. No-op
|
//! per-agent account creation → `<agent-state>/matrix-token`. No-op
|
||||||
//! when the `hive-matrix` container isn't running, so operators who
|
//! when the `hive-matrix` container isn't running, so operators who
|
||||||
//! haven't flipped `services.hyperhive.deploy.matrix.enable = true` pay
|
//! haven't flipped `services.hyperhive.deploy.matrix.enable = true` pay
|
||||||
//! nothing.
|
//! nothing.
|
||||||
//!
|
//!
|
||||||
//! See `docs/integrations/matrix.md::Provisioning flow (registration token)` for
|
//! Accounts are created **as the hive's appservice**, not by presenting a
|
||||||
//! the full UIAA round-trip, token-file shape, and host/container
|
//! shared registration token in a UIAA flow. The difference that matters
|
||||||
//! bind-mount layout.
|
//! here is not the round-trip count: an appservice token is an *identity*
|
||||||
|
//! the homeserver knows, so the secret never has to be the same on both
|
||||||
|
//! sides of the wire, and account creation does not depend on registration
|
||||||
|
//! being open to anyone who learns a token.
|
||||||
|
//!
|
||||||
|
//! See `docs/integrations/matrix.md::Provisioning flow (appservice)` for the
|
||||||
|
//! registration file's shape, how its token reaches both halves, and the
|
||||||
|
//! host/container bind-mount layout.
|
||||||
|
|
||||||
use std::path::PathBuf;
|
use std::path::PathBuf;
|
||||||
|
|
||||||
|
|
@ -49,11 +56,8 @@ fn matrix_base() -> Result<&'static str> {
|
||||||
this path should have been gated on matrix::is_present()",
|
this path should have been gated on matrix::is_present()",
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
/// Length (bytes) of the random registration token. 32 raw bytes ⇒
|
/// HTTP timeout for registration round-trips. Account creation is one
|
||||||
/// 64-char hex string; comfortable for a long-lived shared secret.
|
/// POST; even the slow path should finish well inside this budget.
|
||||||
const REGISTER_TOKEN_BYTES: usize = 32;
|
|
||||||
/// HTTP timeout for registration round-trips. UIAA is two POSTs; even
|
|
||||||
/// the slow path should finish well inside this budget.
|
|
||||||
const HTTP_TIMEOUT_SECS: u64 = 10;
|
const HTTP_TIMEOUT_SECS: u64 = 10;
|
||||||
/// Length (bytes) of the throwaway per-agent matrix password. Random
|
/// Length (bytes) of the throwaway per-agent matrix password. Random
|
||||||
/// 32-byte hex — agents never log in with the password (they
|
/// 32-byte hex — agents never log in with the password (they
|
||||||
|
|
@ -61,10 +65,17 @@ const HTTP_TIMEOUT_SECS: u64 = 10;
|
||||||
/// store it nowhere.
|
/// store it nowhere.
|
||||||
const PASSWORD_BYTES: usize = 32;
|
const PASSWORD_BYTES: usize = 32;
|
||||||
|
|
||||||
/// Matrix localpart for the hive system admin account. Registered
|
/// Matrix localpart for the hive system admin account. Not an agent; has
|
||||||
/// before any agent account in [`ensure_all`] so it becomes the first
|
/// no state dir.
|
||||||
/// user on the homeserver — Conduit/tuwunel grants admin rights to the
|
///
|
||||||
/// first registered user automatically. Not an agent; has no state dir.
|
/// Also the `sender_localpart` of the hive's appservice registration,
|
||||||
|
/// which is what creates this account on a homeserver that has never had
|
||||||
|
/// one: the homeserver creates a registration's sender user itself, at
|
||||||
|
/// startup, before it accepts a request. The account's *admin rights*
|
||||||
|
/// then come from the `admin_execute` promotion beside that registration
|
||||||
|
/// — see `nix/host-modules/hive-matrix.nix`, where this same literal
|
||||||
|
/// appears as `adminLocalpart`. **The two must match**; nothing wires an
|
||||||
|
/// override across.
|
||||||
pub const HIVE_ADMIN_LOCALPART: &str = "hive";
|
pub const HIVE_ADMIN_LOCALPART: &str = "hive";
|
||||||
|
|
||||||
/// Display name of the hive Space. Plain text, no special characters, so
|
/// Display name of the hive Space. Plain text, no special characters, so
|
||||||
|
|
@ -164,30 +175,36 @@ fn random_hex(n: usize) -> Result<String> {
|
||||||
Ok(hex)
|
Ok(hex)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure the registration token file exists; returns its contents.
|
/// Read the hive's appservice token — the `as_token` of the registration
|
||||||
/// Generates a fresh 32-byte hex token on first call (mode 0600,
|
/// the homeserver loaded at boot. Every account this module creates is
|
||||||
/// root-only), then re-reads on subsequent calls. The hive-matrix
|
/// authorised by it.
|
||||||
/// nixos module bind-mounts this file read-only into the tuwunel
|
///
|
||||||
/// container so the homeserver can authenticate registration requests
|
/// **Reads, never mints**, unlike the registration token it replaced.
|
||||||
/// against the same secret hive-c0re holds.
|
/// That token was the whole agreement, so whichever side wrote it first
|
||||||
pub fn ensure_register_token() -> Result<String> {
|
/// was right; this one has a second half — the registration file naming
|
||||||
use std::os::unix::fs::PermissionsExt;
|
/// it, which only the nix side writes. A token minted here would be a
|
||||||
let path = crate::paths::matrix_register_token();
|
/// token the homeserver has never heard of, and the failure would surface
|
||||||
if let Ok(existing) = std::fs::read_to_string(&path) {
|
/// as every request being refused rather than as a missing file.
|
||||||
let trimmed = existing.trim().to_owned();
|
///
|
||||||
if !trimmed.is_empty() {
|
/// # Errors
|
||||||
return Ok(trimmed);
|
/// When the file is absent or empty. That means the host activation
|
||||||
}
|
/// script has not run on this generation yet; callers log it and leave
|
||||||
}
|
/// existing accounts alone rather than trying to proceed.
|
||||||
let token = random_hex(REGISTER_TOKEN_BYTES)?;
|
pub fn read_appservice_token() -> Result<String> {
|
||||||
if let Some(parent) = path.parent() {
|
let path = crate::paths::matrix_appservice_token();
|
||||||
std::fs::create_dir_all(parent).ok();
|
std::fs::read_to_string(&path)
|
||||||
}
|
.ok()
|
||||||
std::fs::write(&path, format!("{token}\n"))
|
.map(|s| s.trim().to_owned())
|
||||||
.with_context(|| format!("write registration token to {}", path.display()))?;
|
.filter(|s| !s.is_empty())
|
||||||
let _ = std::fs::set_permissions(&path, std::fs::Permissions::from_mode(0o600));
|
.with_context(|| {
|
||||||
tracing::info!(path = %path.display(), "matrix: generated registration token");
|
format!(
|
||||||
Ok(token)
|
"matrix appservice token not found at {} — it is minted by the \
|
||||||
|
hive-matrix activation script, which also renders the registration \
|
||||||
|
file naming it; deploy the hive-matrix module (or re-run \
|
||||||
|
`nixos-rebuild switch`) before provisioning matrix users",
|
||||||
|
path.display()
|
||||||
|
)
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Build the localpart of a matrix user id for `agent`. Matrix
|
/// Build the localpart of a matrix user id for `agent`. Matrix
|
||||||
|
|
@ -198,19 +215,20 @@ fn user_localpart(agent: &str) -> &str {
|
||||||
agent
|
agent
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Send a single registration POST and parse the response. Returns
|
/// Send the registration POST as the appservice and parse the response.
|
||||||
/// `Ok((status, body))` on any successful HTTP round-trip (including
|
/// Returns `Ok((status, body))` on any completed HTTP round-trip
|
||||||
/// the expected 401 from the first UIAA leg); errors only on transport
|
/// (including the `M_USER_IN_USE` 400 the caller treats as "already
|
||||||
/// failure. Body is the parsed JSON value — UIAA passes session +
|
/// exists"); errors only on transport failure.
|
||||||
/// flow state via JSON, never via headers.
|
|
||||||
async fn register_post(
|
async fn register_post(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
|
as_token: &str,
|
||||||
body: &serde_json::Value,
|
body: &serde_json::Value,
|
||||||
) -> Result<(StatusCode, serde_json::Value)> {
|
) -> Result<(StatusCode, serde_json::Value)> {
|
||||||
let base = matrix_base()?;
|
let base = matrix_base()?;
|
||||||
let url = format!("{base}/_matrix/client/v3/register");
|
let url = format!("{base}/_matrix/client/v3/register");
|
||||||
let resp = client
|
let resp = client
|
||||||
.post(&url)
|
.post(&url)
|
||||||
|
.bearer_auth(as_token)
|
||||||
.json(body)
|
.json(body)
|
||||||
.send()
|
.send()
|
||||||
.await
|
.await
|
||||||
|
|
@ -233,24 +251,38 @@ pub fn random_password() -> Result<String> {
|
||||||
random_hex(PASSWORD_BYTES)
|
random_hex(PASSWORD_BYTES)
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Run the matrix-spec UIAA flow to register `agent` with the given
|
/// Create the matrix account for `agent` as the hive's appservice and
|
||||||
/// `password` and return the resulting access token. Two round-trips:
|
/// return an access token for it. One round-trip: an appservice-typed
|
||||||
/// first POST elicits the 401 + session id, second POST supplies the
|
/// registration needs no UIAA stage at all, so there is no session to
|
||||||
/// registration token in the `auth` block. If the homeserver returns
|
/// carry and no shared secret to present.
|
||||||
/// 200 on the first POST (no flow stages required — `allow_registration`
|
///
|
||||||
/// with no token), we take the access token directly.
|
/// The account is created **by** the appservice but is an ordinary user
|
||||||
|
/// afterwards — it gets its own device and its own access token, and the
|
||||||
|
/// agent authenticates with that rather than with anything the hive
|
||||||
|
/// holds. The `as_token` never leaves the host.
|
||||||
///
|
///
|
||||||
/// Caller picks the password: agents use [`random_password`] (throwaway
|
/// Caller picks the password: agents use [`random_password`] (throwaway
|
||||||
/// — they auth by `access_token`), operators on the `hivectl` path
|
/// — they auth by `access_token`), operators on the `hivectl` path
|
||||||
/// supply their own so they can log into matrix web clients.
|
/// supply their own so they can log into matrix web clients.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Propagates the homeserver's own body, which is what the
|
||||||
|
/// `M_USER_IN_USE` callers match on. A `M_EXCLUSIVE` body means the
|
||||||
|
/// localpart falls outside the appservice's namespace — the registration
|
||||||
|
/// file's `namespaces.users` regex is the place to look, not this call.
|
||||||
async fn register_user(
|
async fn register_user(
|
||||||
client: &reqwest::Client,
|
client: &reqwest::Client,
|
||||||
agent: &str,
|
agent: &str,
|
||||||
register_token: &str,
|
as_token: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
let localpart = user_localpart(agent);
|
let localpart = user_localpart(agent);
|
||||||
let initial = serde_json::json!({
|
let body = serde_json::json!({
|
||||||
|
// What makes this an appservice registration rather than an
|
||||||
|
// ordinary one. Without it the homeserver treats the request as a
|
||||||
|
// normal client's and asks for a UIAA flow — even holding the
|
||||||
|
// as_token.
|
||||||
|
"type": "m.login.application_service",
|
||||||
"username": localpart,
|
"username": localpart,
|
||||||
"password": password,
|
"password": password,
|
||||||
// device_id stays stable across re-runs of ensure_user_for so
|
// device_id stays stable across re-runs of ensure_user_for so
|
||||||
|
|
@ -259,37 +291,53 @@ async fn register_user(
|
||||||
"initial_device_display_name": format!("hyperhive ({agent})"),
|
"initial_device_display_name": format!("hyperhive ({agent})"),
|
||||||
"inhibit_login": false,
|
"inhibit_login": false,
|
||||||
});
|
});
|
||||||
let (status, body) = register_post(client, &initial).await?;
|
let (status, body) = register_post(client, as_token, &body).await?;
|
||||||
// Some servers accept the first POST when allow_registration is
|
|
||||||
// open. Take the access_token + we're done.
|
|
||||||
if status.is_success() {
|
|
||||||
return extract_access_token(&body);
|
|
||||||
}
|
|
||||||
if status != StatusCode::UNAUTHORIZED {
|
|
||||||
anyhow::bail!("matrix: /register first leg HTTP {status}, body: {body}");
|
|
||||||
}
|
|
||||||
let session = body["session"]
|
|
||||||
.as_str()
|
|
||||||
.with_context(|| format!("matrix: missing UIAA session in 401, body: {body}"))?;
|
|
||||||
let authed = serde_json::json!({
|
|
||||||
"username": localpart,
|
|
||||||
"password": password,
|
|
||||||
"device_id": format!("hyperhive-{agent}"),
|
|
||||||
"initial_device_display_name": format!("hyperhive ({agent})"),
|
|
||||||
"inhibit_login": false,
|
|
||||||
"auth": {
|
|
||||||
"type": "m.login.registration_token",
|
|
||||||
"token": register_token,
|
|
||||||
"session": session,
|
|
||||||
},
|
|
||||||
});
|
|
||||||
let (status, body) = register_post(client, &authed).await?;
|
|
||||||
if !status.is_success() {
|
if !status.is_success() {
|
||||||
anyhow::bail!("matrix: /register auth leg HTTP {status}, body: {body}");
|
anyhow::bail!("matrix: /register as appservice HTTP {status}, body: {body}");
|
||||||
}
|
}
|
||||||
extract_access_token(&body)
|
extract_access_token(&body)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Log in as an **existing** account using the hive's appservice token,
|
||||||
|
/// and return a fresh access token for it. No password involved: the
|
||||||
|
/// appservice is authorised for every localpart in its namespace, so it
|
||||||
|
/// can mint a session for one without knowing anything about the account.
|
||||||
|
///
|
||||||
|
/// This is the recovery path that used to need a stored password or an
|
||||||
|
/// admin-room password reset — an account whose token file was lost is
|
||||||
|
/// re-tokened from the hive's own identity instead. The device id matches
|
||||||
|
/// [`register_user`]'s, so a re-login replaces that device's token rather
|
||||||
|
/// than accumulating devices.
|
||||||
|
async fn appservice_login(client: &reqwest::Client, as_token: &str, agent: &str) -> Result<String> {
|
||||||
|
let base = matrix_base()?;
|
||||||
|
let url = format!("{base}/_matrix/client/v3/login");
|
||||||
|
let body = serde_json::json!({
|
||||||
|
"type": "m.login.application_service",
|
||||||
|
"identifier": {
|
||||||
|
"type": "m.id.user",
|
||||||
|
"user": user_localpart(agent),
|
||||||
|
},
|
||||||
|
"device_id": format!("hyperhive-{agent}"),
|
||||||
|
"initial_device_display_name": format!("hyperhive ({agent})"),
|
||||||
|
});
|
||||||
|
let resp = client
|
||||||
|
.post(&url)
|
||||||
|
.bearer_auth(as_token)
|
||||||
|
.json(&body)
|
||||||
|
.send()
|
||||||
|
.await
|
||||||
|
.context("matrix: POST /login as appservice")?;
|
||||||
|
let status = resp.status();
|
||||||
|
let json = resp
|
||||||
|
.json::<serde_json::Value>()
|
||||||
|
.await
|
||||||
|
.context("matrix: parse appservice /login response")?;
|
||||||
|
if !status.is_success() {
|
||||||
|
anyhow::bail!("matrix: appservice /login HTTP {status} for {agent}, body: {json}");
|
||||||
|
}
|
||||||
|
extract_access_token(&json)
|
||||||
|
}
|
||||||
|
|
||||||
/// Pull `access_token` out of a successful /register response.
|
/// Pull `access_token` out of a successful /register response.
|
||||||
fn extract_access_token(body: &serde_json::Value) -> Result<String> {
|
fn extract_access_token(body: &serde_json::Value) -> Result<String> {
|
||||||
body["access_token"]
|
body["access_token"]
|
||||||
|
|
@ -637,11 +685,7 @@ async fn admin_room_reset_password(
|
||||||
///
|
///
|
||||||
/// `client` is shared across the sweep so we build one reqwest
|
/// `client` is shared across the sweep so we build one reqwest
|
||||||
/// connection pool for all agents rather than one per call.
|
/// connection pool for all agents rather than one per call.
|
||||||
pub async fn ensure_user_for(
|
pub async fn ensure_user_for(client: &reqwest::Client, name: &str, as_token: &str) -> Result<()> {
|
||||||
client: &reqwest::Client,
|
|
||||||
name: &str,
|
|
||||||
register_token: &str,
|
|
||||||
) -> Result<()> {
|
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
let agent = hive_types::Ident::parse(name)
|
let agent = hive_types::Ident::parse(name)
|
||||||
.map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?;
|
.map_err(|e| anyhow::anyhow!("invalid agent name {name:?}: {e}"))?;
|
||||||
|
|
@ -678,7 +722,7 @@ pub async fn ensure_user_for(
|
||||||
}
|
}
|
||||||
|
|
||||||
let password = random_password()?;
|
let password = random_password()?;
|
||||||
let access_token = match register_user(client, name, register_token, &password).await {
|
let access_token = match register_user(client, name, as_token, &password).await {
|
||||||
Ok(token) => {
|
Ok(token) => {
|
||||||
// Successful registration — persist the password so we can
|
// Successful registration — persist the password so we can
|
||||||
// fall back to login if the token file is deleted later.
|
// fall back to login if the token file is deleted later.
|
||||||
|
|
@ -694,8 +738,21 @@ pub async fn ensure_user_for(
|
||||||
token
|
token
|
||||||
}
|
}
|
||||||
Err(reg_err) if reg_err.to_string().contains("M_USER_IN_USE") => {
|
Err(reg_err) if reg_err.to_string().contains("M_USER_IN_USE") => {
|
||||||
// Account already exists — try to re-login with the stored password.
|
// Account already exists and its token file was lost. The
|
||||||
tracing::info!(%name, "matrix: user already exists, attempting login with stored password");
|
// appservice can mint a session for any account in its
|
||||||
|
// namespace, so this needs no password and no admin rights —
|
||||||
|
// try it before the password paths below, which exist for
|
||||||
|
// accounts created before the appservice did (or named
|
||||||
|
// outside its namespace) and stay as the fallback.
|
||||||
|
tracing::info!(%name, "matrix: user already exists, logging in as the appservice");
|
||||||
|
match appservice_login(client, as_token, name).await {
|
||||||
|
Ok(token) => return finish_user_provisioning(name, &token).await,
|
||||||
|
Err(e) => tracing::warn!(
|
||||||
|
%name,
|
||||||
|
error = ?e,
|
||||||
|
"matrix: appservice login failed; falling back to the stored password"
|
||||||
|
),
|
||||||
|
}
|
||||||
let pw_path = password_path(name);
|
let pw_path = password_path(name);
|
||||||
let stored = if let Some(pw) = std::fs::read_to_string(&pw_path)
|
let stored = if let Some(pw) = std::fs::read_to_string(&pw_path)
|
||||||
.ok()
|
.ok()
|
||||||
|
|
@ -734,11 +791,19 @@ pub async fn ensure_user_for(
|
||||||
Err(other) => return Err(other),
|
Err(other) => return Err(other),
|
||||||
};
|
};
|
||||||
|
|
||||||
|
finish_user_provisioning(name, &access_token).await
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Persist a freshly-obtained agent access token and kick the agent's
|
||||||
|
/// matrix daemon. Shared by every way [`ensure_user_for`] can end up
|
||||||
|
/// holding a token — creation, appservice login, password login — so a
|
||||||
|
/// new recovery path cannot forget half of it.
|
||||||
|
async fn finish_user_provisioning(name: &str, access_token: &str) -> Result<()> {
|
||||||
// Write the token via hive-priv (root helper): hive-c0re runs as the
|
// Write the token via hive-priv (root helper): hive-c0re runs as the
|
||||||
// unprivileged `hive-core` user and cannot write to agent-owned state
|
// unprivileged `hive-core` user and cannot write to agent-owned state
|
||||||
// directories directly. hive-priv writes the file 0600 and chowns it
|
// directories directly. hive-priv writes the file 0600 and chowns it
|
||||||
// to the agent user so it is readable from inside the container.
|
// to the agent user so it is readable from inside the container.
|
||||||
crate::priv_client::write_agent_matrix_token(name, &access_token, None, None)
|
crate::priv_client::write_agent_matrix_token(name, access_token, None, None)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("matrix: write matrix-token for {name} via hive-priv"))?;
|
.with_context(|| format!("matrix: write matrix-token for {name} via hive-priv"))?;
|
||||||
tracing::info!(%name, "matrix: provisioned access token");
|
tracing::info!(%name, "matrix: provisioned access token");
|
||||||
|
|
@ -767,30 +832,30 @@ pub async fn ensure_user_for(
|
||||||
/// behaviour.
|
/// behaviour.
|
||||||
///
|
///
|
||||||
/// **Not idempotent** (unlike [`crate::forge::provision_user_token`]): the
|
/// **Not idempotent** (unlike [`crate::forge::provision_user_token`]): the
|
||||||
/// matrix UIAA `/register` endpoint returns `M_USER_IN_USE` (HTTP 400)
|
/// matrix `/register` endpoint returns `M_USER_IN_USE` (HTTP 400) on a
|
||||||
/// on second call for the same localpart. Callers re-running this for
|
/// second call for the same localpart, appservice-authorised or not.
|
||||||
/// a known-existing matrix user should expect a hard error from this
|
/// Callers re-running this for a known-existing matrix user should expect
|
||||||
/// fn and route to a password-reset path instead.
|
/// a hard error from this fn and route to a password-reset path instead.
|
||||||
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,
|
as_token: &str,
|
||||||
password: &str,
|
password: &str,
|
||||||
) -> Result<String> {
|
) -> Result<String> {
|
||||||
register_user(client, name, register_token, password).await
|
register_user(client, name, as_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.
|
||||||
/// All operations are idempotent; failures are logged as warnings but
|
/// All operations are idempotent; failures are logged as warnings but
|
||||||
/// don't abort the caller.
|
/// don't abort the caller.
|
||||||
pub async fn sync_agent(client: &reqwest::Client, name: &str, register_token: &str) {
|
pub async fn sync_agent(client: &reqwest::Client, name: &str, as_token: &str) {
|
||||||
if let Err(e) = ensure_user_for(client, name, register_token).await {
|
if let Err(e) = ensure_user_for(client, name, as_token).await {
|
||||||
tracing::warn!(%name, error = ?e, "matrix: ensure_user failed");
|
tracing::warn!(%name, error = ?e, "matrix: ensure_user failed");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Standalone per-agent sync that handles its own setup: checks if
|
/// Standalone per-agent sync that handles its own setup: checks if
|
||||||
/// hive-matrix is present, reads the registration token, and builds
|
/// hive-matrix is present, reads the appservice token, and builds
|
||||||
/// an HTTP client before delegating to [`sync_agent`]. Mirrors the
|
/// an HTTP client before delegating to [`sync_agent`]. Mirrors the
|
||||||
/// setup in [`ensure_all`] so the rebuild path and the startup sweep
|
/// setup in [`ensure_all`] so the rebuild path and the startup sweep
|
||||||
/// stay equivalent. No-op when the matrix container is absent.
|
/// stay equivalent. No-op when the matrix container is absent.
|
||||||
|
|
@ -798,10 +863,10 @@ pub async fn sync_agent_standalone(name: &str) {
|
||||||
if !is_present() {
|
if !is_present() {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
let register_token = match ensure_register_token() {
|
let as_token = match read_appservice_token() {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(%name, error = ?e, "matrix: ensure_register_token failed");
|
tracing::warn!(%name, error = ?e, "matrix: read_appservice_token failed");
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -815,22 +880,26 @@ pub async fn sync_agent_standalone(name: &str) {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
sync_agent(&client, name, ®ister_token).await;
|
sync_agent(&client, name, &as_token).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Ensure the hive system admin matrix user exists and its token is
|
/// Ensure the hive system admin matrix user exists, that its token is
|
||||||
/// persisted at [`admin_token_path()`]. Must be called BEFORE
|
/// persisted at [`admin_token_path()`], and that it actually holds admin
|
||||||
/// [`ensure_all`]'s agent loop so this account is the first to register
|
/// rights.
|
||||||
/// and becomes the homeserver admin automatically (Conduit/tuwunel:
|
|
||||||
/// first registered user = admin).
|
|
||||||
///
|
///
|
||||||
/// Idempotent — skips when the token file already exists and is
|
/// **Nothing here depends on registration order any more.** The account
|
||||||
/// non-empty. Does NOT promote the account via API (that requires
|
/// used to have to be the first ever registered, to win tuwunel's
|
||||||
/// admin rights which this fn bootstraps); on a fresh homeserver the
|
/// automatic first-user grant — a rule that cannot fire for an
|
||||||
/// first-registered rule fires automatically; on an existing homeserver
|
/// appservice-created account at all, and one that silently did nothing
|
||||||
/// the operator must promote the account once via
|
/// on a homeserver that already had users. Admin rights now come from an
|
||||||
/// `hivectl matrix promote-user hive` or the conduit admin room.
|
/// explicit `make_user_admin`: the `admin_execute` entry beside the
|
||||||
pub async fn ensure_admin_user(client: &reqwest::Client, register_token: &str) -> Result<()> {
|
/// appservice registration performs it at homeserver startup, and
|
||||||
|
/// [`ensure_admin_rights`] checks the result and says so when it is
|
||||||
|
/// missing.
|
||||||
|
///
|
||||||
|
/// Idempotent — skips the account work when the token file already exists
|
||||||
|
/// and is non-empty.
|
||||||
|
pub async fn ensure_admin_user(client: &reqwest::Client, as_token: &str) -> Result<()> {
|
||||||
use std::os::unix::fs::PermissionsExt;
|
use std::os::unix::fs::PermissionsExt;
|
||||||
let path = admin_token_path();
|
let path = admin_token_path();
|
||||||
if path.exists()
|
if path.exists()
|
||||||
|
|
@ -841,8 +910,7 @@ pub async fn ensure_admin_user(client: &reqwest::Client, register_token: &str) -
|
||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
let password = random_password()?;
|
let password = random_password()?;
|
||||||
let access_token = match register_user(client, HIVE_ADMIN_LOCALPART, register_token, &password)
|
let access_token = match register_user(client, HIVE_ADMIN_LOCALPART, as_token, &password).await
|
||||||
.await
|
|
||||||
{
|
{
|
||||||
Ok(token) => {
|
Ok(token) => {
|
||||||
let pw_path = password_path(HIVE_ADMIN_LOCALPART);
|
let pw_path = password_path(HIVE_ADMIN_LOCALPART);
|
||||||
|
|
@ -857,20 +925,34 @@ pub async fn ensure_admin_user(client: &reqwest::Client, register_token: &str) -
|
||||||
token
|
token
|
||||||
}
|
}
|
||||||
Err(reg_err) if reg_err.to_string().contains("M_USER_IN_USE") => {
|
Err(reg_err) if reg_err.to_string().contains("M_USER_IN_USE") => {
|
||||||
tracing::info!("matrix: hive admin user already exists, re-logging in");
|
// The expected path, not an edge case: this account is the
|
||||||
let pw_path = password_path(HIVE_ADMIN_LOCALPART);
|
// appservice's own `sender_localpart`, so the homeserver
|
||||||
let stored = std::fs::read_to_string(&pw_path)
|
// creates it when it loads the registration — before
|
||||||
.ok()
|
// hive-c0re gets a chance to ask. An appservice login needs
|
||||||
.map(|s| s.trim().to_owned())
|
// no password, which is just as well since an account the
|
||||||
.filter(|s| !s.is_empty())
|
// homeserver created has none.
|
||||||
.with_context(|| {
|
tracing::info!("matrix: hive admin user already exists, logging in as the appservice");
|
||||||
format!(
|
match appservice_login(client, as_token, HIVE_ADMIN_LOCALPART).await {
|
||||||
"matrix: hive admin user exists but password missing at {} — \
|
Ok(token) => token,
|
||||||
manual recovery: reset password via admin API or conduit admin room",
|
Err(e) => {
|
||||||
pw_path.display()
|
tracing::warn!(error = ?e, "matrix: appservice login for the hive admin failed; falling back to the stored password");
|
||||||
)
|
let pw_path = password_path(HIVE_ADMIN_LOCALPART);
|
||||||
})?;
|
let stored = std::fs::read_to_string(&pw_path)
|
||||||
login_user(client, HIVE_ADMIN_LOCALPART, &stored).await?
|
.ok()
|
||||||
|
.map(|s| s.trim().to_owned())
|
||||||
|
.filter(|s| !s.is_empty())
|
||||||
|
.with_context(|| {
|
||||||
|
format!(
|
||||||
|
"matrix: hive admin user exists, appservice login failed, and no \
|
||||||
|
password is stored at {} — check that the registration file's \
|
||||||
|
namespace covers @{HIVE_ADMIN_LOCALPART} and that the homeserver \
|
||||||
|
loaded it",
|
||||||
|
pw_path.display()
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
login_user(client, HIVE_ADMIN_LOCALPART, &stored).await?
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
Err(other) => return Err(other),
|
Err(other) => return Err(other),
|
||||||
};
|
};
|
||||||
|
|
@ -884,9 +966,148 @@ pub async fn ensure_admin_user(client: &reqwest::Client, register_token: &str) -
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Check that the hive admin account holds admin rights, and repair it
|
||||||
|
/// through the admin room when it does not.
|
||||||
|
///
|
||||||
|
/// Admin-ness in tuwunel is membership of the admin room, so that is what
|
||||||
|
/// this reads: the account's own joined-rooms list, which needs no
|
||||||
|
/// privileges to fetch. When the admin room is in it there is nothing to
|
||||||
|
/// do and nothing is sent — worth insisting on, because the repair is a
|
||||||
|
/// message in a room and this runs on every sweep.
|
||||||
|
///
|
||||||
|
/// When it is absent, the repair is attempted anyway (a stale or failed
|
||||||
|
/// read is cheaper to retry than to reason about) and a failure is
|
||||||
|
/// reported rather than raised: agent accounts, the hive Space and the
|
||||||
|
/// chat room all work without an admin, so a hive with an unpromoted
|
||||||
|
/// admin is degraded, not broken. Only `hivectl matrix promote-user` /
|
||||||
|
/// `reset-password` need it.
|
||||||
|
///
|
||||||
|
/// # Errors
|
||||||
|
/// Never — the outcome is the return value. `false` means "not admin and
|
||||||
|
/// could not be made one", already logged with what to do about it.
|
||||||
|
async fn ensure_admin_rights(
|
||||||
|
client: &reqwest::Client,
|
||||||
|
admin_token: &str,
|
||||||
|
server_name: &str,
|
||||||
|
) -> bool {
|
||||||
|
let room_id = match discover_admin_room_id(client, admin_token, server_name).await {
|
||||||
|
Ok(id) => id,
|
||||||
|
Err(e) => {
|
||||||
|
tracing::warn!(error = ?e, "matrix: cannot resolve #admins — not checking the hive admin's rights");
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
match joined_rooms(client, admin_token).await {
|
||||||
|
Some(rooms) if rooms.iter().any(|r| r == &room_id) => {
|
||||||
|
tracing::debug!("matrix: hive admin is in the admin room");
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
Some(_) => {
|
||||||
|
tracing::warn!("matrix: hive admin is not in the admin room — attempting to promote it")
|
||||||
|
}
|
||||||
|
None => tracing::debug!(
|
||||||
|
"matrix: could not read the hive admin's joined rooms; attempting to promote it"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
if let Err(e) =
|
||||||
|
promote_user_to_admin(client, admin_token, HIVE_ADMIN_LOCALPART, server_name).await
|
||||||
|
{
|
||||||
|
// The honest message. Promoting through the admin room requires
|
||||||
|
// being admin already, so the one lever that works on a
|
||||||
|
// homeserver with no admin at all is the `admin_execute` entry in
|
||||||
|
// the hive-matrix module — which runs at startup, which means a
|
||||||
|
// restart is the fix rather than another sweep.
|
||||||
|
tracing::warn!(
|
||||||
|
error = ?e,
|
||||||
|
"matrix: hive admin '@{HIVE_ADMIN_LOCALPART}' holds no admin rights. \
|
||||||
|
The homeserver promotes it at startup (admin_execute in hive-matrix.nix), \
|
||||||
|
so `systemctl restart container@hive-matrix` grants it. Agent accounts and \
|
||||||
|
room provisioning are unaffected; `hivectl matrix promote-user` and \
|
||||||
|
`reset-password` need it."
|
||||||
|
);
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
tracing::info!("matrix: promoted the hive admin through the admin room");
|
||||||
|
true
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The rooms `token`'s account has joined, or `None` when the list could
|
||||||
|
/// not be read. `None` is deliberately not an empty list: "no rooms" and
|
||||||
|
/// "no answer" lead to different decisions in [`ensure_admin_rights`].
|
||||||
|
async fn joined_rooms(client: &reqwest::Client, token: &str) -> Option<Vec<String>> {
|
||||||
|
let base = matrix_http()?;
|
||||||
|
let url = format!("{base}/_matrix/client/v3/joined_rooms");
|
||||||
|
let resp = client.get(&url).bearer_auth(token).send().await.ok()?;
|
||||||
|
if !resp.status().is_success() {
|
||||||
|
return None;
|
||||||
|
}
|
||||||
|
let body = resp.json::<serde_json::Value>().await.ok()?;
|
||||||
|
Some(
|
||||||
|
body["joined_rooms"]
|
||||||
|
.as_array()?
|
||||||
|
.iter()
|
||||||
|
.filter_map(|r| r.as_str().map(ToOwned::to_owned))
|
||||||
|
.collect(),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Whether an admin-room reply says a `make-user-admin` succeeded.
|
||||||
|
///
|
||||||
|
/// Three spellings, because the reply is prose and prose changes between
|
||||||
|
/// builds. tuwunel v1.9.0's is `"<user id> has been granted admin
|
||||||
|
/// privileges."` — which the original two patterns here (`done…`,
|
||||||
|
/// `made…admin`) do not match at all, so a promotion that had already
|
||||||
|
/// worked was reported as a 15-second timeout. The older spellings are
|
||||||
|
/// kept: a homeserver is not necessarily the version this was written
|
||||||
|
/// against.
|
||||||
|
fn is_make_admin_success(body: &str) -> Option<()> {
|
||||||
|
let lower = body.to_ascii_lowercase();
|
||||||
|
let says_ok = lower.starts_with("done")
|
||||||
|
|| lower.contains("granted admin privileges")
|
||||||
|
|| (lower.contains("made") && lower.contains("admin"));
|
||||||
|
says_ok.then_some(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[cfg(test)]
|
||||||
|
mod is_make_admin_success_tests {
|
||||||
|
use super::is_make_admin_success;
|
||||||
|
|
||||||
|
/// The reply tuwunel v1.9.0 actually sends
|
||||||
|
/// (`src/admin/user/make_user_admin.rs`). This is the case the
|
||||||
|
/// pre-existing matcher missed.
|
||||||
|
#[test]
|
||||||
|
fn tuwunel_1_9_grant_reply() {
|
||||||
|
let msg = "@hive:pr1ma.darkest.space has been granted admin privileges.";
|
||||||
|
assert_eq!(is_make_admin_success(msg), Some(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn older_spellings_still_match() {
|
||||||
|
assert_eq!(
|
||||||
|
is_make_admin_success("Done: user is now an admin"),
|
||||||
|
Some(())
|
||||||
|
);
|
||||||
|
assert_eq!(is_make_admin_success("Made @x:y an admin"), Some(()));
|
||||||
|
}
|
||||||
|
|
||||||
|
/// The control: an unrelated or failing reply must not read as
|
||||||
|
/// success, or a failed promotion returns Ok and the warning that
|
||||||
|
/// would have named it never fires.
|
||||||
|
#[test]
|
||||||
|
fn failures_and_noise_do_not_match() {
|
||||||
|
assert_eq!(is_make_admin_success("Command not recognised."), None);
|
||||||
|
assert_eq!(is_make_admin_success("User @x:y does not exist"), None);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Promote a user to homeserver admin via the Matrix admin room
|
/// Promote a user to homeserver admin via the Matrix admin room
|
||||||
/// (`#admins:<server>`). Sends `!admin users make-user-admin @<localpart>:<server>` as
|
/// (`#admins:<server>`). Sends `!admin users make-user-admin @<localpart>:<server>` as
|
||||||
/// @hive, polls for the bot's "Done:" success response.
|
/// @hive, polls for the bot's success reply.
|
||||||
|
///
|
||||||
|
/// ⚠️ Requires the **sender** to be an admin already — tuwunel only
|
||||||
|
/// treats a message as a command when its sender is in the admin room.
|
||||||
|
/// So this promotes a *second* user; it cannot bootstrap the first one.
|
||||||
|
/// That is what the `admin_execute` entry in `hive-matrix.nix` is for.
|
||||||
///
|
///
|
||||||
/// Goes through the admin room rather than a direct HTTP call because
|
/// Goes through the admin room rather than a direct HTTP call because
|
||||||
/// tuwunel implements parts of the Synapse admin API but not user
|
/// tuwunel implements parts of the Synapse admin API but not user
|
||||||
|
|
@ -907,14 +1128,7 @@ pub async fn promote_user_to_admin(
|
||||||
server_name,
|
server_name,
|
||||||
&room_url,
|
&room_url,
|
||||||
&command,
|
&command,
|
||||||
|body| {
|
is_make_admin_success,
|
||||||
let lower = body.to_ascii_lowercase();
|
|
||||||
if lower.starts_with("done") || lower.contains("made") && lower.contains("admin") {
|
|
||||||
Some(())
|
|
||||||
} else {
|
|
||||||
None
|
|
||||||
}
|
|
||||||
},
|
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
.with_context(|| {
|
.with_context(|| {
|
||||||
|
|
@ -1637,10 +1851,14 @@ pub async fn ensure_all() -> bool {
|
||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
let mut ok = true;
|
let mut ok = true;
|
||||||
let register_token = match ensure_register_token() {
|
// Loud and non-destructive: with no appservice token this sweep can
|
||||||
|
// create nothing, so it does nothing. Agents that already hold a
|
||||||
|
// token keep using it — their accounts and sessions are untouched by
|
||||||
|
// anything in here.
|
||||||
|
let as_token = match read_appservice_token() {
|
||||||
Ok(t) => t,
|
Ok(t) => t,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
tracing::warn!(error = ?e, "matrix: ensure_register_token failed");
|
tracing::warn!(error = ?e, "matrix: no appservice token; skipping the user sweep");
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
@ -1656,10 +1874,12 @@ pub async fn ensure_all() -> bool {
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
// Provision hive admin user FIRST so it's the first registered
|
// The hive admin first, because everything below provisions THROUGH
|
||||||
// account on a fresh homeserver (Conduit/tuwunel makes the first
|
// it (the Space, the chat room and every invite are sent with its
|
||||||
// registered user admin automatically).
|
// token). Not, any more, so that it wins a first-registered-user
|
||||||
if let Err(e) = ensure_admin_user(&client, ®ister_token).await {
|
// grant: it holds admin rights by explicit promotion, checked in
|
||||||
|
// `provision_space` once the server name is known.
|
||||||
|
if let Err(e) = ensure_admin_user(&client, &as_token).await {
|
||||||
tracing::warn!(error = ?e, "matrix: ensure_admin_user failed");
|
tracing::warn!(error = ?e, "matrix: ensure_admin_user failed");
|
||||||
ok = false;
|
ok = false;
|
||||||
}
|
}
|
||||||
|
|
@ -1672,7 +1892,7 @@ pub async fn ensure_all() -> bool {
|
||||||
let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else {
|
let Some(name) = c.strip_prefix(crate::lifecycle::AGENT_PREFIX) else {
|
||||||
continue;
|
continue;
|
||||||
};
|
};
|
||||||
sync_agent(&client, name, ®ister_token).await;
|
sync_agent(&client, name, &as_token).await;
|
||||||
agent_names.push(name.to_owned());
|
agent_names.push(name.to_owned());
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -1705,6 +1925,11 @@ async fn provision_space(client: &reqwest::Client, agent_names: &[String]) -> bo
|
||||||
return false;
|
return false;
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
// Before the rooms: the one thing in this sweep that is about the
|
||||||
|
// admin account itself rather than about what it provisions.
|
||||||
|
if !ensure_admin_rights(client, &admin_token, &server_name).await {
|
||||||
|
ok = false;
|
||||||
|
}
|
||||||
let room_id = match ensure_hive_space(client, &admin_token).await {
|
let room_id = match ensure_hive_space(client, &admin_token).await {
|
||||||
Ok(id) => id,
|
Ok(id) => id,
|
||||||
Err(e) => {
|
Err(e) => {
|
||||||
|
|
|
||||||
|
|
@ -253,14 +253,15 @@ pub fn gateway_agents_conf() -> PathBuf {
|
||||||
// `nix/host-modules/hive-c0re/default.nix` and `nix/host-modules/hive-ci.nix` — must match.
|
// `nix/host-modules/hive-c0re/default.nix` and `nix/host-modules/hive-ci.nix` — must match.
|
||||||
pub const FORGE_CORE_TOKEN: &str = "/var/lib/hyperhive/forge-core-token";
|
pub const FORGE_CORE_TOKEN: &str = "/var/lib/hyperhive/forge-core-token";
|
||||||
|
|
||||||
/// `matrix-register-token` — shared matrix registration token.
|
/// `matrix-appservice-token` — the `as_token` of the hive's appservice
|
||||||
// nix: bind-mounted into the tuwunel/matrix container (hive-matrix.nix) — must match.
|
/// registration, which authorises every account this daemon creates.
|
||||||
// Not operator-option-driven: `registrationTokenFile` is `internal` on the nix
|
// nix: minted by the `hive-matrix-appservice` activation script in
|
||||||
// side, and an `assertions` entry there rejects any attempt to move it, so this
|
// `nix/host-modules/hive-matrix.nix`, which renders it into the registration
|
||||||
// literal can never diverge from it.
|
// file the homeserver loads — must match. Read-only here on purpose: a token
|
||||||
|
// minted on this side would not be the one in that file.
|
||||||
#[must_use]
|
#[must_use]
|
||||||
pub fn matrix_register_token() -> PathBuf {
|
pub fn matrix_appservice_token() -> PathBuf {
|
||||||
state_root().join("matrix-register-token")
|
state_root().join("matrix-appservice-token")
|
||||||
}
|
}
|
||||||
|
|
||||||
/// `/run/hyperhive` — the runtime root (host admin socket + per-agent dirs).
|
/// `/run/hyperhive` — the runtime root (host admin socket + per-agent dirs).
|
||||||
|
|
|
||||||
|
|
@ -558,8 +558,8 @@ async fn handle_matrix_create_user(
|
||||||
password: Option<&str>,
|
password: Option<&str>,
|
||||||
) -> Result<HostResponse> {
|
) -> Result<HostResponse> {
|
||||||
require_matrix_present()?;
|
require_matrix_present()?;
|
||||||
let register_token =
|
let as_token =
|
||||||
crate::matrix::ensure_register_token().context("read matrix register token")?;
|
crate::matrix::read_appservice_token().context("read matrix appservice token")?;
|
||||||
let client = matrix_http_client()?;
|
let client = matrix_http_client()?;
|
||||||
let mut out = Vec::new();
|
let mut out = Vec::new();
|
||||||
if agent_exists(name)? {
|
if agent_exists(name)? {
|
||||||
|
|
@ -571,7 +571,7 @@ async fn handle_matrix_create_user(
|
||||||
"matrix create-user: a password is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via access_token"
|
"matrix create-user: a password is for non-agent (operator) accounts only; '{name}' is an agent which authenticates via access_token"
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
crate::matrix::ensure_user_for(&client, name.as_str(), ®ister_token)
|
crate::matrix::ensure_user_for(&client, name.as_str(), &as_token)
|
||||||
.await
|
.await
|
||||||
.with_context(|| format!("matrix create-user {name}"))?;
|
.with_context(|| format!("matrix create-user {name}"))?;
|
||||||
let path = Coordinator::agent_notes_dir(name).join("matrix-token");
|
let path = Coordinator::agent_notes_dir(name).join("matrix-token");
|
||||||
|
|
@ -585,7 +585,7 @@ async fn handle_matrix_create_user(
|
||||||
let token = crate::matrix::provision_user_token(
|
let token = crate::matrix::provision_user_token(
|
||||||
&client,
|
&client,
|
||||||
name.as_str(),
|
name.as_str(),
|
||||||
®ister_token,
|
&as_token,
|
||||||
&effective_password,
|
&effective_password,
|
||||||
)
|
)
|
||||||
.await
|
.await
|
||||||
|
|
@ -770,10 +770,10 @@ async fn handle_push_snapshot(
|
||||||
|
|
||||||
async fn handle_matrix_sync_admin() -> Result<HostResponse> {
|
async fn handle_matrix_sync_admin() -> Result<HostResponse> {
|
||||||
require_matrix_present()?;
|
require_matrix_present()?;
|
||||||
let register_token =
|
let as_token =
|
||||||
crate::matrix::ensure_register_token().context("read matrix register token")?;
|
crate::matrix::read_appservice_token().context("read matrix appservice token")?;
|
||||||
let client = matrix_http_client()?;
|
let client = matrix_http_client()?;
|
||||||
crate::matrix::ensure_admin_user(&client, ®ister_token)
|
crate::matrix::ensure_admin_user(&client, &as_token)
|
||||||
.await
|
.await
|
||||||
.context("matrix sync-admin")?;
|
.context("matrix sync-admin")?;
|
||||||
let path = crate::matrix::admin_token_path();
|
let path = crate::matrix::admin_token_path();
|
||||||
|
|
|
||||||
Loading…
Reference in a new issue