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, StateKey},
16	pdu::PduBuilder,
17	utils::FutureBoolExt,
18};
19
20use crate::rooms::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		| HistoryVisibility::Shared | _ =>
123			self.services
124				.state_cache
125				.is_joined(user_id, room_id)
126				.await,
127	}
128}
129
130/// Whether a user is allowed to see an event, based on
131/// the room's history_visibility at that event's state.
132#[implement(super::Service)]
133#[tracing::instrument(skip_all, level = "trace")]
134pub async fn user_can_see_state_events(&self, user_id: &UserId, room_id: &RoomId) -> bool {
135	if self
136		.services
137		.state_cache
138		.is_joined(user_id, room_id)
139		.await
140	{
141		return true;
142	}
143
144	let history_visibility = self
145		.room_state_get_content(room_id, &StateEventType::RoomHistoryVisibility, "")
146		.await
147		.map_or(HistoryVisibility::Shared, |c: RoomHistoryVisibilityEventContent| {
148			c.history_visibility
149		});
150
151	match history_visibility {
152		| HistoryVisibility::WorldReadable => true,
153
154		| HistoryVisibility::Invited =>
155			self.services
156				.state_cache
157				.is_invited(user_id, room_id)
158				.await,
159
160		| HistoryVisibility::Shared =>
161			self.services
162				.state_cache
163				.once_joined(user_id, room_id)
164				.await,
165
166		| _ => false,
167	}
168}
169
170/// Whether a user may see a room: a current or prior membership (joined,
171/// invited, left), or a world-readable room. Forgetting a room clears the
172/// user's left-state, so a forgotten room is not visible.
173#[implement(super::Service)]
174pub async fn user_can_see_room(&self, user_id: &UserId, room_id: &RoomId) -> bool {
175	let state_cache = &self.services.state_cache;
176	let joined = state_cache.is_joined(user_id, room_id);
177	let invited = state_cache.is_invited(user_id, room_id);
178	let left = state_cache.is_left(user_id, room_id);
179	let world_readable = self.is_world_readable(room_id);
180
181	pin_mut!(joined, invited, left, world_readable);
182	joined
183		.or(invited)
184		.or(left)
185		.or(world_readable)
186		.await
187}
188
189#[implement(super::Service)]
190pub async fn user_can_invite(
191	&self,
192	room_id: &RoomId,
193	sender: &UserId,
194	target_user: &UserId,
195	state_lock: &RoomMutexGuard,
196) -> bool {
197	self.services
198		.timeline
199		.create_hash_and_sign_event(
200			PduBuilder::state(
201				target_user.as_str(),
202				&RoomMemberEventContent::new(MembershipState::Invite),
203			),
204			sender,
205			room_id,
206			state_lock,
207		)
208		.await
209		.is_ok()
210}
211
212#[implement(super::Service)]
213pub async fn user_can_tombstone(
214	&self,
215	room_id: &RoomId,
216	user_id: &UserId,
217	state_lock: &RoomMutexGuard,
218) -> bool {
219	if !self
220		.services
221		.state_cache
222		.is_joined(user_id, room_id)
223		.await
224	{
225		return false;
226	}
227
228	self.services
229		.timeline
230		.create_hash_and_sign_event(
231			PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
232				replacement_room: room_id.into(), // placeholder,
233				body: "Not a valid m.room.tombstone.".into(),
234			}),
235			user_id,
236			room_id,
237			state_lock,
238		)
239		.await
240		.is_ok()
241}