Skip to main content

tuwunel_service/admin/
grant.rs

1use futures::FutureExt;
2use ruma::{
3	Int, OwnedUserId, RoomId, UserId,
4	events::{
5		StateEventType,
6		room::{
7			member::{MembershipState, RoomMemberEventContent},
8			message::RoomMessageEventContent,
9			power_levels::{RoomPowerLevelsEventContent, UserPowerLevel},
10		},
11	},
12};
13use tuwunel_core::{
14	Err, Result, debug_info, debug_warn, error, implement,
15	matrix::pdu::PduBuilder,
16	utils::{FutureBoolExt, future::ReadyBoolExt, stream::ReadyExt},
17};
18
19use crate::rooms::state::RoomMutexGuard;
20
21/// Invite the user to the tuwunel admin room.
22///
23/// This is equivalent to granting server admin privileges. A local user must
24/// already hold an account, or the grant would wait for whoever registers the
25/// name.
26#[implement(super::Service)]
27pub async fn make_user_admin(&self, user_id: &UserId) -> Result {
28	let is_local = self.services.globals.user_is_local(user_id);
29
30	if is_local && !self.services.users.exists(user_id).await {
31		return Err!(Request(NotFound("User {user_id} does not exist on this server.")));
32	}
33
34	let Ok(room_id) = self.get_admin_room().await else {
35		debug_warn!(
36			"make_user_admin was called without an admin room being available or created"
37		);
38		return Ok(());
39	};
40
41	let state_lock = self.services.state.mutex.lock(&room_id).await;
42
43	let is_joined = self
44		.services
45		.state_cache
46		.is_joined(user_id, &room_id)
47		.await;
48
49	let is_invited = self
50		.services
51		.state_cache
52		.is_invited(user_id, &room_id)
53		.await;
54
55	let already_member = is_joined || is_invited;
56
57	if !already_member {
58		self.invite_new_admin(user_id, &room_id, &state_lock)
59			.await?;
60	}
61
62	let server_user: &UserId = self.services.globals.server_user.as_ref();
63
64	let mut room_power_levels = self
65		.services
66		.state_accessor
67		.room_state_get_content::<RoomPowerLevelsEventContent>(
68			&room_id,
69			&StateEventType::RoomPowerLevels,
70			"",
71		)
72		.await
73		.unwrap_or_default();
74
75	let server_level = 69420.into();
76	let admin_level = 100.into();
77	let already_granted = room_power_levels.users.get(server_user) == Some(&server_level)
78		&& room_power_levels.users.get(user_id) == Some(&admin_level);
79
80	if !already_granted {
81		room_power_levels
82			.users
83			.insert(server_user.into(), server_level);
84		room_power_levels
85			.users
86			.insert(user_id.into(), admin_level);
87
88		self.services
89			.timeline
90			.build_and_append_pdu(
91				PduBuilder::state(String::new(), &room_power_levels),
92				server_user,
93				&room_id,
94				&state_lock,
95			)
96			.await?;
97	}
98
99	// Set room tag
100	let room_tag = self
101		.services
102		.server
103		.config
104		.admin_room_tag
105		.as_str();
106
107	if !already_granted
108		&& !room_tag.is_empty()
109		&& let Err(e) = self
110			.services
111			.account_data
112			.set_room_tag(user_id, &room_id, room_tag.into(), None)
113			.await
114	{
115		error!(?room_id, ?user_id, ?room_tag, "Failed to set tag for admin grant: {e}");
116	}
117
118	if !already_member && self.services.server.config.admin_room_notices {
119		let welcome_message = String::from(
120			"## Thank you for trying out tuwunel!\n\nTuwunel is a continuation of conduwuit which was technically a hard fork of Conduit.\n\nHelpful links:\n> GitHub Repo: https://github.com/matrix-construct/tuwunel\n> Documentation: https://matrix-construct.github.io/tuwunel\n> Report issues: https://github.com/matrix-construct/tuwunel/issues\n\nFor a list of available commands, send the following message in this room: `!admin --help`",
121		);
122
123		// Send welcome message
124		self.services
125			.timeline
126			.build_and_append_pdu(
127				PduBuilder::timeline(&RoomMessageEventContent::text_markdown(welcome_message)),
128				server_user,
129				&room_id,
130				&state_lock,
131			)
132			.await?;
133	}
134
135	Ok(())
136}
137
138/// Grant room admin powers to a target by impersonating the highest-powered
139/// local member.
140///
141/// The target is granted the impersonated member's power level (100 when that
142/// member is a room-version 12 creator with infinite power), sent as a new
143/// `m.room.power_levels` from that member. When the room is non-public and the
144/// target is neither joined nor invited, the target is invited first.
145#[implement(super::Service)]
146pub async fn make_room_admin(&self, room_id: &RoomId, target: &UserId) -> Result {
147	let state_lock = self.services.state.mutex.lock(room_id).await;
148
149	let power_levels = self
150		.services
151		.state_accessor
152		.get_power_levels(room_id)
153		.await?;
154
155	let sender = self
156		.services
157		.state_cache
158		.local_users_in_room(room_id)
159		.ready_fold(None, |best: Option<(OwnedUserId, UserPowerLevel)>, user| {
160			let level = power_levels.for_user(user);
161			match best {
162				| Some((_, best_level)) if best_level >= level => best,
163				| _ => Some((user.to_owned(), level)),
164			}
165		})
166		.await;
167
168	let Some((sender, sender_level)) = sender else {
169		return Err!(Request(InvalidParam("Server not in room")));
170	};
171
172	if !power_levels.user_can_send_state(&sender, StateEventType::RoomPowerLevels) {
173		return Err!(Request(InvalidParam(
174			"No local admin in room with power to update power levels"
175		)));
176	}
177
178	let grant_level: Int = match sender_level {
179		| UserPowerLevel::Infinite => 100.into(),
180		| UserPowerLevel::Int(level) => level,
181	};
182
183	let is_joined = self
184		.services
185		.state_cache
186		.is_joined(target, room_id);
187
188	let is_invited = self
189		.services
190		.state_cache
191		.is_invited(target, room_id);
192
193	let is_public = self.services.metadata.is_public(room_id);
194
195	// The membership probes lead; the public check is the multi-read leg.
196	let needs_invite = is_joined
197		.is_false()
198		.and2(is_invited.is_false(), is_public.is_false());
199
200	if needs_invite.await {
201		self.services
202			.membership
203			.invite(&sender, target, room_id, None, false)
204			.await?;
205	}
206
207	let mut content = self
208		.services
209		.state_accessor
210		.room_state_get_content::<RoomPowerLevelsEventContent>(
211			room_id,
212			&StateEventType::RoomPowerLevels,
213			"",
214		)
215		.await
216		.unwrap_or_default();
217
218	content.users.insert(target.into(), grant_level);
219
220	self.services
221		.timeline
222		.build_and_append_pdu(
223			PduBuilder::state(String::new(), &content),
224			&sender,
225			room_id,
226			&state_lock,
227		)
228		.await
229		.map(|_| ())
230}
231
232#[implement(super::Service)]
233async fn invite_new_admin(
234	&self,
235	user_id: &UserId,
236	room_id: &RoomId,
237	state_lock: &RoomMutexGuard,
238) -> Result {
239	let server_user = self.services.globals.server_user.as_ref();
240
241	// if this is our local user, just forcefully join them in the room. otherwise,
242	// invite the remote user.
243	if self.services.globals.user_is_local(user_id) {
244		debug_info!("Inviting local user {user_id} to admin room {room_id}");
245		self.services
246			.timeline
247			.build_and_append_pdu(
248				PduBuilder::state(
249					String::from(user_id),
250					&RoomMemberEventContent::new(MembershipState::Invite),
251				),
252				server_user,
253				room_id,
254				state_lock,
255			)
256			.await?;
257
258		debug_info!("Force joining local user {user_id} to admin room {room_id}");
259		self.services
260			.timeline
261			.build_and_append_pdu(
262				PduBuilder::state(
263					String::from(user_id),
264					&RoomMemberEventContent::new(MembershipState::Join),
265				),
266				user_id,
267				room_id,
268				state_lock,
269			)
270			.await?;
271	} else {
272		debug_info!("Inviting remote user {user_id} to admin room {room_id}");
273		self.services
274			.timeline
275			.build_and_append_pdu(
276				PduBuilder::state(
277					user_id.to_string(),
278					&RoomMemberEventContent::new(MembershipState::Invite),
279				),
280				server_user,
281				room_id,
282				state_lock,
283			)
284			.await?;
285	}
286
287	Ok(())
288}
289
290/// Demote an admin, removing its rights.
291///
292/// The server user is refused, since the admin room exists only while the
293/// server user stays joined.
294#[implement(super::Service)]
295pub async fn revoke_admin(&self, user_id: &UserId) -> Result {
296	use MembershipState::{Invite, Join, Knock, Leave};
297
298	if user_id == self.services.globals.server_user {
299		return Err!(Request(InvalidParam("The server user's admin rights cannot be revoked.")));
300	}
301
302	let Ok(room_id) = self.get_admin_room().await else {
303		return Err!(error!("No admin room available or created."));
304	};
305
306	let state_lock = self.services.state.mutex.lock(&room_id).await;
307
308	let event = match self
309		.services
310		.state_accessor
311		.get_member(&room_id, user_id)
312		.await
313	{
314		| Err(e) if e.is_not_found() => return Err!("{user_id} was never an admin."),
315
316		| Err(e) => return Err!(error!(?e, "Failure occurred while attempting revoke.")),
317
318		| Ok(event) if !matches!(event.membership, Invite | Knock | Join) => {
319			return Err!("Cannot revoke {user_id} in membership state {:?}.", event.membership);
320		},
321
322		| Ok(event) => {
323			assert!(
324				matches!(event.membership, Invite | Knock | Join),
325				"Incorrect membership state to remove user."
326			);
327
328			event
329		},
330	};
331
332	self.services
333		.timeline
334		.build_and_append_pdu(
335			PduBuilder::state(user_id.to_string(), &RoomMemberEventContent {
336				membership: Leave,
337				reason: Some("Admin Revoked".into()),
338				is_direct: false,
339				join_authorized_via_users_server: None,
340				third_party_invite: None,
341				..event
342			}),
343			self.services.globals.server_user.as_ref(),
344			&room_id,
345			&state_lock,
346		)
347		.boxed()
348		.await
349		.map(|_| ())
350}