From d9d424323cc8c2bf1792360bbf0f74dc05d94df0 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Mon, 14 Jul 2025 01:09:52 -0400 Subject: [PATCH 1/3] Let admin API add users synchronously as opposed to always launching an asynchronous worker job. This allows callers to have a guarantee that the user is fully created by the time it receives the response to the user creation request. --- crates/handlers/src/admin/v1/users/add.rs | 45 +++++++++++++++++++++-- 1 file changed, 41 insertions(+), 4 deletions(-) diff --git a/crates/handlers/src/admin/v1/users/add.rs b/crates/handlers/src/admin/v1/users/add.rs index b8bd9ccf0..7ba2ce4ef 100644 --- a/crates/handlers/src/admin/v1/users/add.rs +++ b/crates/handlers/src/admin/v1/users/add.rs @@ -10,7 +10,7 @@ use aide::{NoApi, OperationIo, transform::TransformOperation}; use axum::{Json, extract::State, response::IntoResponse}; use hyper::StatusCode; use mas_axum_utils::record_error; -use mas_matrix::HomeserverConnection; +use mas_matrix::{HomeserverConnection, ProvisionRequest}; use mas_storage::{ BoxRng, queue::{ProvisionUserJob, QueueJobRepositoryExt as _}, @@ -106,6 +106,10 @@ pub struct Request { /// tokens (like with admin access) for them #[serde(default)] skip_homeserver_check: bool, + + /// Delay the response until the user has been created on the homeserver. + #[serde(default)] + add_synchronously: bool, } pub fn doc(operation: TransformOperation) -> TransformOperation { @@ -168,9 +172,19 @@ pub async fn handler( let user = repo.user().add(&mut rng, &clock, params.username).await?; - repo.queue_job() - .schedule_job(&mut rng, &clock, ProvisionUserJob::new(&user)) - .await?; + if params.add_synchronously { + homeserver + .provision_user(&ProvisionRequest::new( + homeserver.mxid(&user.username), + &user.sub, + )) + .await + .map_err(RouteError::Homeserver)?; + } else { + repo.queue_job() + .schedule_job(&mut rng, &clock, ProvisionUserJob::new(&user)) + .await?; + } repo.save().await?; @@ -183,6 +197,7 @@ pub async fn handler( #[cfg(test)] mod tests { use hyper::{Request, StatusCode}; + use mas_matrix::HomeserverConnection; use mas_storage::{RepositoryAccess, user::UserRepository}; use sqlx::PgPool; @@ -220,6 +235,28 @@ mod tests { assert_eq!(user.username, "alice"); } + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] + async fn test_add_user_synchronously(pool: PgPool) { + setup(); + let mut state = TestState::from_pool(pool).await.unwrap(); + let token = state.token_with_scope("urn:mas:admin").await; + + let request = Request::post("/api/admin/v1/users") + .bearer(&token) + .json(serde_json::json!({ + "username": "alice", + "add_synchronously": true, + })); + + let response = state.request(request).await; + response.assert_status(StatusCode::CREATED); + + // Check that the user was created on the homeserver + let mxid = state.homeserver_connection.mxid("alice"); + let result = state.homeserver_connection.query_user(&mxid).await; + assert!(result.is_ok()); + } + #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] async fn test_add_user_invalid_username(pool: PgPool) { setup(); From e792b628282898dc1533c4a709164f961747196c Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Mon, 14 Jul 2025 01:44:50 -0400 Subject: [PATCH 2/3] Update schema --- docs/api/spec.json | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/api/spec.json b/docs/api/spec.json index 0082ea37c..2664808bc 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -3807,6 +3807,11 @@ "description": "Skip checking with the homeserver whether the username is available.\n\nUse this with caution! The main reason to use this, is when a user used by an application service needs to exist in MAS to craft special tokens (like with admin access) for them", "default": false, "type": "boolean" + }, + "add_synchronously": { + "description": "Delay the response until the user has been created on the homeserver.", + "default": false, + "type": "boolean" } } }, From 0d613a5203402bbea327cbdb46dd0f654e6b31d2 Mon Sep 17 00:00:00 2001 From: Andrew Ferrazzutti Date: Tue, 15 Jul 2025 11:49:41 -0400 Subject: [PATCH 3/3] Always add users synchronously with admin API --- crates/handlers/src/admin/v1/users/add.rs | 46 ++++------------------- docs/api/spec.json | 5 --- 2 files changed, 8 insertions(+), 43 deletions(-) diff --git a/crates/handlers/src/admin/v1/users/add.rs b/crates/handlers/src/admin/v1/users/add.rs index 7ba2ce4ef..299012d91 100644 --- a/crates/handlers/src/admin/v1/users/add.rs +++ b/crates/handlers/src/admin/v1/users/add.rs @@ -11,10 +11,7 @@ use axum::{Json, extract::State, response::IntoResponse}; use hyper::StatusCode; use mas_axum_utils::record_error; use mas_matrix::{HomeserverConnection, ProvisionRequest}; -use mas_storage::{ - BoxRng, - queue::{ProvisionUserJob, QueueJobRepositoryExt as _}, -}; +use mas_storage::BoxRng; use schemars::JsonSchema; use serde::Deserialize; use tracing::warn; @@ -106,10 +103,6 @@ pub struct Request { /// tokens (like with admin access) for them #[serde(default)] skip_homeserver_check: bool, - - /// Delay the response until the user has been created on the homeserver. - #[serde(default)] - add_synchronously: bool, } pub fn doc(operation: TransformOperation) -> TransformOperation { @@ -172,19 +165,13 @@ pub async fn handler( let user = repo.user().add(&mut rng, &clock, params.username).await?; - if params.add_synchronously { - homeserver - .provision_user(&ProvisionRequest::new( - homeserver.mxid(&user.username), - &user.sub, - )) - .await - .map_err(RouteError::Homeserver)?; - } else { - repo.queue_job() - .schedule_job(&mut rng, &clock, ProvisionUserJob::new(&user)) - .await?; - } + homeserver + .provision_user(&ProvisionRequest::new( + homeserver.mxid(&user.username), + &user.sub, + )) + .await + .map_err(RouteError::Homeserver)?; repo.save().await?; @@ -233,23 +220,6 @@ mod tests { .unwrap(); assert_eq!(user.username, "alice"); - } - - #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")] - async fn test_add_user_synchronously(pool: PgPool) { - setup(); - let mut state = TestState::from_pool(pool).await.unwrap(); - let token = state.token_with_scope("urn:mas:admin").await; - - let request = Request::post("/api/admin/v1/users") - .bearer(&token) - .json(serde_json::json!({ - "username": "alice", - "add_synchronously": true, - })); - - let response = state.request(request).await; - response.assert_status(StatusCode::CREATED); // Check that the user was created on the homeserver let mxid = state.homeserver_connection.mxid("alice"); diff --git a/docs/api/spec.json b/docs/api/spec.json index 2664808bc..0082ea37c 100644 --- a/docs/api/spec.json +++ b/docs/api/spec.json @@ -3807,11 +3807,6 @@ "description": "Skip checking with the homeserver whether the username is available.\n\nUse this with caution! The main reason to use this, is when a user used by an application service needs to exist in MAS to craft special tokens (like with admin access) for them", "default": false, "type": "boolean" - }, - "add_synchronously": { - "description": "Delay the response until the user has been created on the homeserver.", - "default": false, - "type": "boolean" } } },