Skip to main content

tuwunel_service/users/
invite_filter.rs

1use futures::{TryFutureExt, future::join4};
2use ruma::{
3	UserId,
4	events::{
5		GlobalAccountDataEventType,
6		ignored_user_list::{IgnoredUserListEvent, IgnoredUserListEventContent},
7		invite_permission_config::{
8			InvitePermission, InvitePermissionAction, InvitePermissionConfigEvent,
9			InvitePermissionConfigEventContent, UnstableInvitePermissionConfigEvent,
10			UnstableInvitePermissionConfigEventContent,
11		},
12	},
13};
14use serde::Deserialize;
15use tuwunel_core::{implement, utils::future::TryExtExt};
16
17/// A recipient's invite filtering state, evaluable against any sender.
18///
19/// Holds the ignored-user list alongside every `invite_permission_config`
20/// account data slot a client may have written. The slots are read together
21/// once and then judged per sender, so a caller facing many invites pays for
22/// the account data once.
23pub struct InviteFilter {
24	ignored: Option<IgnoredUserListEventContent>,
25	stable: Option<InvitePermissionConfigEventContent>,
26	unstable_blanket: Option<InvitePermissionConfigEventContent>,
27	unstable_lists: Option<InvitePermissionConfigEventContent>,
28}
29
30/// The account-data slot for MSC4380 configs written before spec adoption.
31///
32/// Carries the blanket switch alone, which reads into the same content type as
33/// a `default_action` of `block`. A block written here outlives the stable
34/// slot, since clearing `default_action` there leaves this one standing and no
35/// client writes this type any more.
36const BLANKET_TYPE: &str = "org.matrix.msc4380.invite_permission_config";
37
38/// The account-data slot for MSC4155 configs written before spec adoption.
39///
40/// Carries the rule lists, which the spec has not adopted, so this slot is
41/// the only place a client can write them today.
42const LISTS_TYPE: &str = "org.matrix.msc4155.invite_permission_config";
43
44/// Fetch the invite filtering state a recipient has configured.
45///
46/// The four account data slots are read concurrently, and a slot that is
47/// absent or malformed drops out rather than failing the others.
48#[implement(super::Service)]
49pub async fn invite_filter(&self, recipient: &UserId) -> InviteFilter {
50	let ignored = self
51		.services
52		.account_data
53		.get_global(recipient, GlobalAccountDataEventType::IgnoredUserList)
54		.map_ok(|event: IgnoredUserListEvent| event.content)
55		.ok();
56
57	let stable = self.stable_config(recipient);
58
59	let unstable_blanket = self
60		.services
61		.account_data
62		.get_global(recipient, BLANKET_TYPE.into())
63		.map_ok(|event: UnstableInvitePermissionConfigEvent| event.content.into())
64		.ok();
65
66	let unstable_lists = self
67		.services
68		.account_data
69		.get_global(recipient, LISTS_TYPE.into())
70		.map_ok(|event: InvitePermissionConfigEvent| event.content)
71		.ok();
72
73	let (ignored, stable, unstable_blanket, unstable_lists) =
74		join4(ignored, stable, unstable_blanket, unstable_lists).await;
75
76	InviteFilter {
77		ignored,
78		stable,
79		unstable_blanket,
80		unstable_lists,
81	}
82}
83
84/// The stable slot, keeping its blanket switch even when its lists are not.
85///
86/// MSC4155 invalidates the whole event when a rule list is not an array, but
87/// the blanket block is stable spec and outranks the lists, so it is read back
88/// on its own rather than lost with them.
89#[implement(super::Service)]
90async fn stable_config(&self, recipient: &UserId) -> Option<InvitePermissionConfigEventContent> {
91	let kind = GlobalAccountDataEventType::InvitePermissionConfig;
92
93	let config = self
94		.services
95		.account_data
96		.get_global(recipient, kind.clone())
97		.map_ok(|event: InvitePermissionConfigEvent| event.content)
98		.await;
99
100	match config {
101		| Ok(content) => Some(content),
102		| Err(_) => self
103			.services
104			.account_data
105			.get_global(recipient, kind)
106			.map_ok(|event: BlanketEvent| event.content.default_action)
107			.await
108			.is_ok_and(|action| matches!(action, Some(InvitePermissionAction::Block)))
109			.then(|| UnstableInvitePermissionConfigEventContent::new(true).into()),
110	}
111}
112
113/// The blanket switch alone, for an event MSC4155 considers invalid.
114#[derive(Deserialize)]
115struct BlanketEvent {
116	content: BlanketContent,
117}
118
119#[derive(Deserialize)]
120struct BlanketContent {
121	#[serde(default, deserialize_with = "ruma::serde::default_on_error")]
122	default_action: Option<InvitePermissionAction>,
123}
124
125/// The recipient's verdict on a single invite from one sender.
126///
127/// Fetches the filtering state and discards it, so a caller judging several
128/// senders should hold an [`InviteFilter`] instead.
129#[implement(super::Service)]
130pub async fn invite_permission(&self, sender: &UserId, recipient: &UserId) -> InvitePermission {
131	self.invite_filter(recipient)
132		.await
133		.permission(sender)
134}
135
136/// Whether an invite from `sender` may be served to its recipient.
137///
138/// A `sender` of `None` means the stored invite state named no sender, which is
139/// a defect in what this server wrote rather than anything the recipient asked
140/// for, so every rule needing a sender falls open and the invite is served.
141/// The blanket block is decided without one and still applies, since the
142/// invite-permission module makes withholding those from sync a MUST.
143#[implement(InviteFilter)]
144#[must_use]
145pub fn permits(&self, sender: Option<&UserId>) -> bool {
146	match sender {
147		| None => !self.blocks_all(),
148		| Some(sender) => self
149			.permission(sender)
150			.eq(&InvitePermission::Allow),
151	}
152}
153
154/// The recipient's verdict on an invite from `sender`.
155///
156/// A blanket block outranks everything, since the invite-permission module
157/// requires the 403 even when the sender is also on the ignore list. The
158/// ignored-user list comes next, then the rule lists slot by slot, and the
159/// first verdict other than allow wins.
160#[implement(InviteFilter)]
161#[must_use]
162pub fn permission(&self, sender: &UserId) -> InvitePermission {
163	match self {
164		| _ if self.blocks_all() => InvitePermission::Block,
165		| _ if self.ignores(sender) => InvitePermission::Ignore,
166		| _ => self
167			.configs()
168			.map(|config| config.permission(sender))
169			.find(|permission| permission.ne(&InvitePermission::Allow))
170			.unwrap_or(InvitePermission::Allow),
171	}
172}
173
174/// Whether the recipient accepts no invites at all.
175///
176/// Any slot carrying the blanket switch decides this, so a client writing
177/// one slot is never undone by another slot left behind.
178#[implement(InviteFilter)]
179#[inline]
180fn blocks_all(&self) -> bool {
181	self.configs()
182		.any(|config| config.default_action == Some(InvitePermissionAction::Block))
183}
184
185/// Whether the recipient ignores `sender` account-wide.
186///
187/// This is the ordinary `m.ignored_user_list`, which the invite-permission
188/// module defers to for everything short of a blanket block.
189#[implement(InviteFilter)]
190#[inline]
191fn ignores(&self, sender: &UserId) -> bool {
192	self.ignored
193		.as_ref()
194		.is_some_and(|content| content.ignored_users.contains_key(sender))
195}
196
197/// Whether the recipient configured no filtering at all.
198///
199/// Callers on the sync path lean on this to skip deriving an invite's sender,
200/// which costs a stripped-state load they would otherwise pay per room.
201#[implement(InviteFilter)]
202#[inline]
203#[must_use]
204pub fn is_permissive(&self) -> bool {
205	self.ignored
206		.as_ref()
207		.is_none_or(|ignored| ignored.ignored_users.is_empty())
208		&& self
209			.configs()
210			.all(InvitePermissionConfigEventContent::is_inert)
211}
212
213/// The configuration slots in evaluation order.
214#[implement(InviteFilter)]
215#[inline]
216fn configs(&self) -> impl Iterator<Item = &InvitePermissionConfigEventContent> + Send {
217	self.stable
218		.iter()
219		.chain(self.unstable_blanket.iter())
220		.chain(self.unstable_lists.iter())
221}
222
223#[cfg(test)]
224mod tests {
225	use ruma::{
226		events::invite_permission_config::UnstableInvitePermissionConfigEventContent, user_id,
227	};
228	use serde_json::{from_value, json};
229
230	use super::{InviteFilter, InvitePermission};
231
232	fn filter(
233		ignored: Option<serde_json::Value>,
234		stable: Option<serde_json::Value>,
235		unstable_lists: Option<serde_json::Value>,
236	) -> InviteFilter {
237		InviteFilter {
238			ignored: ignored.map(|content| from_value(content).unwrap()),
239			stable: stable.map(|content| from_value(content).unwrap()),
240			unstable_blanket: None,
241			unstable_lists: unstable_lists.map(|content| from_value(content).unwrap()),
242		}
243	}
244
245	fn blanket_filter(block_all: bool) -> InviteFilter {
246		InviteFilter {
247			ignored: None,
248			stable: None,
249			unstable_blanket: Some(
250				UnstableInvitePermissionConfigEventContent::new(block_all).into(),
251			),
252			unstable_lists: None,
253		}
254	}
255
256	#[test]
257	fn blanket_block_outranks_ignored_list() {
258		let sender = user_id!("@alice:example.org");
259		let ignored = json!({"ignored_users": {"@alice:example.org": {}}});
260
261		let both = filter(Some(ignored.clone()), Some(json!({"default_action": "block"})), None);
262
263		assert_eq!(both.permission(sender), InvitePermission::Block);
264
265		let ignored_only = filter(Some(ignored), None, None);
266
267		assert_eq!(ignored_only.permission(sender), InvitePermission::Ignore);
268	}
269
270	#[test]
271	fn senderless_invite_is_served() {
272		let sender = user_id!("@alice:example.org");
273		let blocked = filter(None, None, Some(json!({"blocked_servers": ["*"]})));
274
275		assert!(!blocked.is_permissive());
276		assert_eq!(blocked.permission(sender), InvitePermission::Block);
277		assert!(blocked.permits(None));
278		assert!(!blocked.permits(Some(sender)));
279
280		let ignored = json!({"ignored_users": {"@alice:example.org": {}}});
281		let ignoring = filter(Some(ignored), None, None);
282
283		assert!(ignoring.permits(None));
284		assert!(!ignoring.permits(Some(sender)));
285
286		let blanket = blanket_filter(true);
287
288		assert!(!blanket.permits(None));
289		assert!(!blanket.permits(Some(sender)));
290	}
291
292	#[test]
293	fn permissive_filter_permits_every_sender() {
294		let permissive = filter(None, None, Some(json!({})));
295
296		assert!(permissive.is_permissive());
297		assert!(permissive.permits(None));
298		assert!(permissive.permits(Some(user_id!("@alice:example.org"))));
299	}
300
301	#[test]
302	fn vacuous_configuration_is_permissive() {
303		let empty_list = filter(Some(json!({"ignored_users": {}})), None, None);
304
305		assert!(empty_list.is_permissive());
306
307		let empty_config = filter(None, Some(json!({})), Some(json!({"allowed_users": []})));
308
309		assert!(empty_config.is_permissive());
310
311		let disabled =
312			filter(None, None, Some(json!({"enabled": false, "blocked_servers": ["*"]})));
313
314		assert!(disabled.is_permissive());
315
316		let filtering = filter(None, None, Some(json!({"blocked_servers": ["*"]})));
317
318		assert!(!filtering.is_permissive());
319		assert!(!blanket_filter(true).is_permissive());
320	}
321
322	#[test]
323	fn unstable_blanket_slot_blocks() {
324		let sender = user_id!("@alice:example.org");
325
326		assert_eq!(blanket_filter(true).permission(sender), InvitePermission::Block);
327		assert_eq!(blanket_filter(false).permission(sender), InvitePermission::Allow);
328		assert!(blanket_filter(false).is_permissive());
329	}
330
331	#[test]
332	fn first_non_allow_verdict_spans_both_slots() {
333		let sender = user_id!("@alice:example.org");
334
335		let cross = filter(
336			None,
337			Some(json!({"allowed_users": ["@alice:example.org"]})),
338			Some(json!({"blocked_servers": ["*"]})),
339		);
340
341		assert_eq!(cross.permission(sender), InvitePermission::Block);
342
343		let unstable_only = filter(None, None, Some(json!({"ignored_users": ["@alice:*"]})));
344
345		assert_eq!(unstable_only.permission(sender), InvitePermission::Ignore);
346	}
347}