mas_handlers/admin/v1/users/
add.rs

1// Copyright 2024 New Vector Ltd.
2// Copyright 2024 The Matrix.org Foundation C.I.C.
3//
4// SPDX-License-Identifier: AGPL-3.0-only
5// Please see LICENSE in the repository root for full details.
6
7use 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
41// XXX: this should be shared with the graphql handler
42fn username_valid(username: &str) -> bool {
43    if username.is_empty() || username.len() > 255 {
44        return false;
45    }
46
47    // Should not start with an underscore
48    if username.starts_with('_') {
49        return false;
50    }
51
52    // Should only contain valid characters
53    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/// # JSON payload for the `POST /api/admin/v1/users` endpoint
94#[derive(Deserialize, JsonSchema)]
95#[serde(rename = "AddUserRequest")]
96pub struct Request {
97    /// The username of the user to add.
98    username: String,
99
100    /// Skip checking with the homeserver whether the username is available.
101    ///
102    /// Use this with caution! The main reason to use this, is when a user used
103    /// by an application service needs to exist in MAS to craft special
104    /// tokens (like with admin access) for them
105    #[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(&params.username).await? {
144        return Err(RouteError::UserAlreadyExists);
145    }
146
147    // Do some basic check on the username
148    if !username_valid(&params.username) {
149        return Err(RouteError::UsernameNotValid);
150    }
151
152    // Ask the homeserver if the username is available
153    let homeserver_available = homeserver
154        .is_localpart_available(&params.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        // If we skipped the check, we still want to shout about it
164        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        // Check that the user was created in the database
210        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        // Reserve a username on the homeserver and try to add it
279        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        // But we can force it with the skip_homeserver_check flag
296        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        // Check that the user was created in the database
311        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}