use std::{ net::IpAddr, time::{Duration, SystemTime}, }; use conduwuit::{ Err, Result, debug, debug_error, debug_warn, err, error, info, trace, utils::{self, stream::TryIgnore}, warn, }; use database::{Deserialized, Json}; use futures::{FutureExt, Stream, StreamExt}; use lettre::Address; use ruma::{ MilliSecondsSinceUnixEpoch, OwnedDeviceId, OwnedUserId, UserId, api::client::profile::PropagateTo, events::{ GlobalAccountDataEventType, ignored_user_list::IgnoredUserListEvent, invite_permission_config::InvitePermissionAction, push_rules::PushRulesEvent, room::message::RoomMessageEventContent, }, profile::ProfileFieldValue, push::Ruleset, room::JoinRule, }; use ruminuwuity::invite_permission_config::{FilterLevel, InvitePermissionConfigEvent}; use crate::{ appservice::RegistrationInfo, users::{HashedPassword, UserSuspension, profile::ProfileFieldChange}, }; /// The status of an access token. pub enum AccessTokenStatus { Valid, Expired, } /// The status of a user account. #[derive(Clone, Copy)] pub enum AccountStatus { NotFound, Active, Deactivated, } impl AccountStatus { #[must_use] pub fn is_found(&self) -> bool { !matches!(self, Self::NotFound) } #[must_use] pub fn is_active(&self) -> bool { matches!(self, Self::Active) } pub fn ensure_active(&self) -> Result<()> { match self { | Self::Active => Ok(()), | Self::Deactivated => Err!(Request(UserDeactivated("This account is deactivated."))), | Self::NotFound => Err!(Request(NotFound("This account does not exist."))), } } } impl super::Service { /// Returns true/false based on whether the recipient/receiving user has /// ignored the sender. pub async fn user_is_ignored(&self, sender_user: &UserId, recipient_user: &UserId) -> bool { self.services .account_data .get_global(recipient_user, GlobalAccountDataEventType::IgnoredUserList) .await .is_ok_and(|ignored: IgnoredUserListEvent| { ignored .content .ignored_users .keys() .any(|blocked_user| blocked_user == sender_user) }) } /// Returns the recipient's filter level for an invite from the sender. /// /// If the sender is ignored by the recipient, `Ignore` is returned. /// Otherwise, the resulting value depends on their invite blocking or /// invite filtering configuration. pub async fn invite_filter_level( &self, sender_user: &UserId, recipient_user: &UserId, ) -> FilterLevel { if self.user_is_ignored(sender_user, recipient_user).await { FilterLevel::Ignore } else { let (stable, unstable) = tokio::join!( self.services .account_data .get_global::( recipient_user, GlobalAccountDataEventType::InvitePermissionConfig ), self.services .account_data .get_global::( recipient_user, "org.matrix.msc4155.invite_permission_config".into() ) // TODO: MSC4155 probably needs upstreaming to ruma at some point ); if stable.is_err() && unstable.is_err() { return FilterLevel::Default; } let content = stable.unwrap_or_else(|_| unstable.unwrap()).content; let verdict = content.user_filter_level(sender_user); if verdict != FilterLevel::Default { return verdict; } // Apply other layers of invite blocking first, so that user/server // denies take priority over implicit allows. if content.default_action == Some(InvitePermissionAction::DenyPublic) && !self .check_msc4494_mutual_room(sender_user, recipient_user) .await { return FilterLevel::Block; } FilterLevel::Default } } /// Checks that sender and recipient share at least one room that has a join /// rule other than `public`. async fn check_msc4494_mutual_room(&self, sender: &UserId, recipient: &UserId) -> bool { let mut mutual_rooms = std::pin::pin!( self.services .state_cache .get_shared_rooms(sender, recipient) ); while let Some(room_id) = mutual_rooms.next().await { if self.services.state_accessor.get_join_rules(&room_id).await != JoinRule::Public { debug!( %sender, %recipient, %room_id, "Sender and recipient share a non-public room" ); return true; } } false } /// Check if a user is an admin #[inline] pub async fn is_admin(&self, user_id: &UserId) -> bool { self.services.admin.user_is_admin(user_id).await } /// Create a new shadow user account on this server. Shadow accounts /// have no password and cannot be logged into. pub async fn create_shadow_account(&self, user_id: &UserId) -> Result<()> { assert!(self.services.globals.user_is_local(user_id), "user id must be local"); if self.status(user_id).await.is_found() { return Err!(Request(UserInUse("An account with this user ID already exists."))); } self.db.userid_password.insert(user_id, ""); Ok(()) } /// Create a new account for a local human or bot user. If `password` is /// None, the account will be a shadow account. pub async fn create_local_account( &self, user_id: &UserId, password: Option, email: Option
, client: Option<&IpAddr>, device_name: Option<&str>, ) -> Result<()> { self.create_shadow_account(user_id).await?; if let Some(password) = password { self.convert_to_local_account(user_id, password).await?; } if let Some(client) = client { let notice = if let Some(device_name) = device_name { format!( "New user \"{user_id}\" registered on this server from IP {client} and \ device display name \"{device_name}\".", ) } else { format!("New user \"{user_id}\" registered on this server from IP {client}.") }; info!("{notice}"); if self.services.config.admin_room_notices { self.services.admin.notice(¬ice).await; } } // Set an initial display name { let mut displayname = user_id.localpart().to_owned(); let suffix = &self.services.config.new_user_displayname_suffix; if !suffix.is_empty() { displayname.push(' '); displayname.push_str(suffix); } self.set_profile_field( user_id, ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)), PropagateTo::None, ) .await .expect("should be able to set display name"); }; // Set default push rules self.services .account_data .update( None, user_id, GlobalAccountDataEventType::PushRules.to_string().into(), &serde_json::to_value(PushRulesEvent::new( Ruleset::server_default(user_id).into(), )) .expect("should be able to serialize push rules"), ) .await .expect("should be able to update account data"); // If the user registered with an email, associate it with their // account. if let Some(email) = email { // This may fail if the email is already in use, but we should have // already checked that when we sent the validation email, so // ignoring the error is acceptable here in the rare case that an // email is sniped by another user between the validation email // being sent and the account being created. let _ = self .services .threepid .associate_localpart_email(user_id.localpart(), &email) .await; } // Attempt to empower the first user and disable first-run mode. let was_first_user = self.services.firstrun.empower_first_user(user_id).await; // If the registering user was not the first and we're suspending users // on register, suspend them. if !was_first_user && self.services.config.suspend_on_register { // Note that we can still do auto joins for suspended users self.suspend_account(user_id, &self.services.globals.server_user) .await; // And send an @room notice to the admin room, to prompt admins to // review the new user and ideally unsuspend them if deemed // appropriate. if self.services.config.admin_room_notices { self.services .admin .send_loud_message(RoomMessageEventContent::text_plain(format!( "User {user_id} has been suspended as they are not the first user on \ this server. Please review and unsuspend them if appropriate." ))) .await .ok(); } } // Autojoin the user to the configured autojoin rooms for room in &self.services.config.auto_join_rooms { let Ok(room_id) = self.services.alias.resolve(room).await else { error!( "Failed to resolve room alias to room ID when attempting to auto join \ {room}, skipping" ); continue; }; if !self .services .state_cache .server_in_room(self.services.globals.server_name(), &room_id) .await { warn!( "Skipping room {room} to automatically join as we have never joined before." ); continue; } if let Some(room_server_name) = room.server_name() { match self .services .membership .join_room( user_id, &room_id, Some("Automatically joining this room upon registration".to_owned()), &[ self.services.globals.server_name().to_owned(), room_server_name.to_owned(), ], ) .boxed() .await { | Err(e) => { // don't return this error so we don't fail // registrations error!( "Failed to automatically join room {room} for user {user_id}: {e}" ); }, | _ => { info!("Automatically joined room {room} for user {user_id}"); }, } } } info!("Created new user account for {user_id}"); Ok(()) } pub async fn determine_registration_user_id( &self, supplied_username: Option, email: Option<&Address>, appservice_info: Option<&RegistrationInfo>, ) -> Result { const RANDOM_USER_ID_LENGTH: usize = 10; let emergency_mode_enabled = self.services.config.emergency_password.is_some(); let supplied_username = supplied_username.or_else(|| { // If the user didn't supply a username but did supply an email, use // the email's user part to avoid falling back to a random username email.map(|address| address.user().to_owned()) }); if let Some(supplied_username) = supplied_username { // The user gets to pick their username. Do some validation to make // sure it's acceptable. // Don't allow registration with forbidden usernames. if self .services .globals .forbidden_usernames() .is_match(&supplied_username) && !emergency_mode_enabled { return Err!(Request(Forbidden("Username is forbidden"))); } // Create and validate the user ID let user_id = match UserId::parse_with_server_name( &supplied_username, self.services.globals.server_name(), ) { | Ok(user_id) => user_id, | Err(e) => { return Err!(Request(InvalidUsername(debug_warn!( "Username {supplied_username} is not valid: {e}" )))); }, }; if let Err(e) = user_id.validate_strict() { // Unless we are in emergency mode, we should follow synapse's // behaviour on not allowing things like spaces and UTF-8 // characters in usernames if !emergency_mode_enabled { return Err!(Request(InvalidUsername(debug_warn!( "Username {supplied_username} contains disallowed characters or spaces: \ {e}" )))); } } // Don't allow registration with user IDs that aren't local if !self.services.globals.user_is_local(&user_id) { return Err!(Request(InvalidUsername( "Username {supplied_username} is not local to this server" ))); } if self.status(&user_id).await.is_found() { return Err!(Request(UserInUse("Username is not available."))); } // Check that the user ID is/is not in an appservice's namespace if let Some(appservice_info) = appservice_info { if !appservice_info.is_user_match(&user_id) && !emergency_mode_enabled { return Err!(Request(Exclusive( "Username is not in this appservice's namespace." ))); } } else if self .services .appservice .is_exclusive_user_id(&user_id) .await && !emergency_mode_enabled { return Err!(Request(Exclusive("Username is reserved by an appservice."))); } Ok(user_id) } else { // The user didn't specify a username. Generate a username for // them. loop { let user_id = UserId::parse_with_server_name( utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(), self.services.globals.server_name(), ) .unwrap(); if !self.status(&user_id).await.is_found() { break Ok(user_id); } } } } /// Deactivates an account, removing all of their device IDs. pub async fn deactivate_account(&self, user_id: &UserId) -> Result<()> { self.status(user_id).await.ensure_active()?; // Remove all associated devices self.all_device_ids(user_id) .for_each(async |device_id| self.remove_device(user_id, &device_id).await) .await; // Mark user as deactivated self.db.userid_deactivated.insert(user_id, ""); // TODO: Unhook 3PID Ok(()) } /// Suspend account, placing it in a read-only state pub async fn suspend_account(&self, user_id: &UserId, suspending_user: &UserId) { self.db.userid_suspension.raw_put( user_id, Json(UserSuspension { suspended: true, suspended_at: MilliSecondsSinceUnixEpoch::now().get().into(), suspended_by: suspending_user.to_string(), }), ); } /// Unsuspend account, placing it in a read-write state pub async fn unsuspend_account(&self, user_id: &UserId) { self.db.userid_suspension.remove(user_id); } /// Locks an account, preventing it being used until it is unlocked. pub async fn lock_account(&self, user_id: &UserId, locking_user: &UserId) { // NOTE: Locking is basically just suspension with a more severe effect, // so we'll just re-use the suspension data structure to store the lock // state. let suspension = self .db .userid_lock .get(user_id) .await .deserialized::() .unwrap_or_else(|_| UserSuspension { suspended: true, suspended_at: MilliSecondsSinceUnixEpoch::now().get().into(), suspended_by: locking_user.to_string(), }); self.db.userid_lock.raw_put(user_id, Json(suspension)); } /// Unlocks an account, allowing the user to log in and use it again. pub async fn unlock_account(&self, user_id: &UserId) { self.db.userid_lock.remove(user_id); } /// Check if account is suspended. Returns false if the user does not exist. pub async fn is_suspended(&self, user_id: &UserId) -> Result { match self .db .userid_suspension .get(user_id) .await .deserialized::() { | Ok(s) => Ok(s.suspended), | Err(e) => if e.is_not_found() { Ok(false) } else { Err(e) }, } } /// Returns true if the user is locked. Returns false if the user does not /// exist or is not locked. pub async fn is_locked(&self, user_id: &UserId) -> Result { match self .db .userid_lock .get(user_id) .await .deserialized::() { | Ok(s) => Ok(s.suspended), | Err(e) => if e.is_not_found() { Ok(false) } else { Err(e) }, } } /// Disables login for a user, preventing them from creating new devices, /// but allows them to continue using their existing sessions unimpeded. pub fn disable_login(&self, user_id: &UserId) { self.db.userid_logindisabled.insert(user_id, ""); } /// Re-enables login for a user, allowing them to create new devices again. pub fn enable_login(&self, user_id: &UserId) { self.db.userid_logindisabled.remove(user_id); } /// Returns true if the target user's login is disabled. pub async fn is_login_disabled(&self, user_id: &UserId) -> bool { self.db .userid_logindisabled .exists(user_id.as_str()) .await .is_ok() } /// Checks the activation status of the provided user ID's account. pub async fn status(&self, user_id: &UserId) -> AccountStatus { if !self.services.globals.user_is_local(user_id) { AccountStatus::NotFound } else if self.db.userid_password.exists(user_id).await.is_ok() { if self.db.userid_deactivated.exists(user_id).await.is_ok() { AccountStatus::Deactivated } else { AccountStatus::Active } } else { AccountStatus::NotFound } } /// Checks if a user is a shadow. pub async fn is_shadow(&self, user_id: &UserId) -> bool { self.db .userid_password .get(user_id) .await .deserialized::() .is_ok_and(|hash| hash.is_empty()) } /// Returns the number of users registered on this server, including /// deactivated and shadow users. #[inline] pub async fn count(&self) -> usize { self.db.userid_password.count().await } /// Find out which user an access token belongs to. Will panic if the access /// token is empty. pub async fn find_from_token( &self, token: &str, ) -> Option<(OwnedUserId, OwnedDeviceId, AccessTokenStatus)> { assert!(!token.is_empty(), "Empty access token"); let (user_id, device_id) = self .db .token_userdeviceid .get(token) .await .deserialized() .ok()?; // Check if the token has expired if let Some(expires) = self .db .userdeviceid_tokenexpires .qry(&(&user_id, &device_id)) .await .deserialized::() .ok() .map(Duration::from_secs) { let expires_at = SystemTime::UNIX_EPOCH .checked_add(expires) .expect("expiry time should not overflow SystemTime"); if SystemTime::now() > expires_at { return Some((user_id, device_id, AccessTokenStatus::Expired)); } } Some((user_id, device_id, AccessTokenStatus::Valid)) } /// Returns an iterator over all local users on this homeserver. pub fn stream_local_users(&self) -> impl Stream + Send { self.db.userid_password.keys().ignore_err() } /// Set a user's password. pub async fn set_password(&self, user_id: &UserId, password: HashedPassword) -> Result { self.status(user_id).await.ensure_active()?; self.db.userid_password.insert(user_id, password.0); Ok(()) } /// Convert an existing user to an active local account. This will turn /// shadow accounts into normal accounts and will clear the deactivation /// flag on the user if it is set. pub async fn convert_to_local_account( &self, user_id: &UserId, password: HashedPassword, ) -> Result { if !self.status(user_id).await.is_found() { return Err!(Request(NotFound("This account does not exist."))); } self.db.userid_deactivated.remove(user_id); self.db.userid_password.insert(user_id, password.0); Ok(()) } /// Convert an existing user to a shadow account. This will clear their /// password and reactivate them if they are deactivated. pub async fn convert_to_shadow_account(&self, user_id: &UserId) -> Result { if !self.status(user_id).await.is_found() { return Err!(Request(NotFound("This account does not exist."))); } self.db.userid_deactivated.remove(user_id); self.db.userid_password.insert(user_id, ""); Ok(()) } /// Check a user's password. pub async fn check_password(&self, user_id: &UserId, password: &str) -> Result { self.status(user_id).await.ensure_active()?; let (hash, user_id): (String, OwnedUserId) = if let Ok(hash) = self.db.userid_password.get(user_id).await.deserialized() { (hash, user_id.to_owned()) } else { // We also check the lowercased version of the user ID to handle // legacy user IDs better let lowercase_user_id = UserId::parse(user_id.as_str().to_lowercase()).unwrap(); let hash = self .db .userid_password .get(lowercase_user_id.as_str()) .await .deserialized() .expect("user should exist"); (hash, lowercase_user_id) }; if hash.is_empty() { // Cannot log into shadow users return Err!(Request(Forbidden("This user cannot log in with a password."))); } utils::hash::verify_password(password, &hash) .inspect_err(|e| debug_error!("{e}")) .map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?; Ok(user_id) } /// Creates an OpenID token, which can be used to prove that a user has /// access to an account (primarily for integrations) pub fn create_openid_token(&self, user_id: &UserId, token: &str) -> Result { use std::num::Saturating as Sat; let expires_in = self.services.config.openid_token_ttl; let expires_at = Sat(utils::millis_since_unix_epoch()) + Sat(expires_in) * Sat(1000); let mut value = expires_at.0.to_be_bytes().to_vec(); value.extend_from_slice(user_id.as_bytes()); self.db .openidtoken_expiresatuserid .insert(token.as_bytes(), value.as_slice()); Ok(expires_in) } /// Find out which user an OpenID access token belongs to. pub async fn find_from_openid_token(&self, token: &str) -> Result { let Ok(value) = self.db.openidtoken_expiresatuserid.get(token).await else { return Err!(Request(Unauthorized("OpenID token is unrecognised"))); }; let (expires_at_bytes, user_bytes) = value.split_at(0_u64.to_be_bytes().len()); let expires_at = u64::from_be_bytes(expires_at_bytes.try_into().map_err(|e| { err!(Database("expires_at in openid_userid is invalid u64. {e}")) })?); if expires_at < utils::millis_since_unix_epoch() { debug_warn!("OpenID token is expired, removing"); self.db.openidtoken_expiresatuserid.remove(token.as_bytes()); return Err!(Request(Unauthorized("OpenID token is expired"))); } let user_string = utils::string_from_bytes(user_bytes) .map_err(|e| err!(Database("User ID in openid_userid is invalid unicode. {e}")))?; OwnedUserId::try_from(user_string) .map_err(|e| err!(Database("User ID in openid_userid is invalid. {e}"))) } /// Creates a short-lived login token, which can be used to log in using the /// `m.login.token` mechanism. pub fn create_login_token(&self, user_id: &UserId, token: &str) -> u64 { use std::num::Saturating as Sat; let expires_in = self.services.config.login_token_ttl; let expires_at = Sat(utils::millis_since_unix_epoch()) + Sat(expires_in); let value = (expires_at.0, user_id); self.db.logintoken_expiresatuserid.raw_put(token, value); expires_in } /// Find out which user a login token belongs to. /// Removes the token to prevent double-use attacks. pub async fn find_from_login_token(&self, token: &str) -> Result { let Ok(value) = self.db.logintoken_expiresatuserid.get(token).await else { return Err!(Request(Forbidden("Login token is unrecognised"))); }; let (expires_at, user_id): (u64, OwnedUserId) = value.deserialized()?; if expires_at < utils::millis_since_unix_epoch() { trace!(%user_id, ?token, "Removing expired login token"); self.db.logintoken_expiresatuserid.remove(token); return Err!(Request(Forbidden("Login token is expired"))); } self.db.logintoken_expiresatuserid.remove(token); Ok(user_id) } }