mas_handlers/admin/v1/users/
add.rs1use std::sync::Arc;
8
9use aide::{NoApi, OperationIo, transform::TransformOperation};
10use axum::{Json, extract::State, response::IntoResponse};
11use hyper::StatusCode;
12use mas_axum_utils::record_error;
13use mas_matrix::HomeserverConnection;
14use mas_storage::{
15 BoxRng,
16 queue::{ProvisionUserJob, QueueJobRepositoryExt as _},
17};
18use schemars::JsonSchema;
19use serde::Deserialize;
20use tracing::warn;
21
22use crate::{
23 admin::{
24 call_context::CallContext,
25 model::User,
26 response::{ErrorResponse, SingleResponse},
27 },
28 impl_from_error_for_route,
29};
30
31fn valid_username_character(c: char) -> bool {
32 c.is_ascii_lowercase()
33 || c.is_ascii_digit()
34 || c == '='
35 || c == '_'
36 || c == '-'
37 || c == '.'
38 || c == '/'
39 || c == '+'
40}
41
42fn username_valid(username: &str) -> bool {
44 if username.is_empty() || username.len() > 255 {
45 return false;
46 }
47
48 if username.starts_with('_') {
50 return false;
51 }
52
53 if !username.chars().all(valid_username_character) {
55 return false;
56 }
57
58 true
59}
60
61#[derive(Debug, thiserror::Error, OperationIo)]
62#[aide(output_with = "Json<ErrorResponse>")]
63pub enum RouteError {
64 #[error(transparent)]
65 Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
66
67 #[error(transparent)]
68 Homeserver(anyhow::Error),
69
70 #[error("Username is not valid")]
71 UsernameNotValid,
72
73 #[error("User already exists")]
74 UserAlreadyExists,
75
76 #[error("Username is reserved by the homeserver")]
77 UsernameReserved,
78}
79
80impl_from_error_for_route!(mas_storage::RepositoryError);
81
82impl IntoResponse for RouteError {
83 fn into_response(self) -> axum::response::Response {
84 let error = ErrorResponse::from_error(&self);
85 let sentry_event_id = record_error!(self, Self::Internal(_) | Self::Homeserver(_));
86 let status = match self {
87 Self::Internal(_) | Self::Homeserver(_) => StatusCode::INTERNAL_SERVER_ERROR,
88 Self::UsernameNotValid => StatusCode::BAD_REQUEST,
89 Self::UserAlreadyExists | Self::UsernameReserved => StatusCode::CONFLICT,
90 };
91 (status, sentry_event_id, Json(error)).into_response()
92 }
93}
94
95#[derive(Deserialize, JsonSchema)]
97#[serde(rename = "AddUserRequest")]
98pub struct Request {
99 username: String,
101
102 #[serde(default)]
108 skip_homeserver_check: bool,
109}
110
111pub fn doc(operation: TransformOperation) -> TransformOperation {
112 operation
113 .id("createUser")
114 .summary("Create a new user")
115 .tag("user")
116 .response_with::<201, Json<SingleResponse<User>>, _>(|t| {
117 let [sample, ..] = User::samples();
118 let response = SingleResponse::new_canonical(sample);
119 t.description("User was created").example(response)
120 })
121 .response_with::<400, RouteError, _>(|t| {
122 let response = ErrorResponse::from_error(&RouteError::UsernameNotValid);
123 t.description("Username is not valid").example(response)
124 })
125 .response_with::<409, RouteError, _>(|t| {
126 let response = ErrorResponse::from_error(&RouteError::UserAlreadyExists);
127 t.description("User already exists").example(response)
128 })
129 .response_with::<409, RouteError, _>(|t| {
130 let response = ErrorResponse::from_error(&RouteError::UsernameReserved);
131 t.description("Username is reserved by the homeserver")
132 .example(response)
133 })
134}
135
136#[tracing::instrument(name = "handler.admin.v1.users.add", skip_all)]
137pub async fn handler(
138 CallContext {
139 mut repo, clock, ..
140 }: CallContext,
141 NoApi(mut rng): NoApi<BoxRng>,
142 State(homeserver): State<Arc<dyn HomeserverConnection>>,
143 Json(params): Json<Request>,
144) -> Result<(StatusCode, Json<SingleResponse<User>>), RouteError> {
145 if repo.user().exists(¶ms.username).await? {
146 return Err(RouteError::UserAlreadyExists);
147 }
148
149 if !username_valid(¶ms.username) {
151 return Err(RouteError::UsernameNotValid);
152 }
153
154 let homeserver_available = homeserver
156 .is_localpart_available(¶ms.username)
157 .await
158 .map_err(RouteError::Homeserver)?;
159
160 if !homeserver_available {
161 if !params.skip_homeserver_check {
162 return Err(RouteError::UsernameReserved);
163 }
164
165 warn!("Skipped homeserver check for username {}", params.username);
167 }
168
169 let user = repo.user().add(&mut rng, &clock, params.username).await?;
170
171 repo.queue_job()
172 .schedule_job(&mut rng, &clock, ProvisionUserJob::new(&user))
173 .await?;
174
175 repo.save().await?;
176
177 Ok((
178 StatusCode::CREATED,
179 Json(SingleResponse::new_canonical(User::from(user))),
180 ))
181}
182
183#[cfg(test)]
184mod tests {
185 use hyper::{Request, StatusCode};
186 use mas_storage::{RepositoryAccess, user::UserRepository};
187 use sqlx::PgPool;
188
189 use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
190
191 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
192 async fn test_add_user(pool: PgPool) {
193 setup();
194 let mut state = TestState::from_pool(pool).await.unwrap();
195 let token = state.token_with_scope("urn:mas:admin").await;
196
197 let request = Request::post("/api/admin/v1/users")
198 .bearer(&token)
199 .json(serde_json::json!({
200 "username": "alice",
201 }));
202
203 let response = state.request(request).await;
204 response.assert_status(StatusCode::CREATED);
205
206 let body: serde_json::Value = response.json();
207 assert_eq!(body["data"]["type"], "user");
208 let id = body["data"]["id"].as_str().unwrap();
209 assert_eq!(body["data"]["attributes"]["username"], "alice");
210
211 let mut repo = state.repository().await.unwrap();
213 let user = repo
214 .user()
215 .lookup(id.parse().unwrap())
216 .await
217 .unwrap()
218 .unwrap();
219
220 assert_eq!(user.username, "alice");
221 }
222
223 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
224 async fn test_add_user_invalid_username(pool: PgPool) {
225 setup();
226 let mut state = TestState::from_pool(pool).await.unwrap();
227 let token = state.token_with_scope("urn:mas:admin").await;
228
229 let request = Request::post("/api/admin/v1/users")
230 .bearer(&token)
231 .json(serde_json::json!({
232 "username": "this is invalid",
233 }));
234
235 let response = state.request(request).await;
236 response.assert_status(StatusCode::BAD_REQUEST);
237
238 let body: serde_json::Value = response.json();
239 assert_eq!(body["errors"][0]["title"], "Username is not valid");
240 }
241
242 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
243 async fn test_add_user_exists(pool: PgPool) {
244 setup();
245 let mut state = TestState::from_pool(pool).await.unwrap();
246 let token = state.token_with_scope("urn:mas:admin").await;
247
248 let request = Request::post("/api/admin/v1/users")
249 .bearer(&token)
250 .json(serde_json::json!({
251 "username": "alice",
252 }));
253
254 let response = state.request(request).await;
255 response.assert_status(StatusCode::CREATED);
256
257 let body: serde_json::Value = response.json();
258 assert_eq!(body["data"]["type"], "user");
259 assert_eq!(body["data"]["attributes"]["username"], "alice");
260
261 let request = Request::post("/api/admin/v1/users")
262 .bearer(&token)
263 .json(serde_json::json!({
264 "username": "alice",
265 }));
266
267 let response = state.request(request).await;
268 response.assert_status(StatusCode::CONFLICT);
269
270 let body: serde_json::Value = response.json();
271 assert_eq!(body["errors"][0]["title"], "User already exists");
272 }
273
274 #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
275 async fn test_add_user_reserved(pool: PgPool) {
276 setup();
277 let mut state = TestState::from_pool(pool).await.unwrap();
278 let token = state.token_with_scope("urn:mas:admin").await;
279
280 state.homeserver_connection.reserve_localpart("bob").await;
282
283 let request = Request::post("/api/admin/v1/users")
284 .bearer(&token)
285 .json(serde_json::json!({
286 "username": "bob",
287 }));
288
289 let response = state.request(request).await;
290
291 let body: serde_json::Value = response.json();
292 assert_eq!(
293 body["errors"][0]["title"],
294 "Username is reserved by the homeserver"
295 );
296
297 let request = Request::post("/api/admin/v1/users")
299 .bearer(&token)
300 .json(serde_json::json!({
301 "username": "bob",
302 "skip_homeserver_check": true,
303 }));
304
305 let response = state.request(request).await;
306 response.assert_status(StatusCode::CREATED);
307
308 let body: serde_json::Value = response.json();
309 let id = body["data"]["id"].as_str().unwrap();
310 assert_eq!(body["data"]["attributes"]["username"], "bob");
311
312 let mut repo = state.repository().await.unwrap();
314 let user = repo
315 .user()
316 .lookup(id.parse().unwrap())
317 .await
318 .unwrap()
319 .unwrap();
320
321 assert_eq!(user.username, "bob");
322 }
323}