refactor: Remove support for guest user registration

This commit is contained in:
Ginger
2026-05-05 09:09:38 -04:00
parent 7436e2f4e1
commit 8c2cf67783
19 changed files with 152 additions and 356 deletions
-2
View File
@@ -7,7 +7,6 @@
[global] [global]
address = "0.0.0.0" address = "0.0.0.0"
allow_device_name_federation = true allow_device_name_federation = true
allow_guest_registration = true
allow_public_room_directory_over_federation = true allow_public_room_directory_over_federation = true
allow_registration = true allow_registration = true
database_path = "/database" database_path = "/database"
@@ -32,7 +31,6 @@ rocksdb_log_level = "info"
rocksdb_max_log_files = 1 rocksdb_max_log_files = 1
rocksdb_recovery_mode = 0 rocksdb_recovery_mode = 0
rocksdb_paranoid_file_checks = true rocksdb_paranoid_file_checks = true
log_guest_registrations = false
allow_legacy_media = true allow_legacy_media = true
startup_netburst = true startup_netburst = true
startup_netburst_keep = -1 startup_netburst_keep = -1
-15
View File
@@ -1271,21 +1271,6 @@
# #
#brotli_compression = false #brotli_compression = false
# Set to true to allow user type "guest" registrations. Some clients like
# Element attempt to register guest users automatically.
#
#allow_guest_registration = false
# Set to true to log guest registrations in the admin room. Note that
# these may be noisy or unnecessary if you're a public homeserver.
#
#log_guest_registrations = false
# Set to true to allow guest registrations/users to auto join any rooms
# specified in `auto_join_rooms`.
#
#allow_guests_auto_join_rooms = false
# Enable the legacy unauthenticated Matrix media repository endpoints. # Enable the legacy unauthenticated Matrix media repository endpoints.
# These endpoints consist of: # These endpoints consist of:
# - /_matrix/media/*/config # - /_matrix/media/*/config
-14
View File
@@ -15,10 +15,6 @@ pub enum UsersCommand {
IterUsers2, IterUsers2,
PasswordHash {
user_id: OwnedUserId,
},
ListDevices { ListDevices {
user_id: OwnedUserId, user_id: OwnedUserId,
}, },
@@ -235,16 +231,6 @@ async fn count_users(&self) -> Result {
.await .await
} }
#[admin_command]
async fn password_hash(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now();
let result = self.services.users.password_hash(&user_id).await;
let query_time = timer.elapsed();
self.write_str(&format!("Query completed in {query_time:?}:\n\n```rs\n{result:#?}\n```"))
.await
}
#[admin_command] #[admin_command]
async fn list_devices(&self, user_id: OwnedUserId) -> Result { async fn list_devices(&self, user_id: OwnedUserId) -> Result {
let timer = tokio::time::Instant::now(); let timer = tokio::time::Instant::now();
+9 -15
View File
@@ -24,6 +24,7 @@ use ruma::{
tag::{TagEvent, TagEventContent, TagInfo}, tag::{TagEvent, TagEventContent, TagInfo},
}, },
}; };
use service::users::HashedPassword;
use crate::{ use crate::{
admin_command, get_room_info, admin_command, get_room_info,
@@ -70,7 +71,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
// Create user // Create user
self.services self.services
.users .users
.create(&user_id, Some(password.as_str())) .create(&user_id, Some(HashedPassword::new(&password)?))
.await?; .await?;
// Default to pretty displayname // Default to pretty displayname
@@ -143,7 +144,6 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
self.services.globals.server_name().to_owned(), self.services.globals.server_name().to_owned(),
room_server_name.to_owned(), room_server_name.to_owned(),
], ],
&None,
) )
.await .await
{ {
@@ -274,17 +274,13 @@ pub(super) async fn reset_password(
let new_password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)); let new_password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
match self self.services
.services
.users .users
.set_password(&user_id, Some(new_password.as_str())) .set_password(&user_id, Some(HashedPassword::new(&new_password)?));
.await
{ self.write_str(&format!(
| Err(e) => return Err!("Couldn't reset the password for user {user_id}: {e}"), "Successfully reset the password for user {user_id}: `{new_password}`"
| Ok(()) => { ))
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
},
}
.await?; .await?;
if logout { if logout {
@@ -562,7 +558,6 @@ pub(super) async fn force_join_list_of_local_users(
&room_id, &room_id,
Some(String::from(BULK_JOIN_REASON)), Some(String::from(BULK_JOIN_REASON)),
&servers, &servers,
&None,
) )
.await .await
{ {
@@ -646,7 +641,6 @@ pub(super) async fn force_join_all_local_users(
&room_id, &room_id,
Some(String::from(BULK_JOIN_REASON)), Some(String::from(BULK_JOIN_REASON)),
&servers, &servers,
&None,
) )
.await .await
{ {
@@ -685,7 +679,7 @@ pub(super) async fn force_join_room(
self.services.globals.user_is_local(&user_id), self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user" "Parsed user_id must be a local user"
); );
join_room_by_id_helper(self.services, &user_id, &room_id, None, &servers, &None).await?; join_room_by_id_helper(self.services, &user_id, &room_id, None, &servers).await?;
self.write_str(&format!("{user_id} has been joined to {room_id}.")) self.write_str(&format!("{user_id} has been joined to {room_id}."))
.await .await
+1 -1
View File
@@ -48,7 +48,7 @@ pub(crate) fn parse_local_user_id(services: &Services, user_id: &str) -> Result<
Ok(user_id) Ok(user_id)
} }
/// Parses user ID that is an active (not guest or deactivated) local user /// Parses user ID that is an active (not deactivated) local user
pub(crate) async fn parse_active_local_user_id( pub(crate) async fn parse_active_local_user_id(
services: &Services, services: &Services,
user_id: &str, user_id: &str,
+5 -14
View File
@@ -24,7 +24,7 @@ use ruma::{
power_levels::RoomPowerLevelsEventContent, power_levels::RoomPowerLevelsEventContent,
}, },
}; };
use service::{mailer::messages, uiaa::Identity}; use service::{mailer::messages, uiaa::Identity, users::HashedPassword};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper}; use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
use crate::Ruma; use crate::Ruma;
@@ -150,8 +150,7 @@ pub(crate) async fn change_password_route(
services services
.users .users
.set_password(&sender_user, Some(&body.new_password)) .set_password(&sender_user, Some(HashedPassword::new(&body.new_password)?));
.await?;
if body.logout_devices { if body.logout_devices {
// Logout all devices except the current one // Logout all devices except the current one
@@ -239,19 +238,11 @@ pub(crate) async fn request_password_change_token_via_email_route(
/// ///
/// Note: Also works for Application Services /// Note: Also works for Application Services
pub(crate) async fn whoami_route( pub(crate) async fn whoami_route(
State(services): State<crate::State>, State(_): State<crate::State>,
body: Ruma<whoami::v3::Request>, body: Ruma<whoami::v3::Request>,
) -> Result<whoami::v3::Response> { ) -> Result<whoami::v3::Response> {
let is_guest = services Ok(assign!(whoami::v3::Response::new(body.sender_user().to_owned(), false), {
.users device_id: body.sender_device,
.is_deactivated(body.sender_user())
.await
.map_err(|_| {
err!(Request(Forbidden("Application service has not registered this user.")))
})? && body.appservice_info.is_none();
Ok(assign!(whoami::v3::Response::new(body.sender_user().to_owned(), is_guest), {
device_id: body.sender_device.clone(),
})) }))
} }
+43 -139
View File
@@ -10,7 +10,6 @@ use conduwuit::{
use conduwuit_service::Services; use conduwuit_service::Services;
use futures::{FutureExt, StreamExt}; use futures::{FutureExt, StreamExt};
use lettre::{Address, message::Mailbox}; use lettre::{Address, message::Mailbox};
use register::RegistrationKind;
use ruma::{ use ruma::{
OwnedUserId, UserId, OwnedUserId, UserId,
api::client::{ api::client::{
@@ -28,7 +27,7 @@ use ruma::{
push, push,
}; };
use serde_json::value::RawValue; use serde_json::value::RawValue;
use service::mailer::messages; use service::{mailer::messages, users::HashedPassword};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper}; use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH, join_room_by_id_helper};
use crate::Ruma; use crate::Ruma;
@@ -42,16 +41,6 @@ const RANDOM_USER_ID_LENGTH: usize = 10;
/// You can use [`GET /// You can use [`GET
/// /_matrix/client/v3/register/available`](fn.get_register_available_route. /// /_matrix/client/v3/register/available`](fn.get_register_available_route.
/// html) to check if the user id is valid and available. /// html) to check if the user id is valid and available.
///
/// - Only works if registration is enabled
/// - If type is guest: ignores all parameters except
/// initial_device_display_name
/// - If sender is not appservice: Requires UIAA (but we only use a dummy stage)
/// - If type is not guest and no username is given: Always fails after UIAA
/// check
/// - Creates a new account and populates it with default account data
/// - If `inhibit_login` is false: Creates a device and returns device id and
/// access_token
#[allow(clippy::doc_markdown)] #[allow(clippy::doc_markdown)]
#[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")] #[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")]
pub(crate) async fn register_route( pub(crate) async fn register_route(
@@ -59,7 +48,6 @@ pub(crate) async fn register_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<register::v3::Request>, body: Ruma<register::v3::Request>,
) -> Result<register::v3::Response> { ) -> Result<register::v3::Response> {
let is_guest = body.kind == RegistrationKind::Guest;
let emergency_mode_enabled = services.config.emergency_password.is_some(); let emergency_mode_enabled = services.config.emergency_password.is_some();
// Allow registration if it's enabled in the config file or if this is the first // Allow registration if it's enabled in the config file or if this is the first
@@ -68,69 +56,19 @@ pub(crate) async fn register_route(
services.config.allow_registration || services.firstrun.is_first_run(); services.config.allow_registration || services.firstrun.is_first_run();
if !allow_registration && body.appservice_info.is_none() { if !allow_registration && body.appservice_info.is_none() {
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
| (Some(username), Some(device_display_name)) => {
info!(
%is_guest,
user = %username,
device_name = %device_display_name,
"Rejecting registration attempt as registration is disabled"
);
},
| (Some(username), _) => {
info!(
%is_guest,
user = %username,
"Rejecting registration attempt as registration is disabled"
);
},
| (_, Some(device_display_name)) => {
info!(
%is_guest,
device_name = %device_display_name,
"Rejecting registration attempt as registration is disabled"
);
},
| (None, _) => {
info!(
%is_guest,
"Rejecting registration attempt as registration is disabled"
);
},
}
return Err!(Request(Forbidden(
"This server is not accepting registrations at this time."
)));
}
if is_guest && !services.config.allow_guest_registration {
info!( info!(
"Guest registration disabled, rejecting guest registration attempt, initial device \ ?body.username,
name: \"{}\"", ?body.initial_device_display_name,
body.initial_device_display_name.as_deref().unwrap_or("") "Rejecting registration attempt as registration is disabled"
); );
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
}
// forbid guests from registering if there is not a real admin user yet. give
// generic user error.
if is_guest && services.firstrun.is_first_run() {
warn!(
"Guest account attempted to register before a real admin user has been registered, \
rejecting registration. Guest's initial device name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err!(Request(Forbidden( return Err!(Request(Forbidden(
"This server is not accepting registrations at this time." "This server is not accepting registrations at this time."
))); )));
} }
// Appeservices and guests get to skip auth let identity = if body.appservice_info.is_some() {
let skip_auth = body.appservice_info.is_some() || is_guest; // Appservices can skip auth
let identity = if skip_auth {
// Appservices and guests have no identity
None None
} else { } else {
// Perform UIAA to determine the user's identity // Perform UIAA to determine the user's identity
@@ -157,13 +95,9 @@ pub(crate) async fn register_route(
} }
}); });
let user_id = determine_registration_user_id( let user_id =
&services, determine_registration_user_id(&services, supplied_username, emergency_mode_enabled)
supplied_username, .await?;
is_guest,
emergency_mode_enabled,
)
.await?;
if body.body.login_type == Some(LoginType::ApplicationService) { if body.body.login_type == Some(LoginType::ApplicationService) {
// For appservice logins, make sure that the user ID is in the appservice's // For appservice logins, make sure that the user ID is in the appservice's
@@ -187,7 +121,13 @@ pub(crate) async fn register_route(
return Err!(Request(Exclusive("Username is reserved by an appservice."))); return Err!(Request(Exclusive("Username is reserved by an appservice.")));
} }
let password = if is_guest { None } else { body.password.as_deref() }; let password = if body.appservice_info.is_some() {
None
} else if let Some(password) = body.password.as_deref() {
Some(HashedPassword::new(password)?)
} else {
return Err!(Request(InvalidParam("A password must be provided")));
};
// Create user // Create user
services.users.create(&user_id, password).await?; services.users.create(&user_id, password).await?;
@@ -222,7 +162,9 @@ pub(crate) async fn register_route(
// Generate new device id if the user didn't specify one // Generate new device id if the user didn't specify one
let (token, device) = if !body.inhibit_login { let (token, device) = if !body.inhibit_login {
let device_id = if is_guest { None } else { body.device_id.clone() } let device_id = body
.device_id
.clone()
.unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into()); .unwrap_or_else(|| utils::random_string(DEVICE_ID_LENGTH).into());
// Generate new token for the device // Generate new token for the device
@@ -263,8 +205,7 @@ pub(crate) async fn register_route(
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or(""); let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
// log in conduit admin channel if a non-guest user registered if body.appservice_info.is_none() {
if body.appservice_info.is_none() && !is_guest {
if !device_display_name.is_empty() { if !device_display_name.is_empty() {
let notice = format!( let notice = format!(
"New user \"{user_id}\" registered on this server from IP {client} and device \ "New user \"{user_id}\" registered on this server from IP {client} and device \
@@ -285,65 +226,32 @@ pub(crate) async fn register_route(
} }
} }
// log in conduit admin channel if a guest registered // Make the first user to register an administrator and disable first-run mode.
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations { let was_first_user = services.firstrun.empower_first_user(&user_id).await?;
debug_info!("New guest user \"{user_id}\" registered on this server.");
if !device_display_name.is_empty() { // If the registering user was not the first and we're suspending users on
if services.server.config.admin_room_notices { // register, suspend them.
services if !was_first_user && services.config.suspend_on_register {
.admin // Note that we can still do auto joins for suspended users
.notice(&format!( services
"Guest user \"{user_id}\" with device display name \ .users
\"{device_display_name}\" registered on this server from IP {client}" .suspend_account(&user_id, &services.globals.server_user)
)) .await;
.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.
} else { if services.server.config.admin_room_notices {
#[allow(clippy::collapsible_else_if)]
if services.server.config.admin_room_notices {
services
.admin
.notice(&format!(
"Guest user \"{user_id}\" with no device display name registered on \
this server from IP {client}",
))
.await;
}
}
}
if !is_guest {
// Make the first user to register an administrator and disable first-run mode.
let was_first_user = 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 && services.config.suspend_on_register {
// Note that we can still do auto joins for suspended users
services services
.users .admin
.suspend_account(&user_id, &services.globals.server_user) .send_loud_message(RoomMessageEventContent::text_plain(format!(
.await; "User {user_id} has been suspended as they are not the first user on this \
// And send an @room notice to the admin room, to prompt admins to review the server. Please review and unsuspend them if appropriate."
// new user and ideally unsuspend them if deemed appropriate. )))
if services.server.config.admin_room_notices { .await
services .ok();
.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();
}
} }
} }
if body.appservice_info.is_none() if body.appservice_info.is_none() && !services.server.config.auto_join_rooms.is_empty() {
&& !services.server.config.auto_join_rooms.is_empty()
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
{
for room in &services.server.config.auto_join_rooms { for room in &services.server.config.auto_join_rooms {
let Ok(room_id) = services.rooms.alias.resolve(room).await else { let Ok(room_id) = services.rooms.alias.resolve(room).await else {
error!( error!(
@@ -372,7 +280,6 @@ pub(crate) async fn register_route(
&room_id, &room_id,
Some("Automatically joining this room upon registration".to_owned()), Some("Automatically joining this room upon registration".to_owned()),
&[services.globals.server_name().to_owned(), room_server_name.to_owned()], &[services.globals.server_name().to_owned(), room_server_name.to_owned()],
&body.appservice_info,
) )
.boxed() .boxed()
.await .await
@@ -511,12 +418,9 @@ async fn create_registration_uiaa_session(
async fn determine_registration_user_id( async fn determine_registration_user_id(
services: &Services, services: &Services,
supplied_username: Option<String>, supplied_username: Option<String>,
is_guest: bool,
emergency_mode_enabled: bool, emergency_mode_enabled: bool,
) -> Result<OwnedUserId> { ) -> Result<OwnedUserId> {
if let Some(supplied_username) = supplied_username if let Some(supplied_username) = supplied_username {
&& !is_guest
{
// The user gets to pick their username. Do some validation to make sure it's // The user gets to pick their username. Do some validation to make sure it's
// acceptable. // acceptable.
@@ -569,7 +473,7 @@ async fn determine_registration_user_id(
Ok(user_id) Ok(user_id)
} else { } else {
// The user is a guest or didn't specify a username. Generate a username for // The user didn't specify a username. Generate a username for
// them. // them.
loop { loop {
-10
View File
@@ -122,16 +122,6 @@ pub(crate) async fn set_room_visibility_route(
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
if services
.users
.is_deactivated(sender_user)
.await
.unwrap_or(false)
&& body.appservice_info.is_none()
{
return Err!(Request(Forbidden("Guests cannot publish to room directories")));
}
if !user_can_publish_room(&services, sender_user, &body.room_id).await? { if !user_can_publish_room(&services, sender_user, &body.room_id).await? {
return Err!(Request(Forbidden("User is not allowed to publish this room"))); return Err!(Request(Forbidden("User is not allowed to publish this room")));
} }
+7 -34
View File
@@ -39,7 +39,6 @@ use ruma::{
}; };
use service::{ use service::{
Services, Services,
appservice::RegistrationInfo,
rooms::{ rooms::{
state::RoomMutexGuard, state::RoomMutexGuard,
state_compressor::{CompressedState, HashSetCompressStateEvent}, state_compressor::{CompressedState, HashSetCompressStateEvent},
@@ -112,16 +111,9 @@ pub(crate) async fn join_room_by_id_route(
shuffle(&mut servers); shuffle(&mut servers);
let servers = deprioritize(servers, &services.config.deprioritize_joins_through_servers); let servers = deprioritize(servers, &services.config.deprioritize_joins_through_servers);
join_room_by_id_helper( join_room_by_id_helper(&services, sender_user, &body.room_id, body.reason.clone(), &servers)
&services, .boxed()
sender_user, .await
&body.room_id,
body.reason.clone(),
&servers,
&body.appservice_info,
)
.boxed()
.await
} }
/// # `POST /_matrix/client/r0/join/{roomIdOrAlias}` /// # `POST /_matrix/client/r0/join/{roomIdOrAlias}`
@@ -140,7 +132,6 @@ pub(crate) async fn join_room_by_id_or_alias_route(
body: Ruma<join_room_by_id_or_alias::v3::Request>, body: Ruma<join_room_by_id_or_alias::v3::Request>,
) -> Result<join_room_by_id_or_alias::v3::Response> { ) -> Result<join_room_by_id_or_alias::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.sender_user();
let appservice_info = &body.appservice_info;
let body = &body.body; let body = &body.body;
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
@@ -235,16 +226,10 @@ pub(crate) async fn join_room_by_id_or_alias_route(
}; };
let servers = deprioritize(servers, &services.config.deprioritize_joins_through_servers); let servers = deprioritize(servers, &services.config.deprioritize_joins_through_servers);
let join_room_response = join_room_by_id_helper( let join_room_response =
&services, join_room_by_id_helper(&services, sender_user, &room_id, body.reason.clone(), &servers)
sender_user, .boxed()
&room_id, .await?;
body.reason.clone(),
&servers,
appservice_info,
)
.boxed()
.await?;
Ok(join_room_by_id_or_alias::v3::Response::new(join_room_response.room_id)) Ok(join_room_by_id_or_alias::v3::Response::new(join_room_response.room_id))
} }
@@ -255,21 +240,9 @@ pub async fn join_room_by_id_helper(
room_id: &RoomId, room_id: &RoomId,
reason: Option<String>, reason: Option<String>,
servers: &[OwnedServerName], servers: &[OwnedServerName],
appservice_info: &Option<RegistrationInfo>,
) -> Result<join_room_by_id::v3::Response> { ) -> Result<join_room_by_id::v3::Response> {
let state_lock = services.rooms.state.mutex.lock(room_id).await; let state_lock = services.rooms.state.mutex.lock(room_id).await;
let user_is_guest = services
.users
.is_deactivated(sender_user)
.await
.unwrap_or(false)
&& appservice_info.is_none();
if user_is_guest && !services.rooms.state_accessor.guest_can_join(room_id).await {
return Err!(Request(Forbidden("Guests are not allowed to join this room")));
}
if services if services
.rooms .rooms
.state_cache .state_cache
+2 -9
View File
@@ -238,15 +238,8 @@ async fn knock_room_by_id_helper(
// join_room_by_id_helper We need to release the lock here and let // join_room_by_id_helper We need to release the lock here and let
// join_room_by_id_helper acquire it again // join_room_by_id_helper acquire it again
drop(state_lock); drop(state_lock);
match join_room_by_id_helper( match join_room_by_id_helper(services, sender_user, room_id, reason.clone(), servers)
services, .await
sender_user,
room_id,
reason.clone(),
servers,
&None,
)
.await
{ {
| Ok(_) => return Ok(knock_room::v3::Response::new(room_id.to_owned())), | Ok(_) => return Ok(knock_room::v3::Response::new(room_id.to_owned())),
| Err(e) => { | Err(e) => {
+3 -43
View File
@@ -4,10 +4,9 @@ use axum::extract::State;
use axum_client_ip::ClientIp; use axum_client_ip::ClientIp;
use conduwuit::{ use conduwuit::{
Err, Result, debug, err, info, Err, Result, debug, err, info,
utils::{self, ReadyExt, hash, stream::BroadbandExt}, utils::{self, ReadyExt, stream::BroadbandExt},
warn, warn,
}; };
use conduwuit_core::debug_error;
use conduwuit_service::Services; use conduwuit_service::Services;
use futures::StreamExt; use futures::StreamExt;
use lettre::Address; use lettre::Address;
@@ -54,37 +53,6 @@ pub(crate) async fn get_login_types_route(
])) ]))
} }
/// Authenticates the given user by its ID and its password.
///
/// Returns the user ID if successful, and an error otherwise.
#[tracing::instrument(skip_all, fields(%user_id), name = "password", level = "debug")]
pub(crate) async fn password_login(
services: &Services,
user_id: &UserId,
lowercased_user_id: &UserId,
password: &str,
) -> Result<OwnedUserId> {
let (hash, user_id) = match services.users.password_hash(user_id).await {
| Ok(hash) => (hash, user_id),
| Err(_) => services
.users
.password_hash(lowercased_user_id)
.await
.map(|hash| (hash, lowercased_user_id))
.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?,
};
if hash.is_empty() {
return Err!(Request(UserDeactivated("The user has been deactivated")));
}
hash::verify_password(password, &hash)
.inspect_err(|e| debug_error!("{e}"))
.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?;
Ok(user_id.to_owned())
}
pub(crate) async fn handle_login( pub(crate) async fn handle_login(
services: &Services, services: &Services,
identifier: Option<&UserIdentifier>, identifier: Option<&UserIdentifier>,
@@ -115,15 +83,7 @@ pub(crate) async fn handle_login(
UserId::parse_with_server_name(user_id_or_localpart, &services.config.server_name) UserId::parse_with_server_name(user_id_or_localpart, &services.config.server_name)
.map_err(|_| err!(Request(InvalidUsername("User ID is malformed"))))?; .map_err(|_| err!(Request(InvalidUsername("User ID is malformed"))))?;
let lowercased_user_id = UserId::parse_with_server_name( if !services.globals.user_is_local(&user_id) {
user_id.localpart().to_lowercase(),
&services.config.server_name,
)
.unwrap();
if !services.globals.user_is_local(&user_id)
|| !services.globals.user_is_local(&lowercased_user_id)
{
return Err!(Request(InvalidParam("User ID does not belong to this homeserver"))); return Err!(Request(InvalidParam("User ID does not belong to this homeserver")));
} }
@@ -136,7 +96,7 @@ pub(crate) async fn handle_login(
return Err!(Request(Forbidden("This account is not permitted to log in."))); return Err!(Request(Forbidden("This account is not permitted to log in.")));
} }
password_login(services, &user_id, &lowercased_user_id, password).await services.users.check_password(&user_id, password).await
} }
/// # `POST /_matrix/client/v3/login` /// # `POST /_matrix/client/v3/login`
+1 -1
View File
@@ -80,7 +80,7 @@ pub(crate) async fn conduwuit_server_version() -> Result<impl IntoResponse> {
/// ///
/// conduwuit-specific API to return the amount of users registered on this /// conduwuit-specific API to return the amount of users registered on this
/// homeserver. Endpoint is disabled if federation is disabled for privacy. This /// homeserver. Endpoint is disabled if federation is disabled for privacy. This
/// only includes active users (not deactivated, no guests, etc) /// only includes active users (not deactivated, etc)
pub(crate) async fn conduwuit_local_user_count( pub(crate) async fn conduwuit_local_user_count(
State(services): State<crate::State>, State(services): State<crate::State>,
) -> Result<impl IntoResponse> { ) -> Result<impl IntoResponse> {
-15
View File
@@ -1486,21 +1486,6 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub brotli_compression: bool, pub brotli_compression: bool,
/// Set to true to allow user type "guest" registrations. Some clients like
/// Element attempt to register guest users automatically.
#[serde(default)]
pub allow_guest_registration: bool,
/// Set to true to log guest registrations in the admin room. Note that
/// these may be noisy or unnecessary if you're a public homeserver.
#[serde(default)]
pub log_guest_registrations: bool,
/// Set to true to allow guest registrations/users to auto join any rooms
/// specified in `auto_join_rooms`.
#[serde(default)]
pub allow_guests_auto_join_rooms: bool,
/// Enable the legacy unauthenticated Matrix media repository endpoints. /// Enable the legacy unauthenticated Matrix media repository endpoints.
/// These endpoints consist of: /// These endpoints consist of:
/// - /_matrix/media/*/config /// - /_matrix/media/*/config
+1 -4
View File
@@ -121,10 +121,7 @@ impl Service {
.unwrap_or(false) .unwrap_or(false)
{ {
// Reactivate the appservice user if it was accidentally deactivated // Reactivate the appservice user if it was accidentally deactivated
self.services self.services.users.set_password(&appservice_user_id, None);
.users
.set_password(&appservice_user_id, None)
.await?;
} }
self.registration_info self.registration_info
+13 -5
View File
@@ -9,7 +9,10 @@ use ruma::{
push::Ruleset, push::Ruleset,
}; };
use crate::{Dep, account_data, config, globals, users}; use crate::{
Dep, account_data, config, globals,
users::{self, HashedPassword},
};
pub struct Service { pub struct Service {
services: Services, services: Services,
@@ -51,10 +54,15 @@ impl Service {
async fn set_emergency_access(&self) -> Result { async fn set_emergency_access(&self) -> Result {
let server_user = &self.services.globals.server_user; let server_user = &self.services.globals.server_user;
self.services self.services.users.set_password(
.users server_user,
.set_password(server_user, self.services.config.emergency_password.as_deref()) self.services
.await?; .config
.emergency_password
.as_deref()
.map(HashedPassword::new)
.transpose()?,
);
let (ruleset, pwd_set) = match self.services.config.emergency_password { let (ruleset, pwd_set) = match self.services.config.emergency_password {
| Some(_) => (Ruleset::server_default(server_user), true), | Some(_) => (Ruleset::server_default(server_user), true),
+5 -3
View File
@@ -6,7 +6,10 @@ use conduwuit::{Err, Result, utils};
use data::{Data, ResetTokenInfo}; use data::{Data, ResetTokenInfo};
use ruma::OwnedUserId; use ruma::OwnedUserId;
use crate::{Dep, globals, users}; use crate::{
Dep, globals,
users::{self, HashedPassword},
};
pub const PASSWORD_RESET_PATH: &str = "/_continuwuity/account/reset_password"; pub const PASSWORD_RESET_PATH: &str = "/_continuwuity/account/reset_password";
pub const RESET_TOKEN_QUERY_PARAM: &str = "token"; pub const RESET_TOKEN_QUERY_PARAM: &str = "token";
@@ -100,8 +103,7 @@ impl Service {
self.db.remove_token(&token); self.db.remove_token(&token);
self.services self.services
.users .users
.set_password(&info.user, Some(new_password)) .set_password(&info.user, Some(HashedPassword::new(new_password)?));
.await?;
} }
Ok(()) Ok(())
+2 -2
View File
@@ -238,7 +238,7 @@ pub async fn room_joined_count(&self, room_id: &RoomId) -> Result<u64> {
#[implement(Service)] #[implement(Service)]
#[tracing::instrument(skip(self), level = "debug")] #[tracing::instrument(skip(self), level = "debug")]
/// Returns an iterator of all our local users in the room, even if they're /// Returns an iterator of all our local users in the room, even if they're
/// deactivated/guests /// deactivated
pub fn local_users_in_room<'a>( pub fn local_users_in_room<'a>(
&'a self, &'a self,
room_id: &'a RoomId, room_id: &'a RoomId,
@@ -248,7 +248,7 @@ pub fn local_users_in_room<'a>(
} }
/// Returns an iterator of all our local joined users in a room who are /// Returns an iterator of all our local joined users in a room who are
/// active (not deactivated, not guest) /// active (not deactivated)
#[implement(Service)] #[implement(Service)]
#[tracing::instrument(skip(self), level = "trace")] #[tracing::instrument(skip(self), level = "trace")]
pub fn active_local_users_in_room<'a>( pub fn active_local_users_in_room<'a>(
+8 -10
View File
@@ -4,7 +4,7 @@ use std::{
sync::Arc, sync::Arc,
}; };
use conduwuit::{Err, Error, Result, error, utils, utils::hash}; use conduwuit::{Err, Error, Result, error, utils};
use lettre::Address; use lettre::Address;
use ruma::{ use ruma::{
UserId, UserId,
@@ -377,15 +377,13 @@ impl Service {
)); ));
}; };
// Check if password is correct if self
let mut password_verified = false; .services
.users
// First try local password hash verification .check_password(&user_id, password)
if let Ok(hash) = self.services.users.password_hash(&user_id).await { .await
password_verified = hash::verify_password(password, &hash).is_ok(); .is_ok()
} {
if password_verified {
identity.try_set_localpart(user_id.localpart().to_owned())?; identity.try_set_localpart(user_id.localpart().to_owned())?;
Ok(AuthType::Password) Ok(AuthType::Password)
+52 -20
View File
@@ -3,7 +3,7 @@ pub(super) mod dehydrated_device;
use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc}; use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc};
use conduwuit::{ use conduwuit::{
Err, Error, Result, Server, debug_warn, err, trace, Err, Error, Result, Server, debug_error, debug_warn, err, trace,
utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted}, utils::{self, ReadyExt, stream::TryIgnore, string::Unquoted},
}; };
use database::{Deserialized, Ignore, Interfix, Json, Map}; use database::{Deserialized, Ignore, Interfix, Json, Map};
@@ -38,6 +38,19 @@ pub struct UserSuspension {
pub suspended_by: String, pub suspended_by: String,
} }
/// A password hash. This is only for use when setting a user's password,
/// if the hash needs to be kept around for a while without keeping the password
/// in memory.
pub struct HashedPassword(String);
impl HashedPassword {
pub fn new(password: &str) -> Result<Self> {
Ok(Self(utils::hash::password(password).map_err(|e| {
err!(Request(InvalidParam("Password does not meet the requirements: {e}")))
})?))
}
}
pub struct Service { pub struct Service {
services: Services, services: Services,
db: Data, db: Data,
@@ -171,16 +184,23 @@ impl Service {
/// Create a new user account on this homeserver. /// Create a new user account on this homeserver.
#[inline] #[inline]
pub async fn create(&self, user_id: &UserId, password: Option<&str>) -> Result<()> { pub async fn create(&self, user_id: &UserId, password: Option<HashedPassword>) -> Result<()> {
if !self.services.globals.user_is_local(user_id) && password.is_some() { if !self.services.globals.user_is_local(user_id) && password.is_some() {
return Err!("Cannot create a nonlocal user with a set password"); return Err!("Cannot create a nonlocal user with a set password");
} }
self.set_password(user_id, password).await?; self.set_password(user_id, password);
Ok(()) Ok(())
} }
// /// Create a new account for a local human or bot user.
// pub async fn create_local_account(
// &self,
// username: String,
// password:
// )
/// Deactivate account /// Deactivate account
pub async fn deactivate_account(&self, user_id: &UserId) -> Result<()> { pub async fn deactivate_account(&self, user_id: &UserId) -> Result<()> {
// Remove all associated devices // Remove all associated devices
@@ -192,7 +212,7 @@ impl Service {
// result in an empty string, so the user will not be able to log in again. // result in an empty string, so the user will not be able to log in again.
// Systems like changing the password without logging in should check if the // Systems like changing the password without logging in should check if the
// account is deactivated. // account is deactivated.
self.set_password(user_id, None).await?; self.set_password(user_id, None);
// TODO: Unhook 3PID // TODO: Unhook 3PID
Ok(()) Ok(())
@@ -338,25 +358,37 @@ impl Service {
.ready_filter_map(|(u, p): (OwnedUserId, &[u8])| (!p.is_empty()).then_some(u)) .ready_filter_map(|(u, p): (OwnedUserId, &[u8])| (!p.is_empty()).then_some(u))
} }
/// Returns the password hash for the given user. /// Set a user's password.
pub async fn password_hash(&self, user_id: &UserId) -> Result<String> { pub fn set_password(&self, user_id: &UserId, password: Option<HashedPassword>) {
self.db.userid_password.get(user_id).await.deserialized() if let Some(hash) = password {
self.db.userid_password.insert(user_id, hash.0);
} else {
self.db.userid_password.insert(user_id, b"");
}
} }
/// Hash and set the user's password to the Argon2 hash /// Check a user's password.
pub async fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> { pub async fn check_password(&self, user_id: &UserId, password: &str) -> Result<OwnedUserId> {
password let (hash, user_id): (String, OwnedUserId) =
.map(utils::hash::password) if let Ok(hash) = self.db.userid_password.get(user_id).await.deserialized() {
.transpose() (hash, user_id.to_owned())
.map_err(|e| { } else {
err!(Request(InvalidParam("Password does not meet the requirements: {e}"))) // We also check the lowercased version of the user ID to handle legacy user IDs
})? // better
.map_or_else( let lowercase_user_id = UserId::parse(user_id.as_str().to_lowercase()).unwrap();
|| self.db.userid_password.insert(user_id, b""),
|hash| self.db.userid_password.insert(user_id, hash),
);
Ok(()) if let Ok(hash) = self.db.userid_password.get(user_id).await.deserialized() {
(hash, lowercase_user_id)
} else {
return Err!(Request(UserDeactivated("This user is deactivated.")));
}
};
utils::hash::verify_password(password, &hash)
.inspect_err(|e| debug_error!("{e}"))
.map_err(|_| err!(Request(Forbidden("Invalid identifier or password."))))?;
Ok(user_id)
} }
/// Returns the displayname of a user on this homeserver. /// Returns the displayname of a user on this homeserver.