Skip to main content

tuwunel_service/users/
mod.rs

1mod dehydrated_device;
2pub mod device;
3mod keys;
4mod ldap;
5mod register;
6
7use std::sync::Arc;
8
9use futures::{Stream, StreamExt, TryFutureExt};
10use ruma::{
11	MilliSecondsSinceUnixEpoch, OwnedUserId, UserId,
12	api::client::filter::FilterDefinition,
13	events::{
14		GlobalAccountDataEventType,
15		ignored_user_list::IgnoredUserListEvent,
16		invite_permission_config::{InvitePermissionAction, InvitePermissionConfigEvent},
17	},
18};
19use serde::{Deserialize, Serialize};
20use tuwunel_core::{
21	Err, Result, debug_warn, err, is_equal_to,
22	matrix::pdu::PduCount,
23	trace,
24	utils::{self, ReadyExt, stream::TryIgnore},
25};
26use tuwunel_database::{Deserialized, Json, Map};
27
28pub use self::{dehydrated_device::DehydratedDevice, keys::parse_master_key, register::Register};
29
30pub const PASSWORD_SENTINEL: &str = "*";
31pub const PASSWORD_DISABLED: &str = "";
32
33/// Forensic record for a moderation action (MSC3823 suspend, MSC3939 lock).
34/// Presence of the row is the load-bearing fact; this body is written but
35/// never read on the hot path.
36#[derive(Clone, Debug, Serialize, Deserialize)]
37pub struct Moderation {
38	pub when: MilliSecondsSinceUnixEpoch,
39	pub by: OwnedUserId,
40}
41
42pub struct Service {
43	services: Arc<crate::services::OnceServices>,
44	db: Data,
45}
46
47struct Data {
48	keychangeid_userid: Arc<Map>,
49	keyid_key: Arc<Map>,
50	onetimekeyid4225_otk: Option<Arc<Map>>,
51	openidtoken_expiresatuserid: Arc<Map>,
52	logintoken_expiresatuserid: Arc<Map>,
53	todeviceid_events: Arc<Map>,
54	spentrefresh_userdeviceid: Arc<Map>,
55	token_userdeviceid: Arc<Map>,
56	userdeviceid_metadata: Arc<Map>,
57	userdeviceid_token: Arc<Map>,
58	userdeviceidtoken_index: Arc<Map>,
59	userdeviceid_refresh: Arc<Map>,
60	userdeviceid_spentrefresh: Arc<Map>,
61	userdeviceidalgorithm_fallback: Arc<Map>,
62	oidcdevice_userdeviceid: Arc<Map>,
63	oidccskeybypass_userid: Arc<Map>,
64	userfilterid_filter: Arc<Map>,
65	userid_dehydrateddevice: Arc<Map>,
66	userid_devicelistversion: Arc<Map>,
67	userid_erased: Arc<Map>,
68	userid_lastonetimekeyupdate: Arc<Map>,
69	userid_locked: Arc<Map>,
70	userid_masterkeyid: Arc<Map>,
71	userid_password: Arc<Map>,
72	userid_origin: Arc<Map>,
73	userid_selfsigningkeyid: Arc<Map>,
74	userid_suspended: Arc<Map>,
75	userid_usersigningkeyid: Arc<Map>,
76}
77
78impl crate::Service for Service {
79	fn build(args: &crate::Args<'_>) -> Result<Arc<Self>> {
80		Ok(Arc::new(Self {
81			services: args.services.clone(),
82			db: Data {
83				keychangeid_userid: args.db["keychangeid_userid"].clone(),
84				keyid_key: args.db["keyid_key"].clone(),
85				onetimekeyid4225_otk: args.db.get("onetimekeyid4225_otk").ok().cloned(),
86				openidtoken_expiresatuserid: args.db["openidtoken_expiresatuserid"].clone(),
87				logintoken_expiresatuserid: args.db["logintoken_expiresatuserid"].clone(),
88				oidcdevice_userdeviceid: args.db["oidcdevice_userdeviceid"].clone(),
89				oidccskeybypass_userid: args.db["oidccskeybypass_userid"].clone(),
90				todeviceid_events: args.db["todeviceid_events"].clone(),
91				spentrefresh_userdeviceid: args.db["spentrefresh_userdeviceid"].clone(),
92				token_userdeviceid: args.db["token_userdeviceid"].clone(),
93				userdeviceid_metadata: args.db["userdeviceid_metadata"].clone(),
94				userdeviceid_token: args.db["userdeviceid_token"].clone(),
95				userdeviceidtoken_index: args.db["userdeviceidtoken_index"].clone(),
96				userdeviceid_refresh: args.db["userdeviceid_refresh"].clone(),
97				userdeviceid_spentrefresh: args.db["userdeviceid_spentrefresh"].clone(),
98				userdeviceidalgorithm_fallback: args.db["userdeviceidalgorithm_fallback"].clone(),
99				userfilterid_filter: args.db["userfilterid_filter"].clone(),
100				userid_dehydrateddevice: args.db["userid_dehydrateddevice"].clone(),
101				userid_devicelistversion: args.db["userid_devicelistversion"].clone(),
102				userid_erased: args.db["userid_erased"].clone(),
103				userid_lastonetimekeyupdate: args.db["userid_lastonetimekeyupdate"].clone(),
104				userid_locked: args.db["userid_locked"].clone(),
105				userid_masterkeyid: args.db["userid_masterkeyid"].clone(),
106				userid_password: args.db["userid_password"].clone(),
107				userid_origin: args.db["userid_origin"].clone(),
108				userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
109				userid_suspended: args.db["userid_suspended"].clone(),
110				userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(),
111			},
112		}))
113	}
114
115	fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
116}
117
118impl Service {
119	/// Returns true/false based on whether the recipient/receiving user has
120	/// blocked the sender
121	pub async fn user_is_ignored(&self, sender_user: &UserId, recipient_user: &UserId) -> bool {
122		self.services
123			.account_data
124			.get_global(recipient_user, GlobalAccountDataEventType::IgnoredUserList)
125			.await
126			.is_ok_and(|ignored: IgnoredUserListEvent| {
127				ignored
128					.content
129					.ignored_users
130					.keys()
131					.any(|blocked_user| blocked_user == sender_user)
132			})
133	}
134
135	/// MSC4380: `m.invite_permission_config.default_action == "block"`.
136	pub async fn invites_blocked(&self, user_id: &UserId) -> bool {
137		self.services
138			.account_data
139			.get_global(user_id, GlobalAccountDataEventType::InvitePermissionConfig)
140			.await
141			.is_ok_and(|event: InvitePermissionConfigEvent| {
142				matches!(event.content.default_action, Some(InvitePermissionAction::Block))
143			})
144	}
145
146	/// Create a new user account on this homeserver.
147	///
148	/// User origin is by default "password" (meaning that it will login using
149	/// its user_id/password). Users with other origins (currently only "ldap"
150	/// is available) have special login processes.
151	#[inline]
152	pub async fn create(
153		&self,
154		user_id: &UserId,
155		password: Option<&str>,
156		origin: Option<&str>,
157	) -> Result {
158		let origin = origin.unwrap_or("password");
159		self.db.userid_origin.insert(user_id, origin);
160		self.set_password(user_id, password).await
161	}
162
163	/// Deactivate account
164	pub async fn deactivate_account(&self, user_id: &UserId) -> Result {
165		// Revoke any SSO authorizations
166		self.services
167			.oauth
168			.revoke_user_tokens(user_id)
169			.await;
170
171		// Remove all associated devices
172		self.all_device_ids(user_id)
173			.for_each(|device_id| self.remove_device(user_id, device_id))
174			.await;
175
176		// Set the password to "" to indicate a deactivated account. Hashes will never
177		// result in an empty string, so the user will not be able to log in again.
178		// Systems like changing the password without logging in should check if the
179		// account is deactivated.
180		self.set_password(user_id, None).await?;
181
182		// TODO: Unhook 3PID
183		Ok(())
184	}
185
186	/// Check if a user has an account on this homeserver.
187	#[inline]
188	pub async fn exists(&self, user_id: &UserId) -> bool {
189		self.db.userid_password.get(user_id).await.is_ok()
190	}
191
192	/// Check if account is deactivated
193	pub async fn is_deactivated(&self, user_id: &UserId) -> Result<bool> {
194		self.db
195			.userid_password
196			.get(user_id)
197			.map_ok(|val| val.is_empty())
198			.map_err(|_| err!(Request(NotFound("User does not exist."))))
199			.await
200	}
201
202	/// Check if account is active, infallible
203	pub async fn is_active(&self, user_id: &UserId) -> bool {
204		!self.is_deactivated(user_id).await.unwrap_or(true)
205	}
206
207	/// Check if account is active, infallible
208	pub async fn is_active_local(&self, user_id: &UserId) -> bool {
209		self.services.globals.user_is_local(user_id) && self.is_active(user_id).await
210	}
211
212	/// MSC3823: account is suspended (read-mostly mode, sessions retained).
213	pub async fn is_suspended(&self, user_id: &UserId) -> bool {
214		self.db
215			.userid_suspended
216			.get(user_id)
217			.await
218			.is_ok()
219	}
220
221	/// MSC3939: account is locked (401 + soft_logout, sessions retained).
222	pub async fn is_locked(&self, user_id: &UserId) -> bool {
223		self.db.userid_locked.get(user_id).await.is_ok()
224	}
225
226	/// MSC4025: the user's events serve as pruned copies to recipients not
227	/// joined at the event. Presence-only for the serving gate.
228	pub async fn is_erased(&self, user_id: &UserId) -> bool {
229		self.db.userid_erased.get(user_id).await.is_ok()
230	}
231
232	/// MSC4025: the global count recorded at erasure, for admin surfacing;
233	/// the serving gate never reads it.
234	pub async fn erasure_count(&self, user_id: &UserId) -> Option<PduCount> {
235		self.db
236			.userid_erased
237			.get(user_id)
238			.await
239			.deserialized()
240			.map(PduCount::from_unsigned)
241			.ok()
242	}
243
244	/// MSC3823: forensic record for the active suspension, if any.
245	pub async fn get_suspension(&self, user_id: &UserId) -> Option<Moderation> {
246		self.db
247			.userid_suspended
248			.get(user_id)
249			.await
250			.deserialized::<Json<_>>()
251			.map(|Json(m)| m)
252			.ok()
253	}
254
255	/// MSC3939: forensic record for the active lock, if any.
256	pub async fn get_lock(&self, user_id: &UserId) -> Option<Moderation> {
257		self.db
258			.userid_locked
259			.get(user_id)
260			.await
261			.deserialized::<Json<_>>()
262			.map(|Json(m)| m)
263			.ok()
264	}
265
266	pub fn set_suspended(&self, user_id: &UserId, by: &UserId) {
267		let entry = Moderation {
268			when: MilliSecondsSinceUnixEpoch::now(),
269			by: by.to_owned(),
270		};
271
272		self.db
273			.userid_suspended
274			.raw_put(user_id, Json(entry));
275	}
276
277	pub fn clear_suspended(&self, user_id: &UserId) { self.db.userid_suspended.remove(user_id); }
278
279	/// MSC4025: mark the user erased, recording the current global count.
280	pub fn set_erased(&self, user_id: &UserId) {
281		let count = self.services.globals.current_count();
282
283		self.db.userid_erased.raw_put(user_id, count);
284	}
285
286	/// MSC4025: erasure is reversible; clearing the marker restores the
287	/// unredacted view.
288	pub fn clear_erased(&self, user_id: &UserId) { self.db.userid_erased.remove(user_id); }
289
290	pub fn set_locked(&self, user_id: &UserId, by: &UserId) {
291		let entry = Moderation {
292			when: MilliSecondsSinceUnixEpoch::now(),
293			by: by.to_owned(),
294		};
295
296		self.db
297			.userid_locked
298			.raw_put(user_id, Json(entry));
299	}
300
301	pub fn clear_locked(&self, user_id: &UserId) { self.db.userid_locked.remove(user_id); }
302
303	/// Returns the number of users registered on this server.
304	#[inline]
305	pub async fn count(&self) -> usize { self.db.userid_password.count().await }
306
307	/// Returns an iterator over all users on this homeserver.
308	pub fn stream(&self) -> impl Stream<Item = &UserId> + Send {
309		self.db.userid_password.keys().ignore_err()
310	}
311
312	/// Returns a list of local users as list of usernames.
313	///
314	/// A user account is considered `local` if the length of it's password is
315	/// greater then zero.
316	pub fn list_local_users(&self) -> impl Stream<Item = &UserId> + Send + '_ {
317		self.db
318			.userid_password
319			.stream()
320			.ignore_err()
321			.ready_filter_map(|(u, p): (&UserId, &[u8])| (!p.is_empty()).then_some(u))
322	}
323
324	/// Returns the origin of the user (password/LDAP/...).
325	pub async fn origin(&self, user_id: &UserId) -> Result<String> {
326		self.db
327			.userid_origin
328			.get(user_id)
329			.await
330			.deserialized()
331	}
332
333	/// Returns whether the user has a password. Disabled accounts and
334	/// registrations setting a sentinel password will return false here.
335	pub async fn has_password(&self, user_id: &UserId) -> Result<bool> {
336		self.password_hash(user_id)
337			.map_ok(|value| value != PASSWORD_DISABLED && value != PASSWORD_SENTINEL)
338			.await
339	}
340
341	/// Returns the password hash for the given user.
342	pub async fn password_hash(&self, user_id: &UserId) -> Result<String> {
343		self.db
344			.userid_password
345			.get(user_id)
346			.await
347			.deserialized()
348	}
349
350	/// Hash and set the user's password to the Argon2 hash
351	pub async fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result {
352		// Cannot change the password of a LDAP user. There are two special cases :
353		// - a `None` password can be used to deactivate a LDAP user
354		// - a "*" password is used as the default password of an active LDAP user
355		//
356		// The above now applies to all non-password origin users by default unless an
357		// exception is made for that origin in the condition below. Note that users
358		// with no origin are also password-origin users.
359		let allowed_origins = ["password", "sso"];
360		if password.is_some() && password != Some(PASSWORD_SENTINEL) {
361			let origin = self.origin(user_id).await;
362			let origin = origin.as_deref().unwrap_or("password");
363
364			if !allowed_origins.iter().any(is_equal_to!(&origin)) {
365				return Err!(Request(InvalidParam(
366					"Cannot change password of an {origin:?} user."
367				)));
368			}
369		}
370
371		match password.map(utils::hash::password) {
372			| None => {
373				self.db
374					.userid_password
375					.insert(user_id, PASSWORD_DISABLED);
376			},
377			| Some(Ok(_)) if password == Some(PASSWORD_SENTINEL) => {
378				self.db
379					.userid_password
380					.insert(user_id, PASSWORD_SENTINEL);
381			},
382			| Some(Ok(hash)) => {
383				self.db.userid_password.insert(user_id, hash);
384				self.db.userid_origin.insert(user_id, "password");
385			},
386			| Some(Err(e)) => {
387				return Err!(Request(InvalidParam(
388					"Password does not meet the requirements: {e}"
389				)));
390			},
391		}
392
393		Ok(())
394	}
395
396	/// Creates a new sync filter. Returns the filter id.
397	#[must_use]
398	pub fn create_filter(&self, user_id: &UserId, filter: &FilterDefinition) -> String {
399		let filter_id = utils::random_string(4);
400
401		let key = (user_id, &filter_id);
402		self.db.userfilterid_filter.put(key, Json(filter));
403
404		filter_id
405	}
406
407	pub async fn get_filter(
408		&self,
409		user_id: &UserId,
410		filter_id: &str,
411	) -> Result<FilterDefinition> {
412		let key = (user_id, filter_id);
413		self.db
414			.userfilterid_filter
415			.qry(&key)
416			.await
417			.deserialized()
418	}
419
420	/// Creates an OpenID token, which can be used to prove that a user has
421	/// access to an account (primarily for integrations)
422	pub fn create_openid_token(&self, user_id: &UserId, token: &str) -> Result<u64> {
423		use std::num::Saturating as Sat;
424
425		let expires_in = self.services.server.config.openid_token_ttl;
426		let expires_at = Sat(utils::millis_since_unix_epoch()) + Sat(expires_in) * Sat(1000);
427
428		let mut value = expires_at.0.to_be_bytes().to_vec();
429		value.extend_from_slice(user_id.as_bytes());
430
431		self.db
432			.openidtoken_expiresatuserid
433			.insert(token.as_bytes(), value.as_slice());
434
435		Ok(expires_in)
436	}
437
438	/// Find out which user an OpenID access token belongs to.
439	pub async fn find_from_openid_token(&self, token: &str) -> Result<OwnedUserId> {
440		let Ok(value) = self
441			.db
442			.openidtoken_expiresatuserid
443			.get(token)
444			.await
445		else {
446			return Err!(Request(Unauthorized("OpenID token is unrecognised")));
447		};
448
449		let (expires_at_bytes, user_bytes) = value.split_at(0_u64.to_be_bytes().len());
450		let expires_at =
451			u64::from_be_bytes(expires_at_bytes.try_into().map_err(|e| {
452				err!(Database("expires_at in openid_userid is invalid u64. {e}"))
453			})?);
454
455		if expires_at < utils::millis_since_unix_epoch() {
456			debug_warn!("OpenID token is expired, removing");
457			self.db
458				.openidtoken_expiresatuserid
459				.remove(token.as_bytes());
460
461			return Err!(Request(Unauthorized("OpenID token is expired")));
462		}
463
464		let user_string = utils::string_from_bytes(user_bytes)
465			.map_err(|e| err!(Database("User ID in openid_userid is invalid unicode. {e}")))?;
466
467		OwnedUserId::try_from(user_string)
468			.map_err(|e| err!(Database("User ID in openid_userid is invalid. {e}")))
469	}
470
471	/// Creates a short-lived login token, which can be used to log in using the
472	/// `m.login.token` mechanism.
473	#[must_use]
474	pub fn create_login_token(&self, user_id: &UserId, token: &str) -> u64 {
475		use std::num::Saturating as Sat;
476
477		let expires_in = self.services.server.config.login_token_ttl;
478		let expires_at = Sat(utils::millis_since_unix_epoch()) + Sat(expires_in);
479
480		let value = (expires_at.0, user_id);
481		self.db
482			.logintoken_expiresatuserid
483			.raw_put(token, value);
484
485		expires_in
486	}
487
488	/// Verify a login token is valid and return its owner without consuming it.
489	/// Unlike `find_from_login_token`, the token remains in the database
490	/// after this call and can still be consumed later.
491	pub async fn peek_login_token(&self, token: &str) -> Result<OwnedUserId> {
492		let Ok(value) = self
493			.db
494			.logintoken_expiresatuserid
495			.get(token)
496			.await
497		else {
498			return Err!(Request(Forbidden("Login token is unrecognised")));
499		};
500		let (expires_at, user_id): (u64, OwnedUserId) = value.deserialized()?;
501
502		if expires_at < utils::millis_since_unix_epoch() {
503			trace!(?user_id, ?token, "Removing expired login token");
504			self.db.logintoken_expiresatuserid.remove(token);
505			return Err!(Request(Forbidden("Login token is expired")));
506		}
507
508		Ok(user_id)
509	}
510
511	/// Find out which user a login token belongs to.
512	/// Removes the token to prevent double-use attacks.
513	pub async fn find_from_login_token(&self, token: &str) -> Result<OwnedUserId> {
514		let Ok(value) = self
515			.db
516			.logintoken_expiresatuserid
517			.get(token)
518			.await
519		else {
520			return Err!(Request(Forbidden("Login token is unrecognised")));
521		};
522		let (expires_at, user_id): (u64, OwnedUserId) = value.deserialized()?;
523
524		if expires_at < utils::millis_since_unix_epoch() {
525			trace!(?user_id, ?token, "Removing expired login token");
526
527			self.db.logintoken_expiresatuserid.remove(token);
528
529			return Err!(Request(Forbidden("Login token is expired")));
530		}
531
532		self.db.logintoken_expiresatuserid.remove(token);
533
534		Ok(user_id)
535	}
536
537	#[cfg(not(feature = "ldap"))]
538	#[expect(clippy::unused_async)]
539	pub async fn search_ldap(&self, _user_id: &UserId) -> Result<Vec<(String, bool)>> {
540		Err!(FeatureDisabled("ldap"))
541	}
542
543	#[cfg(not(feature = "ldap"))]
544	#[expect(clippy::unused_async)]
545	pub async fn auth_ldap(&self, _user_dn: &str, _password: &str) -> Result {
546		Err!(FeatureDisabled("ldap"))
547	}
548
549	#[cfg(not(feature = "ldap"))]
550	#[must_use]
551	pub fn ldap_bind_dn(&self, _localpart: &str) -> Option<String> { None }
552}