mas_handlers/admin/v1/users/
set_admin.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 aide::{OperationIo, transform::TransformOperation};
8use axum::{Json, response::IntoResponse};
9use hyper::StatusCode;
10use mas_axum_utils::record_error;
11use schemars::JsonSchema;
12use serde::Deserialize;
13use ulid::Ulid;
14
15use crate::{
16    admin::{
17        call_context::CallContext,
18        model::{Resource, User},
19        params::UlidPathParam,
20        response::{ErrorResponse, SingleResponse},
21    },
22    impl_from_error_for_route,
23};
24
25#[derive(Debug, thiserror::Error, OperationIo)]
26#[aide(output_with = "Json<ErrorResponse>")]
27pub enum RouteError {
28    #[error(transparent)]
29    Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
30
31    #[error("User ID {0} not found")]
32    NotFound(Ulid),
33}
34
35impl_from_error_for_route!(mas_storage::RepositoryError);
36
37impl IntoResponse for RouteError {
38    fn into_response(self) -> axum::response::Response {
39        let error = ErrorResponse::from_error(&self);
40        let sentry_event_id = record_error!(self, Self::Internal(_));
41        let status = match self {
42            Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
43            Self::NotFound(_) => StatusCode::NOT_FOUND,
44        };
45        (status, sentry_event_id, Json(error)).into_response()
46    }
47}
48
49/// # JSON payload for the `POST /api/admin/v1/users/:id/set-admin` endpoint
50#[derive(Deserialize, JsonSchema)]
51#[serde(rename = "UserSetAdminRequest")]
52pub struct Request {
53    /// Whether the user can request admin privileges.
54    admin: bool,
55}
56
57pub fn doc(operation: TransformOperation) -> TransformOperation {
58    operation
59        .id("userSetAdmin")
60        .summary("Set whether a user can request admin")
61        .description("Calling this endpoint will not have any effect on existing sessions, meaning that their existing sessions will keep admin access if they were granted it.")
62        .tag("user")
63        .response_with::<200, Json<SingleResponse<User>>, _>(|t| {
64            // In the samples, the second user is the one which can request admin
65            let [_alice, bob, ..] = User::samples();
66            let id = bob.id();
67            let response = SingleResponse::new(bob, format!("/api/admin/v1/users/{id}/set-admin"));
68            t.description("User had admin privileges set").example(response)
69        })
70        .response_with::<404, RouteError, _>(|t| {
71            let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
72            t.description("User ID not found").example(response)
73        })
74}
75
76#[tracing::instrument(name = "handler.admin.v1.users.set_admin", skip_all)]
77pub async fn handler(
78    CallContext { mut repo, .. }: CallContext,
79    id: UlidPathParam,
80    Json(params): Json<Request>,
81) -> Result<Json<SingleResponse<User>>, RouteError> {
82    let id = *id;
83    let user = repo
84        .user()
85        .lookup(id)
86        .await?
87        .ok_or(RouteError::NotFound(id))?;
88
89    let user = repo
90        .user()
91        .set_can_request_admin(user, params.admin)
92        .await?;
93
94    repo.save().await?;
95
96    Ok(Json(SingleResponse::new(
97        User::from(user),
98        format!("/api/admin/v1/users/{id}/set-admin"),
99    )))
100}
101
102#[cfg(test)]
103mod tests {
104    use hyper::{Request, StatusCode};
105    use mas_storage::{RepositoryAccess, user::UserRepository};
106    use sqlx::PgPool;
107
108    use crate::test_utils::{RequestBuilderExt, ResponseExt, TestState, setup};
109
110    #[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
111    async fn test_change_can_request_admin(pool: PgPool) {
112        setup();
113        let mut state = TestState::from_pool(pool).await.unwrap();
114        let token = state.token_with_scope("urn:mas:admin").await;
115
116        let mut repo = state.repository().await.unwrap();
117        let user = repo
118            .user()
119            .add(&mut state.rng(), &state.clock, "alice".to_owned())
120            .await
121            .unwrap();
122        repo.save().await.unwrap();
123
124        let request = Request::post(format!("/api/admin/v1/users/{}/set-admin", user.id))
125            .bearer(&token)
126            .json(serde_json::json!({
127                "admin": true,
128            }));
129
130        let response = state.request(request).await;
131        response.assert_status(StatusCode::OK);
132        let body: serde_json::Value = response.json();
133
134        assert_eq!(body["data"]["attributes"]["admin"], true);
135
136        // Look at the state from the repository
137        let mut repo = state.repository().await.unwrap();
138        let user = repo.user().lookup(user.id).await.unwrap().unwrap();
139        assert!(user.can_request_admin);
140        repo.save().await.unwrap();
141
142        // Flip it back
143        let request = Request::post(format!("/api/admin/v1/users/{}/set-admin", user.id))
144            .bearer(&token)
145            .json(serde_json::json!({
146                "admin": false,
147            }));
148
149        let response = state.request(request).await;
150        response.assert_status(StatusCode::OK);
151        let body: serde_json::Value = response.json();
152
153        assert_eq!(body["data"]["attributes"]["admin"], false);
154
155        // Look at the state from the repository
156        let mut repo = state.repository().await.unwrap();
157        let user = repo.user().lookup(user.id).await.unwrap().unwrap();
158        assert!(!user.can_request_admin);
159        repo.save().await.unwrap();
160    }
161}