mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-05 00:31:40 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e212c91ebf | |||
| 83f3314f08 | |||
| 8c2cf67783 | |||
| 7436e2f4e1 | |||
| 9ba406761b | |||
| 97f49d6357 | |||
| 1a49bc6f87 | |||
| 833216256b | |||
| 5fa3087401 |
@@ -45,7 +45,6 @@
|
||||
- [ ] I have [tested my contribution][c1t] (or proof-read it for documentation-only changes)
|
||||
myself, if applicable. This includes ensuring code compiles.
|
||||
- [ ] My commit messages follow the [commit message format][c1cm] and are descriptive.
|
||||
- [ ] I have written a [news fragment][n1] for this PR, if applicable<!--(can be done after hitting open!)-->.
|
||||
|
||||
<!--
|
||||
Notes on these requirements:
|
||||
@@ -79,4 +78,3 @@
|
||||
[c1pc]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#pre-commit-checks
|
||||
[c1t]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#running-tests-locally
|
||||
[c1cm]: https://forgejo.ellis.link/continuwuation/continuwuity/src/branch/main/CONTRIBUTING.md#commit-messages
|
||||
[n1]: https://towncrier.readthedocs.io/en/stable/tutorial.html#creating-news-fragments
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Removed support for guest user registration, a little-used and deprecated approach to room previews.
|
||||
@@ -0,0 +1 @@
|
||||
Added support for fallback encryption keys.
|
||||
@@ -7,7 +7,6 @@
|
||||
[global]
|
||||
address = "0.0.0.0"
|
||||
allow_device_name_federation = true
|
||||
allow_guest_registration = true
|
||||
allow_public_room_directory_over_federation = true
|
||||
allow_registration = true
|
||||
database_path = "/database"
|
||||
@@ -32,7 +31,6 @@ rocksdb_log_level = "info"
|
||||
rocksdb_max_log_files = 1
|
||||
rocksdb_recovery_mode = 0
|
||||
rocksdb_paranoid_file_checks = true
|
||||
log_guest_registrations = false
|
||||
allow_legacy_media = true
|
||||
startup_netburst = true
|
||||
startup_netburst_keep = -1
|
||||
|
||||
@@ -1271,21 +1271,6 @@
|
||||
#
|
||||
#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.
|
||||
# These endpoints consist of:
|
||||
# - /_matrix/media/*/config
|
||||
@@ -1934,102 +1919,6 @@
|
||||
#
|
||||
#foci = []
|
||||
|
||||
[global.ldap]
|
||||
|
||||
# Whether to enable LDAP login.
|
||||
#
|
||||
# example: "true"
|
||||
#
|
||||
#enable = false
|
||||
|
||||
# Whether to force LDAP authentication or authorize classical password
|
||||
# login.
|
||||
#
|
||||
# example: "true"
|
||||
#
|
||||
#ldap_only = false
|
||||
|
||||
# URI of the LDAP server.
|
||||
#
|
||||
# example: "ldap://ldap.example.com:389"
|
||||
#
|
||||
#uri = ""
|
||||
|
||||
# StartTLS for LDAP connections.
|
||||
#
|
||||
#use_starttls = false
|
||||
|
||||
# Skip TLS certificate verification, possibly dangerous.
|
||||
#
|
||||
#disable_tls_verification = false
|
||||
|
||||
# Root of the searches.
|
||||
#
|
||||
# example: "ou=users,dc=example,dc=org"
|
||||
#
|
||||
#base_dn = ""
|
||||
|
||||
# Bind DN if anonymous search is not enabled.
|
||||
#
|
||||
# You can use the variable `{username}` that will be replaced by the
|
||||
# entered username. In such case, the password used to bind will be the
|
||||
# one provided for the login and not the one given by
|
||||
# `bind_password_file`. Beware: automatically granting admin rights will
|
||||
# not work if you use this direct bind instead of a LDAP search.
|
||||
#
|
||||
# example: "cn=ldap-reader,dc=example,dc=org" or
|
||||
# "cn={username},ou=users,dc=example,dc=org"
|
||||
#
|
||||
#bind_dn = ""
|
||||
|
||||
# Path to a file on the system that contains the password for the
|
||||
# `bind_dn`.
|
||||
#
|
||||
# The server must be able to access the file, and it must not be empty.
|
||||
#
|
||||
#bind_password_file = ""
|
||||
|
||||
# Search filter to limit user searches.
|
||||
#
|
||||
# You can use the variable `{username}` that will be replaced by the
|
||||
# entered username for more complex filters.
|
||||
#
|
||||
# example: "(&(objectClass=person)(memberOf=matrix))"
|
||||
#
|
||||
#filter = "(objectClass=*)"
|
||||
|
||||
# Attribute to use to uniquely identify the user.
|
||||
#
|
||||
# example: "uid" or "cn"
|
||||
#
|
||||
#uid_attribute = "uid"
|
||||
|
||||
# Attribute containing the display name of the user.
|
||||
#
|
||||
# example: "givenName" or "sn"
|
||||
#
|
||||
#name_attribute = "givenName"
|
||||
|
||||
# Root of the searches for admin users.
|
||||
#
|
||||
# Defaults to `base_dn` if empty.
|
||||
#
|
||||
# example: "ou=admins,dc=example,dc=org"
|
||||
#
|
||||
#admin_base_dn = ""
|
||||
|
||||
# The LDAP search filter to find administrative users for continuwuity.
|
||||
#
|
||||
# If left blank, administrative state must be configured manually for each
|
||||
# user.
|
||||
#
|
||||
# You can use the variable `{username}` that will be replaced by the
|
||||
# entered username for more complex filters.
|
||||
#
|
||||
# example: "(objectClass=conduwuitAdmin)" or "(uid={username})"
|
||||
#
|
||||
#admin_filter = ""
|
||||
|
||||
#[global.antispam]
|
||||
|
||||
#[global.antispam.meowlnir]
|
||||
|
||||
@@ -7,7 +7,7 @@ ## Running commands
|
||||
|
||||
* All commands listed here may be used by server administrators in the admin room by sending them as messages.
|
||||
* If the `admin_escape_commands` configuration option is enabled, server administrators may run certain commands in public rooms by prefixing them with a single backslash. These commands will only run on _their_ homeserver, even if they are a member of another homeserver's admin room. Some sensitive commands cannot be used outside the admin room and will return an error.
|
||||
* All commands listed here may be used in the server's console, if it is enabled. Commands entered in the console do not require the `!admin` prefix. If Continuwuity is deployed via Docker, be sure to set the appropriate options detailed in [the Docker deployment guide](../../deploying/docker.mdx#accessing-the-servers-console) to enable access to the server's console.
|
||||
* All commands listed here may be used in the server's console, if it is enabled. Commands entered in the console do not require the `!admin` prefix.
|
||||
|
||||
## Categories
|
||||
|
||||
|
||||
@@ -15,10 +15,6 @@ pub enum UsersCommand {
|
||||
|
||||
IterUsers2,
|
||||
|
||||
PasswordHash {
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
|
||||
ListDevices {
|
||||
user_id: OwnedUserId,
|
||||
},
|
||||
@@ -235,16 +231,6 @@ async fn count_users(&self) -> Result {
|
||||
.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]
|
||||
async fn list_devices(&self, user_id: OwnedUserId) -> Result {
|
||||
let timer = tokio::time::Instant::now();
|
||||
|
||||
@@ -24,6 +24,7 @@
|
||||
tag::{TagEvent, TagEventContent, TagInfo},
|
||||
},
|
||||
};
|
||||
use service::users::HashedPassword;
|
||||
|
||||
use crate::{
|
||||
admin_command, get_room_info,
|
||||
@@ -70,7 +71,7 @@ pub(super) async fn create_user(&self, username: String, password: Option<String
|
||||
// Create user
|
||||
self.services
|
||||
.users
|
||||
.create(&user_id, Some(password.as_str()))
|
||||
.create(&user_id, Some(HashedPassword::new(&password)?))
|
||||
.await?;
|
||||
|
||||
// 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(),
|
||||
room_server_name.to_owned(),
|
||||
],
|
||||
&None,
|
||||
)
|
||||
.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));
|
||||
|
||||
match self
|
||||
.services
|
||||
self.services
|
||||
.users
|
||||
.set_password(&user_id, Some(new_password.as_str()))
|
||||
.await
|
||||
{
|
||||
| Err(e) => return Err!("Couldn't reset the password for user {user_id}: {e}"),
|
||||
| Ok(()) => {
|
||||
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
|
||||
},
|
||||
}
|
||||
.set_password(&user_id, Some(HashedPassword::new(&new_password)?));
|
||||
|
||||
self.write_str(&format!(
|
||||
"Successfully reset the password for user {user_id}: `{new_password}`"
|
||||
))
|
||||
.await?;
|
||||
|
||||
if logout {
|
||||
@@ -562,7 +558,6 @@ pub(super) async fn force_join_list_of_local_users(
|
||||
&room_id,
|
||||
Some(String::from(BULK_JOIN_REASON)),
|
||||
&servers,
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -646,7 +641,6 @@ pub(super) async fn force_join_all_local_users(
|
||||
&room_id,
|
||||
Some(String::from(BULK_JOIN_REASON)),
|
||||
&servers,
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
{
|
||||
@@ -685,7 +679,7 @@ pub(super) async fn force_join_room(
|
||||
self.services.globals.user_is_local(&user_id),
|
||||
"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}."))
|
||||
.await
|
||||
|
||||
+1
-1
@@ -48,7 +48,7 @@ pub(crate) fn parse_local_user_id(services: &Services, user_id: &str) -> Result<
|
||||
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(
|
||||
services: &Services,
|
||||
user_id: &str,
|
||||
|
||||
@@ -24,7 +24,7 @@
|
||||
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 crate::Ruma;
|
||||
@@ -150,8 +150,7 @@ pub(crate) async fn change_password_route(
|
||||
|
||||
services
|
||||
.users
|
||||
.set_password(&sender_user, Some(&body.new_password))
|
||||
.await?;
|
||||
.set_password(&sender_user, Some(HashedPassword::new(&body.new_password)?));
|
||||
|
||||
if body.logout_devices {
|
||||
// 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
|
||||
pub(crate) async fn whoami_route(
|
||||
State(services): State<crate::State>,
|
||||
State(_): State<crate::State>,
|
||||
body: Ruma<whoami::v3::Request>,
|
||||
) -> Result<whoami::v3::Response> {
|
||||
let is_guest = services
|
||||
.users
|
||||
.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(),
|
||||
Ok(assign!(whoami::v3::Response::new(body.sender_user().to_owned(), false), {
|
||||
device_id: body.sender_device,
|
||||
}))
|
||||
}
|
||||
|
||||
|
||||
@@ -10,12 +10,11 @@
|
||||
use conduwuit_service::Services;
|
||||
use futures::{FutureExt, StreamExt};
|
||||
use lettre::{Address, message::Mailbox};
|
||||
use register::RegistrationKind;
|
||||
use ruma::{
|
||||
OwnedUserId, UserId,
|
||||
api::client::{
|
||||
account::{
|
||||
register::{self, LoginType},
|
||||
register::{self, LoginType, RegistrationKind},
|
||||
request_registration_token_via_email,
|
||||
},
|
||||
uiaa::{AuthFlow, AuthType},
|
||||
@@ -28,7 +27,7 @@
|
||||
push,
|
||||
};
|
||||
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 crate::Ruma;
|
||||
@@ -42,16 +41,6 @@
|
||||
/// You can use [`GET
|
||||
/// /_matrix/client/v3/register/available`](fn.get_register_available_route.
|
||||
/// 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)]
|
||||
#[tracing::instrument(skip_all, fields(%client), name = "register", level = "info")]
|
||||
pub(crate) async fn register_route(
|
||||
@@ -59,7 +48,10 @@ pub(crate) async fn register_route(
|
||||
ClientIp(client): ClientIp,
|
||||
body: Ruma<register::v3::Request>,
|
||||
) -> Result<register::v3::Response> {
|
||||
let is_guest = body.kind == RegistrationKind::Guest;
|
||||
if body.kind != RegistrationKind::User {
|
||||
return Err!(Request(GuestAccessForbidden("Guests may not register on this server.")));
|
||||
}
|
||||
|
||||
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
|
||||
@@ -68,69 +60,19 @@ pub(crate) async fn register_route(
|
||||
services.config.allow_registration || services.firstrun.is_first_run();
|
||||
|
||||
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!(
|
||||
"Guest registration disabled, rejecting guest registration attempt, initial device \
|
||||
name: \"{}\"",
|
||||
body.initial_device_display_name.as_deref().unwrap_or("")
|
||||
?body.username,
|
||||
?body.initial_device_display_name,
|
||||
"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(
|
||||
"This server is not accepting registrations at this time."
|
||||
)));
|
||||
}
|
||||
|
||||
// Appeservices and guests get to skip auth
|
||||
let skip_auth = body.appservice_info.is_some() || is_guest;
|
||||
|
||||
let identity = if skip_auth {
|
||||
// Appservices and guests have no identity
|
||||
let identity = if body.appservice_info.is_some() {
|
||||
// Appservices can skip auth
|
||||
None
|
||||
} else {
|
||||
// Perform UIAA to determine the user's identity
|
||||
@@ -157,13 +99,9 @@ pub(crate) async fn register_route(
|
||||
}
|
||||
});
|
||||
|
||||
let user_id = determine_registration_user_id(
|
||||
&services,
|
||||
supplied_username,
|
||||
is_guest,
|
||||
emergency_mode_enabled,
|
||||
)
|
||||
.await?;
|
||||
let user_id =
|
||||
determine_registration_user_id(&services, supplied_username, emergency_mode_enabled)
|
||||
.await?;
|
||||
|
||||
if body.body.login_type == Some(LoginType::ApplicationService) {
|
||||
// For appservice logins, make sure that the user ID is in the appservice's
|
||||
@@ -187,7 +125,13 @@ pub(crate) async fn register_route(
|
||||
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
|
||||
services.users.create(&user_id, password).await?;
|
||||
@@ -222,7 +166,9 @@ pub(crate) async fn register_route(
|
||||
|
||||
// Generate new device id if the user didn't specify one
|
||||
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());
|
||||
|
||||
// Generate new token for the device
|
||||
@@ -263,8 +209,7 @@ pub(crate) async fn register_route(
|
||||
|
||||
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() && !is_guest {
|
||||
if body.appservice_info.is_none() {
|
||||
if !device_display_name.is_empty() {
|
||||
let notice = format!(
|
||||
"New user \"{user_id}\" registered on this server from IP {client} and device \
|
||||
@@ -285,65 +230,32 @@ pub(crate) async fn register_route(
|
||||
}
|
||||
}
|
||||
|
||||
// log in conduit admin channel if a guest registered
|
||||
if body.appservice_info.is_none() && is_guest && services.config.log_guest_registrations {
|
||||
debug_info!("New guest user \"{user_id}\" registered on this server.");
|
||||
// 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 !device_display_name.is_empty() {
|
||||
if services.server.config.admin_room_notices {
|
||||
services
|
||||
.admin
|
||||
.notice(&format!(
|
||||
"Guest user \"{user_id}\" with device display name \
|
||||
\"{device_display_name}\" registered on this server from IP {client}"
|
||||
))
|
||||
.await;
|
||||
}
|
||||
} else {
|
||||
#[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
|
||||
// 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
|
||||
.users
|
||||
.suspend_account(&user_id, &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 services.server.config.admin_room_notices {
|
||||
services
|
||||
.users
|
||||
.suspend_account(&user_id, &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 services.server.config.admin_room_notices {
|
||||
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();
|
||||
}
|
||||
.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()
|
||||
&& !services.server.config.auto_join_rooms.is_empty()
|
||||
&& (services.config.allow_guests_auto_join_rooms || !is_guest)
|
||||
{
|
||||
if body.appservice_info.is_none() && !services.server.config.auto_join_rooms.is_empty() {
|
||||
for room in &services.server.config.auto_join_rooms {
|
||||
let Ok(room_id) = services.rooms.alias.resolve(room).await else {
|
||||
error!(
|
||||
@@ -372,7 +284,6 @@ pub(crate) async fn register_route(
|
||||
&room_id,
|
||||
Some("Automatically joining this room upon registration".to_owned()),
|
||||
&[services.globals.server_name().to_owned(), room_server_name.to_owned()],
|
||||
&body.appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
@@ -511,12 +422,9 @@ async fn create_registration_uiaa_session(
|
||||
async fn determine_registration_user_id(
|
||||
services: &Services,
|
||||
supplied_username: Option<String>,
|
||||
is_guest: bool,
|
||||
emergency_mode_enabled: bool,
|
||||
) -> Result<OwnedUserId> {
|
||||
if let Some(supplied_username) = supplied_username
|
||||
&& !is_guest
|
||||
{
|
||||
if let Some(supplied_username) = supplied_username {
|
||||
// The user gets to pick their username. Do some validation to make sure it's
|
||||
// acceptable.
|
||||
|
||||
@@ -569,7 +477,7 @@ async fn determine_registration_user_id(
|
||||
|
||||
Ok(user_id)
|
||||
} 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.
|
||||
|
||||
loop {
|
||||
|
||||
@@ -122,16 +122,6 @@ pub(crate) async fn set_room_visibility_route(
|
||||
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? {
|
||||
return Err!(Request(Forbidden("User is not allowed to publish this room")));
|
||||
}
|
||||
|
||||
@@ -64,6 +64,27 @@ pub(crate) async fn upload_keys_route(
|
||||
.await?;
|
||||
}
|
||||
|
||||
for (key_id, fallback_key) in &body.fallback_keys {
|
||||
if fallback_key
|
||||
.deserialize()
|
||||
.inspect_err(|e| {
|
||||
debug_warn!(
|
||||
%key_id,
|
||||
?fallback_key,
|
||||
"Invalid one time key JSON submitted by client, skipping: {e}"
|
||||
);
|
||||
})
|
||||
.is_err()
|
||||
{
|
||||
continue;
|
||||
}
|
||||
|
||||
services
|
||||
.users
|
||||
.add_fallback_key(sender_user, sender_device, key_id, fallback_key, false)
|
||||
.await?;
|
||||
}
|
||||
|
||||
if let Some(device_keys) = &body.device_keys {
|
||||
let deser_device_keys = device_keys.deserialize().map_err(|e| {
|
||||
err!(Request(BadJson(debug_warn!(
|
||||
|
||||
@@ -39,7 +39,6 @@
|
||||
};
|
||||
use service::{
|
||||
Services,
|
||||
appservice::RegistrationInfo,
|
||||
rooms::{
|
||||
state::RoomMutexGuard,
|
||||
state_compressor::{CompressedState, HashSetCompressStateEvent},
|
||||
@@ -112,16 +111,9 @@ pub(crate) async fn join_room_by_id_route(
|
||||
shuffle(&mut servers);
|
||||
let servers = deprioritize(servers, &services.config.deprioritize_joins_through_servers);
|
||||
|
||||
join_room_by_id_helper(
|
||||
&services,
|
||||
sender_user,
|
||||
&body.room_id,
|
||||
body.reason.clone(),
|
||||
&servers,
|
||||
&body.appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
.await
|
||||
join_room_by_id_helper(&services, sender_user, &body.room_id, body.reason.clone(), &servers)
|
||||
.boxed()
|
||||
.await
|
||||
}
|
||||
|
||||
/// # `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>,
|
||||
) -> Result<join_room_by_id_or_alias::v3::Response> {
|
||||
let sender_user = body.sender_user();
|
||||
let appservice_info = &body.appservice_info;
|
||||
let body = &body.body;
|
||||
if services.users.is_suspended(sender_user).await? {
|
||||
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 join_room_response = join_room_by_id_helper(
|
||||
&services,
|
||||
sender_user,
|
||||
&room_id,
|
||||
body.reason.clone(),
|
||||
&servers,
|
||||
appservice_info,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
let join_room_response =
|
||||
join_room_by_id_helper(&services, sender_user, &room_id, body.reason.clone(), &servers)
|
||||
.boxed()
|
||||
.await?;
|
||||
|
||||
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,
|
||||
reason: Option<String>,
|
||||
servers: &[OwnedServerName],
|
||||
appservice_info: &Option<RegistrationInfo>,
|
||||
) -> Result<join_room_by_id::v3::Response> {
|
||||
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
|
||||
.rooms
|
||||
.state_cache
|
||||
|
||||
@@ -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 acquire it again
|
||||
drop(state_lock);
|
||||
match join_room_by_id_helper(
|
||||
services,
|
||||
sender_user,
|
||||
room_id,
|
||||
reason.clone(),
|
||||
servers,
|
||||
&None,
|
||||
)
|
||||
.await
|
||||
match join_room_by_id_helper(services, sender_user, room_id, reason.clone(), servers)
|
||||
.await
|
||||
{
|
||||
| Ok(_) => return Ok(knock_room::v3::Response::new(room_id.to_owned())),
|
||||
| Err(e) => {
|
||||
|
||||
@@ -4,10 +4,9 @@
|
||||
use axum_client_ip::ClientIp;
|
||||
use conduwuit::{
|
||||
Err, Result, debug, err, info,
|
||||
utils::{self, ReadyExt, hash, stream::BroadbandExt},
|
||||
utils::{self, ReadyExt, stream::BroadbandExt},
|
||||
warn,
|
||||
};
|
||||
use conduwuit_core::debug_error;
|
||||
use conduwuit_service::Services;
|
||||
use futures::StreamExt;
|
||||
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(
|
||||
services: &Services,
|
||||
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)
|
||||
.map_err(|_| err!(Request(InvalidUsername("User ID is malformed"))))?;
|
||||
|
||||
let lowercased_user_id = UserId::parse_with_server_name(
|
||||
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)
|
||||
{
|
||||
if !services.globals.user_is_local(&user_id) {
|
||||
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.")));
|
||||
}
|
||||
|
||||
password_login(services, &user_id, &lowercased_user_id, password).await
|
||||
services.users.check_password(&user_id, password).await
|
||||
}
|
||||
|
||||
/// # `POST /_matrix/client/v3/login`
|
||||
|
||||
@@ -395,6 +395,10 @@ pub(crate) async fn build_sync_events(
|
||||
.users
|
||||
.count_one_time_keys(syncing_user, syncing_device);
|
||||
|
||||
let unused_fallback_key_types = services
|
||||
.users
|
||||
.list_unused_fallback_key_types(syncing_user, syncing_device);
|
||||
|
||||
let (
|
||||
(joined_rooms, mut device_list_updates),
|
||||
left_rooms,
|
||||
@@ -405,6 +409,7 @@ pub(crate) async fn build_sync_events(
|
||||
to_device_events,
|
||||
keys_changed,
|
||||
device_one_time_keys_count,
|
||||
unused_fallback_key_types,
|
||||
) = async {
|
||||
futures::join!(
|
||||
joined_rooms,
|
||||
@@ -415,7 +420,8 @@ pub(crate) async fn build_sync_events(
|
||||
account_data,
|
||||
to_device_events,
|
||||
keys_changed,
|
||||
device_one_time_keys_count
|
||||
device_one_time_keys_count,
|
||||
unused_fallback_key_types,
|
||||
)
|
||||
}
|
||||
.boxed()
|
||||
@@ -433,8 +439,7 @@ pub(crate) async fn build_sync_events(
|
||||
account_data: assign!(GlobalAccountData::new(), { events: account_data }),
|
||||
device_lists: device_list_updates.into(),
|
||||
device_one_time_keys_count,
|
||||
// Fallback keys are not yet supported
|
||||
device_unused_fallback_key_types: None,
|
||||
device_unused_fallback_key_types: Some(unused_fallback_key_types),
|
||||
presence: assign!(Presence::new(), {
|
||||
events: presence_updates
|
||||
.into_iter()
|
||||
|
||||
@@ -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
|
||||
/// 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(
|
||||
State(services): State<crate::State>,
|
||||
) -> Result<impl IntoResponse> {
|
||||
|
||||
@@ -1486,21 +1486,6 @@ pub struct Config {
|
||||
#[serde(default)]
|
||||
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.
|
||||
/// These endpoints consist of:
|
||||
/// - /_matrix/media/*/config
|
||||
|
||||
+8
-2
@@ -288,8 +288,14 @@ fn deserialize_option<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||
}
|
||||
|
||||
#[cfg_attr(unabridged, tracing::instrument(level = "trace", skip_all))]
|
||||
fn deserialize_bool<V: Visitor<'de>>(self, _visitor: V) -> Result<V::Value> {
|
||||
unhandled!("deserialize bool not implemented")
|
||||
fn deserialize_bool<V: Visitor<'de>>(self, visitor: V) -> Result<V::Value> {
|
||||
let byte = self
|
||||
.buf
|
||||
.get(self.pos)
|
||||
.ok_or(Self::Error::SerdeDe("bool buffer underflow".into()))?;
|
||||
self.inc_pos(1);
|
||||
|
||||
visitor.visit_bool(*byte != 0x00)
|
||||
}
|
||||
|
||||
#[cfg_attr(unabridged, tracing::instrument(level = "trace", skip_all))]
|
||||
|
||||
@@ -120,6 +120,10 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
|
||||
name: "onetimekeyid_onetimekeys",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "fallbackkeyid_fallbackkey",
|
||||
..descriptor::RANDOM_SMALL
|
||||
},
|
||||
Descriptor {
|
||||
name: "passwordresettoken_info",
|
||||
..descriptor::RANDOM_SMALL
|
||||
|
||||
+2
-2
@@ -297,8 +297,8 @@ fn serialize_u16(self, _v: u16) -> Result<Self::Ok> {
|
||||
|
||||
fn serialize_u8(self, v: u8) -> Result<Self::Ok> { self.write(&[v]) }
|
||||
|
||||
fn serialize_bool(self, _v: bool) -> Result<Self::Ok> {
|
||||
unhandled!("serialize bool not implemented")
|
||||
fn serialize_bool(self, v: bool) -> Result<Self::Ok> {
|
||||
if v { self.write(&[0x01]) } else { self.write(&[0x00]) }
|
||||
}
|
||||
|
||||
fn serialize_unit(self) -> Result<Self::Ok> { unhandled!("serialize unit not implemented") }
|
||||
|
||||
@@ -121,10 +121,7 @@ async fn start_appservice(&self, id: String, registration: Registration) -> Resu
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// Reactivate the appservice user if it was accidentally deactivated
|
||||
self.services
|
||||
.users
|
||||
.set_password(&appservice_user_id, None)
|
||||
.await?;
|
||||
self.services.users.set_password(&appservice_user_id, None);
|
||||
}
|
||||
|
||||
self.registration_info
|
||||
|
||||
@@ -9,7 +9,10 @@
|
||||
push::Ruleset,
|
||||
};
|
||||
|
||||
use crate::{Dep, account_data, config, globals, users};
|
||||
use crate::{
|
||||
Dep, account_data, config, globals,
|
||||
users::{self, HashedPassword},
|
||||
};
|
||||
|
||||
pub struct Service {
|
||||
services: Services,
|
||||
@@ -51,10 +54,15 @@ impl Service {
|
||||
async fn set_emergency_access(&self) -> Result {
|
||||
let server_user = &self.services.globals.server_user;
|
||||
|
||||
self.services
|
||||
.users
|
||||
.set_password(server_user, self.services.config.emergency_password.as_deref())
|
||||
.await?;
|
||||
self.services.users.set_password(
|
||||
server_user,
|
||||
self.services
|
||||
.config
|
||||
.emergency_password
|
||||
.as_deref()
|
||||
.map(HashedPassword::new)
|
||||
.transpose()?,
|
||||
);
|
||||
|
||||
let (ruleset, pwd_set) = match self.services.config.emergency_password {
|
||||
| Some(_) => (Ruleset::server_default(server_user), true),
|
||||
|
||||
@@ -6,7 +6,10 @@
|
||||
use data::{Data, ResetTokenInfo};
|
||||
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 RESET_TOKEN_QUERY_PARAM: &str = "token";
|
||||
@@ -100,8 +103,7 @@ pub async fn consume_token(
|
||||
self.db.remove_token(&token);
|
||||
self.services
|
||||
.users
|
||||
.set_password(&info.user, Some(new_password))
|
||||
.await?;
|
||||
.set_password(&info.user, Some(HashedPassword::new(new_password)?));
|
||||
}
|
||||
|
||||
Ok(())
|
||||
|
||||
@@ -238,7 +238,7 @@ pub async fn room_joined_count(&self, room_id: &RoomId) -> Result<u64> {
|
||||
#[implement(Service)]
|
||||
#[tracing::instrument(skip(self), level = "debug")]
|
||||
/// 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>(
|
||||
&'a self,
|
||||
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
|
||||
/// active (not deactivated, not guest)
|
||||
/// active (not deactivated)
|
||||
#[implement(Service)]
|
||||
#[tracing::instrument(skip(self), level = "trace")]
|
||||
pub fn active_local_users_in_room<'a>(
|
||||
|
||||
+8
-10
@@ -4,7 +4,7 @@
|
||||
sync::Arc,
|
||||
};
|
||||
|
||||
use conduwuit::{Err, Error, Result, error, utils, utils::hash};
|
||||
use conduwuit::{Err, Error, Result, error, utils};
|
||||
use lettre::Address;
|
||||
use ruma::{
|
||||
UserId,
|
||||
@@ -377,15 +377,13 @@ async fn check_stage(
|
||||
));
|
||||
};
|
||||
|
||||
// Check if password is correct
|
||||
let mut password_verified = false;
|
||||
|
||||
// First try local password hash verification
|
||||
if let Ok(hash) = self.services.users.password_hash(&user_id).await {
|
||||
password_verified = hash::verify_password(password, &hash).is_ok();
|
||||
}
|
||||
|
||||
if password_verified {
|
||||
if self
|
||||
.services
|
||||
.users
|
||||
.check_password(&user_id, password)
|
||||
.await
|
||||
.is_ok()
|
||||
{
|
||||
identity.try_set_localpart(user_id.localpart().to_owned())?;
|
||||
|
||||
Ok(AuthType::Password)
|
||||
|
||||
+162
-27
@@ -3,14 +3,14 @@
|
||||
use std::{collections::BTreeMap, mem, net::IpAddr, sync::Arc};
|
||||
|
||||
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},
|
||||
};
|
||||
use database::{Deserialized, Ignore, Interfix, Json, Map};
|
||||
use futures::{Stream, StreamExt, TryFutureExt};
|
||||
use ruma::{
|
||||
DeviceId, KeyId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId,
|
||||
OneTimeKeyName, OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedUserId, RoomId, UInt, UserId,
|
||||
DeviceId, MilliSecondsSinceUnixEpoch, OneTimeKeyAlgorithm, OneTimeKeyId, OneTimeKeyName,
|
||||
OwnedDeviceId, OwnedKeyId, OwnedMxcUri, OwnedOneTimeKeyId, OwnedUserId, RoomId, UInt, UserId,
|
||||
api::{
|
||||
client::{device::Device, filter::FilterDefinition},
|
||||
error::ErrorKind,
|
||||
@@ -38,6 +38,19 @@ pub struct UserSuspension {
|
||||
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 {
|
||||
services: Services,
|
||||
db: Data,
|
||||
@@ -57,6 +70,7 @@ struct Data {
|
||||
keychangeid_userid: Arc<Map>,
|
||||
keyid_key: Arc<Map>,
|
||||
onetimekeyid_onetimekeys: Arc<Map>,
|
||||
fallbackkeyid_fallbackkey: Arc<Map>,
|
||||
openidtoken_expiresatuserid: Arc<Map>,
|
||||
logintoken_expiresatuserid: Arc<Map>,
|
||||
todeviceid_events: Arc<Map>,
|
||||
@@ -97,6 +111,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
|
||||
keychangeid_userid: args.db["keychangeid_userid"].clone(),
|
||||
keyid_key: args.db["keyid_key"].clone(),
|
||||
onetimekeyid_onetimekeys: args.db["onetimekeyid_onetimekeys"].clone(),
|
||||
fallbackkeyid_fallbackkey: args.db["fallbackkeyid_fallbackkey"].clone(),
|
||||
openidtoken_expiresatuserid: args.db["openidtoken_expiresatuserid"].clone(),
|
||||
logintoken_expiresatuserid: args.db["logintoken_expiresatuserid"].clone(),
|
||||
todeviceid_events: args.db["todeviceid_events"].clone(),
|
||||
@@ -169,16 +184,23 @@ pub async fn is_admin(&self, user_id: &UserId) -> bool {
|
||||
|
||||
/// Create a new user account on this homeserver.
|
||||
#[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() {
|
||||
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(())
|
||||
}
|
||||
|
||||
// /// Create a new account for a local human or bot user.
|
||||
// pub async fn create_local_account(
|
||||
// &self,
|
||||
// username: String,
|
||||
// password:
|
||||
// )
|
||||
|
||||
/// Deactivate account
|
||||
pub async fn deactivate_account(&self, user_id: &UserId) -> Result<()> {
|
||||
// Remove all associated devices
|
||||
@@ -190,7 +212,7 @@ pub async fn deactivate_account(&self, user_id: &UserId) -> Result<()> {
|
||||
// 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
|
||||
// account is deactivated.
|
||||
self.set_password(user_id, None).await?;
|
||||
self.set_password(user_id, None);
|
||||
|
||||
// TODO: Unhook 3PID
|
||||
Ok(())
|
||||
@@ -336,25 +358,42 @@ pub fn list_local_users(&self) -> impl Stream<Item = OwnedUserId> + Send + '_ {
|
||||
.ready_filter_map(|(u, p): (OwnedUserId, &[u8])| (!p.is_empty()).then_some(u))
|
||||
}
|
||||
|
||||
/// Returns the password hash for the given user.
|
||||
pub async fn password_hash(&self, user_id: &UserId) -> Result<String> {
|
||||
self.db.userid_password.get(user_id).await.deserialized()
|
||||
/// Set a user's password.
|
||||
pub fn set_password(&self, user_id: &UserId, password: Option<HashedPassword>) {
|
||||
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
|
||||
pub async fn set_password(&self, user_id: &UserId, password: Option<&str>) -> Result<()> {
|
||||
password
|
||||
.map(utils::hash::password)
|
||||
.transpose()
|
||||
.map_err(|e| {
|
||||
err!(Request(InvalidParam("Password does not meet the requirements: {e}")))
|
||||
})?
|
||||
.map_or_else(
|
||||
|| self.db.userid_password.insert(user_id, b""),
|
||||
|hash| self.db.userid_password.insert(user_id, hash),
|
||||
);
|
||||
/// Check a user's password.
|
||||
pub async fn check_password(&self, user_id: &UserId, password: &str) -> Result<OwnedUserId> {
|
||||
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();
|
||||
|
||||
Ok(())
|
||||
if let Ok(hash) = self.db.userid_password.get(user_id).await.deserialized() {
|
||||
(hash, lowercase_user_id)
|
||||
} else {
|
||||
return Err!(Request(InvalidParam("This user cannot log in with a password.")));
|
||||
}
|
||||
};
|
||||
|
||||
if hash.is_empty() {
|
||||
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.
|
||||
@@ -550,7 +589,7 @@ pub async fn add_one_time_key(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
one_time_key_key: &KeyId<OneTimeKeyAlgorithm, OneTimeKeyName>,
|
||||
one_time_key_key: &OneTimeKeyId,
|
||||
one_time_key_value: &Raw<OneTimeKey>,
|
||||
) -> Result {
|
||||
// All devices have metadata
|
||||
@@ -587,6 +626,39 @@ pub async fn add_one_time_key(
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Save a fallback key for the given user, device, and algorithm
|
||||
/// This key will replace an existing fallback key
|
||||
pub async fn add_fallback_key(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
fallback_key_id: &OneTimeKeyId,
|
||||
fallback_key: &Raw<OneTimeKey>,
|
||||
used: bool,
|
||||
) -> Result {
|
||||
// All devices have metadata
|
||||
// Only existing devices should be able to call this, but we shouldn't assert
|
||||
// either...
|
||||
let key = (user_id, device_id);
|
||||
if self.db.userdeviceid_metadata.qry(&key).await.is_err() {
|
||||
return Err!(Database(error!(
|
||||
%user_id,
|
||||
%device_id,
|
||||
"User does not exist or device has no metadata."
|
||||
)));
|
||||
}
|
||||
|
||||
// There is one fallback key slot per user, per device, per algorithm
|
||||
// Therefore we use this as the DB key for this column
|
||||
let db_key = (user_id, device_id, fallback_key_id.algorithm());
|
||||
|
||||
self.db
|
||||
.fallbackkeyid_fallbackkey
|
||||
.put(db_key, (used, fallback_key_id.as_str(), Json(fallback_key)));
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub async fn last_one_time_keys_update(&self, user_id: &UserId) -> u64 {
|
||||
self.db
|
||||
.userid_lastonetimekeyupdate
|
||||
@@ -618,6 +690,8 @@ pub async fn take_one_time_key(
|
||||
.onetimekeyid_onetimekeys
|
||||
.raw_stream_prefix(&prefix)
|
||||
.ignore_err()
|
||||
.next()
|
||||
.await
|
||||
.map(|(key, val)| {
|
||||
self.db.onetimekeyid_onetimekeys.remove(key);
|
||||
|
||||
@@ -636,11 +710,44 @@ pub async fn take_one_time_key(
|
||||
.unwrap();
|
||||
|
||||
(key, val)
|
||||
})
|
||||
.next()
|
||||
.await;
|
||||
});
|
||||
|
||||
one_time_key.ok_or_else(|| err!(Request(NotFound("No one-time-key found"))))
|
||||
if let Some(result) = one_time_key {
|
||||
return Ok(result);
|
||||
}
|
||||
|
||||
// No one-time key has been found. Look for a fallback key.
|
||||
|
||||
let db_key = (user_id, device_id, key_algorithm);
|
||||
|
||||
let fallback_key = self
|
||||
.db
|
||||
.fallbackkeyid_fallbackkey
|
||||
.qry(&db_key)
|
||||
.await
|
||||
.ok()
|
||||
.and_then(|handle| {
|
||||
handle
|
||||
.deserialized::<(bool, OwnedOneTimeKeyId, Raw<OneTimeKey>)>()
|
||||
.ok()
|
||||
});
|
||||
|
||||
if let Some((used, fallback_key_id, fallback_key_value)) = fallback_key {
|
||||
if !used {
|
||||
// write the key to the database again to mark it as used
|
||||
self.add_fallback_key(
|
||||
user_id,
|
||||
device_id,
|
||||
&fallback_key_id,
|
||||
&fallback_key_value,
|
||||
true,
|
||||
)
|
||||
.await?;
|
||||
}
|
||||
return Ok((fallback_key_id, fallback_key_value));
|
||||
}
|
||||
|
||||
Err(err!(Request(NotFound("No one-time key or fallback key found"))))
|
||||
}
|
||||
|
||||
pub async fn count_one_time_keys(
|
||||
@@ -673,6 +780,34 @@ pub async fn count_one_time_keys(
|
||||
algorithm_counts
|
||||
}
|
||||
|
||||
pub async fn list_unused_fallback_key_types(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
device_id: &DeviceId,
|
||||
) -> Vec<OneTimeKeyAlgorithm> {
|
||||
type KeyVal = ((String, String, OneTimeKeyAlgorithm), (bool, String, Ignore));
|
||||
|
||||
let mut query = user_id.as_bytes().to_vec();
|
||||
query.push(0xFF);
|
||||
query.extend_from_slice(device_id.as_bytes());
|
||||
query.push(0xFF);
|
||||
|
||||
let mut unused_algorithms = Vec::new();
|
||||
|
||||
self.db
|
||||
.fallbackkeyid_fallbackkey
|
||||
.stream_prefix(&query)
|
||||
.ignore_err()
|
||||
.ready_for_each(|((_, _, fallback_key_algorithm), (used, ..)): KeyVal| {
|
||||
if !used {
|
||||
unused_algorithms.push(fallback_key_algorithm);
|
||||
}
|
||||
})
|
||||
.await;
|
||||
|
||||
unused_algorithms
|
||||
}
|
||||
|
||||
pub async fn add_device_keys(
|
||||
&self,
|
||||
user_id: &UserId,
|
||||
|
||||
Reference in New Issue
Block a user