Skip to main content

tuwunel_service/rooms/alias/
mod.rs

1#[cfg(test)]
2mod tests;
3
4use std::sync::Arc;
5
6use futures::{Stream, StreamExt};
7use ruma::{
8	OwnedRoomId, OwnedServerName, OwnedUserId, RoomAliasId, RoomId, RoomOrAliasId, UserId,
9	api::federation::query::get_room_information::v1::Request, events::StateEventType,
10};
11use tuwunel_core::{
12	Err, Result, err, implement,
13	matrix::Event,
14	utils::{ReadyExt, stream::TryIgnore},
15};
16use tuwunel_database::{Deserialized, Ignore, Interfix, Map};
17
18use crate::appservice::RegistrationInfo;
19
20pub struct Service {
21	db: Data,
22	services: Arc<crate::services::OnceServices>,
23}
24
25struct Data {
26	alias_userid: Arc<Map>,
27	alias_roomid: Arc<Map>,
28	aliasid_alias: Arc<Map>,
29}
30
31impl crate::Service for Service {
32	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
33		Ok(Arc::new(Self {
34			db: Data {
35				alias_userid: args.db["alias_userid"].clone(),
36				alias_roomid: args.db["alias_roomid"].clone(),
37				aliasid_alias: args.db["aliasid_alias"].clone(),
38			},
39			services: args.services.clone(),
40		}))
41	}
42
43	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
44}
45
46/// Refuses a user's alias creation when `allow_room_alias_creation` is off.
47///
48/// Appservices and server admins are exempt. Callers run it before creating
49/// anything, so a refusal leaves no room or alias behind.
50#[implement(Service)]
51#[tracing::instrument(skip(self, appservice_info), level = "trace")]
52pub async fn creation_check(
53	&self,
54	user_id: &UserId,
55	appservice_info: Option<&RegistrationInfo>,
56) -> Result {
57	if self.services.config.allow_room_alias_creation
58		|| appservice_info.is_some()
59		|| self.services.admin.user_is_admin(user_id).await
60	{
61		return Ok(());
62	}
63
64	Err!(Request(Forbidden("Room alias creation has been disabled.")))
65}
66
67impl Service {
68	pub fn set_alias(&self, alias: &RoomAliasId, room_id: &RoomId) -> Result {
69		self.check_alias_local(alias)?;
70
71		self.set_alias_by(alias, room_id, &self.services.globals.server_user)
72	}
73
74	#[tracing::instrument(skip(self))]
75	pub fn set_alias_by(
76		&self,
77		alias: &RoomAliasId,
78		room_id: &RoomId,
79		user_id: &UserId,
80	) -> Result {
81		self.check_alias_local(alias)?;
82
83		if alias == self.services.admin.admin_alias
84			&& user_id != self.services.globals.server_user
85		{
86			return Err!(Request(Forbidden("Only the server user can set this alias")));
87		}
88
89		let count = self.services.globals.next_count();
90
91		let localpart = alias.alias();
92
93		// Comes first as we don't want a stuck alias
94		self.db.alias_userid.insert(localpart, user_id);
95
96		self.db.alias_roomid.insert(localpart, room_id);
97
98		self.db
99			.aliasid_alias
100			.put_raw((room_id, *count), alias);
101
102		Ok(())
103	}
104
105	/// Removes a local alias on a user's behalf and returns its room.
106	///
107	/// The user must be the alias creator, a server admin, or otherwise
108	/// permitted to change the room's canonical alias.
109	pub async fn remove_alias_by(
110		&self,
111		alias: &RoomAliasId,
112		user_id: &UserId,
113	) -> Result<OwnedRoomId> {
114		if !self.user_can_remove_alias(alias, user_id).await? {
115			return Err!(Request(Forbidden("User is not permitted to remove this alias.")));
116		}
117
118		self.remove_alias(alias).await
119	}
120
121	/// Removes a local alias from the directory and returns its room.
122	///
123	/// The returned id names the room the removed mapping pointed at, which a
124	/// caller cannot recover afterwards because the alias no longer resolves.
125	/// It comes back whether or not the alias was that room's canonical one.
126	#[tracing::instrument(skip(self))]
127	pub async fn remove_alias(&self, alias: &RoomAliasId) -> Result<OwnedRoomId> {
128		let localpart = alias.alias();
129		let Ok(room_id) = self
130			.db
131			.alias_roomid
132			.get(&localpart)
133			.await
134			.deserialized()
135		else {
136			return Err!(Request(NotFound("Alias does not exist or is invalid.")));
137		};
138
139		let prefix = (&room_id, Interfix);
140
141		// The row key carries the counter it was written under, so the row to drop is
142		// found by its value; deleting the prefix would take the room's other aliases.
143		self.db
144			.aliasid_alias
145			.stream_prefix_raw(&prefix)
146			.ignore_err()
147			.ready_filter_map(|(key, stored)| stored.eq(alias.as_bytes()).then_some(key))
148			.ready_for_each(|key| self.db.aliasid_alias.remove(key))
149			.await;
150
151		self.db.alias_roomid.remove(localpart.as_bytes());
152		self.db.alias_userid.remove(localpart.as_bytes());
153
154		Ok(room_id)
155	}
156
157	#[inline]
158	pub async fn maybe_resolve(&self, room: &RoomOrAliasId) -> Result<OwnedRoomId> {
159		match <&RoomId>::try_from(room) {
160			| Ok(room_id) => Ok(room_id.to_owned()),
161			| Err(alias) => Ok(self.resolve_alias(alias).await?.0),
162		}
163	}
164
165	pub async fn maybe_resolve_with_servers(
166		&self,
167		room: &RoomOrAliasId,
168		servers: Option<&[OwnedServerName]>,
169	) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
170		match <&RoomId>::try_from(room) {
171			| Ok(room_id) => Ok((room_id.to_owned(), Vec::from(servers.unwrap_or_default()))),
172			| Err(alias) => self.resolve_alias(alias).await,
173		}
174	}
175
176	#[tracing::instrument(skip(self), name = "resolve")]
177	pub async fn resolve_alias(
178		&self,
179		room_alias: &RoomAliasId,
180	) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
181		if self.services.globals.alias_is_local(room_alias) {
182			if let Ok(room_id) = self.resolve_local_alias(room_alias).await {
183				return Ok((room_id, Vec::new()));
184			}
185
186			if let Ok(room_id) = self.resolve_appservice_alias(room_alias).await {
187				return Ok((room_id, Vec::new()));
188			}
189
190			return Err!(Request(NotFound("Room with alias not found.")));
191		}
192
193		return self.remote_resolve(room_alias).await;
194	}
195
196	async fn remote_resolve(
197		&self,
198		room_alias: &RoomAliasId,
199	) -> Result<(OwnedRoomId, Vec<OwnedServerName>)> {
200		let server = room_alias.server_name();
201
202		let request = Request { room_alias: room_alias.to_owned() };
203
204		let response = self
205			.services
206			.federation
207			.execute(server, request)
208			.await?;
209
210		Ok((response.room_id, response.servers))
211	}
212
213	#[tracing::instrument(skip(self), level = "trace")]
214	pub async fn resolve_local_alias(&self, alias: &RoomAliasId) -> Result<OwnedRoomId> {
215		self.check_alias_local(alias)?;
216		self.db
217			.alias_roomid
218			.get(alias.alias())
219			.await
220			.deserialized()
221	}
222
223	#[tracing::instrument(skip(self), level = "debug")]
224	pub fn local_aliases_for_room<'a>(
225		&'a self,
226		room_id: &'a RoomId,
227	) -> impl Stream<Item = &RoomAliasId> + Send + 'a {
228		let prefix = (room_id, Interfix);
229		self.db
230			.aliasid_alias
231			.stream_prefix(&prefix)
232			.ignore_err()
233			.map(|(_, alias): (Ignore, &RoomAliasId)| alias)
234	}
235
236	#[tracing::instrument(skip(self), level = "debug")]
237	pub fn all_local_aliases(&self) -> impl Stream<Item = (&RoomId, &str)> + Send + '_ {
238		self.db
239			.alias_roomid
240			.stream()
241			.ignore_err()
242			.map(|(alias_localpart, room_id): (&str, &RoomId)| (room_id, alias_localpart))
243	}
244
245	async fn user_can_remove_alias(&self, alias: &RoomAliasId, user_id: &UserId) -> Result<bool> {
246		self.check_alias_local(alias)?;
247
248		let room_id = self
249			.resolve_local_alias(alias)
250			.await
251			.map_err(|_| err!(Request(NotFound("Alias not found."))))?;
252
253		// The creator of an alias can remove it
254		if self
255            .who_created_alias(alias).await
256            .is_ok_and(|user| user == user_id)
257            // Server admins can remove any local alias
258            || self.services.admin.user_is_admin(user_id).await
259		{
260			return Ok(true);
261		}
262
263		// Checking whether the user is able to change canonical aliases of the room
264		if let Ok(power_levels) = self
265			.services
266			.state_accessor
267			.get_power_levels(&room_id)
268			.await
269		{
270			return Ok(
271				power_levels.user_can_send_state(user_id, StateEventType::RoomCanonicalAlias)
272			);
273		}
274
275		// If there is no power levels event, only the room creator can change
276		// canonical aliases
277		if let Ok(event) = self
278			.services
279			.state_accessor
280			.room_state_get(&room_id, &StateEventType::RoomCreate, "")
281			.await
282		{
283			return Ok(event.sender() == user_id);
284		}
285
286		Err!(Database("Room has no m.room.create event"))
287	}
288
289	async fn who_created_alias(&self, alias: &RoomAliasId) -> Result<OwnedUserId> {
290		self.check_alias_local(alias)?;
291
292		self.db
293			.alias_userid
294			.get(alias.alias())
295			.await
296			.deserialized()
297	}
298
299	async fn resolve_appservice_alias(&self, room_alias: &RoomAliasId) -> Result<OwnedRoomId> {
300		use ruma::api::appservice::query::query_room_alias;
301
302		self.check_alias_local(room_alias)?;
303
304		for appservice in self.services.appservice.read().await.values() {
305			if appservice.aliases.is_match(room_alias.as_str())
306				&& matches!(
307					self.services
308						.appservice
309						.send_request(
310							appservice.registration.clone(),
311							query_room_alias::v1::Request { room_alias: room_alias.to_owned() },
312						)
313						.await,
314					Ok(Some(_opt_result))
315				) {
316				return self
317					.resolve_local_alias(room_alias)
318					.await
319					.map_err(|_| err!(Request(NotFound("Room does not exist."))));
320			}
321		}
322
323		Err!(Request(NotFound("Room does not exist.")))
324	}
325
326	fn check_alias_local(&self, alias: &RoomAliasId) -> Result {
327		if !self.services.globals.alias_is_local(alias) {
328			return Err!(Request(InvalidParam("Alias is from another server.")));
329		}
330
331		Ok(())
332	}
333
334	#[tracing::instrument(skip(self, appservice_info), level = "trace")]
335	pub async fn appservice_checks(
336		&self,
337		room_alias: &RoomAliasId,
338		appservice_info: &Option<RegistrationInfo>,
339	) -> Result {
340		self.check_alias_local(room_alias)?;
341		if let Some(info) = appservice_info {
342			if !info.aliases.is_match(room_alias.as_str()) {
343				return Err!(Request(Exclusive("Room alias is not in namespace.")));
344			}
345		} else if self
346			.services
347			.appservice
348			.is_exclusive_alias(room_alias)
349			.await
350		{
351			return Err!(Request(Exclusive("Room alias reserved by appservice.")));
352		}
353
354		Ok(())
355	}
356}