Skip to content
This repository was archived by the owner on Sep 10, 2024. It is now read-only.

Commit d6af0b9

Browse files
committed
admin: set can_request_admin API
1 parent 9ea77a9 commit d6af0b9

File tree

4 files changed

+274
-0
lines changed

4 files changed

+274
-0
lines changed

crates/handlers/src/admin/v1/mod.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,13 @@ where
4545
"/users/by-username/:username",
4646
get_with(self::users::by_username, self::users::by_username_doc),
4747
)
48+
.api_route(
49+
"/users/:id/set-can-request-admin",
50+
post_with(
51+
self::users::set_can_request_admin,
52+
self::users::set_can_request_admin_doc,
53+
),
54+
)
4855
.api_route(
4956
"/users/:id/deactivate",
5057
post_with(self::users::deactivate, self::users::deactivate_doc),

crates/handlers/src/admin/v1/users/mod.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,6 +18,7 @@ mod deactivate;
1818
mod get;
1919
mod list;
2020
mod lock;
21+
mod set_can_request_admin;
2122
mod unlock;
2223

2324
pub use self::{
@@ -27,5 +28,6 @@ pub use self::{
2728
get::{doc as get_doc, handler as get},
2829
list::{doc as list_doc, handler as list},
2930
lock::{doc as lock_doc, handler as lock},
31+
set_can_request_admin::{doc as set_can_request_admin_doc, handler as set_can_request_admin},
3032
unlock::{doc as unlock_doc, handler as unlock},
3133
};
Lines changed: 173 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,173 @@
1+
// Copyright 2024 The Matrix.org Foundation C.I.C.
2+
//
3+
// Licensed under the Apache License, Version 2.0 (the "License");
4+
// you may not use this file except in compliance with the License.
5+
// You may obtain a copy of the License at
6+
//
7+
// http://www.apache.org/licenses/LICENSE-2.0
8+
//
9+
// Unless required by applicable law or agreed to in writing, software
10+
// distributed under the License is distributed on an "AS IS" BASIS,
11+
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
// See the License for the specific language governing permissions and
13+
// limitations under the License.
14+
15+
use aide::{transform::TransformOperation, OperationIo};
16+
use axum::{response::IntoResponse, Json};
17+
use hyper::StatusCode;
18+
use schemars::JsonSchema;
19+
use serde::Deserialize;
20+
use ulid::Ulid;
21+
22+
use crate::{
23+
admin::{
24+
call_context::CallContext,
25+
model::{Resource, User},
26+
params::UlidPathParam,
27+
response::{ErrorResponse, SingleResponse},
28+
},
29+
impl_from_error_for_route,
30+
};
31+
32+
#[derive(Debug, thiserror::Error, OperationIo)]
33+
#[aide(output_with = "Json<ErrorResponse>")]
34+
pub enum RouteError {
35+
#[error(transparent)]
36+
Internal(Box<dyn std::error::Error + Send + Sync + 'static>),
37+
38+
#[error("User ID {0} not found")]
39+
NotFound(Ulid),
40+
}
41+
42+
impl_from_error_for_route!(mas_storage::RepositoryError);
43+
44+
impl IntoResponse for RouteError {
45+
fn into_response(self) -> axum::response::Response {
46+
let error = ErrorResponse::from_error(&self);
47+
let status = match self {
48+
Self::Internal(_) => StatusCode::INTERNAL_SERVER_ERROR,
49+
Self::NotFound(_) => StatusCode::NOT_FOUND,
50+
};
51+
(status, Json(error)).into_response()
52+
}
53+
}
54+
55+
/// # JSON payload for the `POST /api/admin/v1/users/:id/set-can-request-admin` endpoint
56+
#[derive(Deserialize, JsonSchema)]
57+
#[serde(rename = "SetCanRequestAdminRequest")]
58+
pub struct Request {
59+
/// Whether the user can request admin privileges.
60+
can_request_admin: bool,
61+
}
62+
63+
pub fn doc(operation: TransformOperation) -> TransformOperation {
64+
operation
65+
.id("setCanRequestAdmin")
66+
.summary("Set whether a user can request admin")
67+
.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.")
68+
.tag("user")
69+
.response_with::<200, Json<SingleResponse<User>>, _>(|t| {
70+
// In the samples, the second user is the one which can request admin
71+
let [_alice, bob, ..] = User::samples();
72+
let id = bob.id();
73+
let response = SingleResponse::new(bob, format!("/api/admin/v1/users/{id}/set-can-request-admin"));
74+
t.description("User had admin privileges set").example(response)
75+
})
76+
.response_with::<404, RouteError, _>(|t| {
77+
let response = ErrorResponse::from_error(&RouteError::NotFound(Ulid::nil()));
78+
t.description("User ID not found").example(response)
79+
})
80+
}
81+
82+
#[tracing::instrument(name = "handler.admin.v1.users.set_can_request_admin", skip_all, err)]
83+
pub async fn handler(
84+
CallContext { mut repo, .. }: CallContext,
85+
id: UlidPathParam,
86+
Json(params): Json<Request>,
87+
) -> Result<Json<SingleResponse<User>>, RouteError> {
88+
let id = *id;
89+
let user = repo
90+
.user()
91+
.lookup(id)
92+
.await?
93+
.ok_or(RouteError::NotFound(id))?;
94+
95+
let user = repo
96+
.user()
97+
.set_can_request_admin(user, params.can_request_admin)
98+
.await?;
99+
100+
repo.save().await?;
101+
102+
Ok(Json(SingleResponse::new(
103+
User::from(user),
104+
format!("/api/admin/v1/users/{id}/set-can-request-admin"),
105+
)))
106+
}
107+
108+
#[cfg(test)]
109+
mod tests {
110+
use hyper::{Request, StatusCode};
111+
use mas_storage::{user::UserRepository, RepositoryAccess};
112+
use sqlx::PgPool;
113+
114+
use crate::test_utils::{setup, RequestBuilderExt, ResponseExt, TestState};
115+
116+
#[sqlx::test(migrator = "mas_storage_pg::MIGRATOR")]
117+
async fn test_change_can_request_admin(pool: PgPool) {
118+
setup();
119+
let mut state = TestState::from_pool(pool).await.unwrap();
120+
let token = state.token_with_scope("urn:mas:admin").await;
121+
122+
let mut repo = state.repository().await.unwrap();
123+
let user = repo
124+
.user()
125+
.add(&mut state.rng(), &state.clock, "alice".to_owned())
126+
.await
127+
.unwrap();
128+
repo.save().await.unwrap();
129+
130+
let request = Request::post(format!(
131+
"/api/admin/v1/users/{}/set-can-request-admin",
132+
user.id
133+
))
134+
.bearer(&token)
135+
.json(serde_json::json!({
136+
"can_request_admin": true,
137+
}));
138+
139+
let response = state.request(request).await;
140+
response.assert_status(StatusCode::OK);
141+
let body: serde_json::Value = response.json();
142+
143+
assert_eq!(body["data"]["attributes"]["can_request_admin"], true);
144+
145+
// Look at the state from the repository
146+
let mut repo = state.repository().await.unwrap();
147+
let user = repo.user().lookup(user.id).await.unwrap().unwrap();
148+
assert!(user.can_request_admin);
149+
repo.save().await.unwrap();
150+
151+
// Flip it back
152+
let request = Request::post(format!(
153+
"/api/admin/v1/users/{}/set-can-request-admin",
154+
user.id
155+
))
156+
.bearer(&token)
157+
.json(serde_json::json!({
158+
"can_request_admin": false,
159+
}));
160+
161+
let response = state.request(request).await;
162+
response.assert_status(StatusCode::OK);
163+
let body: serde_json::Value = response.json();
164+
165+
assert_eq!(body["data"]["attributes"]["can_request_admin"], false);
166+
167+
// Look at the state from the repository
168+
let mut repo = state.repository().await.unwrap();
169+
let user = repo.user().lookup(user.id).await.unwrap().unwrap();
170+
assert!(!user.can_request_admin);
171+
repo.save().await.unwrap();
172+
}
173+
}

docs/api/spec.json

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -379,6 +379,85 @@
379379
}
380380
}
381381
},
382+
"/api/admin/v1/users/{id}/set-can-request-admin": {
383+
"post": {
384+
"tags": [
385+
"user"
386+
],
387+
"summary": "Set whether a user can request admin",
388+
"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.",
389+
"operationId": "setCanRequestAdmin",
390+
"parameters": [
391+
{
392+
"in": "path",
393+
"name": "id",
394+
"required": true,
395+
"schema": {
396+
"title": "The ID of the resource",
397+
"$ref": "#/components/schemas/ULID"
398+
},
399+
"style": "simple"
400+
}
401+
],
402+
"requestBody": {
403+
"content": {
404+
"application/json": {
405+
"schema": {
406+
"$ref": "#/components/schemas/SetCanRequestAdminRequest"
407+
}
408+
}
409+
},
410+
"required": true
411+
},
412+
"responses": {
413+
"200": {
414+
"description": "User had admin privileges set",
415+
"content": {
416+
"application/json": {
417+
"schema": {
418+
"$ref": "#/components/schemas/SingleResponse_for_User"
419+
},
420+
"example": {
421+
"data": {
422+
"type": "user",
423+
"id": "02081040G2081040G2081040G2",
424+
"attributes": {
425+
"username": "bob",
426+
"created_at": "1970-01-01T00:00:00Z",
427+
"locked_at": null,
428+
"can_request_admin": true
429+
},
430+
"links": {
431+
"self": "/api/admin/v1/users/02081040G2081040G2081040G2"
432+
}
433+
},
434+
"links": {
435+
"self": "/api/admin/v1/users/02081040G2081040G2081040G2/set-can-request-admin"
436+
}
437+
}
438+
}
439+
}
440+
},
441+
"404": {
442+
"description": "User ID not found",
443+
"content": {
444+
"application/json": {
445+
"schema": {
446+
"$ref": "#/components/schemas/ErrorResponse"
447+
},
448+
"example": {
449+
"errors": [
450+
{
451+
"title": "User ID 00000000000000000000000000 not found"
452+
}
453+
]
454+
}
455+
}
456+
}
457+
}
458+
}
459+
}
460+
},
382461
"/api/admin/v1/users/{id}/deactivate": {
383462
"post": {
384463
"tags": [
@@ -901,6 +980,19 @@
901980
"type": "string"
902981
}
903982
}
983+
},
984+
"SetCanRequestAdminRequest": {
985+
"title": "JSON payload for the `POST /api/admin/v1/users/:id/set-can-request-admin` endpoint",
986+
"type": "object",
987+
"required": [
988+
"can_request_admin"
989+
],
990+
"properties": {
991+
"can_request_admin": {
992+
"description": "Whether the user can request admin privileges.",
993+
"type": "boolean"
994+
}
995+
}
904996
}
905997
}
906998
},

0 commit comments

Comments
 (0)