Skip to main content

tuwunel_service/rooms/state_accessor/
user_can.rs

1use futures::pin_mut;
2use ruma::{
3	EventId, RoomId, UserId,
4	events::{
5		StateEventType, TimelineEventType,
6		room::{
7			history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
8			member::{MembershipState, RoomMemberEventContent},
9			tombstone::RoomTombstoneEventContent,
10		},
11	},
12};
13use tuwunel_core::{
14	Err, Result, implement,
15	matrix::{Event, PduCount, StateKey},
16	pdu::PduBuilder,
17	utils::FutureBoolExt,
18};
19
20use crate::rooms::{short::ShortStateHash, state::RoomMutexGuard};
21
22/// Checks if a given user can redact a given event
23///
24/// If federation is true, it allows redaction events from any user of the
25/// same server as the original event sender
26#[implement(super::Service)]
27pub async fn user_can_redact(
28	&self,
29	redacts: &EventId,
30	sender: &UserId,
31	room_id: &RoomId,
32	federation: bool,
33) -> Result<bool> {
34	let redacting_event = self.services.timeline.get_pdu(redacts).await;
35
36	if redacting_event
37		.as_ref()
38		.is_ok_and(|pdu| *pdu.kind() == TimelineEventType::RoomCreate)
39	{
40		return Err!(Request(Forbidden("Redacting m.room.create is not safe, forbidding.")));
41	}
42
43	if redacting_event
44		.as_ref()
45		.is_ok_and(|pdu| *pdu.kind() == TimelineEventType::RoomServerAcl)
46	{
47		return Err!(Request(Forbidden(
48			"Redacting m.room.server_acl will result in the room being inaccessible for \
49			 everyone (empty allow key), forbidding."
50		)));
51	}
52
53	match self.get_power_levels(room_id).await {
54		| Ok(power_levels) => Ok(power_levels.user_can_redact_event_of_other(sender)
55			|| power_levels.user_can_redact_own_event(sender)
56				&& match redacting_event {
57					| Ok(redacting_event) =>
58						if federation {
59							redacting_event.sender().server_name() == sender.server_name()
60						} else {
61							redacting_event.sender() == sender
62						},
63					| _ => false,
64				}),
65		| _ => {
66			// Falling back on m.room.create to judge power level
67			match self
68				.room_state_get(room_id, &StateEventType::RoomCreate, "")
69				.await
70			{
71				| Ok(room_create) => Ok(room_create.sender() == sender
72					|| redacting_event
73						.as_ref()
74						.is_ok_and(|redacting_event| redacting_event.sender() == sender)),
75				| _ => Err!(Database(
76					"No m.room.power_levels or m.room.create events in database for room"
77				)),
78			}
79		},
80	}
81}
82
83/// Whether a user is allowed to see an event, based on
84/// the room's history_visibility at that event's state.
85#[implement(super::Service)]
86#[tracing::instrument(skip_all, level = "trace")]
87pub async fn user_can_see_event(
88	&self,
89	user_id: &UserId,
90	room_id: &RoomId,
91	event_id: &EventId,
92) -> bool {
93	let Ok(shortstatehash) = self
94		.services
95		.state
96		.pdu_shortstatehash(event_id)
97		.await
98	else {
99		return true;
100	};
101
102	let history_visibility = self
103		.state_get_content(shortstatehash, &StateEventType::RoomHistoryVisibility, "")
104		.await
105		.map_or(HistoryVisibility::Shared, |c: RoomHistoryVisibilityEventContent| {
106			c.history_visibility
107		});
108
109	match history_visibility {
110		| HistoryVisibility::WorldReadable => true,
111
112		// Allow if any member on requesting server was AT LEAST invited, else deny
113		| HistoryVisibility::Invited =>
114			self.user_was_invited(shortstatehash, user_id)
115				.await,
116
117		// Allow if any member on requested server was joined, else deny
118		| HistoryVisibility::Joined =>
119			self.user_was_joined(shortstatehash, user_id)
120				.await,
121
122		// An unrecognized value is treated as shared.
123		| HistoryVisibility::Shared | _ =>
124			self.user_shared_history(shortstatehash, room_id, event_id, user_id)
125				.await,
126	}
127}
128
129/// Whether a user may see an event under `shared` history visibility.
130///
131/// A current member sees the whole room, which the first check answers without
132/// touching room state. A former member keeps events through their latest
133/// leave, and lookup failures deny access.
134#[implement(super::Service)]
135async fn user_shared_history(
136	&self,
137	shortstatehash: ShortStateHash,
138	room_id: &RoomId,
139	event_id: &EventId,
140	user_id: &UserId,
141) -> bool {
142	let state_cache = &self.services.state_cache;
143
144	if state_cache.is_joined(user_id, room_id).await
145		|| self
146			.user_was_joined(shortstatehash, user_id)
147			.await
148	{
149		return true;
150	}
151
152	if !state_cache.once_joined(user_id, room_id).await {
153		return false;
154	}
155
156	let Ok(left_count) = state_cache.get_left_count(room_id, user_id).await else {
157		return false;
158	};
159
160	let Ok(event_count) = self
161		.services
162		.timeline
163		.get_pdu_count(event_id)
164		.await
165	else {
166		return false;
167	};
168
169	event_count <= PduCount::from_unsigned(left_count)
170}
171
172/// Whether a user is allowed to see an event, based on
173/// the room's history_visibility at that event's state.
174#[implement(super::Service)]
175#[tracing::instrument(skip_all, level = "trace")]
176pub async fn user_can_see_state_events(&self, user_id: &UserId, room_id: &RoomId) -> bool {
177	if self
178		.services
179		.state_cache
180		.is_joined(user_id, room_id)
181		.await
182	{
183		return true;
184	}
185
186	let history_visibility = self
187		.room_state_get_content(room_id, &StateEventType::RoomHistoryVisibility, "")
188		.await
189		.map_or(HistoryVisibility::Shared, |c: RoomHistoryVisibilityEventContent| {
190			c.history_visibility
191		});
192
193	match history_visibility {
194		| HistoryVisibility::WorldReadable => true,
195
196		| HistoryVisibility::Invited =>
197			self.services
198				.state_cache
199				.is_invited(user_id, room_id)
200				.await,
201
202		| HistoryVisibility::Shared =>
203			self.services
204				.state_cache
205				.once_joined(user_id, room_id)
206				.await,
207
208		| _ => false,
209	}
210}
211
212/// Whether a user may see a room: a current or prior membership (joined,
213/// invited, left), or a world-readable room. Forgetting a room clears the
214/// user's left-state, so a forgotten room is not visible.
215#[implement(super::Service)]
216pub async fn user_can_see_room(&self, user_id: &UserId, room_id: &RoomId) -> bool {
217	let state_cache = &self.services.state_cache;
218	let joined = state_cache.is_joined(user_id, room_id);
219	let invited = state_cache.is_invited(user_id, room_id);
220	let left = state_cache.is_left(user_id, room_id);
221	let world_readable = self.is_world_readable(room_id);
222
223	pin_mut!(joined, invited, left, world_readable);
224	joined
225		.or(invited)
226		.or(left)
227		.or(world_readable)
228		.await
229}
230
231#[implement(super::Service)]
232pub async fn user_can_invite(
233	&self,
234	room_id: &RoomId,
235	sender: &UserId,
236	target_user: &UserId,
237	state_lock: &RoomMutexGuard,
238) -> bool {
239	self.services
240		.timeline
241		.create_hash_and_sign_event(
242			PduBuilder::state(
243				target_user.as_str(),
244				&RoomMemberEventContent::new(MembershipState::Invite),
245			),
246			sender,
247			room_id,
248			state_lock,
249		)
250		.await
251		.is_ok()
252}
253
254#[implement(super::Service)]
255pub async fn user_can_tombstone(
256	&self,
257	room_id: &RoomId,
258	user_id: &UserId,
259	state_lock: &RoomMutexGuard,
260) -> bool {
261	if !self
262		.services
263		.state_cache
264		.is_joined(user_id, room_id)
265		.await
266	{
267		return false;
268	}
269
270	self.services
271		.timeline
272		.create_hash_and_sign_event(
273			PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
274				replacement_room: room_id.into(), // placeholder,
275				body: "Not a valid m.room.tombstone.".into(),
276			}),
277			user_id,
278			room_id,
279			state_lock,
280		)
281		.await
282		.is_ok()
283}