fix: replace Synapse admin API in promote_user_to_admin with admin-room command

This commit is contained in:
atlas 2026-06-04 15:17:20 +02:00
commit 2252eac650

View file

@ -794,33 +794,106 @@ pub async fn ensure_admin_user(client: &reqwest::Client, register_token: &str) -
Ok(())
}
/// Call `PUT /_synapse/admin/v2/users/@{localpart}:{server_name}` with
/// `{"admin": true}` to promote a user to homeserver admin.
/// Requires the hive admin access token at [`admin_token_path()`].
/// Promote a user to homeserver admin via the Matrix admin room
/// (`#admins:<server>`). Sends `make-user-admin @<localpart>:<server>` to
/// the room as @hive, then polls for the bot's success response.
///
/// tuwunel 1.6.x does not implement `/_synapse/admin/v2/users`; all admin
/// operations go through the admin room.
pub async fn promote_user_to_admin(
client: &reqwest::Client,
admin_token: &str,
localpart: &str,
server_name: &str,
) -> Result<()> {
// URL-encode the @user:server path segment manually — only `@` and
// `:` need escaping; localpart + server_name use only safe chars.
let url = format!("{MATRIX_HTTP}/_synapse/admin/v2/users/%40{localpart}%3A{server_name}");
let resp = client
.put(&url)
let room_id = discover_admin_room_id(client, admin_token, server_name).await?;
let room_url = encode_room_id_for_url(&room_id);
// Anchor for forward-poll — grab current end-token before sending.
let msgs_back_url = format!(
"{MATRIX_HTTP}/_matrix/client/v3/rooms/{room_url}/messages?dir=b&limit=1"
);
let msgs_back = client
.get(&msgs_back_url)
.bearer_auth(admin_token)
.json(&serde_json::json!({"admin": true}))
.send()
.await
.context("matrix: PUT /_synapse/admin/v2/users (promote)")?;
let status = resp.status();
if status.is_success() {
return Ok(());
.context("matrix: admin room messages (back, promote)")?
.json::<serde_json::Value>()
.await
.context("matrix: parse admin room messages response (promote)")?;
let mut from_token = msgs_back["end"]
.as_str()
.ok_or_else(|| {
anyhow::anyhow!(
"matrix: admin room messages response missing 'end' token: {msgs_back}"
)
})?
.to_owned();
// Send the promote command.
let txn_id = random_hex(8)?;
let send_url = format!(
"{MATRIX_HTTP}/_matrix/client/v3/rooms/{room_url}/send/m.room.message/{txn_id}"
);
let command = format!("make-user-admin @{localpart}:{server_name}");
let send_resp = client
.put(&send_url)
.bearer_auth(admin_token)
.json(&serde_json::json!({"msgtype": "m.text", "body": command}))
.send()
.await
.context("matrix: PUT admin room message (promote)")?;
if !send_resp.status().is_success() {
let body = send_resp
.json::<serde_json::Value>()
.await
.unwrap_or_default();
anyhow::bail!("matrix: admin room send failed (promote): {body}");
}
let body = resp.json::<serde_json::Value>().await.unwrap_or_default();
// Poll for a bot "Done:" response (up to 5 × 1 s).
for attempt in 0..5_u8 {
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
let from_encoded = from_token.replace(':', "%3A").replace('+', "%2B");
let poll_url = format!(
"{MATRIX_HTTP}/_matrix/client/v3/rooms/{room_url}/messages?dir=f&from={from_encoded}&limit=10"
);
let poll_json = client
.get(&poll_url)
.bearer_auth(admin_token)
.send()
.await
.context("matrix: admin room poll (promote)")?
.json::<serde_json::Value>()
.await
.context("matrix: parse admin room poll response (promote)")?;
if let Some(events) = poll_json["chunk"].as_array() {
let own_user_id = format!("@{HIVE_ADMIN_LOCALPART}:{server_name}");
for event in events {
if event["type"].as_str() != Some("m.room.message") {
continue;
}
if event["sender"].as_str() == Some(own_user_id.as_str()) {
continue;
}
let body = event["content"]["body"].as_str().unwrap_or("").to_ascii_lowercase();
if body.starts_with("done") || body.contains("made") && body.contains("admin") {
tracing::info!(%localpart, attempt, "matrix: admin room make-user-admin succeeded");
return Ok(());
}
}
}
if let Some(end) = poll_json["end"].as_str() {
from_token = end.to_owned();
}
}
anyhow::bail!(
"matrix: promote @{localpart}:{server_name} to admin: HTTP {status}, body: {body}\n\
note: tuwunel must implement /_synapse/admin/v2/users; if 404 use the conduit admin room instead"
"matrix: admin room make-user-admin for @{localpart}:{server_name}: no success \
response within 5 seconds. Command sent: '{command}'. \
Verify the admin room accepts 'make-user-admin @user:server' commands."
)
}