Skip to main content

tuwunel_api/client/
admin.rs

1use axum::extract::State;
2use futures::future::join3;
3use ruma::{
4	UserId,
5	api::client::admin::{is_user_locked, is_user_suspended, lock_user, suspend_user},
6};
7use tuwunel_core::{Err, Result};
8
9use crate::Ruma;
10
11/// MSC4323: ordered access control for the four admin endpoints.
12///
13/// Spec mandates: authorization MUST be checked before account lookups
14/// (anti-enumeration). The pure self-target check fails first; the three
15/// IO checks (admin gate + existence + admin-target) run concurrently;
16/// failures are then reported in spec-mandated priority.
17async fn authorize(services: &crate::State, caller: &UserId, target: &UserId) -> Result {
18	if caller == target {
19		return Err!(Request(Forbidden("You cannot suspend or lock your own account")));
20	}
21
22	if !services.globals.user_is_local(target) {
23		return Err!(Request(InvalidParam("User is not local to this server")));
24	}
25
26	let (caller_admin, target_active, target_admin) = join3(
27		services.admin.user_is_admin(caller),
28		services.users.is_active(target),
29		services.admin.user_is_admin(target),
30	)
31	.await;
32
33	if !caller_admin {
34		return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
35	}
36
37	if !target_active {
38		return Err!(Request(NotFound("Unknown user")));
39	}
40
41	if target_admin {
42		return Err!(Request(Forbidden(
43			"You cannot suspend or lock another server administrator"
44		)));
45	}
46
47	Ok(())
48}
49
50/// # `GET /_matrix/client/v1/admin/suspend/{userId}`
51pub(crate) async fn is_user_suspended_route(
52	State(services): State<crate::State>,
53	body: Ruma<is_user_suspended::v1::Request>,
54) -> Result<is_user_suspended::v1::Response> {
55	let user_id = &body.user_id;
56
57	authorize(&services, body.sender_user(), user_id).await?;
58
59	Ok(is_user_suspended::v1::Response::new(services.users.is_suspended(user_id).await))
60}
61
62/// # `PUT /_matrix/client/v1/admin/suspend/{userId}`
63pub(crate) async fn suspend_user_route(
64	State(services): State<crate::State>,
65	body: Ruma<suspend_user::v1::Request>,
66) -> Result<suspend_user::v1::Response> {
67	let sender_user = body.sender_user();
68	let user_id = &body.user_id;
69
70	authorize(&services, sender_user, user_id).await?;
71
72	if services.users.is_suspended(user_id).await == body.suspended {
73		return Ok(suspend_user::v1::Response::new(body.suspended));
74	}
75
76	let action = match body.suspended {
77		| true => {
78			services.users.set_suspended(user_id, sender_user);
79			"suspended"
80		},
81		| false => {
82			services.users.clear_suspended(user_id);
83			"unsuspended"
84		},
85	};
86
87	if services.server.config.admin_room_notices {
88		services
89			.admin
90			.send_text(&format!("{user_id} has been {action} by {sender_user}."))
91			.await;
92	}
93
94	Ok(suspend_user::v1::Response::new(body.suspended))
95}
96
97/// # `GET /_matrix/client/v1/admin/lock/{userId}`
98pub(crate) async fn is_user_locked_route(
99	State(services): State<crate::State>,
100	body: Ruma<is_user_locked::v1::Request>,
101) -> Result<is_user_locked::v1::Response> {
102	let user_id = &body.user_id;
103
104	authorize(&services, body.sender_user(), user_id).await?;
105
106	Ok(is_user_locked::v1::Response::new(services.users.is_locked(user_id).await))
107}
108
109/// # `PUT /_matrix/client/v1/admin/lock/{userId}`
110pub(crate) async fn lock_user_route(
111	State(services): State<crate::State>,
112	body: Ruma<lock_user::v1::Request>,
113) -> Result<lock_user::v1::Response> {
114	let sender_user = body.sender_user();
115	let user_id = &body.user_id;
116
117	authorize(&services, sender_user, user_id).await?;
118
119	if services.users.is_locked(user_id).await == body.locked {
120		return Ok(lock_user::v1::Response::new(body.locked));
121	}
122
123	let action = match body.locked {
124		| true => {
125			services.users.set_locked(user_id, sender_user);
126			"locked"
127		},
128		| false => {
129			services.users.clear_locked(user_id);
130			"unlocked"
131		},
132	};
133
134	if services.server.config.admin_room_notices {
135		services
136			.admin
137			.send_text(&format!("{user_id} has been {action} by {sender_user}."))
138			.await;
139	}
140
141	Ok(lock_user::v1::Response::new(body.locked))
142}