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