Skip to main content

tuwunel_service/rooms/state_accessor/
user_can.rs

1//! Evaluates user visibility and event-authoring permissions.
2//!
3//! Historical visibility checks combine event-time state with retained
4//! membership metadata. Invite and tombstone checks use the normal event-build
5//! pipeline as non-persisting authorization probes.
6
7use futures::pin_mut;
8use ruma::{
9	EventId, RoomId, UserId,
10	events::{
11		StateEventType, TimelineEventType,
12		room::{
13			history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
14			member::{MembershipState, RoomMemberEventContent},
15			tombstone::RoomTombstoneEventContent,
16		},
17	},
18};
19use tuwunel_core::{
20	Err, Result, implement,
21	matrix::{Event, PduCount, StateKey},
22	pdu::PduBuilder,
23	utils::FutureBoolExt,
24};
25
26use crate::rooms::{short::ShortStateHash, state::RoomMutexGuard};
27
28/// Reports whether a user may redact an event in a room.
29///
30/// Cross-room targets are denied, while create and server-ACL targets return a
31/// forbidden error. Federation permits the own-event rule for any sender on
32/// the target sender's server; power-level failures fall back to create-event
33/// ownership and exact sender equality.
34#[implement(super::Service)]
35pub async fn user_can_redact(
36	&self,
37	redacts: &EventId,
38	sender: &UserId,
39	room_id: &RoomId,
40	federation: bool,
41) -> Result<bool> {
42	let redacting_event = self.services.timeline.get_pdu(redacts).await;
43
44	if redacting_event
45		.as_ref()
46		.is_ok_and(|pdu| pdu.room_id() != room_id)
47	{
48		return Ok(false);
49	}
50
51	if redacting_event
52		.as_ref()
53		.is_ok_and(|pdu| *pdu.kind() == TimelineEventType::RoomCreate)
54	{
55		return Err!(Request(Forbidden("Redacting m.room.create is not safe, forbidding.")));
56	}
57
58	if redacting_event
59		.as_ref()
60		.is_ok_and(|pdu| *pdu.kind() == TimelineEventType::RoomServerAcl)
61	{
62		return Err!(Request(Forbidden(
63			"Redacting m.room.server_acl will result in the room being inaccessible for \
64			 everyone (empty allow key), forbidding."
65		)));
66	}
67
68	match self.get_power_levels(room_id).await {
69		| Ok(power_levels) => Ok(power_levels.user_can_redact_event_of_other(sender)
70			|| power_levels.user_can_redact_own_event(sender)
71				&& match redacting_event {
72					| Ok(redacting_event) =>
73						if federation {
74							redacting_event.sender().server_name() == sender.server_name()
75						} else {
76							redacting_event.sender() == sender
77						},
78					| _ => false,
79				}),
80		| _ => {
81			// Falling back on m.room.create to judge power level
82			match self
83				.room_state_get(room_id, &StateEventType::RoomCreate, "")
84				.await
85			{
86				| Ok(room_create) => Ok(room_create.sender() == sender
87					|| redacting_event
88						.as_ref()
89						.is_ok_and(|redacting_event| redacting_event.sender() == sender)),
90				| _ => Err!(Database(
91					"No m.room.power_levels or m.room.create events in database for room"
92				)),
93			}
94		},
95	}
96}
97
98/// Reports whether a user may see an event under its historical visibility.
99///
100/// Missing event state is allowed, and missing or invalid history visibility
101/// defaults to `shared`. The `shared` decision also accounts for the user's
102/// membership intervals around the event. Under `joined` and `invited`, the
103/// user's own membership event is visible when the membership it sets
104/// qualifies, per the spec's before-or-after rule.
105#[implement(super::Service)]
106#[tracing::instrument(skip_all, level = "trace")]
107pub async fn user_can_see_event<Pdu>(&self, user_id: &UserId, pdu: &Pdu) -> bool
108where
109	Pdu: Event,
110{
111	let Some((shortstatehash, history_visibility)) =
112		self.history_visibility_at(pdu.event_id()).await
113	else {
114		return true;
115	};
116
117	match history_visibility {
118		| HistoryVisibility::WorldReadable => true,
119
120		// Allow the user's own invite or join, or a user at least invited at the event
121		| HistoryVisibility::Invited =>
122			matches!(
123				pdu.membership_for(user_id),
124				Some(MembershipState::Join | MembershipState::Invite)
125			) || self
126				.user_was_invited(shortstatehash, user_id)
127				.await,
128
129		// Allow the user's own join, or a user joined at the event
130		| HistoryVisibility::Joined =>
131			matches!(pdu.membership_for(user_id), Some(MembershipState::Join))
132				|| self
133					.user_was_joined(shortstatehash, user_id)
134					.await,
135
136		// An unrecognized value is treated as shared.
137		| HistoryVisibility::Shared | _ =>
138			self.user_shared_history(shortstatehash, pdu.room_id(), pdu.event_id(), user_id)
139				.await,
140	}
141}
142
143/// The room state an event was sent in and the history visibility it carried.
144///
145/// Missing or invalid history visibility reads as `shared`, and `None` means
146/// the event has no recorded state.
147#[implement(super::Service)]
148#[tracing::instrument(skip_all, level = "trace")]
149async fn history_visibility_at(
150	&self,
151	event_id: &EventId,
152) -> Option<(ShortStateHash, HistoryVisibility)> {
153	let shortstatehash = self
154		.services
155		.state
156		.pdu_shortstatehash(event_id)
157		.await
158		.ok()?;
159
160	let history_visibility = self
161		.state_get_content(shortstatehash, &StateEventType::RoomHistoryVisibility, "")
162		.await
163		.map_or(HistoryVisibility::Shared, |c: RoomHistoryVisibilityEventContent| {
164			c.history_visibility
165		});
166
167	Some((shortstatehash, history_visibility))
168}
169
170/// Whether a user may see an event under `shared` history visibility.
171///
172/// A current member sees the whole room, which the first check answers without
173/// touching room state. A former member keeps events through their latest
174/// leave, and lookup failures deny access.
175#[implement(super::Service)]
176async fn user_shared_history(
177	&self,
178	shortstatehash: ShortStateHash,
179	room_id: &RoomId,
180	event_id: &EventId,
181	user_id: &UserId,
182) -> bool {
183	let state_cache = &self.services.state_cache;
184
185	if state_cache.is_joined(user_id, room_id).await
186		|| self
187			.user_was_joined(shortstatehash, user_id)
188			.await
189	{
190		return true;
191	}
192
193	if !state_cache.once_joined(user_id, room_id).await {
194		return false;
195	}
196
197	let Ok(left_count) = state_cache.get_left_count(room_id, user_id).await else {
198		return false;
199	};
200
201	let Ok(event_count) = self
202		.services
203		.timeline
204		.get_pdu_count(event_id)
205		.await
206	else {
207		return false;
208	};
209
210	event_count <= PduCount::from_unsigned(left_count)
211}
212
213/// Reports whether a user may read the room's current state events.
214///
215/// Current membership grants immediate access. Otherwise world-readable
216/// history grants access; invited and shared visibility consult current
217/// invitation or retained once-joined metadata. Missing visibility defaults
218/// to `shared`.
219#[implement(super::Service)]
220#[tracing::instrument(skip_all, level = "trace")]
221pub async fn user_can_see_state_events(&self, user_id: &UserId, room_id: &RoomId) -> bool {
222	if self
223		.services
224		.state_cache
225		.is_joined(user_id, room_id)
226		.await
227	{
228		return true;
229	}
230
231	let history_visibility = self
232		.room_state_get_content(room_id, &StateEventType::RoomHistoryVisibility, "")
233		.await
234		.map_or(HistoryVisibility::Shared, |c: RoomHistoryVisibilityEventContent| {
235			c.history_visibility
236		});
237
238	match history_visibility {
239		| HistoryVisibility::WorldReadable => true,
240
241		| HistoryVisibility::Invited =>
242			self.services
243				.state_cache
244				.is_invited(user_id, room_id)
245				.await,
246
247		| HistoryVisibility::Shared =>
248			self.services
249				.state_cache
250				.once_joined(user_id, room_id)
251				.await,
252
253		| _ => false,
254	}
255}
256
257/// Reports whether a user may discover or inspect a room.
258///
259/// Current join, invite, retained left membership, or world-readable history
260/// grants access. Forgetting a room clears the retained left membership and can
261/// therefore remove this visibility.
262#[implement(super::Service)]
263pub async fn user_can_see_room(&self, user_id: &UserId, room_id: &RoomId) -> bool {
264	let state_cache = &self.services.state_cache;
265	let joined = state_cache.is_joined(user_id, room_id);
266	let invited = state_cache.is_invited(user_id, room_id);
267	let left = state_cache.is_left(user_id, room_id);
268	let world_readable = self.is_world_readable(room_id);
269
270	pin_mut!(joined, invited, left, world_readable);
271	joined
272		.or(invited)
273		.or(left)
274		.or(world_readable)
275		.await
276}
277
278/// Reports whether a user may peek into a room, as a room preview does.
279///
280/// A member always may, and anyone else only while the room's history is
281/// world-readable. Unlike `user_can_see_room`, a pending invite or a retained
282/// left membership grants nothing here.
283#[implement(super::Service)]
284#[tracing::instrument(skip_all, level = "trace")]
285pub async fn user_can_peek(&self, user_id: &UserId, room_id: &RoomId) -> bool {
286	// Sequenced rather than raced, so a member's point get skips the state read.
287	self.services
288		.state_cache
289		.is_joined(user_id, room_id)
290		.await
291		|| self.is_world_readable(room_id).await
292}
293
294/// Reports whether the room's history was world-readable at an event.
295///
296/// A peek may show only such events, so an event without recorded state, or
297/// with missing or invalid history visibility, does not qualify. The event that
298/// makes the room world-readable counts as well, as the spec requires.
299#[implement(super::Service)]
300#[tracing::instrument(skip_all, level = "trace")]
301pub async fn is_world_readable_at<Pdu>(&self, pdu: &Pdu) -> bool
302where
303	Pdu: Event,
304{
305	let opens_history = pdu.is_type_and_state_key(&TimelineEventType::RoomHistoryVisibility, "")
306		&& pdu
307			.get_content()
308			.is_ok_and(|c: RoomHistoryVisibilityEventContent| {
309				c.history_visibility == HistoryVisibility::WorldReadable
310			});
311
312	opens_history
313		|| self
314			.history_visibility_at(pdu.event_id())
315			.await
316			.is_some_and(|(_, history_visibility)| {
317				history_visibility == HistoryVisibility::WorldReadable
318			})
319}
320
321/// Probes whether a sender may invite a target user.
322///
323/// The normal event-build, authorization, and signing path runs under the
324/// caller's room-state lock, but the synthetic membership event is not stored.
325/// Any build error denies the invite, while build-time ID allocations may remain.
326#[implement(super::Service)]
327pub async fn user_can_invite(
328	&self,
329	room_id: &RoomId,
330	sender: &UserId,
331	target_user: &UserId,
332	state_lock: &RoomMutexGuard,
333) -> bool {
334	self.services
335		.timeline
336		.create_hash_and_sign_event(
337			PduBuilder::state(
338				target_user.as_str(),
339				&RoomMemberEventContent::new(MembershipState::Invite),
340			),
341			sender,
342			room_id,
343			state_lock,
344		)
345		.await
346		.is_ok()
347}
348
349/// Probes whether a user may send a room tombstone.
350///
351/// The user must currently be joined. The normal event-build, authorization,
352/// and signing path evaluates a synthetic tombstone without storing it; any
353/// build error denies permission, while build-time ID allocations may remain.
354#[implement(super::Service)]
355pub async fn user_can_tombstone(
356	&self,
357	room_id: &RoomId,
358	user_id: &UserId,
359	state_lock: &RoomMutexGuard,
360) -> bool {
361	if !self
362		.services
363		.state_cache
364		.is_joined(user_id, room_id)
365		.await
366	{
367		return false;
368	}
369
370	self.services
371		.timeline
372		.create_hash_and_sign_event(
373			PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
374				replacement_room: room_id.into(), // placeholder,
375				body: "Not a valid m.room.tombstone.".into(),
376			}),
377			user_id,
378			room_id,
379			state_lock,
380		)
381		.await
382		.is_ok()
383}
384
385#[cfg(test)]
386mod tests;