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_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
42// XXX: this should be shared with the graphql handler
43fn username_valid(username: &str) -> bool {
44    if username.is_empty() || username.len() > 255 {
45        return false;
46    }
47
48    // Should not start with an underscore
49    if username.starts_with('_') {
50        return false;
51    }
52
53    // Should only contain valid characters
54    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/// # JSON payload for the `POST /api/admin/v1/users` endpoint
96#[derive(Deserialize, JsonSchema)]
97#[serde(rename = "AddUserRequest")]
98pub struct Request {
99    /// The username of the user to add.
100    username: String,
101
102    /// Skip checking with the homeserver whether the username is available.
103    ///
104    /// Use this with caution! The main reason to use this, is when a user used
105    /// by an application service needs to exist in MAS to craft special
106    /// tokens (like with admin access) for them
107    #[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(&params.username).await? {
146        return Err(RouteError::UserAlreadyExists);
147    }
148
149    // Do some basic check on the username
150    if !username_valid(&params.username) {
151        return Err(RouteError::UsernameNotValid);
152    }
153
154    // Ask the homeserver if the username is available
155    let homeserver_available = homeserver
156        .is_localpart_available(&params.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        // If we skipped the check, we still want to shout about it
166        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        // Check that the user was created in the database
212        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        // Reserve a username on the homeserver and try to add it
281        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        // But we can force it with the skip_homeserver_check flag
298        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        // Check that the user was created in the database
313        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}