Compare commits

..

3 Commits

Author SHA1 Message Date
Ginger bec8ee7599 fix: Add unstable feature flag 2026-05-26 13:17:05 +00:00
Ginger 66e35aaaba chore: News fragment 2026-05-26 13:17:05 +00:00
Ginger 9c523e8fa1 feat: Add support for MSC4466 2026-05-26 13:17:05 +00:00
210 changed files with 2123 additions and 7675 deletions
Generated
+11 -78
View File
@@ -1088,7 +1088,6 @@ dependencies = [
"serde",
"serde-saphyr",
"serde_json",
"serde_urlencoded",
"sha2 0.11.0",
"termimad",
"tokio",
@@ -1108,29 +1107,18 @@ dependencies = [
"axum",
"axum-extra",
"base64 0.22.1",
"conduwuit_api",
"conduwuit_build_metadata",
"conduwuit_core",
"conduwuit_database",
"conduwuit_service",
"form_urlencoded",
"futures",
"lettre",
"memory-serve",
"rand 0.10.1",
"recaptcha-verify",
"reqwest 0.12.28",
"ruma",
"serde",
"serde_json",
"serde_urlencoded",
"thiserror",
"tower-http",
"tower-sec-fetch",
"tower-sessions",
"tower-sessions-core",
"tracing",
"url",
"validator",
]
@@ -1538,7 +1526,6 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [
"powerfmt",
"serde_core",
]
[[package]]
@@ -4164,7 +4151,7 @@ dependencies = [
[[package]]
name = "ruma"
version = "0.15.1"
source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
dependencies = [
"assign",
"js_int",
@@ -4183,7 +4170,7 @@ dependencies = [
[[package]]
name = "ruma-appservice-api"
version = "0.15.0"
source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
dependencies = [
"js_int",
"ruma-common",
@@ -4195,7 +4182,7 @@ dependencies = [
[[package]]
name = "ruma-client-api"
version = "0.23.1"
source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
dependencies = [
"as_variant",
"assign",
@@ -4217,7 +4204,7 @@ dependencies = [
[[package]]
name = "ruma-common"
version = "0.18.0"
source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
dependencies = [
"as_variant",
"base64 0.22.1",
@@ -4250,7 +4237,7 @@ dependencies = [
[[package]]
name = "ruma-events"
version = "0.33.0"
source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
dependencies = [
"as_variant",
"indexmap",
@@ -4271,7 +4258,7 @@ dependencies = [
[[package]]
name = "ruma-federation-api"
version = "0.14.0"
source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
dependencies = [
"bytes",
"headers",
@@ -4294,7 +4281,7 @@ dependencies = [
[[package]]
name = "ruma-identifiers-validation"
version = "0.12.1"
source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
dependencies = [
"js_int",
"thiserror",
@@ -4303,7 +4290,7 @@ dependencies = [
[[package]]
name = "ruma-macros"
version = "0.18.0"
source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
dependencies = [
"as_variant",
"cfg-if",
@@ -4319,7 +4306,7 @@ dependencies = [
[[package]]
name = "ruma-push-gateway-api"
version = "0.14.0"
source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
dependencies = [
"js_int",
"ruma-common",
@@ -4331,7 +4318,7 @@ dependencies = [
[[package]]
name = "ruma-signatures"
version = "0.20.0"
source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
dependencies = [
"base64 0.22.1",
"ed25519-dalek",
@@ -4347,7 +4334,7 @@ dependencies = [
[[package]]
name = "ruma-state-res"
version = "0.16.0"
source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b"
dependencies = [
"js_int",
"ruma-common",
@@ -5556,22 +5543,6 @@ dependencies = [
"tracing",
]
[[package]]
name = "tower-cookies"
version = "0.11.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "151b5a3e3c45df17466454bb74e9ecedecc955269bdedbf4d150dfa393b55a36"
dependencies = [
"axum-core",
"cookie",
"futures-util",
"http",
"parking_lot",
"pin-project-lite",
"tower-layer",
"tower-service",
]
[[package]]
name = "tower-http"
version = "0.6.11"
@@ -5620,44 +5591,6 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3"
[[package]]
name = "tower-sessions"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "518dca34b74a17cadfcee06e616a09d2bd0c3984eff1769e1e76d58df978fc78"
dependencies = [
"async-trait",
"http",
"time",
"tokio",
"tower-cookies",
"tower-layer",
"tower-service",
"tower-sessions-core",
"tracing",
]
[[package]]
name = "tower-sessions-core"
version = "0.15.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "568531ec3dfcf3ffe493de1958ae5662a0284ac5d767476ecdb6a34ff8c6b06c"
dependencies = [
"async-trait",
"axum-core",
"base64 0.22.1",
"futures",
"http",
"parking_lot",
"rand 0.9.4",
"serde",
"serde_json",
"thiserror",
"time",
"tokio",
"tracing",
]
[[package]]
name = "tracing"
version = "0.1.44"
+2 -4
View File
@@ -344,7 +344,7 @@ version = "1.1.1"
[workspace.dependencies.ruma]
# version = "0.14.1"
git = "https://github.com/ruma/ruma.git"
rev = "9c9dccc93f054bbd28f23f630223fffa6289ecbc"
rev = "3ecd80b92794d2d93f657a7b3db62d4be237526b"
features = [
"appservice-api-c",
"client-api",
@@ -379,6 +379,7 @@ features = [
"unstable-msc4293",
"unstable-msc4406",
"unstable-msc4439",
"unstable-msc4466",
"unstable-extensible-events",
]
@@ -559,9 +560,6 @@ features = ["std"]
[workspace.dependencies.nonzero_ext]
version = "0.3.0"
[workspace.dependencies.serde_urlencoded]
version = "0.7.1"
#
# Patches
#
-1
View File
@@ -1 +0,0 @@
Users may now be forbidden from deactivating their own accounts with the new `allow_deactivation` config option. Contributed by @ginger.
-1
View File
@@ -1 +0,0 @@
Added support for authenticating clients using the new OAuth 2.0 login API. Contributed by @ginger.
+1
View File
@@ -0,0 +1 @@
Added support for MSC4466, which allows clients to customize how changes to a user's global profile are propagated. Contributed by @ginger.
+9 -37
View File
@@ -521,15 +521,17 @@
#
#recaptcha_private_site_key =
# Controls whether users are allowed to deactivate their own accounts
# through the account management panel or their Matrix clients. Server
# admins can always deactivate users using the relevant admin commands.
# Policy documents, such as terms and conditions or a privacy policy,
# which users must agree to when registering an account.
#
# Note that, in some jurisdictions, you may be legally required to honor
# users who request to deactivate their accounts if you set this option
# to `false`.
# Example:
# ```ignore
# [global.registration_terms.privacy_policy]
# en = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
# es = { name = "Política de Privacidad", url = "https://homeserver.example/es/privacy_policy.html" }
# ```
#
#allow_deactivation = true
#registration_terms = {}
# Controls whether encrypted rooms and events are allowed.
#
@@ -1985,33 +1987,3 @@
# `require_email_for_registration`.
#
#require_email_for_token_registration = false
#[global.registration_terms]
# The language code to provide to clients along with the policy documents.
#
#language = "en"
# Policy documents, such as terms and conditions or a privacy policy,
# which users must agree to when registering an account.
#
# Example:
# ```ignore
# [global.registration_terms.documents]
# privacy_policy = { name = "Privacy Policy", url = "https://homeserver.example/en/privacy_policy.html" }
# ```
#
#documents = {}
#[global.oauth]
# The compatibility mode to use for OAuth.
#
# - "disabled": OAuth will be unavailable. Users will only be able to log
# in using legacy authentication.
# - "hybrid": OAuth and legacy authentication will both be available. Some
# clients may only use one or the other.
# - "exclusive": Only OAuth will be available. Clients which require
# legacy authentication will be unable to log in.
#
#compatibility_mode = "hybrid"
+1 -1
View File
@@ -16,7 +16,7 @@ use crate::{
};
#[derive(Debug, Parser)]
#[command(name = conduwuit_core::BRANDING, version = conduwuit_core::version())]
#[command(name = conduwuit_core::name(), version = conduwuit_core::version())]
pub enum AdminCommand {
#[command(subcommand)]
/// Commands for managing appservices
+2 -25
View File
@@ -30,37 +30,14 @@ pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
.issue_token(self.sender_or_service_user().into(), expires);
self.write_str(&format!(
"New registration token issued: `{token}` . {}.",
"New registration token issued: `{token}`. {}.",
if let Some(expires) = info.expires {
format!("{expires}")
} else {
"Never expires".to_owned()
}
))
.await?;
if self
.services
.config
.oauth
.compatibility_mode
.oauth_available()
{
self.write_str(&format!(
"\nInvite link using this token: {}",
self.services
.config
.get_client_domain()
.join(&format!(
"{}/account/register/?flow=trusted&token={token}",
conduwuit::ROUTE_PREFIX
))
.unwrap()
))
.await?;
}
Ok(())
.await
}
#[admin_command]
+147 -11
View File
@@ -1,10 +1,13 @@
use std::collections::{BTreeMap, HashSet};
use std::{
collections::{BTreeMap, HashSet},
fmt::Write as _,
};
use api::client::{
full_user_deactivate, leave_room, recreate_push_rules_and_return, remote_leave_room,
};
use conduwuit::{
Err, Result, debug_warn, info,
Err, Result, debug_warn, error, info,
matrix::{Event, pdu::PartialPdu},
utils::{self, ReadyExt},
warn,
@@ -50,22 +53,130 @@ pub(super) async fn list_users(&self) -> Result {
#[admin_command]
pub(super) async fn create_user(&self, username: String, password: Option<String>) -> Result {
// Validate user id
let user_id = self
.services
let user_id = parse_local_user_id(self.services, &username)?;
if let Err(e) = user_id.validate_strict() {
if self.services.config.emergency_password.is_none() {
return Err!("Username {user_id} contains disallowed characters or spaces: {e}");
}
}
if self.services.users.exists(&user_id).await {
return Err!("User {user_id} already exists");
}
let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
// Create user
self.services
.users
.determine_registration_user_id(Some(username), None, None)
.create(&user_id, Some(HashedPassword::new(&password)?))
.await?;
let password = HashedPassword::new(
&password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)),
)?;
// Default to pretty displayname
let mut displayname = user_id.localpart().to_owned();
// If `new_user_displayname_suffix` is set, registration will push whatever
// content is set to the user's display name with a space before it
if !self
.services
.server
.config
.new_user_displayname_suffix
.is_empty()
{
write!(displayname, " {}", self.services.server.config.new_user_displayname_suffix)?;
}
self.services
.users
.create_local_account(&user_id, password, None)
.await;
.set_displayname(&user_id, Some(displayname));
self.write_str(&format!("Created user {user_id}")).await
// Initial account data
self.services
.account_data
.update(
None,
&user_id,
ruma::events::GlobalAccountDataEventType::PushRules
.to_string()
.into(),
&serde_json::to_value(ruma::events::push_rules::PushRulesEvent::new(
ruma::events::push_rules::PushRulesEventContent::new(
ruma::push::Ruleset::server_default(&user_id),
),
))
.unwrap(),
)
.await?;
if !self.services.server.config.auto_join_rooms.is_empty() {
for room in &self.services.server.config.auto_join_rooms {
let Ok(room_id) = self.services.rooms.alias.resolve(room).await else {
error!(
%user_id,
"Failed to resolve room alias to room ID when attempting to auto join {room}, skipping"
);
continue;
};
if !self
.services
.rooms
.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
.rooms
.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(),
],
)
.await
{
| Ok(_response) => {
info!("Automatically joined room {room} for user {user_id}");
},
| 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}"
);
self.services
.admin
.send_text(&format!(
"Failed to automatically join room {room} for user {user_id}: \
{e}"
))
.await;
},
}
}
}
}
// we dont add a device since we're not the user, just the creator
// Make the first user to register an administrator and disable first-run mode.
self.services.firstrun.empower_first_user(&user_id).await?;
self.write_str(&format!("Created user with user_id: {user_id} and password: `{password}`"))
.await
}
#[admin_command]
@@ -191,6 +302,31 @@ pub(super) async fn reset_password(
Ok(())
}
#[admin_command]
pub(super) async fn issue_password_reset_link(&self, username: String) -> Result {
use conduwuit_service::password_reset::{PASSWORD_RESET_PATH, RESET_TOKEN_QUERY_PARAM};
self.bail_restricted()?;
let mut reset_url = self
.services
.config
.get_client_domain()
.join(PASSWORD_RESET_PATH)
.unwrap();
let user_id = parse_local_user_id(self.services, &username)?;
let token = self.services.password_reset.issue_token(user_id).await?;
reset_url
.query_pairs_mut()
.append_pair(RESET_TOKEN_QUERY_PARAM, &token.token);
self.write_str(&format!("Password reset link issued for {username}: {reset_url}"))
.await?;
Ok(())
}
#[admin_command]
pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result {
if self.body.len() < 2
+6
View File
@@ -29,6 +29,12 @@ pub enum UserCommand {
password: Option<String>,
},
/// Issue a self-service password reset link for a user.
IssuePasswordResetLink {
/// Username of the user who may use the link
username: String,
},
/// Get a user's associated email address.
GetEmail {
user_id: String,
-2
View File
@@ -62,8 +62,6 @@ zstd_compression = [
"reqwest/zstd",
]
admin_api = []
[dependencies]
async-trait.workspace = true
axum-client-ip.workspace = true
+1
View File
@@ -0,0 +1 @@
pub mod rooms;
@@ -6,14 +6,14 @@ use ruminuwuity::admin::continuwuity::rooms;
use crate::{Ruma, client::leave_room};
/// # `PUT /_continuwuity/admin/v1/rooms/{roomID}/ban`
/// # `PUT /_continuwuity/admin/rooms/{roomID}/ban`
///
/// Bans or unbans a room.
pub(crate) async fn ban_room(
State(services): State<crate::State>,
body: Ruma<rooms::ban::v1::Request>,
) -> Result<rooms::ban::v1::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
+36
View File
@@ -0,0 +1,36 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use futures::StreamExt;
use ruma::OwnedRoomId;
use ruminuwuity::admin::continuwuity::rooms;
use crate::Ruma;
/// # `GET /_continuwuity/admin/rooms/list`
///
/// Lists all rooms known to this server, excluding banned ones.
pub(crate) async fn list_rooms(
State(services): State<crate::State>,
body: Ruma<rooms::list::v1::Request>,
) -> Result<rooms::list::v1::Response> {
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
let mut rooms: Vec<OwnedRoomId> = services
.rooms
.metadata
.iter_ids()
.filter_map(|room_id| async move {
if !services.rooms.metadata.is_banned(&room_id).await {
Some(room_id.clone())
} else {
None
}
})
.collect()
.await;
rooms.sort();
Ok(rooms::list::v1::Response::new(rooms))
}
+2
View File
@@ -0,0 +1,2 @@
pub mod ban;
pub mod list;
+47 -43
View File
@@ -24,10 +24,10 @@ use ruma::{
power_levels::RoomPowerLevelsEventContent,
},
};
use service::{mailer::messages, uiaa::UiaaInitiator, users::HashedPassword};
use service::{mailer::messages, uiaa::Identity, users::HashedPassword};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::{Ruma, router::ClientIdentity};
use crate::Ruma;
pub(crate) mod register;
pub(crate) mod threepid;
@@ -49,16 +49,41 @@ pub(crate) async fn get_register_available_route(
ClientIp(client): ClientIp,
body: Ruma<get_username_availability::v3::Request>,
) -> Result<get_username_availability::v3::Response> {
let _ = services
.users
.determine_registration_user_id(
Some(body.username.clone()),
None,
body.identity
.as_ref()
.and_then(ClientIdentity::appservice_info),
)
.await?;
// Validate user id
let user_id =
match UserId::parse_with_server_name(&body.username, services.globals.server_name()) {
| Ok(user_id) => {
if let Err(e) = user_id.validate_strict() {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {} contains disallowed characters or spaces: {e}",
body.username
))));
}
user_id
},
| Err(e) => {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {} is not valid: {e}",
body.username
))));
},
};
// Check if username is creative enough
if services.users.exists(&user_id).await {
return Err!(Request(UserInUse("User ID is not available.")));
}
if let Some(ref info) = body.appservice_info {
if !info.is_user_match(&user_id) {
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
}
}
if services.appservice.is_exclusive_user_id(&user_id).await {
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
Ok(get_username_availability::v3::Response::new(true))
}
@@ -86,7 +111,7 @@ pub(crate) async fn change_password_route(
ClientIp(client): ClientIp,
body: Ruma<change_password::v3::Request>,
) -> Result<change_password::v3::Response> {
let identity = if let Some(identity) = body.identity.as_ref() {
let identity = if let Some(ref user_id) = body.sender_user {
// A signed-in user is trying to change their password, prompt them for their
// existing one
@@ -96,7 +121,7 @@ pub(crate) async fn change_password_route(
&body.auth,
vec![AuthFlow::new(vec![AuthType::Password])],
Box::default(),
Some(UiaaInitiator::new(identity.sender_user(), identity.sender_device())),
Some(Identity::from_user_id(user_id)),
)
.await?
} else {
@@ -132,12 +157,7 @@ pub(crate) async fn change_password_route(
services
.users
.all_device_ids(&sender_user)
.ready_filter(|id| {
body.identity
.as_ref()
.and_then(|identity| identity.sender_device())
.is_none_or(|sender_device| sender_device != *id)
})
.ready_filter(|id| *id != body.sender_device())
.for_each(async |id| services.users.remove_device(&sender_user, &id).await)
.await;
@@ -153,12 +173,7 @@ pub(crate) async fn change_password_route(
.await
.ok()
.as_ref()
.is_some_and(|pusher_device| {
body.identity
.as_ref()
.and_then(|identity| identity.sender_device())
.is_none_or(|sender_device| sender_device != *pusher_device)
})
.is_some_and(|pusher_device| pusher_device != body.sender_device())
.then_some(pushkey)
})
.for_each(async |pushkey| {
@@ -226,11 +241,9 @@ pub(crate) async fn whoami_route(
State(_): State<crate::State>,
body: Ruma<whoami::v3::Request>,
) -> Result<whoami::v3::Response> {
Ok(
assign!(whoami::v3::Response::new(body.identity.sender_user().to_owned(), false), {
device_id: body.identity.sender_device().map(ToOwned::to_owned),
}),
)
Ok(assign!(whoami::v3::Response::new(body.sender_user().to_owned(), false), {
device_id: body.sender_device,
}))
}
/// # `POST /_matrix/client/r0/account/deactivate`
@@ -252,24 +265,15 @@ pub(crate) async fn deactivate_route(
) -> Result<deactivate::v3::Response> {
// Authentication for this endpoint is technically optional,
// but we require the user to be logged in
let identity = body
.identity
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let sender_user = identity.sender_user();
if !services.config.allow_deactivation {
return Err!(Request(Unauthorized(
"You may not deactivate your own account. Contact your server's administrator for \
assistance."
)));
}
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
.authenticate_password(&body.auth, sender_user, identity.sender_device(), None)
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
// Remove profile pictures and display name
+298 -60
View File
@@ -1,15 +1,17 @@
use std::collections::HashMap;
use std::{collections::HashMap, fmt::Write};
use axum::extract::State;
use axum_client_ip::ClientIp;
use conduwuit::{
Err, Result, debug_info, info,
Err, Result, debug_info, error, info,
utils::{self},
warn,
};
use conduwuit_service::Services;
use futures::StreamExt;
use futures::{FutureExt, StreamExt};
use lettre::{Address, message::Mailbox};
use ruma::{
OwnedUserId, UserId,
api::client::{
account::{
register::{self, LoginType, RegistrationKind},
@@ -18,6 +20,11 @@ use ruma::{
uiaa::{AuthFlow, AuthType},
},
assign,
events::{
GlobalAccountDataEventType, push_rules::PushRulesEvent,
room::message::RoomMessageEventContent,
},
push,
};
use serde_json::value::RawValue;
use service::{mailer::messages, users::HashedPassword};
@@ -25,6 +32,8 @@ use service::{mailer::messages, users::HashedPassword};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::Ruma;
const RANDOM_USER_ID_LENGTH: usize = 10;
/// # `POST /_matrix/client/v3/register`
///
/// Register an account on this homeserver.
@@ -43,12 +52,14 @@ pub(crate) async fn register_route(
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
// run (so the first user account can be created)
let allow_registration =
services.config.allow_registration || services.firstrun.is_first_run();
if !allow_registration && body.identity.is_none() {
if !allow_registration && body.appservice_info.is_none() {
info!(
?body.username,
?body.initial_device_display_name,
@@ -60,59 +71,101 @@ pub(crate) async fn register_route(
)));
}
let user_id = if body.body.login_type == Some(LoginType::ApplicationService) {
let Some(appservice_info) = &body.identity else {
return Err!(Request(Forbidden(
"Only appservices can use the appservice login type."
)));
};
let user_id = services
.users
.determine_registration_user_id(body.username.clone(), None, Some(appservice_info))
.await?;
services.users.create(&user_id, None).await?;
user_id
let identity = if body.appservice_info.is_some() {
// Appservices can skip auth
None
} else {
// Perform UIAA to determine the user's identity
let (flows, params) = create_registration_uiaa_session(&services).await?;
let identity = services
.uiaa
.authenticate(&body.auth, flows, params, None)
.await?;
let password = if let Some(password) = &body.password {
HashedPassword::new(password)?
} else {
return Err!(Request(InvalidParam("A password must be provided.")));
};
let user_id = services
.users
.determine_registration_user_id(body.username.clone(), identity.email.as_ref(), None)
.await?;
services
.users
.create_local_account(&user_id, password, identity.email)
.await;
services.users.join_auto_join_rooms(&user_id).await;
user_id
Some(
services
.uiaa
.authenticate(&body.auth, flows, params, None)
.await?,
)
};
let (token, device) = if !body.inhibit_login {
// If UIAA is disabled, we can't create a device. In that case only appservices
// can reach this point in the first place, so we return an error for them.
if !services.config.oauth.compatibility_mode.uiaa_available() {
return Err!(Request(AppserviceLoginUnsupported(
"User-interactive appservice registration is not available on this server."
)));
// If the user didn't supply a username but did supply an email, use
// the email's user as their initial localpart to avoid falling back to
// a randomly generated localpart
let supplied_username = body.username.clone().or_else(|| {
if let Some(identity) = &identity
&& let Some(email) = &identity.email
{
Some(email.user().to_owned())
} else {
None
}
});
// Generate new device id if the user didn't specify one
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
// namespace
match body.appservice_info {
| Some(ref info) =>
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
return Err!(Request(Exclusive(
"Username is not in an appservice namespace."
)));
},
| _ => {
return Err!(Request(MissingToken("Missing appservice token.")));
},
}
} else if services.appservice.is_exclusive_user_id(&user_id).await && !emergency_mode_enabled
{
// For non-appservice logins, ban user IDs which are in an appservice's
// namespace (unless emergency mode is enabled)
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
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?;
// Set an initial display name
let mut displayname = user_id.localpart().to_owned();
// Apply the new user displayname suffix, if it's set
if !services.globals.new_user_displayname_suffix().is_empty()
&& body.appservice_info.is_none()
{
write!(displayname, " {}", services.server.config.new_user_displayname_suffix)?;
}
services
.users
.set_displayname(&user_id, Some(displayname.clone()));
// Initial account data
services
.account_data
.update(
None,
&user_id,
GlobalAccountDataEventType::PushRules.to_string().into(),
&serde_json::to_value(PushRulesEvent::new(
push::Ruleset::server_default(&user_id).into(),
))
.expect("should be able to serialize push rules"),
)
.await?;
// Generate new device id if the user didn't specify one
let (token, device) = if !body.inhibit_login {
let device_id = body
.device_id
.clone()
@@ -128,7 +181,6 @@ pub(crate) async fn register_route(
&user_id,
&device_id,
&new_token,
None,
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
@@ -139,7 +191,118 @@ pub(crate) async fn register_route(
(None, None)
};
debug_info!(%user_id, ?device, "New account created via legacy registration");
debug_info!(%user_id, ?device, "User account was created");
// If the user registered with an email, associate it with their account.
if let Some(identity) = identity
&& let Some(email) = identity.email
{
// This may fail if the email is already in use, but we already check for that
// in `/requestToken`, so ignoring the error is acceptable here in the rare case
// that an email is sniped by another user between the `/requestToken` request
// and the `/register` request.
let _ = services
.threepid
.associate_localpart_email(user_id.localpart(), &email)
.await;
}
let device_display_name = body.initial_device_display_name.as_deref().unwrap_or("");
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 \
display name \"{device_display_name}\""
);
info!("{notice}");
if services.server.config.admin_room_notices {
services.admin.notice(&notice).await;
}
} else {
let notice = format!("New user \"{user_id}\" registered on this server.");
info!("{notice}");
if services.server.config.admin_room_notices {
services.admin.notice(&notice).await;
}
}
}
// 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
.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();
}
}
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!(
"Failed to resolve room alias to room ID when attempting to auto join \
{room}, skipping"
);
continue;
};
if !services
.rooms
.state_cache
.server_in_room(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 services
.rooms
.membership
.join_room(
&user_id,
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
&[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}");
},
}
}
}
}
Ok(assign!(register::v3::Response::new(user_id), {
access_token: token,
@@ -211,21 +374,21 @@ async fn create_registration_uiaa_session(
// Require all users to agree to the terms and conditions, if configured
let terms = &services.config.registration_terms;
if !terms.documents.is_empty() {
let mut terms_map = HashMap::new();
if !terms.is_empty() {
let mut terms =
serde_json::to_value(terms.clone()).expect("failed to serialize terms");
for (id, document) in &terms.documents {
terms_map.insert(id.to_owned(), serde_json::json!({
terms.language.clone(): serde_json::to_value(document).expect("should be able to serialize document")
}));
// Insert a dummy `version` field
for (_, documents) in terms.as_object_mut().unwrap() {
let documents = documents.as_object_mut().unwrap();
documents.insert("version".to_owned(), "latest".into());
}
terms_map.insert("version".to_owned(), "latest".into());
params.insert(
AuthType::Terms.as_str().to_owned(),
serde_json::json!({
"policies": terms_map,
"policies": terms,
}),
);
@@ -258,6 +421,81 @@ async fn create_registration_uiaa_session(
Ok((flows, params))
}
async fn determine_registration_user_id(
services: &Services,
supplied_username: Option<String>,
emergency_mode_enabled: bool,
) -> Result<OwnedUserId> {
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 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,
services.globals.server_name(),
) {
| Ok(user_id) => {
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 !services.globals.user_is_local(&user_id) {
return Err!(Request(InvalidUsername(
"Username {supplied_username} is not local to this server"
)));
}
user_id
},
| Err(e) => {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {supplied_username} is not valid: {e}"
))));
},
};
if services.users.exists(&user_id).await {
return Err!(Request(UserInUse("User ID is not available.")));
}
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(),
services.globals.server_name(),
)
.unwrap();
if !services.users.exists(&user_id).await {
break Ok(user_id);
}
}
}
}
/// # `POST /_matrix/client/v3/register/email/requestToken`
///
/// Requests a validation email for the purpose of registering a new account.
+13 -23
View File
@@ -11,9 +11,9 @@ use ruma::{
},
thirdparty::{Medium, ThirdPartyIdentifierInit},
};
use service::mailer::messages;
use service::{mailer::messages, uiaa::Identity};
use crate::{Ruma, router::ClientIdentity};
use crate::Ruma;
/// # `GET _matrix/client/v3/account/3pid`
///
@@ -22,7 +22,7 @@ pub(crate) async fn third_party_route(
State(services): State<crate::State>,
body: Ruma<get_3pids::v3::Request>,
) -> Result<get_3pids::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let mut threepids = vec![];
if let Some(email) = services
@@ -53,14 +53,6 @@ pub(crate) async fn request_3pid_management_token_via_email_route(
State(services): State<crate::State>,
body: Ruma<request_3pid_management_token_via_email::v3::Request>,
) -> Result<request_3pid_management_token_via_email::v3::Response> {
// Authentication for this endpoint is technically optional,
// but we require the user to be logged in
let sender_user = body
.identity
.as_ref()
.map(ClientIdentity::sender_user)
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
if !services.threepid.email_requirement().may_change() {
return Err!(Request(Forbidden("You may not change your email address.")));
}
@@ -84,7 +76,7 @@ pub(crate) async fn request_3pid_management_token_via_email_route(
Mailbox::new(None, email),
|verification_link| messages::ChangeEmail {
server_name: services.config.server_name.as_str(),
user_id: Some(sender_user),
user_id: body.sender_user.as_deref(),
verification_link,
},
&body.client_secret,
@@ -115,6 +107,8 @@ pub(crate) async fn add_3pid_route(
State(services): State<crate::State>,
body: Ruma<add_3pid::v3::Request>,
) -> Result<add_3pid::v3::Response> {
let sender_user = body.sender_user();
if !services.threepid.email_requirement().may_change() {
return Err!(Request(Forbidden("You may not change your email address.")));
}
@@ -122,24 +116,18 @@ pub(crate) async fn add_3pid_route(
// Require password auth to add an email
let _ = services
.uiaa
.authenticate_password(
&body.auth,
body.identity.sender_user(),
body.identity.sender_device(),
None,
)
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
let email = services
.threepid
.get_valid_session(&body.sid, &body.client_secret)
.consume_valid_session(&body.sid, &body.client_secret)
.await
.map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?
.consume();
.map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?;
services
.threepid
.associate_localpart_email(body.identity.sender_user().localpart(), &email)
.associate_localpart_email(sender_user.localpart(), &email)
.await?;
Ok(add_3pid::v3::Response::new())
@@ -150,6 +138,8 @@ pub(crate) async fn delete_3pid_route(
State(services): State<crate::State>,
body: Ruma<delete_3pid::v3::Request>,
) -> Result<delete_3pid::v3::Response> {
let sender_user = body.sender_user();
if body.medium != Medium::Email {
return Ok(delete_3pid::v3::Response::new(ThirdPartyIdRemovalStatus::NoSupport));
}
@@ -160,7 +150,7 @@ pub(crate) async fn delete_3pid_route(
if services
.threepid
.disassociate_localpart_email(body.identity.sender_user().localpart())
.disassociate_localpart_email(sender_user.localpart())
.await
.is_none()
{
+8 -8
View File
@@ -22,9 +22,9 @@ pub(crate) async fn set_global_account_data_route(
State(services): State<crate::State>,
body: Ruma<set_global_account_data::v3::Request>,
) -> Result<set_global_account_data::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if sender_user != body.user_id && !body.identity.is_appservice() {
if sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot set account data for other users.")));
}
@@ -47,9 +47,9 @@ pub(crate) async fn set_room_account_data_route(
State(services): State<crate::State>,
body: Ruma<set_room_account_data::v3::Request>,
) -> Result<set_room_account_data::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if sender_user != body.user_id && !body.identity.is_appservice() {
if sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot set account data for other users.")));
}
@@ -72,9 +72,9 @@ pub(crate) async fn get_global_account_data_route(
State(services): State<crate::State>,
body: Ruma<get_global_account_data::v3::Request>,
) -> Result<get_global_account_data::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if sender_user != body.user_id && !body.identity.is_appservice() {
if sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot get account data of other users.")));
}
@@ -94,9 +94,9 @@ pub(crate) async fn get_room_account_data_route(
State(services): State<crate::State>,
body: Ruma<get_room_account_data::v3::Request>,
) -> Result<get_room_account_data::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if sender_user != body.user_id && !body.identity.is_appservice() {
if sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot get account data of other users.")));
}
-89
View File
@@ -1,89 +0,0 @@
use axum::extract::State;
use conduwuit::Err;
use futures::future::{join, join3};
use ruma::api::client::admin::{is_user_locked, lock_user};
use crate::router::Ruma;
/// # `GET /_matrix/client/v1/admin/lock/{userId}`
///
/// Check the account lock status of a target user
pub(crate) async fn get_locked_status(
State(services): State<crate::State>,
body: Ruma<is_user_locked::v1::Request>,
) -> conduwuit::Result<is_user_locked::v1::Response> {
let sender_user = body.sender_user();
let (admin, active) =
join(services.users.is_admin(sender_user), services.users.is_active(&body.user_id)).await;
if !admin {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
if !services.globals.user_is_local(&body.user_id) {
return Err!(Request(InvalidParam("Can only check the lock status of local users")));
}
if !active {
return Err!(Request(NotFound("Unknown user")));
}
Ok(is_user_locked::v1::Response::new(
services.users.is_locked(&body.user_id).await?,
))
}
/// # `PUT /_matrix/client/v1/admin/lock/{userId}`
///
/// Set the account lock status of a target user
pub(crate) async fn put_locked_status(
State(services): State<crate::State>,
body: Ruma<lock_user::v1::Request>,
) -> conduwuit::Result<lock_user::v1::Response> {
let sender_user = body.sender_user();
let (sender_admin, active, target_admin) = join3(
services.users.is_admin(sender_user),
services.users.is_active(&body.user_id),
services.users.is_admin(&body.user_id),
)
.await;
if !sender_admin {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
if !services.globals.user_is_local(&body.user_id) {
return Err!(Request(InvalidParam("Can only set the locked status of local users")));
}
if !active {
return Err!(Request(NotFound("Unknown user")));
}
if body.user_id == *sender_user {
return Err!(Request(Forbidden("You cannot lock yourself")));
}
if target_admin {
return Err!(Request(Forbidden("You cannot lock another server administrator")));
}
if services.users.is_locked(&body.user_id).await? == body.locked {
// No change
return Ok(lock_user::v1::Response::new(body.locked));
}
let action = if body.locked {
services
.users
.lock_account(&body.user_id, sender_user)
.await;
"suspended"
} else {
services.users.unlock_account(&body.user_id).await;
"unsuspended"
};
if services.config.admin_room_notices {
// Notify the admin room that an account has been un/suspended
services
.admin
.send_text(&format!("{} has been {} by {}.", body.user_id, action, sender_user))
.await;
}
Ok(lock_user::v1::Response::new(body.locked))
}
+1 -3
View File
@@ -1,5 +1,3 @@
mod lock;
pub(crate) mod site;
mod suspend;
pub(crate) use self::{lock::*, suspend::*};
pub(crate) use self::suspend::*;
-2
View File
@@ -1,2 +0,0 @@
pub(crate) mod rooms;
pub(crate) mod users;
-188
View File
@@ -1,188 +0,0 @@
use axum::extract::State;
use conduwuit::{
Err, Event, Result,
utils::stream::{BroadbandExt, WidebandExt},
};
use futures::StreamExt;
use ruma::{
OwnedRoomId,
events::{
StateEventType,
room::{
create::RoomCreateEventContent,
encryption::PossiblyRedactedRoomEncryptionEventContent,
tombstone::PossiblyRedactedRoomTombstoneEventContent,
},
},
};
use ruminuwuity::admin::continuwuity::rooms;
use tokio::join;
use crate::Ruma;
/// # `GET /_continuwuity/admin/rooms`
///
/// Lists all room IDs known to this server, excluding banned ones.
///
/// This is the legacy version of the endpoint, which does not support
/// pagination or including banned rooms. It is recommended to use the
/// `/v1/rooms` endpoint instead. This endpoint may be removed in a future
/// release.
pub(crate) async fn legacy_list_rooms_route(
State(services): State<crate::State>,
body: Ruma<rooms::list::unstable::Request>,
) -> Result<rooms::list::unstable::Response> {
let sender_user = body.identity.sender_user();
if !services.users.is_admin(sender_user).await {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
let mut rooms: Vec<OwnedRoomId> = services
.rooms
.metadata
.iter_ids()
.filter_map(|room_id| async move {
if !services.rooms.metadata.is_banned(&room_id).await {
Some(room_id.clone())
} else {
None
}
})
.collect()
.await;
rooms.sort();
Ok(rooms::list::unstable::Response::new(rooms))
}
/// # `GET /_continuwuity/admin/v1/rooms`
///
/// Lists rooms known to this server.
pub(crate) async fn list_rooms_route(
State(services): State<crate::State>,
body: Ruma<rooms::list::v1::Request>,
) -> Result<rooms::list::v1::Response> {
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
let include_banned_rooms = body.include_banned_rooms;
let rooms = services
.rooms
.metadata
.iter_ids()
.wide_filter_map(|room_id| async move {
if include_banned_rooms || !services.rooms.metadata.is_banned(&room_id).await {
Some(room_id.clone())
} else {
None
}
})
.skip(body.offset.unwrap_or_default())
.take(body.limit.unwrap_or(100).min(100))
.broad_filter_map(|room_id| async move {
let (
banned,
disabled,
member_count,
local_member_count,
resident_server_count,
published,
create_event,
encryption_event,
name_event,
topic_event,
canonical_alias_event,
join_rules_event,
history_visibility_event,
tombstone_event,
) = join!(
services.rooms.metadata.is_banned(&room_id),
services.rooms.metadata.is_disabled(&room_id),
services.rooms.state_cache.room_joined_count(&room_id),
services
.rooms
.state_cache
.active_local_users_in_room(&room_id)
.count(),
services.rooms.state_cache.room_servers(&room_id).count(),
services.rooms.directory.is_public_room(&room_id),
services.rooms.state_accessor.room_state_get(
&room_id,
&StateEventType::RoomCreate,
""
),
services
.rooms
.state_accessor
.room_state_get_content::<PossiblyRedactedRoomEncryptionEventContent>(
&room_id,
&StateEventType::RoomEncryption,
""
),
services.rooms.state_accessor.room_state_get_content(
&room_id,
&StateEventType::RoomName,
""
),
services.rooms.state_accessor.room_state_get_content(
&room_id,
&StateEventType::RoomTopic,
""
),
services.rooms.state_accessor.room_state_get_content(
&room_id,
&StateEventType::RoomCanonicalAlias,
""
),
services.rooms.state_accessor.room_state_get_content(
&room_id,
&StateEventType::RoomJoinRules,
""
),
services.rooms.state_accessor.room_state_get_content(
&room_id,
&StateEventType::RoomHistoryVisibility,
""
),
services
.rooms
.state_accessor
.room_state_get_content::<PossiblyRedactedRoomTombstoneEventContent>(
&room_id,
&StateEventType::RoomTombstone,
""
),
);
let Ok(create_event) = create_event else {
return None;
};
let create_content = create_event
.get_content::<RoomCreateEventContent>()
.expect("m.room.create content must be valid");
Some(rooms::list::v1::MinimalRoomInfo {
room_id,
banned,
disabled,
member_count: usize::try_from(member_count.unwrap_or_default())
.expect("u64 should fit in usize"),
local_member_count,
resident_server_count,
creators: vec![create_event.sender],
encrypted: encryption_event.is_ok_and(|c| c.algorithm.is_some()),
federated: create_content.federate,
published,
version: create_content.room_version,
name: name_event.unwrap_or(None),
topic: topic_event.unwrap_or(None),
canonical_alias: canonical_alias_event.unwrap_or(None),
join_rules: join_rules_event.unwrap_or(None),
history_visibility: history_visibility_event.unwrap_or(None),
predecessor: create_content.predecessor.map(|c| c.room_id),
successor: tombstone_event.map_or(None, |c| c.replacement_room),
})
})
.collect()
.await;
Ok(rooms::list::v1::Response::new(rooms))
}
-5
View File
@@ -1,5 +0,0 @@
mod ban;
mod list;
pub(crate) use ban::ban_room;
pub(crate) use list::*;
-119
View File
@@ -1,119 +0,0 @@
use axum::extract::State;
use conduwuit::{
Err, err, error, info,
utils::{IterStream, stream::BroadbandExt},
warn,
};
use futures::{FutureExt, StreamExt};
use ruma::UserId;
use ruminuwuity::admin::continuwuity::users;
use service::users::HashedPassword;
use crate::router::Ruma;
/// # `POST /_continuwuity/admin/v1/users/create`
///
/// Creates a new user.
pub(crate) async fn create_user_route(
State(services): State<crate::State>,
body: Ruma<users::create::v1::Request>,
) -> conduwuit::Result<users::create::v1::Response> {
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
let user_id =
&UserId::parse_with_server_name(&body.localpart, services.globals.server_name())?;
if services.users.is_active_local(user_id).await {
return Err!(Conflict("A user with this username already exists"));
}
services
.users
.create_local_account(
user_id,
HashedPassword::new(&body.password)?,
body.email
.clone()
.map(lettre::Address::try_from)
.transpose()
.map_err(|e| err!(Request(BadJson("Invalid email address: {e}"))))?,
)
.await;
if body.suspended {
services.users.suspend_account(user_id, sender_user).await;
}
if body.locked {
services.users.lock_account(user_id, sender_user).await;
}
if body.login_disabled {
services.users.disable_login(user_id);
}
if let Some(ref value) = body.display_name {
services.users.set_profile_key(
user_id,
"displayname",
Some(serde_json::to_value(value)?),
);
}
if let Some(ref value) = body.avatar_url {
services
.users
.set_profile_key(user_id, "avatar_url", Some(serde_json::to_value(value)?));
}
if body.admin {
services
.admin
.make_user_admin(user_id)
.await
.inspect_err(|e| error!("failed to make new user {user_id} an admin: {e}"))
.ok();
}
if !body.skip_auto_join {
services.users.join_auto_join_rooms(user_id).await;
}
body.auto_join_rooms
.clone()
.into_iter()
.stream()
.broad_filter_map(|room| async move {
services
.rooms
.alias
.resolve_with_servers(&room, None)
.await
.inspect_err(|e| {
warn!(
"Failed to resolve room alias to room ID when attempting to auto join \
{room}: {e}"
);
})
.ok()
})
.for_each_concurrent(None, |(room_id, servers)| async move {
match services
.rooms
.membership
.join_room(
user_id,
&room_id,
Some("Automatically joining this room upon registration".to_owned()),
servers.as_ref(),
)
.boxed()
.await
{
| Err(e) => {
warn!("Failed to automatically join {user_id} to {room_id}: {e}");
},
| _ => {
info!("Automatically joined room {user_id} to {room_id}");
},
}
})
.await;
Ok(users::create::v1::Response::new(user_id.to_owned()))
}
-48
View File
@@ -1,48 +0,0 @@
use axum::extract::State;
use conduwuit::{Err, utils::stream::WidebandExt};
use futures::StreamExt;
use ruminuwuity::admin::continuwuity::users;
use tokio::join;
use crate::router::Ruma;
/// # `GET /_continuwuity/admin/v1/users`
///
/// Lists all users on this homeserver.
pub(crate) async fn list_users_route(
State(services): State<crate::State>,
body: Ruma<users::list::v1::Request>,
) -> conduwuit::Result<users::list::v1::Response> {
let sender_user = body.sender_user();
if !services.users.is_admin(sender_user).await {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
let users = services
.users
.list_local_users()
.skip(body.offset.unwrap_or_default())
.take(body.limit.unwrap_or(100).min(100))
.wide_filter_map(|user_id| async move {
let (deactivated, suspended, locked, admin, login_disabled) = join!(
services.users.is_deactivated(&user_id),
services.users.is_suspended(&user_id),
services.users.is_locked(&user_id),
services.users.is_admin(&user_id),
services.users.is_login_disabled(&user_id),
);
Some(users::list::v1::User {
user_id: user_id.clone(),
deactivated: deactivated.unwrap_or_default(),
suspended: suspended.unwrap_or_default(),
locked: locked.unwrap_or_default(),
admin,
login_disabled,
})
})
.collect()
.await;
Ok(users::list::v1::Response::new(users))
}
-5
View File
@@ -1,5 +0,0 @@
mod create;
mod list;
pub(crate) use create::*;
pub(crate) use list::*;
+13 -14
View File
@@ -1,7 +1,7 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use futures::future::{join, join3};
use ruma::api::client::admin::{is_user_suspended, suspend_user};
use ruminuwuity::admin::{get_suspended, set_suspended};
use crate::Ruma;
@@ -10,13 +10,12 @@ use crate::Ruma;
/// Check the suspension status of a target user
pub(crate) async fn get_suspended_status(
State(services): State<crate::State>,
body: Ruma<is_user_suspended::v1::Request>,
) -> Result<is_user_suspended::v1::Response> {
let (admin, active) = join(
services.users.is_admin(body.identity.sender_user()),
services.users.is_active(&body.user_id),
)
.await;
body: Ruma<get_suspended::v1::Request>,
) -> Result<get_suspended::v1::Response> {
let sender_user = body.sender_user();
let (admin, active) =
join(services.users.is_admin(sender_user), services.users.is_active(&body.user_id)).await;
if !admin {
return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
}
@@ -26,7 +25,7 @@ pub(crate) async fn get_suspended_status(
if !active {
return Err!(Request(NotFound("Unknown user")));
}
Ok(is_user_suspended::v1::Response::new(
Ok(get_suspended::v1::Response::new(
services.users.is_suspended(&body.user_id).await?,
))
}
@@ -36,9 +35,9 @@ pub(crate) async fn get_suspended_status(
/// Set the suspension status of a target user
pub(crate) async fn put_suspended_status(
State(services): State<crate::State>,
body: Ruma<suspend_user::v1::Request>,
) -> Result<suspend_user::v1::Response> {
let sender_user = body.identity.sender_user();
body: Ruma<set_suspended::v1::Request>,
) -> Result<set_suspended::v1::Response> {
let sender_user = body.sender_user();
let (sender_admin, active, target_admin) = join3(
services.users.is_admin(sender_user),
@@ -64,7 +63,7 @@ pub(crate) async fn put_suspended_status(
}
if services.users.is_suspended(&body.user_id).await? == body.suspended {
// No change
return Ok(suspend_user::v1::Response::new(body.suspended));
return Ok(set_suspended::v1::Response::new(body.suspended));
}
let action = if body.suspended {
@@ -86,5 +85,5 @@ pub(crate) async fn put_suspended_status(
.await;
}
Ok(suspend_user::v1::Response::new(body.suspended))
Ok(set_suspended::v1::Response::new(body.suspended))
}
+4 -4
View File
@@ -11,7 +11,7 @@ pub(crate) async fn create_alias_route(
State(services): State<crate::State>,
body: Ruma<create_alias::v3::Request>,
) -> Result<create_alias::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
@@ -19,7 +19,7 @@ pub(crate) async fn create_alias_route(
services
.rooms
.alias
.appservice_checks(&body.room_alias, body.identity.appservice_info())
.appservice_checks(&body.room_alias, &body.appservice_info)
.await?;
// this isn't apart of alias_checks or delete alias route because we should
@@ -59,7 +59,7 @@ pub(crate) async fn delete_alias_route(
State(services): State<crate::State>,
body: Ruma<delete_alias::v3::Request>,
) -> Result<delete_alias::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
@@ -67,7 +67,7 @@ pub(crate) async fn delete_alias_route(
services
.rooms
.alias
.appservice_checks(&body.room_alias, body.identity.appservice_info())
.appservice_checks(&body.room_alias, &body.appservice_info)
.await?;
services
+4 -2
View File
@@ -1,5 +1,5 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use conduwuit::{Err, Result, err};
use ruma::{
api::{appservice::ping, client::appservice::request_ping},
assign,
@@ -15,7 +15,9 @@ pub(crate) async fn appservice_ping(
State(services): State<crate::State>,
body: Ruma<request_ping::v1::Request>,
) -> Result<request_ping::v1::Response> {
let appservice_info = &body.identity;
let appservice_info = body.appservice_info.as_ref().ok_or_else(|| {
err!(Request(Forbidden("This endpoint can only be called by appservices.")))
})?;
if body.appservice_id != appservice_info.registration.id {
return Err!(Request(Forbidden(
+26 -50
View File
@@ -25,7 +25,7 @@ pub(crate) async fn create_backup_version_route(
) -> Result<create_backup_version::v3::Response> {
let version = services
.key_backups
.create_backup(body.identity.sender_user(), &body.algorithm)?;
.create_backup(body.sender_user(), &body.algorithm)?;
Ok(create_backup_version::v3::Response::new(version))
}
@@ -40,7 +40,7 @@ pub(crate) async fn update_backup_version_route(
) -> Result<update_backup_version::v3::Response> {
services
.key_backups
.update_backup(body.identity.sender_user(), &body.version, &body.algorithm)
.update_backup(body.sender_user(), &body.version, &body.algorithm)
.await?;
Ok(update_backup_version::v3::Response::new())
@@ -55,11 +55,11 @@ pub(crate) async fn get_latest_backup_info_route(
) -> Result<get_latest_backup_info::v3::Response> {
let (version, algorithm) = services
.key_backups
.get_latest_backup(body.identity.sender_user())
.get_latest_backup(body.sender_user())
.await
.map_err(|_| err!(Request(NotFound("Key backup does not exist."))))?;
let (count, etag) = get_count_etag(&services, body.identity.sender_user(), &version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &version).await;
Ok(get_latest_backup_info::v3::Response::new(algorithm, count, etag, version))
}
@@ -73,14 +73,13 @@ pub(crate) async fn get_backup_info_route(
) -> Result<get_backup_info::v3::Response> {
let algorithm = services
.key_backups
.get_backup(body.identity.sender_user(), &body.version)
.get_backup(body.sender_user(), &body.version)
.await
.map_err(|_| {
err!(Request(NotFound("Key backup does not exist at version {:?}", body.version)))
})?;
let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(get_backup_info::v3::Response::new(algorithm, count, etag, body.version.clone()))
}
@@ -97,7 +96,7 @@ pub(crate) async fn delete_backup_version_route(
) -> Result<delete_backup_version::v3::Response> {
services
.key_backups
.delete_backup(body.identity.sender_user(), &body.version)
.delete_backup(body.sender_user(), &body.version)
.await;
Ok(delete_backup_version::v3::Response::new())
@@ -117,7 +116,7 @@ pub(crate) async fn add_backup_keys_route(
) -> Result<add_backup_keys::v3::Response> {
if services
.key_backups
.get_latest_backup_version(body.identity.sender_user())
.get_latest_backup_version(body.sender_user())
.await
.is_ok_and(|version| version != body.version)
{
@@ -130,19 +129,12 @@ pub(crate) async fn add_backup_keys_route(
for (session_id, key_data) in &room.sessions {
services
.key_backups
.add_key(
body.identity.sender_user(),
&body.version,
room_id,
session_id,
key_data,
)
.add_key(body.sender_user(), &body.version, room_id, session_id, key_data)
.await?;
}
}
let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(add_backup_keys::v3::Response::new(etag, count))
}
@@ -161,7 +153,7 @@ pub(crate) async fn add_backup_keys_for_room_route(
) -> Result<add_backup_keys_for_room::v3::Response> {
if services
.key_backups
.get_latest_backup_version(body.identity.sender_user())
.get_latest_backup_version(body.sender_user())
.await
.is_ok_and(|version| version != body.version)
{
@@ -173,18 +165,11 @@ pub(crate) async fn add_backup_keys_for_room_route(
for (session_id, key_data) in &body.sessions {
services
.key_backups
.add_key(
body.identity.sender_user(),
&body.version,
&body.room_id,
session_id,
key_data,
)
.add_key(body.sender_user(), &body.version, &body.room_id, session_id, key_data)
.await?;
}
let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(add_backup_keys_for_room::v3::Response::new(etag, count))
}
@@ -203,7 +188,7 @@ pub(crate) async fn add_backup_keys_for_session_route(
) -> Result<add_backup_keys_for_session::v3::Response> {
if services
.key_backups
.get_latest_backup_version(body.identity.sender_user())
.get_latest_backup_version(body.sender_user())
.await
.is_ok_and(|version| version != body.version)
{
@@ -216,7 +201,7 @@ pub(crate) async fn add_backup_keys_for_session_route(
let mut ok_to_replace = true;
if let Some(old_key) = &services
.key_backups
.get_session(body.identity.sender_user(), &body.version, &body.room_id, &body.session_id)
.get_session(body.sender_user(), &body.version, &body.room_id, &body.session_id)
.await
.ok()
{
@@ -275,7 +260,7 @@ pub(crate) async fn add_backup_keys_for_session_route(
services
.key_backups
.add_key(
body.identity.sender_user(),
body.sender_user(),
&body.version,
&body.room_id,
&body.session_id,
@@ -284,8 +269,7 @@ pub(crate) async fn add_backup_keys_for_session_route(
.await?;
}
let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(add_backup_keys_for_session::v3::Response::new(etag, count))
}
@@ -299,7 +283,7 @@ pub(crate) async fn get_backup_keys_route(
) -> Result<get_backup_keys::v3::Response> {
let rooms = services
.key_backups
.get_all(body.identity.sender_user(), &body.version)
.get_all(body.sender_user(), &body.version)
.await;
Ok(get_backup_keys::v3::Response::new(rooms))
@@ -314,7 +298,7 @@ pub(crate) async fn get_backup_keys_for_room_route(
) -> Result<get_backup_keys_for_room::v3::Response> {
let sessions = services
.key_backups
.get_room(body.identity.sender_user(), &body.version, &body.room_id)
.get_room(body.sender_user(), &body.version, &body.room_id)
.await;
Ok(get_backup_keys_for_room::v3::Response::new(sessions))
@@ -329,7 +313,7 @@ pub(crate) async fn get_backup_keys_for_session_route(
) -> Result<get_backup_keys_for_session::v3::Response> {
let key_data = services
.key_backups
.get_session(body.identity.sender_user(), &body.version, &body.room_id, &body.session_id)
.get_session(body.sender_user(), &body.version, &body.room_id, &body.session_id)
.await
.map_err(|_| {
err!(Request(NotFound(debug_error!("Backup key not found for this user's session."))))
@@ -347,11 +331,10 @@ pub(crate) async fn delete_backup_keys_route(
) -> Result<delete_backup_keys::v3::Response> {
services
.key_backups
.delete_all_keys(body.identity.sender_user(), &body.version)
.delete_all_keys(body.sender_user(), &body.version)
.await;
let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(delete_backup_keys::v3::Response::new(etag, count))
}
@@ -365,11 +348,10 @@ pub(crate) async fn delete_backup_keys_for_room_route(
) -> Result<delete_backup_keys_for_room::v3::Response> {
services
.key_backups
.delete_room_keys(body.identity.sender_user(), &body.version, &body.room_id)
.delete_room_keys(body.sender_user(), &body.version, &body.room_id)
.await;
let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(delete_backup_keys_for_room::v3::Response::new(etag, count))
}
@@ -383,16 +365,10 @@ pub(crate) async fn delete_backup_keys_for_session_route(
) -> Result<delete_backup_keys_for_session::v3::Response> {
services
.key_backups
.delete_room_key(
body.identity.sender_user(),
&body.version,
&body.room_id,
&body.session_id,
)
.delete_room_key(body.sender_user(), &body.version, &body.room_id, &body.session_id)
.await;
let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await;
Ok(delete_backup_keys_for_session::v3::Response::new(etag, count))
}
+5 -1
View File
@@ -48,7 +48,11 @@ pub(crate) async fn get_capabilities_route(
json!({"enabled": services.config.forget_forced_upon_leave}),
)?;
if services.users.is_admin(body.identity.sender_user()).await {
if services
.users
.is_admin(body.sender_user.as_ref().unwrap())
.await
{
// Advertise suspension API
capabilities.set("uk.timedout.msc4323", json!({"suspend": true, "lock": false}))?;
}
+3 -3
View File
@@ -37,8 +37,8 @@ pub(crate) async fn get_context_route(
State(services): State<crate::State>,
body: Ruma<get_context::v3::Request>,
) -> Result<get_context::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_device = body.identity.sender_device();
let sender = body.sender();
let (sender_user, sender_device) = sender;
let room_id = &body.room_id;
let event_id = &body.event_id;
let filter = &body.filter;
@@ -143,7 +143,7 @@ pub(crate) async fn get_context_route(
let lazy_loading_context = lazy_loading::Context {
user_id: sender_user,
device_id: sender_device,
device_id: Some(sender_device),
room_id,
token: Some(base_count.into_unsigned()),
options: Some(&filter.lazy_load_options),
+10 -5
View File
@@ -25,11 +25,16 @@ pub(crate) async fn put_dehydrated_device_route(
ClientIp(client): ClientIp,
body: Ruma<put_dehydrated_device::Request>,
) -> Result<put_dehydrated_device::Response> {
let device_id = body.device_id.clone();
let sender_user = body
.sender_user
.as_deref()
.expect("AccessToken authentication required");
let device_id = body.body.device_id.clone();
services
.users
.set_dehydrated_device(body.identity.sender_user(), body.body)
.set_dehydrated_device(sender_user, body.body)
.await?;
Ok(put_dehydrated_device::Response::new(device_id))
@@ -44,7 +49,7 @@ pub(crate) async fn delete_dehydrated_device_route(
ClientIp(client): ClientIp,
body: Ruma<delete_dehydrated_device::Request>,
) -> Result<delete_dehydrated_device::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let device_id = services.users.get_dehydrated_device_id(sender_user).await?;
@@ -62,7 +67,7 @@ pub(crate) async fn get_dehydrated_device_route(
ClientIp(client): ClientIp,
body: Ruma<get_dehydrated_device::Request>,
) -> Result<get_dehydrated_device::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let device = services.users.get_dehydrated_device(sender_user).await?;
@@ -78,7 +83,7 @@ pub(crate) async fn get_dehydrated_events_route(
ClientIp(client): ClientIp,
body: Ruma<get_events::Request>,
) -> Result<get_events::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let device_id = &body.body.device_id;
let existing_id = services.users.get_dehydrated_device_id(sender_user).await;
+13 -11
View File
@@ -8,6 +8,7 @@ use ruma::{
self, delete_device, delete_devices, get_device, get_devices, update_device,
},
};
use service::uiaa::Identity;
use crate::{Ruma, client::DEVICE_ID_LENGTH};
@@ -20,7 +21,7 @@ pub(crate) async fn get_devices_route(
) -> Result<get_devices::v3::Response> {
let devices: Vec<device::Device> = services
.users
.all_devices_metadata(body.identity.sender_user())
.all_devices_metadata(body.sender_user())
.collect()
.await;
@@ -36,7 +37,7 @@ pub(crate) async fn get_device_route(
) -> Result<get_device::v3::Response> {
let device = services
.users
.get_device_metadata(body.identity.sender_user(), &body.body.device_id)
.get_device_metadata(body.sender_user(), &body.body.device_id)
.await
.map_err(|_| err!(Request(NotFound("Device not found."))))?;
@@ -52,8 +53,8 @@ pub(crate) async fn update_device_route(
ClientIp(client): ClientIp,
body: Ruma<update_device::v3::Request>,
) -> Result<update_device::v3::Response> {
let sender_user = body.identity.sender_user();
let appservice = body.identity.appservice_info();
let sender_user = body.sender_user();
let appservice = body.appservice_info.as_ref();
match services
.users
@@ -94,7 +95,6 @@ pub(crate) async fn update_device_route(
&device_id,
&appservice.registration.as_token,
None,
None,
Some(client.to_string()),
)
.await?;
@@ -118,14 +118,15 @@ pub(crate) async fn delete_device_route(
State(services): State<crate::State>,
body: Ruma<delete_device::v3::Request>,
) -> Result<delete_device::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let appservice = body.appservice_info.as_ref();
// Appservices get to skip UIAA for this endpoint
if let Some(sender_device) = body.identity.sender_device() {
if appservice.is_none() {
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
.authenticate_password(&body.auth, sender_user, Some(sender_device), None)
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
}
@@ -153,14 +154,15 @@ pub(crate) async fn delete_devices_route(
State(services): State<crate::State>,
body: Ruma<delete_devices::v3::Request>,
) -> Result<delete_devices::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let appservice = body.appservice_info.as_ref();
// Appservices get to skip UIAA for this endpoint
if let Some(sender_device) = body.identity.sender_device() {
if appservice.is_none() {
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
.authenticate_password(&body.auth, sender_user, Some(sender_device), None)
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
}
+2 -2
View File
@@ -112,7 +112,7 @@ pub(crate) async fn set_room_visibility_route(
ClientIp(client): ClientIp,
body: Ruma<set_room_visibility::v3::Request>,
) -> Result<set_room_visibility::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if !services.rooms.metadata.exists(&body.room_id).await {
// Return 404 if the room doesn't exist
@@ -130,7 +130,7 @@ pub(crate) async fn set_room_visibility_route(
| room::Visibility::Public => {
if services.server.config.lockdown_public_room_directory
&& !services.users.is_admin(sender_user).await
&& !body.identity.is_appservice()
&& body.appservice_info.is_none()
{
info!(
"Non-admin user {sender_user} tried to publish {0} to the room directory \
+2 -2
View File
@@ -15,7 +15,7 @@ pub(crate) async fn get_filter_route(
) -> Result<get_filter::v3::Response> {
services
.users
.get_filter(body.identity.sender_user(), &body.filter_id)
.get_filter(body.sender_user(), &body.filter_id)
.await
.map(get_filter::v3::Response::new)
.map_err(|_| err!(Request(NotFound("Filter not found."))))
@@ -30,7 +30,7 @@ pub(crate) async fn create_filter_route(
) -> Result<create_filter::v3::Response> {
let filter_id = services
.users
.create_filter(body.identity.sender_user(), &body.filter);
.create_filter(body.sender_user(), &body.filter);
Ok(create_filter::v3::Response::new(filter_id))
}
+7 -13
View File
@@ -26,7 +26,7 @@ use ruma::{
serde::Raw,
};
use serde_json::json;
use service::oauth::OAuthTicket;
use service::uiaa::Identity;
use crate::Ruma;
@@ -41,8 +41,7 @@ pub(crate) async fn upload_keys_route(
State(services): State<crate::State>,
body: Ruma<upload_keys::v3::Request>,
) -> Result<upload_keys::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_device = body.identity.expect_sender_device()?;
let (sender_user, sender_device) = body.sender();
for (key_id, one_time_key) in &body.one_time_keys {
if one_time_key
@@ -155,7 +154,7 @@ pub(crate) async fn get_keys_route(
State(services): State<crate::State>,
body: Ruma<get_keys::v3::Request>,
) -> Result<get_keys::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
get_keys_helper(
&services,
@@ -192,7 +191,7 @@ pub(crate) async fn upload_signing_keys_route(
State(services): State<crate::State>,
body: Ruma<upload_signing_keys::v3::Request>,
) -> Result<upload_signing_keys::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if uiaa_needed_to_upload_keys(
services,
@@ -205,12 +204,7 @@ pub(crate) async fn upload_signing_keys_route(
{
let _ = services
.uiaa
.authenticate_password(
&body.auth,
sender_user,
body.identity.sender_device(),
Some(OAuthTicket::CrossSigningReset),
)
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
}
@@ -293,7 +287,7 @@ pub(crate) async fn upload_signatures_route(
return Ok(upload_signatures::v3::Response::new());
}
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
for (user_id, keys) in &body.signed_keys {
for (key_id, key) in keys {
@@ -346,7 +340,7 @@ pub(crate) async fn get_key_changes_route(
State(services): State<crate::State>,
body: Ruma<get_key_changes::v3::Request>,
) -> Result<get_key_changes::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let mut device_list_updates = HashSet::new();
+5 -5
View File
@@ -53,7 +53,7 @@ pub(crate) async fn create_content_route(
ClientIp(client): ClientIp,
body: Ruma<create_content::v3::Request>,
) -> Result<create_content::v3::Response> {
let user = body.identity.sender_user();
let user = body.sender_user();
if services.users.is_suspended(user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
@@ -92,7 +92,7 @@ pub(crate) async fn get_content_thumbnail_route(
ClientIp(client): ClientIp,
body: Ruma<get_content_thumbnail::v1::Request>,
) -> Result<get_content_thumbnail::v1::Response> {
let user = body.identity.sender_user();
let user = body.sender_user();
let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
let mxc = Mxc {
@@ -142,7 +142,7 @@ pub(crate) async fn get_content_route(
ClientIp(client): ClientIp,
body: Ruma<get_content::v1::Request>,
) -> Result<get_content::v1::Response> {
let user = body.identity.sender_user();
let user = body.sender_user();
let mxc = Mxc {
server_name: &body.server_name,
@@ -189,7 +189,7 @@ pub(crate) async fn get_content_as_filename_route(
ClientIp(client): ClientIp,
body: Ruma<get_content_as_filename::v1::Request>,
) -> Result<get_content_as_filename::v1::Response> {
let user = body.identity.sender_user();
let user = body.sender_user();
let mxc = Mxc {
server_name: &body.server_name,
@@ -240,7 +240,7 @@ pub(crate) async fn get_media_preview_route(
ClientIp(client): ClientIp,
body: Ruma<get_media_preview::v1::Request>,
) -> Result<get_media_preview::v1::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let url = &body.url;
let url = Url::parse(&body.url).map_err(|e| {
+1 -1
View File
@@ -56,7 +56,7 @@ pub(crate) async fn get_media_preview_legacy_route(
ClientIp(client): ClientIp,
body: Ruma<get_media_preview::v3::Request>,
) -> Result<get_media_preview::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let url = &body.url;
let url = Url::parse(&body.url).map_err(|e| {
+1 -1
View File
@@ -15,7 +15,7 @@ pub(crate) async fn ban_user_route(
State(services): State<crate::State>,
body: Ruma<ban_user::v3::Request>,
) -> Result<ban_user::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if sender_user == body.user_id {
return Err!(Request(Forbidden("You cannot ban yourself.")));
+1 -1
View File
@@ -18,7 +18,7 @@ pub(crate) async fn forget_room_route(
State(services): State<crate::State>,
body: Ruma<forget_room::v3::Request>,
) -> Result<forget_room::v3::Response> {
let user_id = body.identity.sender_user();
let user_id = body.sender_user();
let room_id = &body.room_id;
let joined = services.rooms.state_cache.is_joined(user_id, room_id);
+1 -1
View File
@@ -29,7 +29,7 @@ pub(crate) async fn invite_user_route(
ClientIp(client): ClientIp,
body: Ruma<invite_user::v3::Request>,
) -> Result<invite_user::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
+2 -2
View File
@@ -28,7 +28,7 @@ pub(crate) async fn join_room_by_id_route(
ClientIp(client): ClientIp,
body: Ruma<join_room_by_id::v3::Request>,
) -> Result<join_room_by_id::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
@@ -97,7 +97,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
ClientIp(client): ClientIp,
body: Ruma<join_room_by_id_or_alias::v3::Request>,
) -> Result<join_room_by_id_or_alias::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let body = &body.body;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
+1 -1
View File
@@ -15,7 +15,7 @@ pub(crate) async fn kick_user_route(
State(services): State<crate::State>,
body: Ruma<kick_user::v3::Request>,
) -> Result<kick_user::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
+1 -1
View File
@@ -51,7 +51,7 @@ pub(crate) async fn knock_room_route(
ClientIp(client): ClientIp,
body: Ruma<knock_room::v3::Request>,
) -> Result<knock_room::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let body = &body.body;
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
+1 -1
View File
@@ -32,7 +32,7 @@ pub(crate) async fn leave_room_route(
State(services): State<crate::State>,
body: Ruma<leave_room::v3::Request>,
) -> Result<leave_room::v3::Response> {
leave_room(&services, body.identity.sender_user(), &body.room_id, body.reason.clone())
leave_room(&services, body.sender_user(), &body.room_id, body.reason.clone())
.boxed()
.await
.map(|()| leave_room::v3::Response::new())
+2 -2
View File
@@ -30,7 +30,7 @@ pub(crate) async fn get_member_events_route(
State(services): State<crate::State>,
body: Ruma<get_member_events::v3::Request>,
) -> Result<get_member_events::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let membership = body.membership.as_ref();
let not_membership = body.not_membership.as_ref();
@@ -72,7 +72,7 @@ pub(crate) async fn joined_members_route(
if !services
.rooms
.state_accessor
.user_can_see_state_events(body.identity.sender_user(), &body.room_id)
.user_can_see_state_events(body.sender_user(), &body.room_id)
.await
{
return Err!(Request(Forbidden("You don't have permission to view this room.")));
+1 -1
View File
@@ -40,7 +40,7 @@ pub(crate) async fn joined_rooms_route(
let joined_rooms = services
.rooms
.state_cache
.rooms_joined(body.identity.sender_user())
.rooms_joined(body.sender_user())
.collect()
.await;
+1 -1
View File
@@ -14,7 +14,7 @@ pub(crate) async fn unban_user_route(
State(services): State<crate::State>,
body: Ruma<unban_user::v3::Request>,
) -> Result<unban_user::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
+15 -4
View File
@@ -23,7 +23,7 @@ use conduwuit_service::{
};
use futures::{FutureExt, StreamExt, TryFutureExt, future::OptionFuture, pin_mut};
use ruma::{
RoomId, UserId,
DeviceId, RoomId, UserId,
api::{
Direction,
client::{filter::RoomEventFilter, message::get_message_events},
@@ -37,6 +37,7 @@ use ruma::{
serde::Raw,
};
use ruminuwuity::invite_permission_config::FilterLevel;
use tracing::warn;
use crate::Ruma;
@@ -75,8 +76,8 @@ pub(crate) async fn get_message_events_route(
ClientIp(client_ip): ClientIp,
body: Ruma<get_message_events::v3::Request>,
) -> Result<get_message_events::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_device = body.identity.sender_device();
let sender_user = body.sender_user();
let sender_device = body.sender_device.as_deref();
let room_id = &body.room_id;
let filter = &body.filter;
@@ -157,7 +158,17 @@ pub(crate) async fn get_message_events_route(
let lazy_loading_context = lazy_loading::Context {
user_id: sender_user,
device_id: sender_device,
device_id: sender_device.or_else(|| {
if let Some(registration) = body.appservice_info.as_ref() {
Some(<&DeviceId>::from(registration.registration.id.as_str()))
} else {
warn!(
"No device_id provided and no appservice registration found, this should be \
unreachable"
);
None
}
}),
room_id,
token: Some(from.into_unsigned()),
options: Some(&filter.lazy_load_options),
-3
View File
@@ -16,7 +16,6 @@ pub(super) mod media_legacy;
pub(super) mod membership;
pub(super) mod message;
pub(super) mod mutual_rooms;
pub(super) mod oauth;
pub(super) mod openid;
pub(super) mod presence;
pub(super) mod profile;
@@ -62,7 +61,6 @@ pub(super) use membership::*;
pub use membership::{leave_all_rooms, leave_room, remote_leave_room};
pub(super) use message::*;
pub(super) use mutual_rooms::*;
pub(super) use oauth::*;
pub(super) use openid::*;
pub(super) use presence::*;
pub(super) use profile::*;
@@ -75,7 +73,6 @@ pub(super) use report::*;
pub(super) use room::*;
pub(super) use search::*;
pub(super) use send::*;
pub use session::handle_login;
pub(super) use session::*;
pub(super) use space::*;
pub(super) use state::*;
+1 -1
View File
@@ -15,7 +15,7 @@ pub(crate) async fn get_mutual_rooms_route(
State(services): State<crate::State>,
body: Ruma<mutual_rooms::unstable::Request>,
) -> Result<mutual_rooms::unstable::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if sender_user == body.user_id {
return Err!(Request(Unknown("You cannot request rooms in common with yourself.")));
-56
View File
@@ -1,56 +0,0 @@
use axum::{
Json, Router,
extract::{Request, State},
middleware::{self, Next},
response::{IntoResponse, Response},
routing::method_routing::{get, post},
};
use const_str::concat;
use http::StatusCode;
use serde_json::json;
pub(crate) use server_metadata::*;
mod register_client;
mod server_metadata;
mod token;
const BASE_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/oauth2/");
const AUTH_CODE_PATH: &str = "grant/authorization_code";
const JWKS_URI_PATH: &str = "client/keys.json";
const CLIENT_REGISTER_PATH: &str = "client/register";
const TOKEN_REVOKE_PATH: &str = "client/revoke";
const TOKEN_PATH: &str = "grant/token";
const ACCOUNT_MANAGEMENT_PATH: &str = concat!(conduwuit_core::ROUTE_PREFIX, "/account/deeplink");
pub(crate) fn router(state: crate::State) -> Router<crate::State> {
Router::new()
.nest(BASE_PATH, oauth_router())
.route(
"/.well-known/openid-configuration",
get(
// TODO(unspecced): used by old versions of the matrix-js-sdk
async |State(services): State<crate::State>| {
Json(authorization_server_metadata(&services).await)
},
),
)
.layer(middleware::from_fn_with_state(
state,
async |State(state): State<crate::State>, request: Request, next: Next| -> Response {
if state.config.oauth.compatibility_mode.oauth_available() {
next.run(request).await
} else {
(StatusCode::NOT_FOUND, "OAuth is unavailable on this server").into_response()
}
},
))
}
fn oauth_router() -> Router<crate::State> {
Router::new()
.route(concat!("/", CLIENT_REGISTER_PATH), post(register_client::register_client_route))
// TODO(unspecced): used by old versions of the matrix-js-sdk
.route(concat!("/", JWKS_URI_PATH), get(async || Json(json!({"keys": []}))))
.route(concat!("/", TOKEN_PATH), post(token::token_route))
.route(concat!("/", TOKEN_REVOKE_PATH), post(token::revoke_token_route))
}
-28
View File
@@ -1,28 +0,0 @@
use axum::{
Json,
extract::State,
response::{IntoResponse, Response},
};
use http::StatusCode;
use serde::Serialize;
use service::oauth::client_metadata::ClientMetadata;
#[derive(Serialize)]
struct RegisteredClient {
client_id: String,
#[serde(flatten)]
metadata: ClientMetadata,
}
pub(crate) async fn register_client_route(
State(services): State<crate::State>,
Json(metadata): Json<ClientMetadata>,
) -> Result<Response, Response> {
let client_id = services
.oauth
.register_client(&metadata)
.await
.map_err(|err| (StatusCode::BAD_REQUEST, err.to_owned()).into_response())?;
Ok(Json(RegisteredClient { client_id, metadata }).into_response())
}
-62
View File
@@ -1,62 +0,0 @@
use axum::extract::State;
use conduwuit::{Err, Result};
use ruma::{
api::client::discovery::get_authorization_server_metadata::{
self, v1::AccountManagementAction,
},
serde::Raw,
};
use serde_json::{Value, json};
use service::Services;
use crate::{
Ruma,
client::oauth::{
ACCOUNT_MANAGEMENT_PATH, AUTH_CODE_PATH, CLIENT_REGISTER_PATH, JWKS_URI_PATH, TOKEN_PATH,
TOKEN_REVOKE_PATH,
},
};
pub(crate) async fn get_authorization_server_metadata_route(
State(services): State<crate::State>,
_body: Ruma<get_authorization_server_metadata::v1::Request>,
) -> Result<get_authorization_server_metadata::v1::Response> {
if !services.config.oauth.compatibility_mode.oauth_available() {
return Err!(Request(Unrecognized("OAuth is unavailable on this server")));
}
let metadata = Raw::new(&authorization_server_metadata(&services).await).unwrap();
Ok(get_authorization_server_metadata::v1::Response::new(metadata.cast_unchecked()))
}
pub(crate) async fn authorization_server_metadata(services: &Services) -> Value {
let endpoint_base = services
.config
.get_client_domain()
.join(super::BASE_PATH)
.unwrap();
json!({
"account_management_uri": endpoint_base.join(ACCOUNT_MANAGEMENT_PATH).unwrap(),
"account_management_actions_supported": [
AccountManagementAction::AccountDeactivate,
AccountManagementAction::CrossSigningReset,
AccountManagementAction::DeviceDelete,
AccountManagementAction::DeviceView,
AccountManagementAction::DevicesList,
AccountManagementAction::Profile,
],
"authorization_endpoint": endpoint_base.join(AUTH_CODE_PATH).unwrap(),
"code_challenge_methods_supported": ["S256"],
"grant_types_supported": ["authorization_code", "refresh_token"],
"issuer": services.config.get_client_domain(),
"jwks_uri": endpoint_base.join(JWKS_URI_PATH).unwrap(),
"prompt_values_supported": ["create"],
"registration_endpoint": endpoint_base.join(CLIENT_REGISTER_PATH).unwrap(),
"response_modes_supported": ["query", "fragment"],
"response_types_supported": ["code"],
"revocation_endpoint": endpoint_base.join(TOKEN_REVOKE_PATH).unwrap(),
"token_endpoint": endpoint_base.join(TOKEN_PATH).unwrap(),
})
}
-23
View File
@@ -1,23 +0,0 @@
use axum::{Form, Json, extract::State, response::IntoResponse};
use http::StatusCode;
use service::oauth::grant::{RevokeTokenRequest, TokenRequest};
pub(crate) async fn token_route(
State(services): State<crate::State>,
Form(request): Form<TokenRequest>,
) -> impl IntoResponse {
match services.oauth.issue_token(request).await {
| Ok(response) => Ok(Json(response)),
| Err(err) => Err((StatusCode::BAD_REQUEST, err.message())),
}
}
pub(crate) async fn revoke_token_route(
State(services): State<crate::State>,
Form(request): Form<RevokeTokenRequest>,
) -> impl IntoResponse {
match services.oauth.revoke_token(request.token).await {
| Ok(()) => Ok(StatusCode::OK),
| Err(err) => Err((StatusCode::BAD_REQUEST, err.message())),
}
}
+1 -1
View File
@@ -16,7 +16,7 @@ pub(crate) async fn create_openid_token_route(
State(services): State<crate::State>,
body: Ruma<account::request_openid_token::v3::Request>,
) -> Result<account::request_openid_token::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if sender_user != body.user_id {
return Err!(Request(InvalidParam(
+3 -9
View File
@@ -20,19 +20,13 @@ pub(crate) async fn set_presence_route(
return Err!(Request(Forbidden("Presence is disabled on this server")));
}
if body.identity.sender_user() != body.user_id && !body.identity.is_appservice() {
if body.sender_user() != body.user_id && body.appservice_info.is_none() {
return Err!(Request(InvalidParam("Not allowed to set presence of other users")));
}
services
.presence
.set_presence(
body.identity.sender_user(),
&body.presence,
None,
None,
body.status_msg.clone(),
)
.set_presence(body.sender_user(), &body.presence, None, None, body.status_msg.clone())
.await?;
Ok(set_presence::v3::Response::new())
@@ -55,7 +49,7 @@ pub(crate) async fn get_presence_route(
let has_shared_rooms = services
.rooms
.state_cache
.user_sees_user(body.identity.sender_user(), &body.user_id)
.user_sees_user(body.sender_user(), &body.user_id)
.await;
if has_shared_rooms {
+129 -59
View File
@@ -8,12 +8,12 @@ use ruma::{
UserId,
api::{
client::profile::{
delete_profile_field, get_profile, get_profile_field, set_profile_field,
PropagateTo, delete_profile_field, get_profile, get_profile_field, set_profile_field,
},
federation,
},
assign,
events::room::member::{MembershipState, RoomMemberEventContent},
events::room::member::MembershipState,
presence::PresenceState,
profile::{ProfileFieldName, ProfileFieldValue},
};
@@ -51,12 +51,9 @@ pub(crate) async fn set_profile_field_route(
State(services): State<crate::State>,
body: Ruma<set_profile_field::v3::Request>,
) -> Result<set_profile_field::v3::Response> {
if body.user_id != body.identity.sender_user()
&& !(body.identity.is_appservice()
|| services
.admin
.user_is_admin(body.identity.sender_user())
.await)
if body.user_id != body.sender_user()
&& !(body.appservice_info.is_some()
|| services.admin.user_is_admin(body.sender_user()).await)
{
return Err!(Request(Forbidden("You may not change other users' profile data.")));
}
@@ -65,8 +62,13 @@ pub(crate) async fn set_profile_field_route(
return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
}
set_profile_field(&services, &body.user_id, ProfileFieldChange::Set(body.value.clone()))
.await?;
set_profile_field(
&services,
&body.user_id,
ProfileFieldChange::Set(body.value.clone()),
body.propagate_to.clone(),
)
.await?;
Ok(set_profile_field::v3::Response::new())
}
@@ -75,12 +77,9 @@ pub(crate) async fn delete_profile_field_route(
State(services): State<crate::State>,
body: Ruma<delete_profile_field::v3::Request>,
) -> Result<delete_profile_field::v3::Response> {
if body.user_id != body.identity.sender_user()
&& !(body.identity.is_appservice()
|| services
.admin
.user_is_admin(body.identity.sender_user())
.await)
if body.user_id != body.sender_user()
&& !(body.appservice_info.is_some()
|| services.admin.user_is_admin(body.sender_user()).await)
{
return Err!(Request(Forbidden("You may not change other users' profile data.")));
}
@@ -89,8 +88,13 @@ pub(crate) async fn delete_profile_field_route(
return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
}
set_profile_field(&services, &body.user_id, ProfileFieldChange::Delete(body.field.clone()))
.await?;
set_profile_field(
&services,
&body.user_id,
ProfileFieldChange::Delete(body.field.clone()),
body.propagate_to.clone(),
)
.await?;
Ok(delete_profile_field::v3::Response::new())
}
@@ -125,7 +129,13 @@ async fn fetch_full_profile(
continue;
};
let _ = set_profile_field(services, user_id, ProfileFieldChange::Set(value)).await;
let _ = set_profile_field(
services,
user_id,
ProfileFieldChange::Set(value),
PropagateTo::None,
)
.await;
}
Some(BTreeMap::from_iter(response))
@@ -159,8 +169,13 @@ async fn fetch_profile_field(
if let Some(value) = response.get(field.as_str()).map(ToOwned::to_owned) {
if let Ok(value) = ProfileFieldValue::new(field.as_str(), value) {
let _ = set_profile_field(services, user_id, ProfileFieldChange::Set(value.clone()))
.await;
let _ = set_profile_field(
services,
user_id,
ProfileFieldChange::Set(value.clone()),
PropagateTo::None,
)
.await;
Ok(Some(value))
} else {
@@ -169,7 +184,13 @@ async fn fetch_profile_field(
)))
}
} else {
let _ = set_profile_field(services, user_id, ProfileFieldChange::Delete(field)).await;
let _ = set_profile_field(
services,
user_id,
ProfileFieldChange::Delete(field),
PropagateTo::None,
)
.await;
Ok(None)
}
@@ -262,6 +283,7 @@ async fn set_profile_field(
services: &Services,
user_id: &UserId,
change: ProfileFieldChange,
propagate_to: PropagateTo,
) -> Result<()> {
const MAX_KEY_LENGTH_BYTES: usize = 255;
const MAX_PROFILE_LENGTH_BYTES: usize = 65536;
@@ -309,6 +331,91 @@ async fn set_profile_field(
}
}
// If the user is local and changed their displayname or avatar_url, update it
// in all their joined rooms. This is done before updating their profile data
// so we can check the old value of the field if `propagate_to` is `unchanged`.
if matches!(field_name, ProfileFieldName::AvatarUrl | ProfileFieldName::DisplayName)
&& matches!(propagate_to, PropagateTo::All | PropagateTo::Unchanged)
&& services.globals.user_is_local(user_id)
{
let current_displayname = services.users.displayname(user_id).await.ok();
let current_avatar_url = services.users.avatar_url(user_id).await.ok();
let mut all_joined_rooms = services.rooms.state_cache.rooms_joined(user_id);
while let Some(room_id) = all_joined_rooms.next().await {
// TODO: this clobbers any custom fields on the event content
let mut current_membership = services
.rooms
.state_accessor
.get_member(&room_id, user_id)
.await
.expect("should be able to fetch membership event for joined room");
assert_eq!(
current_membership.membership,
MembershipState::Join,
"user should be joined"
);
// If `propagate_to` is `unchanged`, and the current value of the field we're
// updating was changed from its global value in this room, skip it.
if matches!(propagate_to, PropagateTo::Unchanged) {
let field_changed_from_global = match field_name {
| ProfileFieldName::AvatarUrl =>
current_membership.avatar_url.as_ref() != current_avatar_url.as_ref(),
| ProfileFieldName::DisplayName =>
current_membership.displayname.as_ref() != current_displayname.as_ref(),
| _ => unreachable!(),
};
if field_changed_from_global {
continue;
}
}
let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await;
// Preserve keys in accordance with the key copying rules
current_membership.reason = None;
current_membership.join_authorized_via_users_server = None;
match &change {
| ProfileFieldChange::Set(ProfileFieldValue::AvatarUrl(avatar_url)) => {
current_membership.avatar_url = Some(avatar_url.clone());
},
| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
current_membership.displayname = Some(displayname.clone());
},
| ProfileFieldChange::Delete(ProfileFieldName::AvatarUrl) => {
current_membership.avatar_url = None;
},
| ProfileFieldChange::Delete(ProfileFieldName::DisplayName) => {
current_membership.displayname = None;
},
| _ => unreachable!(),
}
let _ = services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(user_id.to_string(), &current_membership),
user_id,
Some(&room_id),
&state_lock,
)
.await;
}
if services.config.allow_local_presence {
// Send a presence EDU to indicate the profile changed
let _ = services
.presence
.ping_presence(user_id, &PresenceState::Online)
.await;
}
}
match change {
| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
services
@@ -332,42 +439,5 @@ async fn set_profile_field(
.set_profile_key(user_id, other.field_name().as_str(), other.value()),
}
// If the user is local and changed their displayname or avatar_url, update it
// in all their joined rooms
if matches!(field_name, ProfileFieldName::AvatarUrl | ProfileFieldName::DisplayName)
&& services.globals.user_is_local(user_id)
{
let displayname = services.users.displayname(user_id).await.ok();
let avatar_url = services.users.avatar_url(user_id).await.ok();
let membership_content = assign!(
RoomMemberEventContent::new(MembershipState::Join), { displayname, avatar_url }
);
let mut all_joined_rooms = services.rooms.state_cache.rooms_joined(user_id);
while let Some(room_id) = all_joined_rooms.next().await {
let state_lock = services.rooms.state.mutex.lock(room_id.as_str()).await;
let _ = services
.rooms
.timeline
.build_and_append_pdu(
PartialPdu::state(user_id.to_string(), &membership_content),
user_id,
Some(&room_id),
&state_lock,
)
.await;
}
if services.config.allow_local_presence {
// Send a presence EDU to indicate the profile changed
let _ = services
.presence
.ping_presence(user_id, &PresenceState::Online)
.await;
}
}
Ok(())
}
+12 -13
View File
@@ -30,7 +30,7 @@ pub(crate) async fn get_pushrules_all_route(
State(services): State<crate::State>,
body: Ruma<get_pushrules_all::v3::Request>,
) -> Result<get_pushrules_all::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let Some(content_value) = services
.account_data
@@ -101,7 +101,7 @@ pub(crate) async fn get_pushrules_global_route(
State(services): State<crate::State>,
body: Ruma<get_pushrules_global_scope::v3::Request>,
) -> Result<get_pushrules_global_scope::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let Some(content_value) = services
.account_data
@@ -189,7 +189,7 @@ pub(crate) async fn get_pushrule_route(
State(services): State<crate::State>,
body: Ruma<get_pushrule::v3::Request>,
) -> Result<get_pushrule::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
// remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)]
@@ -226,7 +226,7 @@ pub(crate) async fn set_pushrule_route(
State(services): State<crate::State>,
body: Ruma<set_pushrule::v3::Request>,
) -> Result<set_pushrule::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let body = &body.body;
let mut account_data: PushRulesEvent = services
.account_data
@@ -282,7 +282,7 @@ pub(crate) async fn get_pushrule_actions_route(
State(services): State<crate::State>,
body: Ruma<get_pushrule_actions::v3::Request>,
) -> Result<get_pushrule_actions::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
// remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)]
@@ -316,7 +316,7 @@ pub(crate) async fn set_pushrule_actions_route(
State(services): State<crate::State>,
body: Ruma<set_pushrule_actions::v3::Request>,
) -> Result<set_pushrule_actions::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let mut account_data: PushRulesEvent = services
.account_data
@@ -349,7 +349,7 @@ pub(crate) async fn get_pushrule_enabled_route(
State(services): State<crate::State>,
body: Ruma<get_pushrule_enabled::v3::Request>,
) -> Result<get_pushrule_enabled::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
// remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)]
@@ -383,7 +383,7 @@ pub(crate) async fn set_pushrule_enabled_route(
State(services): State<crate::State>,
body: Ruma<set_pushrule_enabled::v3::Request>,
) -> Result<set_pushrule_enabled::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let mut account_data: PushRulesEvent = services
.account_data
@@ -416,7 +416,7 @@ pub(crate) async fn delete_pushrule_route(
State(services): State<crate::State>,
body: Ruma<delete_pushrule::v3::Request>,
) -> Result<delete_pushrule::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let mut account_data: PushRulesEvent = services
.account_data
@@ -458,7 +458,7 @@ pub(crate) async fn get_pushers_route(
State(services): State<crate::State>,
body: Ruma<get_pushers::v3::Request>,
) -> Result<get_pushers::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
Ok(get_pushers::v3::Response::new(services.pusher.get_pushers(sender_user).await))
}
@@ -472,12 +472,11 @@ pub(crate) async fn set_pushers_route(
State(services): State<crate::State>,
body: Ruma<set_pusher::v3::Request>,
) -> Result<set_pusher::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_device = body.identity.expect_sender_device()?;
let sender_user = body.sender_user();
services
.pusher
.set_pusher(sender_user, sender_device, &body.action)
.set_pusher(sender_user, body.sender_device(), &body.action)
.await?;
Ok(set_pusher::v3::Response::new())
+3 -3
View File
@@ -26,7 +26,7 @@ pub(crate) async fn set_read_marker_route(
State(services): State<crate::State>,
body: Ruma<set_read_marker::v3::Request>,
) -> Result<set_read_marker::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if let Some(event) = &body.fully_read {
let fully_read_event = FullyReadEvent::new(FullyReadEventContent::new(event.to_owned()));
@@ -118,10 +118,10 @@ pub(crate) async fn create_receipt_route(
ClientIp(client_ip): ClientIp,
body: Ruma<create_receipt::v3::Request>,
) -> Result<create_receipt::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
services
.users
.update_device_last_seen(sender_user, body.identity.sender_device(), client_ip)
.update_device_last_seen(sender_user, body.sender_device.as_deref(), client_ip)
.await;
if matches!(
+2 -2
View File
@@ -17,10 +17,10 @@ pub(crate) async fn redact_event_route(
ClientIp(client_ip): ClientIp,
body: Ruma<redact_event::v3::Request>,
) -> Result<redact_event::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
services
.users
.update_device_last_seen(sender_user, body.identity.sender_device(), client_ip)
.update_device_last_seen(sender_user, body.sender_device.as_deref(), client_ip)
.await;
let body = &body.body;
if services.users.is_suspended(sender_user).await? {
+3 -3
View File
@@ -28,7 +28,7 @@ pub(crate) async fn get_relating_events_with_rel_type_and_event_type_route(
) -> Result<get_relating_events_with_rel_type_and_event_type::v1::Response> {
paginate_relations_with_filter(
&services,
body.identity.sender_user(),
body.sender_user(),
&body.room_id,
&body.event_id,
body.event_type.clone().into(),
@@ -56,7 +56,7 @@ pub(crate) async fn get_relating_events_with_rel_type_route(
) -> Result<get_relating_events_with_rel_type::v1::Response> {
paginate_relations_with_filter(
&services,
body.identity.sender_user(),
body.sender_user(),
&body.room_id,
&body.event_id,
None,
@@ -84,7 +84,7 @@ pub(crate) async fn get_relating_events_route(
) -> Result<get_relating_events::v1::Response> {
paginate_relations_with_filter(
&services,
body.identity.sender_user(),
body.sender_user(),
&body.room_id,
&body.event_id,
None,
+4 -4
View File
@@ -36,7 +36,7 @@ pub(crate) async fn report_room_route(
ClientIp(client): ClientIp,
body: Ruma<report_room::v3::Request>,
) -> Result<report_room::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
@@ -92,7 +92,7 @@ pub(crate) async fn report_event_route(
body: Ruma<report_content::v3::Request>,
) -> Result<report_content::v3::Response> {
// user authentication
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
@@ -135,8 +135,8 @@ pub(crate) async fn report_user_route(
ClientIp(client): ClientIp,
body: Ruma<report_user::v3::Request>,
) -> Result<report_user::v3::Response> {
let sender_user = body.identity.sender_user();
// user authentication
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
+1 -1
View File
@@ -15,7 +15,7 @@ pub(crate) async fn get_room_aliases_route(
State(services): State<crate::State>,
body: Ruma<aliases::v3::Request>,
) -> Result<aliases::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if !services
.rooms
+4 -4
View File
@@ -61,10 +61,10 @@ pub(crate) async fn create_room_route(
) -> Result<create_room::v3::Response> {
use create_room::v3::RoomPreset;
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if !services.globals.allow_room_creation()
&& !body.identity.is_appservice()
&& body.appservice_info.is_none()
&& !services.users.is_admin(sender_user).await
{
return Err!(Request(Forbidden("Room creation has been disabled.",)));
@@ -130,7 +130,7 @@ pub(crate) async fn create_room_route(
if body.visibility == room::Visibility::Public
&& services.server.config.lockdown_public_room_directory
&& !services.users.is_admin(sender_user).await
&& !body.identity.is_appservice()
&& body.appservice_info.is_none()
{
warn!(
"Non-admin user {sender_user} tried to publish {room_id:?} to the room directory \
@@ -186,7 +186,7 @@ pub(crate) async fn create_room_route(
let alias: Option<OwnedRoomAliasId> = match body.room_alias_name.as_ref() {
| Some(alias) =>
Some(room_alias_check(&services, alias, body.identity.appservice_info()).await?),
Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?),
| _ => None,
};
+4 -4
View File
@@ -24,25 +24,25 @@ pub(crate) async fn get_room_event_route(
let visible = services
.rooms
.state_accessor
.user_can_see_event(body.identity.sender_user(), room_id, event_id)
.user_can_see_event(body.sender_user(), room_id, event_id)
.map(Ok);
let (mut event, visible) = try_join(event, visible).await?;
if !visible || is_ignored_pdu(services, &event, body.identity.sender_user()).await? {
if !visible || is_ignored_pdu(services, &event, body.sender_user()).await? {
return Err!(Request(Forbidden("You don't have permission to view this event.")));
}
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(body.identity.sender_user(), &mut event)
.add_bundled_aggregations_to_pdu(body.sender_user(), &mut event)
.await
{
debug_warn!("Failed to add bundled aggregations to event: {e}");
}
event.set_unsigned(Some(body.identity.sender_user()));
event.set_unsigned(body.sender_user.as_deref());
Ok(get_room_event::v3::Response::new(event.into_format()))
}
+12 -10
View File
@@ -22,7 +22,7 @@ pub(crate) async fn room_initial_sync_route(
if !services
.rooms
.state_accessor
.user_can_see_state_events(body.identity.sender_user(), room_id)
.user_can_see_state_events(body.sender_user(), room_id)
.await
{
return Err!(Request(Forbidden("No room preview available.")));
@@ -31,7 +31,7 @@ pub(crate) async fn room_initial_sync_route(
let membership = services
.rooms
.state_cache
.user_membership(body.identity.sender_user(), room_id)
.user_membership(body.sender_user(), room_id)
.map(Ok);
let visibility = services.rooms.directory.visibility(room_id).map(Ok);
@@ -52,14 +52,16 @@ pub(crate) async fn room_initial_sync_route(
.pdus_rev(room_id, None)
.try_take(limit)
.and_then(async |mut pdu| {
pdu.1.set_unsigned(Some(body.identity.sender_user()));
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(body.identity.sender_user(), &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
pdu.1.set_unsigned(body.sender_user.as_deref());
if let Some(sender_user) = body.sender_user.as_deref() {
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1)
.await
{
debug_warn!("Failed to add bundled aggregations: {e}");
}
}
Ok(pdu)
})
+2 -6
View File
@@ -4,7 +4,7 @@ use conduwuit::{Err, Result};
use ruma::api::client::room::get_summary;
use service::rooms::summary::Accessibility;
use crate::{Ruma, router::ClientIdentity};
use crate::Ruma;
/// # `GET /_matrix/client/v1/room_summary/{roomIdOrAlias}`
///
@@ -28,11 +28,7 @@ pub(crate) async fn get_room_summary(
let summary = services
.rooms
.summary
.get_room_summary_for_user(
body.identity.as_ref().map(ClientIdentity::sender_user),
&room_id,
&servers,
)
.get_room_summary_for_user(body.sender_user.as_deref(), &room_id, &servers)
.await?;
match summary {
+1 -1
View File
@@ -277,7 +277,7 @@ pub(crate) async fn upgrade_room_route(
State(services): State<crate::State>,
body: Ruma<upgrade_room::v3::Request>,
) -> Result<upgrade_room::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let (supported, forbid_unstable, is_unstable) = (
services.server.supported_room_version(&body.new_version),
+1 -1
View File
@@ -43,7 +43,7 @@ pub(crate) async fn search_events_route(
State(services): State<crate::State>,
body: Ruma<Request>,
) -> Result<Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let next_batch = body.next_batch.as_deref();
let mut result_categories = ResultCategories::new();
+5 -9
View File
@@ -22,16 +22,16 @@ pub(crate) async fn send_message_event_route(
ClientIp(client_ip): ClientIp,
body: Ruma<send_message_event::v3::Request>,
) -> Result<send_message_event::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_device = body.identity.sender_device();
let sender_user = body.sender_user();
let sender_device = body.sender_device.as_deref();
let appservice_info = body.appservice_info.as_ref();
if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
}
services
.users
.update_device_last_seen(sender_user, sender_device, client_ip)
.update_device_last_seen(sender_user, body.sender_device.as_deref(), client_ip)
.await;
// Forbid m.room.encrypted if encryption is disabled
@@ -83,11 +83,7 @@ pub(crate) async fn send_message_event_route(
event_type: body.event_type.clone().into(),
content,
unsigned: Some(unsigned),
timestamp: if body.identity.is_appservice() {
body.timestamp
} else {
None
},
timestamp: appservice_info.and(body.timestamp),
..Default::default()
},
sender_user,
+24 -37
View File
@@ -21,7 +21,7 @@ use ruma::{
},
login::{
self,
v3::{DiscoveryInfo, HomeserverInfo, LoginInfo},
v3::{DiscoveryInfo, HomeserverInfo},
},
logout, logout_all,
},
@@ -29,6 +29,7 @@ use ruma::{
},
assign,
};
use service::uiaa::Identity;
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::Ruma;
@@ -43,12 +44,6 @@ pub(crate) async fn get_login_types_route(
ClientIp(client): ClientIp,
_body: Ruma<get_login_types::v3::Request>,
) -> Result<get_login_types::v3::Response> {
if !services.config.oauth.compatibility_mode.uiaa_available() {
return Err!(Request(Unrecognized(
"User-interactive authentication is not available on this server."
)));
}
Ok(get_login_types::v3::Response::new(vec![
get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()),
@@ -58,7 +53,7 @@ pub(crate) async fn get_login_types_route(
]))
}
pub async fn handle_login(
pub(crate) async fn handle_login(
services: &Services,
identifier: Option<&UserIdentifier>,
password: &str,
@@ -92,6 +87,10 @@ pub async fn handle_login(
return Err!(Request(InvalidParam("User ID does not belong to this homeserver")));
}
if services.users.is_locked(&user_id).await? {
return Err!(Request(UserLocked("This account has been locked.")));
}
if services.users.is_login_disabled(&user_id).await {
warn!(%user_id, "user attempted to log in with a login-disabled account");
return Err!(Request(Forbidden("This account is not permitted to log in.")));
@@ -120,29 +119,19 @@ pub(crate) async fn login_route(
ClientIp(client): ClientIp,
body: Ruma<login::v3::Request>,
) -> Result<login::v3::Response> {
if !services.config.oauth.compatibility_mode.uiaa_available() {
return match body.login_info {
| LoginInfo::ApplicationService(_) => {
Err!(Request(AppserviceLoginUnsupported(
"User-interactive appservice login is not available on this server."
)))
},
| _ => {
Err!(Request(Unrecognized(
"User-interactive authentication is not available on this server."
)))
},
};
}
let emergency_mode_enabled = services.config.emergency_password.is_some();
// Validate login method
// TODO: Other login methods
let user_id = match &body.login_info {
#[allow(deprecated)]
| LoginInfo::Password(login::v3::Password { identifier, password, user, .. }) =>
handle_login(&services, identifier.as_ref(), password, user.as_ref()).await?,
| LoginInfo::Token(login::v3::Token { token, .. }) => {
| login::v3::LoginInfo::Password(login::v3::Password {
identifier,
password,
user,
..
}) => handle_login(&services, identifier.as_ref(), password, user.as_ref()).await?,
| login::v3::LoginInfo::Token(login::v3::Token { token, .. }) => {
debug!("Got token login type");
if !services.server.config.login_via_existing_session {
return Err!(Request(Unknown("Token login is not enabled.")));
@@ -150,14 +139,14 @@ pub(crate) async fn login_route(
services.users.find_from_login_token(token).await?
},
#[allow(deprecated)]
| LoginInfo::ApplicationService(login::v3::ApplicationService {
| login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService {
identifier,
user,
..
}) => {
debug!("Got appservice login type");
let Some(ref info) = body.identity else {
let Some(ref info) = body.appservice_info else {
return Err!(Request(MissingToken("Missing appservice token.")));
};
@@ -184,6 +173,7 @@ pub(crate) async fn login_route(
user_id
},
| _ => {
debug!("/login json_body: {:?}", &body.json_body);
return Err!(Request(Unknown(
debug_warn!(?body.login_info, "Invalid or unsupported login type")
)));
@@ -213,7 +203,7 @@ pub(crate) async fn login_route(
if device_exists {
services
.users
.set_token(&user_id, &device_id, &token, None)
.set_token(&user_id, &device_id, &token)
.await?;
} else {
services
@@ -222,7 +212,6 @@ pub(crate) async fn login_route(
&user_id,
&device_id,
&token,
None,
body.initial_device_display_name.clone(),
Some(client.to_string()),
)
@@ -261,16 +250,16 @@ pub(crate) async fn login_token_route(
ClientIp(client): ClientIp,
body: Ruma<get_login_token::v1::Request>,
) -> Result<get_login_token::v1::Response> {
if !services.config.login_via_existing_session {
if !services.server.config.login_via_existing_session {
return Err!(Request(Forbidden("Login via an existing session is not enabled")));
}
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
// Prompt the user to confirm with their password using UIAA
let _ = services
.uiaa
.authenticate_password(&body.auth, sender_user, body.identity.sender_device(), None)
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user)))
.await?;
let login_token = utils::random_string(TOKEN_LENGTH);
@@ -297,9 +286,7 @@ pub(crate) async fn logout_route(
ClientIp(client): ClientIp,
body: Ruma<logout::v3::Request>,
) -> Result<logout::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_device = body.identity.expect_sender_device()?;
let (sender_user, sender_device) = body.sender();
services
.users
.remove_device(sender_user, sender_device)
@@ -345,7 +332,7 @@ pub(crate) async fn logout_all_route(
ClientIp(client): ClientIp,
body: Ruma<logout_all::v3::Request>,
) -> Result<logout_all::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
services
.users
.all_device_ids(sender_user)
+1 -1
View File
@@ -27,7 +27,7 @@ pub(crate) async fn get_hierarchy_route(
.rooms
.summary
.get_room_hierarchy_for_user(
body.identity.sender_user(),
body.sender_user(),
body.room_id.clone(),
max_depth,
body.suggested_only,
+5 -5
View File
@@ -38,10 +38,10 @@ pub(crate) async fn send_state_event_for_key_route(
ClientIp(ip): ClientIp,
body: Ruma<send_state_event::v3::Request>,
) -> Result<send_state_event::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
services
.users
.update_device_last_seen(sender_user, body.identity.sender_device(), ip)
.update_device_last_seen(sender_user, body.sender_device.as_deref(), ip)
.await;
if services.users.is_suspended(sender_user).await? {
@@ -55,7 +55,7 @@ pub(crate) async fn send_state_event_for_key_route(
&body.event_type,
&body.body.body,
&body.state_key,
if body.identity.is_appservice() {
if body.appservice_info.is_some() {
body.timestamp
} else {
None
@@ -91,7 +91,7 @@ pub(crate) async fn get_state_events_route(
State(services): State<crate::State>,
body: Ruma<get_state_events::v3::Request>,
) -> Result<get_state_events::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if !services
.rooms
@@ -125,7 +125,7 @@ pub(crate) async fn get_state_event_for_key_route(
State(services): State<crate::State>,
body: Ruma<get_state_event_for_key::v3::Request>,
) -> Result<get_state_event_for_key::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
if !services
.rooms
+2 -4
View File
@@ -181,8 +181,7 @@ pub(crate) async fn sync_events_route(
ClientIp(client_ip): ClientIp,
body: Ruma<sync_events::v3::Request>,
) -> Result<sync_events::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_device = body.identity.expect_sender_device()?;
let (sender_user, sender_device) = body.sender();
// Presence update
if services.config.allow_local_presence {
@@ -226,8 +225,7 @@ pub(crate) async fn build_sync_events(
services: &Services,
body: &Ruma<sync_events::v3::Request>,
) -> Result<sync_events::v3::Response> {
let syncing_user = body.identity.sender_user();
let syncing_device = body.identity.sender_device().expect("should have a device");
let (syncing_user, syncing_device) = body.sender();
let current_count = services.globals.current_count()?;
+4 -3
View File
@@ -69,8 +69,9 @@ pub(crate) async fn sync_events_v5_route(
ClientIp(client_ip): ClientIp,
body: Ruma<sync_events::v5::Request>,
) -> Result<sync_events::v5::Response> {
let sender_user = body.identity.sender_user();
let sender_device = body.identity.expect_sender_device()?;
debug_assert!(DEFAULT_BUMP_TYPES.is_sorted(), "DEFAULT_BUMP_TYPES is not sorted");
let ref sender_user = body.sender_user().to_owned();
let ref sender_device = body.sender_device().to_owned();
services
.users
@@ -92,7 +93,7 @@ pub(crate) async fn sync_events_v5_route(
.and_then(|string| string.parse().ok())
.unwrap_or(0);
let snake_key = into_snake_key(sender_user, sender_device.as_str(), conn_id);
let snake_key = into_snake_key(sender_user.as_ref(), sender_device.as_str(), conn_id);
if globalsince != 0 && !services.sync.snake_connection_cached(&snake_key) {
return Err!(Request(UnknownPos(
+3 -3
View File
@@ -21,7 +21,7 @@ pub(crate) async fn update_tag_route(
State(services): State<crate::State>,
body: Ruma<create_tag::v3::Request>,
) -> Result<create_tag::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let mut tags_event = services
.account_data
@@ -56,7 +56,7 @@ pub(crate) async fn delete_tag_route(
State(services): State<crate::State>,
body: Ruma<delete_tag::v3::Request>,
) -> Result<delete_tag::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let mut tags_event = services
.account_data
@@ -88,7 +88,7 @@ pub(crate) async fn get_tags_route(
State(services): State<crate::State>,
body: Ruma<get_tags::v3::Request>,
) -> Result<get_tags::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let tags_event = services
.account_data
+3 -3
View File
@@ -34,14 +34,14 @@ pub(crate) async fn get_threads_route(
let threads: Vec<(PduCount, PduEvent)> = services
.rooms
.threads
.threads_until(body.identity.sender_user(), &body.room_id, from, &body.include)
.threads_until(body.sender_user(), &body.room_id, from, &body.include)
.await?
.take(limit)
.filter_map(|(count, pdu)| async move {
services
.rooms
.state_accessor
.user_can_see_event(body.identity.sender_user(), &body.room_id, &pdu.event_id)
.user_can_see_event(body.sender_user(), &body.room_id, &pdu.event_id)
.await
.then_some((count, pdu))
})
@@ -49,7 +49,7 @@ pub(crate) async fn get_threads_route(
if let Err(e) = services
.rooms
.pdu_metadata
.add_bundled_aggregations_to_pdu(body.identity.sender_user(), &mut pdu)
.add_bundled_aggregations_to_pdu(body.sender_user(), &mut pdu)
.await
{
debug_warn!("Failed to add bundled aggregations to thread: {e}");
+2 -2
View File
@@ -22,8 +22,8 @@ pub(crate) async fn send_event_to_device_route(
State(services): State<crate::State>,
body: Ruma<send_event_to_device::v3::Request>,
) -> Result<send_event_to_device::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_device = body.identity.sender_device();
let sender_user = body.sender_user();
let sender_device = body.sender_device.as_deref();
// Check if this is a new transaction id
if services
+3 -3
View File
@@ -14,13 +14,13 @@ pub(crate) async fn create_typing_event_route(
body: Ruma<create_typing_event::v3::Request>,
) -> Result<create_typing_event::v3::Response> {
use create_typing_event::v3::Typing;
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
services
.users
.update_device_last_seen(sender_user, body.identity.sender_device(), ip)
.update_device_last_seen(sender_user, body.sender_device.as_deref(), ip)
.await;
if sender_user != body.user_id && !body.identity.is_appservice() {
if sender_user != body.user_id && body.appservice_info.is_none() {
return Err!(Request(Forbidden("You cannot update typing status of other users.")));
}
+2 -2
View File
@@ -35,8 +35,8 @@ pub(crate) async fn get_supported_versions_route(
/// `/_matrix/federation/v1/version`
pub(crate) async fn conduwuit_server_version() -> Result<impl IntoResponse> {
Ok(Json(serde_json::json!({
"name": conduwuit::BRANDING,
"version": conduwuit::version(),
"name": conduwuit::version::name(),
"version": conduwuit::version::version(),
})))
}
+1 -1
View File
@@ -26,7 +26,7 @@ pub(crate) async fn search_users_route(
State(services): State<crate::State>,
body: Ruma<search_users::v3::Request>,
) -> Result<search_users::v3::Response> {
let sender_user = body.identity.sender_user();
let sender_user = body.sender_user();
let limit = usize::try_from(body.limit)
.map_or(LIMIT_DEFAULT, usize::from)
.min(LIMIT_MAX);
+13 -3
View File
@@ -2,13 +2,15 @@ use std::time::{Duration, SystemTime};
use axum::extract::State;
use base64::{Engine as _, engine::general_purpose};
use conduwuit::{Err, Result};
use conduwuit::{Err, Result, utils};
use hmac::{Hmac, KeyInit, Mac};
use ruma::{SecondsSinceUnixEpoch, api::client::voip::get_turn_server_info};
use ruma::{SecondsSinceUnixEpoch, UserId, api::client::voip::get_turn_server_info};
use sha1::Sha1;
use crate::Ruma;
const RANDOM_USER_ID_LENGTH: usize = 10;
type HmacSha1 = Hmac<Sha1>;
/// # `GET /_matrix/client/r0/voip/turnServer`
@@ -33,7 +35,15 @@ pub(crate) async fn turn_server_route(
)
.expect("time is valid");
let username: String = format!("{}:{}", expiry.get(), body.identity.sender_user());
let user = body.sender_user.unwrap_or_else(|| {
UserId::parse_with_server_name(
utils::random_string(RANDOM_USER_ID_LENGTH).to_lowercase(),
&services.server.name,
)
.unwrap()
});
let username: String = format!("{}:{}", expiry.get(), user);
let mut mac = HmacSha1::new_from_slice(turn_secret.as_bytes())
.expect("HMAC can take key of any size");
+42 -2
View File
@@ -3,7 +3,8 @@ use conduwuit::{Err, Result};
use ruma::{
api::client::discovery::{
discover_homeserver::{self, HomeserverInfo},
discover_policy_server, discover_support,
discover_policy_server,
discover_support::{self, Contact, ContactRole},
},
assign,
};
@@ -66,7 +67,46 @@ pub(crate) async fn well_known_support(
.as_ref()
.map(ToString::to_string);
let contacts = services.admin.get_support_contacts().await;
let email_address = services.config.well_known.support_email.clone();
let matrix_id = services.config.well_known.support_mxid.clone();
let pgp_key = services.config.well_known.support_pgp_key.clone();
// TODO: support defining multiple contacts in the config
let mut contacts: Vec<Contact> = vec![];
let role = services
.config
.well_known
.support_role
.clone()
.unwrap_or(ContactRole::Admin);
// Add configured contact if at least one contact method is specified
let configured_contact = match (matrix_id, email_address) {
| (Some(matrix_id), email_address) =>
Some(assign!(Contact::with_matrix_id(role, matrix_id), { email_address })),
| (None, Some(email_address)) => Some(Contact::with_email_address(role, email_address)),
| (None, None) => None,
};
if let Some(mut configured_contact) = configured_contact {
configured_contact.pgp_key = pgp_key;
contacts.push(configured_contact);
}
// Try to add admin users as contacts if no contacts are configured
if contacts.is_empty() {
let admin_users = services.admin.get_admins().await;
for user_id in &admin_users {
if *user_id == services.globals.server_user {
continue;
}
contacts.push(Contact::with_matrix_id(ContactRole::Admin, user_id.to_owned()));
}
}
if contacts.is_empty() && support_page.is_none() {
// No admin room, no configured contacts, and no support page
+2 -1
View File
@@ -1,5 +1,4 @@
#![type_length_limit = "16384"] //TODO: reduce me
#![recursion_limit = "256"] // My Giant Async Function
#![allow(clippy::toplevel_ref_arg)]
extern crate conduwuit_core as conduwuit;
@@ -11,6 +10,8 @@ pub mod client;
pub mod router;
pub mod server;
pub mod admin;
pub(crate) use self::router::{Ruma, RumaResponse, State};
conduwuit::mod_ctor! {}
+8 -22
View File
@@ -10,18 +10,16 @@ use axum::{
response::{IntoResponse, Redirect},
routing::{any, get, post},
};
use conduwuit::err;
use conduwuit::{Server, err};
pub(super) use conduwuit_service::state::State;
use http::{Uri, uri};
use self::handler::RouterExt;
pub(super) use self::{args::Args as Ruma, auth::ClientIdentity, response::RumaResponse};
#[cfg(feature = "admin_api")]
use crate::client::admin::site as admin_api;
use crate::{client, server};
pub(super) use self::{args::Args as Ruma, response::RumaResponse};
use crate::{admin, client, server};
pub fn build(router: Router<State>, state: State) -> Router<State> {
let config = &state.server.config;
pub fn build(router: Router<State>, server: &Server) -> Router<State> {
let config = &server.config;
let mut router = router
.ruma_route(&client::appservice_ping)
.ruma_route(&client::get_supported_versions_route)
@@ -183,17 +181,15 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
.ruma_route(&client::get_room_summary)
.ruma_route(&client::get_suspended_status)
.ruma_route(&client::put_suspended_status)
.ruma_route(&client::get_locked_status)
.ruma_route(&client::put_locked_status)
.ruma_route(&client::well_known_support)
.ruma_route(&client::well_known_client)
.ruma_route(&client::well_known_policy_server)
.ruma_route(&client::get_rtc_transports)
.ruma_route(&client::room_initial_sync_route)
.ruma_route(&client::get_authorization_server_metadata_route)
.merge(client::oauth::router(state))
.route("/_conduwuit/server_version", get(client::conduwuit_server_version))
.route("/_continuwuity/server_version", get(client::conduwuit_server_version));
.route("/_continuwuity/server_version", get(client::conduwuit_server_version))
.ruma_route(&admin::rooms::ban::ban_room)
.ruma_route(&admin::rooms::list::list_rooms);
if config.allow_federation {
router = router
@@ -279,16 +275,6 @@ pub fn build(router: Router<State>, state: State) -> Router<State> {
.route("/_matrix/media/r0/preview_url", any(redirect_legacy_preview));
}
#[cfg(feature = "admin_api")]
{
router = router
.ruma_route(&admin_api::users::list_users_route)
.ruma_route(&admin_api::users::create_user_route)
.ruma_route(&admin_api::rooms::ban_room)
.ruma_route(&admin_api::rooms::legacy_list_rooms_route)
.ruma_route(&admin_api::rooms::list_rooms_route);
};
router
}
+68 -13
View File
@@ -6,14 +6,17 @@ use axum::{
extract::{FromRequest, Path, Query},
};
use conduwuit::{Error, Result, err};
use ruma::{CanonicalJsonObject, api::IncomingRequest};
use ruma::{
CanonicalJsonObject, DeviceId, OwnedDeviceId, OwnedServerName, OwnedUserId, ServerName,
UserId, api::IncomingRequest,
};
use serde::Deserialize;
use crate::{State, router::auth::CheckAuth};
use crate::{State, router::auth::CheckAuth, service::appservice::RegistrationInfo};
/// Query parameters needed to authenticate requests
#[derive(Deserialize)]
pub(crate) struct AuthQueryParams {
pub(super) struct AuthQueryParams {
pub(super) user_id: Option<String>,
/// Device ID for appservice device masquerading (MSC3202/MSC4190).
/// Can be provided as `device_id` or `org.matrix.msc3202.device_id`.
@@ -22,22 +25,67 @@ pub(crate) struct AuthQueryParams {
}
/// Extractor for Ruma request structs
pub(crate) struct Args<R: IncomingRequest<Authentication: CheckAuth> + Send + Sync + 'static> {
pub(crate) struct Args<T> {
/// Request struct body
pub(crate) body: R,
pub(crate) body: T,
/// Parsed JSON body. None when body is not JSON.
/// Federation server authentication: X-Matrix origin
/// None when not a federation server.
pub(crate) origin: Option<OwnedServerName>,
/// Local user authentication: user_id.
/// None when not an authenticated local user.
pub(crate) sender_user: Option<OwnedUserId>,
/// Local user authentication: device_id.
/// None when not an authenticated local user or no device.
pub(crate) sender_device: Option<OwnedDeviceId>,
/// Appservice authentication; registration info.
/// None when not an appservice.
pub(crate) appservice_info: Option<RegistrationInfo>,
/// Parsed JSON content.
/// None when body is not a valid string
pub(crate) json_body: Option<CanonicalJsonObject>,
/// Identity of the requesting entity
pub(crate) identity: <R::Authentication as CheckAuth>::Identity,
}
impl<R> Deref for Args<R>
impl<T> Args<T>
where
R: IncomingRequest<Authentication: CheckAuth> + Send + Sync + 'static,
T: IncomingRequest + Send + Sync + 'static,
{
type Target = R;
#[inline]
pub(crate) fn sender(&self) -> (&UserId, &DeviceId) {
(self.sender_user(), self.sender_device())
}
#[inline]
pub(crate) fn sender_user(&self) -> &UserId {
self.sender_user
.as_deref()
.expect("user must be authenticated for this handler")
}
#[inline]
pub(crate) fn sender_device(&self) -> &DeviceId {
self.sender_device
.as_deref()
.expect("user must be authenticated and device identified")
}
#[inline]
pub(crate) fn origin(&self) -> &ServerName {
self.origin
.as_deref()
.expect("server must be authenticated for this handler")
}
}
impl<T> Deref for Args<T>
where
T: IncomingRequest + Send + Sync + 'static,
{
type Target = T;
fn deref(&self) -> &Self::Target { &self.body }
}
@@ -97,6 +145,13 @@ where
let body = R::try_from_http_request(request, &path)
.map_err(|e| err!(Request(BadJson(debug_warn!("{e}")))))?;
Ok(Self { body, json_body, identity: auth })
Ok(Self {
body,
origin: auth.origin,
sender_user: auth.sender_user,
sender_device: auth.sender_device,
appservice_info: auth.appservice_info,
json_body,
})
}
}
+97 -156
View File
@@ -1,9 +1,8 @@
use std::any::{Any, TypeId};
use conduwuit::{Err, Error, Result, err};
use http::StatusCode;
use conduwuit::{Err, Result, err};
use ruma::{
DeviceId, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
api::{
IncomingRequest,
auth_scheme::{
@@ -11,70 +10,30 @@ use ruma::{
AuthScheme, NoAccessToken, NoAuthentication,
},
client,
error::{ErrorKind, UnknownTokenErrorData},
federation::authentication::ServerSignatures,
},
assign,
};
use service::{
Services,
server_keys::{PubKeyMap, PubKeys},
users::AccessTokenStatus,
};
use crate::{router::args::AuthQueryParams, service::appservice::RegistrationInfo};
pub(crate) enum ClientIdentity {
User {
sender_user: OwnedUserId,
sender_device: OwnedDeviceId,
},
Appservice {
sender_user: OwnedUserId,
sender_device: Option<OwnedDeviceId>,
appservice_info: Box<RegistrationInfo>,
},
#[derive(Default)]
pub(super) struct Auth {
pub(super) origin: Option<OwnedServerName>,
pub(super) sender_user: Option<OwnedUserId>,
pub(super) sender_device: Option<OwnedDeviceId>,
pub(super) appservice_info: Option<RegistrationInfo>,
}
impl ClientIdentity {
pub(crate) fn sender_user(&self) -> &UserId {
match self {
| Self::User { sender_user, .. } | Self::Appservice { sender_user, .. } =>
sender_user,
}
}
pub(crate) fn sender_device(&self) -> Option<&DeviceId> {
match self {
| Self::User { sender_device, .. } => Some(sender_device),
| Self::Appservice { sender_device, .. } => sender_device.as_deref(),
}
}
pub(crate) fn expect_sender_device(&self) -> Result<&DeviceId> {
self.sender_device().ok_or_else(|| {
err!(Request(Forbidden("Appservices must masquerade to use this endpoint.")))
})
}
pub(crate) fn appservice_info(&self) -> Option<&RegistrationInfo> {
match self {
| Self::User { .. } => None,
| Self::Appservice { appservice_info, .. } => Some(appservice_info),
}
}
pub(crate) fn is_appservice(&self) -> bool { matches!(self, Self::Appservice { .. }) }
}
pub(crate) trait CheckAuth: AuthScheme {
type Identity: Send;
pub(super) trait CheckAuth: AuthScheme {
fn authenticate<R: IncomingRequest + Any, B: AsRef<[u8]> + Sync>(
services: &Services,
incoming_request: &hyper::Request<B>,
query: AuthQueryParams,
) -> impl Future<Output = Result<Self::Identity>> + Send {
) -> impl Future<Output = Result<Auth>> + Send {
async move {
let route = TypeId::of::<R>();
@@ -95,19 +54,17 @@ pub(crate) trait CheckAuth: AuthScheme {
request: &hyper::Request<B>,
query: AuthQueryParams,
route: TypeId,
) -> impl Future<Output = Result<Self::Identity>> + Send;
) -> impl Future<Output = Result<Auth>> + Send;
}
impl CheckAuth for ServerSignatures {
type Identity = OwnedServerName;
async fn verify<B: AsRef<[u8]> + Sync>(
services: &Services,
output: Self::Output,
request: &hyper::Request<B>,
_query: AuthQueryParams,
_route: TypeId,
) -> Result<Self::Identity> {
) -> Result<Auth> {
let destination = services.globals.server_name();
if output
.destination
@@ -139,7 +96,10 @@ impl CheckAuth for ServerSignatures {
)));
}
Ok(output.origin)
Ok(Auth {
origin: Some(output.origin.clone()),
..Default::default()
})
},
| Err(err) =>
Err!(Request(Unauthorized(warn!("Failed to verify X-Matrix header: {err}")))),
@@ -148,181 +108,162 @@ impl CheckAuth for ServerSignatures {
}
impl CheckAuth for AccessToken {
type Identity = ClientIdentity;
async fn verify<B: AsRef<[u8]> + Sync>(
services: &Services,
output: Self::Output,
_request: &hyper::Request<B>,
query: AuthQueryParams,
route: TypeId,
) -> Result<Self::Identity> {
if let Some((sender_user, sender_device, status)) =
services.users.find_from_token(&output).await
{
// If the token is expired we return a soft logout
if matches!(status, AccessTokenStatus::Expired) {
return Err(Error::Request(
ErrorKind::UnknownToken(
assign!(UnknownTokenErrorData::new(), { soft_logout: true }),
),
"This token has expired".into(),
StatusCode::UNAUTHORIZED,
));
}
) -> Result<Auth> {
// Check for appservice tokens first
// Locked users can only use /logout and /logout/all
if services
.users
.is_locked(&sender_user)
.await
.is_ok_and(std::convert::identity)
let (sender_user, sender_device, appservice_info) = {
if let Ok((sender_user, sender_device)) =
services.users.find_from_token(&output).await
{
if !(route == TypeId::of::<client::session::logout::v3::Request>()
|| route == TypeId::of::<client::session::logout_all::v3::Request>())
// Locked users can only use /logout and /logout/all
if services
.users
.is_locked(&sender_user)
.await
.is_ok_and(std::convert::identity)
{
return Err!(Request(UserLocked("Your account is locked.")));
}
}
Ok(ClientIdentity::User { sender_user, sender_device })
} else if let Ok(appservice_info) = services.appservice.find_from_token(&output).await {
let Ok(sender_user) = query.user_id.clone().map_or_else(
|| {
UserId::parse_with_server_name(
appservice_info.registration.sender_localpart.as_str(),
services.globals.server_name(),
)
},
UserId::parse,
) else {
return Err!(Request(InvalidUsername("Username is invalid.")));
};
if !appservice_info.is_user_match(&sender_user) {
return Err!(Request(Exclusive("User is not in namespace.")));
}
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
// The device_id can be provided via `device_id` or
// `org.matrix.msc3202.device_id` query parameter.
let sender_device =
if let Some(device_id) = query.device_id.as_deref().map(Into::into) {
// Verify the device exists for this user
if services
.users
.get_device_metadata(&sender_user, device_id)
.await
.is_err()
if !(route == TypeId::of::<client::session::logout::v3::Request>()
|| route == TypeId::of::<client::session::logout_all::v3::Request>())
{
return Err!(Request(Forbidden(
"Device does not exist for user or appservice cannot masquerade as \
this device."
)));
return Err!(Request(Unauthorized("Your account is locked.")));
}
}
Some(device_id.to_owned())
} else {
None
(Some(sender_user), Some(sender_device), None)
} else if let Ok(appservice_info) = services.appservice.find_from_token(&output).await
{
let Ok(sender_user) = query.user_id.clone().map_or_else(
|| {
UserId::parse_with_server_name(
appservice_info.registration.sender_localpart.as_str(),
services.globals.server_name(),
)
},
UserId::parse,
) else {
return Err!(Request(InvalidUsername("Username is invalid.")));
};
Ok(ClientIdentity::Appservice {
sender_user,
sender_device,
appservice_info: Box::new(appservice_info),
})
} else {
Err(Error::Request(
ErrorKind::UnknownToken(UnknownTokenErrorData::new()),
"Invalid token".into(),
StatusCode::UNAUTHORIZED,
))
}
if !appservice_info.is_user_match(&sender_user) {
return Err!(Request(Exclusive("User is not in namespace.")));
}
// MSC3202/MSC4190: Handle device_id masquerading for appservices.
// The device_id can be provided via `device_id` or
// `org.matrix.msc3202.device_id` query parameter.
let sender_device =
if let Some(device_id) = query.device_id.as_deref().map(Into::into) {
// Verify the device exists for this user
if services
.users
.get_device_metadata(&sender_user, device_id)
.await
.is_err()
{
return Err!(Request(Forbidden(
"Device does not exist for user or appservice cannot masquerade \
as this device."
)));
}
Some(device_id.to_owned())
} else {
None
};
(Some(sender_user), sender_device, Some(appservice_info))
} else {
return Err!(Request(Unauthorized("Invalid access token.")));
}
};
Ok(Auth {
sender_user,
sender_device,
appservice_info,
..Default::default()
})
}
}
impl CheckAuth for AccessTokenOptional {
type Identity = Option<ClientIdentity>;
async fn verify<B: AsRef<[u8]> + Sync>(
services: &Services,
output: Self::Output,
request: &hyper::Request<B>,
query: AuthQueryParams,
route: TypeId,
) -> Result<Self::Identity> {
) -> Result<Auth> {
match output {
| Some(token) =>
<AccessToken as CheckAuth>::verify(services, token, request, query, route)
.await
.map(Some),
| None => Ok(None),
<AccessToken as CheckAuth>::verify(services, token, request, query, route).await,
| None => Ok(Auth::default()),
}
}
}
impl CheckAuth for AppserviceToken {
type Identity = RegistrationInfo;
async fn verify<B: AsRef<[u8]> + Sync>(
services: &Services,
output: Self::Output,
_request: &hyper::Request<B>,
_query: AuthQueryParams,
_route: TypeId,
) -> Result<Self::Identity> {
) -> Result<Auth> {
let Ok(appservice_info) = services.appservice.find_from_token(&output).await else {
return Err!(Request(Unauthorized("Invalid appservice token.")));
};
Ok(appservice_info)
Ok(Auth {
appservice_info: Some(appservice_info),
..Default::default()
})
}
}
impl CheckAuth for AppserviceTokenOptional {
type Identity = Option<RegistrationInfo>;
async fn verify<B: AsRef<[u8]> + Sync>(
services: &Services,
output: Self::Output,
request: &hyper::Request<B>,
query: AuthQueryParams,
route: TypeId,
) -> Result<Self::Identity> {
) -> Result<Auth> {
match output {
| Some(token) =>
<AppserviceToken as CheckAuth>::verify(services, token, request, query, route)
.await
.map(Some),
| None => Ok(None),
.await,
| None => Ok(Auth::default()),
}
}
}
impl CheckAuth for NoAuthentication {
type Identity = ();
async fn verify<B: AsRef<[u8]> + Sync>(
_services: &Services,
_output: Self::Output,
_request: &hyper::Request<B>,
_query: AuthQueryParams,
_route: TypeId,
) -> Result<Self::Identity> {
Ok(())
) -> Result<Auth> {
Ok(Auth::default())
}
}
impl CheckAuth for NoAccessToken {
type Identity = Option<ClientIdentity>;
async fn verify<B: AsRef<[u8]> + Sync>(
services: &Services,
_output: Self::Output,
request: &hyper::Request<B>,
query: AuthQueryParams,
route: TypeId,
) -> Result<Self::Identity> {
) -> Result<Auth> {
// We handle these the same as AccessTokenOptional
let token = AccessTokenOptional::extract_authentication(request).map_err(|err| {
err!(Request(Unauthorized(warn!("Failed to extract authorization: {}", err))))
+3 -3
View File
@@ -28,7 +28,7 @@ pub(crate) async fn get_backfill_route(
) -> Result<get_backfill::v1::Response> {
AccessCheck {
services: &services,
origin: &body.identity,
origin: body.origin(),
room_id: &body.room_id,
event_id: None,
}
@@ -41,7 +41,7 @@ pub(crate) async fn get_backfill_route(
.await
{
info!(
origin = body.identity.as_str(),
origin = body.origin().as_str(),
"Refusing to serve backfill for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
@@ -76,7 +76,7 @@ pub(crate) async fn get_backfill_route(
Ok(services
.rooms
.state_accessor
.server_can_see_event(&body.identity, &pdu.room_id_or_hash(), &pdu.event_id)
.server_can_see_event(body.origin(), &pdu.room_id_or_hash(), &pdu.event_id)
.await
.then_some(pdu))
})
+2 -2
View File
@@ -40,7 +40,7 @@ pub(crate) async fn get_event_route(
AccessCheck {
services: &services,
origin: &body.identity,
origin: body.origin(),
room_id,
event_id: Some(&body.event_id),
}
@@ -54,7 +54,7 @@ pub(crate) async fn get_event_route(
.await
{
info!(
origin = body.identity.as_str(),
origin = body.origin().as_str(),
"Refusing to serve state for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
+2 -2
View File
@@ -19,7 +19,7 @@ pub(crate) async fn get_event_authorization_route(
) -> Result<get_event_authorization::v1::Response> {
AccessCheck {
services: &services,
origin: &body.identity,
origin: body.origin(),
room_id: &body.room_id,
event_id: None,
}
@@ -42,7 +42,7 @@ pub(crate) async fn get_event_authorization_route(
.await
{
info!(
origin = body.identity.as_str(),
origin = body.origin().as_str(),
"Refusing to serve state for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
+4 -4
View File
@@ -22,7 +22,7 @@ pub(crate) async fn get_missing_events_route(
) -> Result<get_missing_events::v1::Response> {
AccessCheck {
services: &services,
origin: &body.identity,
origin: body.origin(),
room_id: &body.room_id,
event_id: None,
}
@@ -36,7 +36,7 @@ pub(crate) async fn get_missing_events_route(
.await
{
info!(
origin = body.identity.as_str(),
origin = body.origin().as_str(),
"Refusing to serve state for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
@@ -91,10 +91,10 @@ pub(crate) async fn get_missing_events_route(
if !services
.rooms
.state_accessor
.server_can_see_event(&body.identity, &body.room_id, pdu.event_id())
.server_can_see_event(body.origin(), &body.room_id, pdu.event_id())
.await
{
debug!(%next_event_id, origin = %body.identity, "redacting event origin cannot see");
debug!(%next_event_id, origin = %body.origin(), "redacting event origin cannot see");
pdu.redact(&room_version, json!({}))?;
}
+2 -2
View File
@@ -20,7 +20,7 @@ pub(crate) async fn get_hierarchy_route(
.await
{
info!(
origin = body.identity.as_str(),
origin = body.origin().as_str(),
"Refusing to serve state for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
@@ -29,7 +29,7 @@ pub(crate) async fn get_hierarchy_route(
let response = services
.rooms
.summary
.get_local_room_summary_for_server(&body.identity, &body.room_id, body.suggested_only)
.get_local_room_summary_for_server(body.origin(), &body.room_id, body.suggested_only)
.await;
if let Accessibility::Accessible(response) = response {
+5 -4
View File
@@ -32,7 +32,7 @@ pub(crate) async fn create_invite_route(
services
.rooms
.event_handler
.acl_check(&body.identity, &body.room_id)
.acl_check(body.origin(), &body.room_id)
.await?;
if !services.server.supported_room_version(&body.room_version) {
@@ -54,11 +54,12 @@ pub(crate) async fn create_invite_route(
if services
.moderation
.is_remote_server_forbidden(&body.identity)
.is_remote_server_forbidden(body.origin())
{
warn!(
"Received federated/remote invite from banned server {} for room ID {}. Rejecting.",
body.identity, body.room_id
body.origin(),
body.room_id
);
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
@@ -104,7 +105,7 @@ pub(crate) async fn create_invite_route(
.and_then(Result::ok)
.ok_or_else(|| err!(Request(InvalidParam("Invalid sender property"))))?;
if sender_user.server_name() != body.identity {
if sender_user.server_name() != body.origin() {
return Err!(Request(Forbidden("Sender's server does not match the origin server.",)));
}
+8 -6
View File
@@ -30,7 +30,7 @@ use crate::Ruma;
/// # `GET /_matrix/federation/v1/make_join/{roomId}/{userId}`
///
/// Creates a join template.
#[tracing::instrument(skip_all, fields(room_id = %body.room_id, user_id = %body.user_id, origin = %body.identity), level = "info")]
#[tracing::instrument(skip_all, fields(room_id = %body.room_id, user_id = %body.user_id, origin = %body.origin()), level = "info")]
pub(crate) async fn create_join_event_template_route(
State(services): State<crate::State>,
body: Ruma<prepare_join_event::v1::Request>,
@@ -45,14 +45,14 @@ pub(crate) async fn create_join_event_template_route(
.await
{
info!(
origin = body.identity.as_str(),
origin = body.origin().as_str(),
room_id = %body.room_id,
"Refusing to serve make_join for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
if body.user_id.server_name() != body.identity {
if body.user_id.server_name() != body.origin() {
return Err!(Request(BadJson("Not allowed to join on behalf of another server/user.")));
}
@@ -60,17 +60,19 @@ pub(crate) async fn create_join_event_template_route(
services
.rooms
.event_handler
.acl_check(&body.identity, &body.room_id)
.acl_check(body.origin(), &body.room_id)
.await?;
if services
.moderation
.is_remote_server_forbidden(&body.identity)
.is_remote_server_forbidden(body.origin())
{
warn!(
"Server {} for remote user {} tried joining room ID {} which has a server name that \
is globally forbidden. Rejecting.",
body.identity, &body.user_id, &body.room_id,
body.origin(),
&body.user_id,
&body.room_id,
);
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
+7 -5
View File
@@ -28,14 +28,14 @@ pub(crate) async fn create_knock_event_template_route(
.await
{
info!(
origin = body.identity.as_str(),
origin = body.origin().as_str(),
room_id = %body.room_id,
"Refusing to serve make_knock for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
if body.user_id.server_name() != body.identity {
if body.user_id.server_name() != body.origin() {
return Err!(Request(BadJson("Not allowed to knock on behalf of another server/user.")));
}
@@ -43,17 +43,19 @@ pub(crate) async fn create_knock_event_template_route(
services
.rooms
.event_handler
.acl_check(&body.identity, &body.room_id)
.acl_check(body.origin(), &body.room_id)
.await?;
if services
.moderation
.is_remote_server_forbidden(&body.identity)
.is_remote_server_forbidden(body.origin())
{
warn!(
"Server {} for remote user {} tried knocking room ID {} which has a server name \
that is globally forbidden. Rejecting.",
body.identity, &body.user_id, &body.room_id,
body.origin(),
&body.user_id,
&body.room_id,
);
return Err!(Request(Forbidden("Server is banned on this homeserver.")));
}
+3 -3
View File
@@ -26,13 +26,13 @@ pub(crate) async fn create_leave_event_template_route(
.await
{
info!(
origin = body.identity.as_str(),
origin = body.origin().as_str(),
"Refusing to serve make_leave for room we aren't participating in"
);
return Err!(Request(NotFound("This server is not participating in that room.")));
}
if body.user_id.server_name() != body.identity {
if body.user_id.server_name() != body.origin() {
return Err!(Request(Forbidden(
"Not allowed to leave on behalf of another server/user."
)));
@@ -42,7 +42,7 @@ pub(crate) async fn create_leave_event_template_route(
services
.rooms
.event_handler
.acl_check(&body.identity, &body.room_id)
.acl_check(body.origin(), &body.room_id)
.await?;
let room_version = services.rooms.state.get_room_version(&body.room_id).await?;
+4 -4
View File
@@ -62,7 +62,7 @@ pub(crate) async fn send_transaction_message_route(
ClientIp(client): ClientIp,
body: Ruma<send_transaction_message::v1::Request>,
) -> Result<send_transaction_message::v1::Response> {
if body.identity != body.body.origin {
if body.origin() != body.body.origin {
return Err!(Request(Forbidden(
"Not allowed to send transactions on behalf of other servers"
)));
@@ -80,7 +80,7 @@ pub(crate) async fn send_transaction_message_route(
)));
}
let txn_key = (body.identity.clone(), body.transaction_id.clone());
let txn_key = (body.origin().to_owned(), body.transaction_id.clone());
// Atomically check cache, join active, or start new transaction
match services
@@ -136,7 +136,7 @@ async fn wait_for_result(
skip_all,
fields(
id = ?body.transaction_id.as_str(),
origin = ?body.identity
origin = ?body.origin()
)
)]
async fn process_inbound_transaction(
@@ -164,7 +164,7 @@ async fn process_inbound_transaction(
.stream();
debug!(pdus = body.pdus.len(), edus = body.edus.len(), "Processing transaction",);
let results = match handle(&services, &client, &body.identity, pdus, edus).await {
let results = match handle(&services, &client, body.origin(), pdus, edus).await {
| Ok(results) => results,
| Err(err) => {
fail_federation_txn(services, &txn_key, &sender, err);

Some files were not shown because too many files have changed in this diff Show More