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