Compare commits

..

49 Commits

Author SHA1 Message Date
timedout f84f0c46b1 feat: Add user creation endpoint 2026-05-26 14:30:36 -04:00
timedout cef2260dc7 feat: Include predecessor and successor information in room list 2026-05-26 14:30:35 -04:00
timedout 4ffe321b6a feat: Add pagination to rooms list & include more information 2026-05-26 14:30:35 -04:00
timedout 61a3749c24 feat: Enable pagination for the users list route 2026-05-26 14:30:34 -04:00
timedout 374a216fc8 feat: Define routes for listing and creating users 2026-05-26 14:30:34 -04:00
timedout e9f6dc29b6 feat: Add version part to admin API URLs
This is a surprise tool that will help us later
2026-05-26 14:30:33 -04:00
timedout 688855631e chore: Add some documentation to API stuff 2026-05-26 14:30:33 -04:00
timedout 0c0ae68070 feat: Drop ruminuwuity msc4323 definitions 2026-05-26 14:30:33 -04:00
timedout c8c14f248b feat: Use upstream ruma defs for msc4323, add locking endpoints 2026-05-26 14:30:33 -04:00
Ginger 6756bacfc8 refactor: Update logic for checking if a username is available 2026-05-26 14:27:10 -04:00
Ginger 26d0d80737 fix: CSS adjustments 2026-05-26 14:27:10 -04:00
Ginger 7ee9e09b05 fix: Adjust error codes to comply with MSC4190 2026-05-26 14:27:10 -04:00
Ginger 61ab6b4eb8 feat: Mark spec version 1.15 as supported 2026-05-26 14:27:10 -04:00
Ginger 4da00fa28a feat: Add a page with some information about the server 2026-05-26 14:27:10 -04:00
Ginger a0aeebd237 fix: Correct config file example section name 2026-05-26 14:27:10 -04:00
Ginger e02d7c8029 chore: My Giant Future 2026-05-26 14:27:10 -04:00
Ginger 8882d1d4c7 feat: Improve account panel UI for locked and suspended accounts 2026-05-26 14:27:10 -04:00
Ginger fa6c5aa942 fix: Include query parameters in link back to login on register page 2026-05-26 14:27:10 -04:00
Ginger 6c54f592ee fix: CSS tweaks 2026-05-26 14:27:09 -04:00
Ginger 6a9123baf1 feat: Improve registration UI in first-run mode 2026-05-26 14:27:09 -04:00
Ginger 91923d0afa fix: Minor wording improvements 2026-05-26 14:27:09 -04:00
Ginger 242414c0b8 fix: Set default for allow_deactivation 2026-05-26 14:27:09 -04:00
Ginger 8c6904ab33 fix: Fix registration terms example in config 2026-05-26 14:27:09 -04:00
Ginger 3c07857e1f feat: Implement support for prompt=create in the authorization code flow 2026-05-26 14:27:09 -04:00
Ginger 851d6e219f fix: Don't let logged-in users access the registration page 2026-05-26 14:27:09 -04:00
Ginger 66aba9d5d0 feat: Allow self-service deactivation to be disabled 2026-05-26 14:27:09 -04:00
Ginger 5ca1341bf7 refactor: Use more consistent terminology for email validation pages 2026-05-26 14:27:09 -04:00
Ginger baf76cd4dc feat: Add support for registering accounts with the web UI 2026-05-26 14:27:09 -04:00
Ginger 53d51cf831 refactor: Change template context to allow using a CSP nonce 2026-05-26 14:27:09 -04:00
Ginger 9bfc331a26 fix: Minor CSS improvements 2026-05-26 14:27:09 -04:00
Ginger 1cda559a18 fix: Remove errant whitespace in device details 2026-05-26 14:27:09 -04:00
Ginger 94655acffd chore: News fragment 2026-05-26 14:27:09 -04:00
Ginger 4bbbbb854e feat: Allow configuring the OAuth compatibility mode 2026-05-26 14:27:09 -04:00
Ginger 81388162f0 fix: Use button styling for account management link on index page 2026-05-26 14:27:09 -04:00
Ginger a912dcc106 fix: Use the right text color on input elements 2026-05-26 14:27:09 -04:00
Ginger 950d7ae3d9 feat: Add support for account management deeplinks 2026-05-26 14:27:09 -04:00
Ginger 7f36c44763 fix: Return the correct error code for expired access tokens 2026-05-26 14:27:09 -04:00
Ginger 3e8403de64 feat: Add a page for viewing a device's details 2026-05-26 14:27:09 -04:00
Ginger 2ef8a1edd7 fix: Use SameSite=Lax for session cookie 2026-05-26 14:27:09 -04:00
Ginger 6f17868525 feat: Allow devices to be removed from the account panel 2026-05-26 14:27:09 -04:00
Ginger ee73a2b36d feat: Implement oauth token revocation 2026-05-26 14:27:09 -04:00
Ginger 3dc4c7d4fc chore: Clippy fixes 2026-05-26 14:27:09 -04:00
Ginger 13917bb5c3 feat: Implement oauth auth code and refresh token flows 2026-05-26 14:27:09 -04:00
Ginger f269fb5cfc chore: Clippy fixes 2026-05-26 14:27:09 -04:00
Ginger 6b0b8344d4 feat: Implement a web-based account management dashboard 2026-05-26 14:27:09 -04:00
Ginger 02948960fa feat: Implement oauth service and client registration 2026-05-26 14:27:09 -04:00
Ginger 30c9d6d2df chore: Clippy fixes 2026-05-26 18:26:02 +00:00
Ginger 74841b6711 refactor: Represent route auth information in the type system 2026-05-26 18:26:02 +00:00
timedout dabbdc7517 fix: Don't be so aggressive when validating policy server signatures 2026-05-26 16:16:48 +01:00
210 changed files with 7673 additions and 2121 deletions
Generated
+78 -11
View File
@@ -1088,6 +1088,7 @@ dependencies = [
"serde", "serde",
"serde-saphyr", "serde-saphyr",
"serde_json", "serde_json",
"serde_urlencoded",
"sha2 0.11.0", "sha2 0.11.0",
"termimad", "termimad",
"tokio", "tokio",
@@ -1107,18 +1108,29 @@ dependencies = [
"axum", "axum",
"axum-extra", "axum-extra",
"base64 0.22.1", "base64 0.22.1",
"conduwuit_api",
"conduwuit_build_metadata", "conduwuit_build_metadata",
"conduwuit_core", "conduwuit_core",
"conduwuit_database",
"conduwuit_service", "conduwuit_service",
"form_urlencoded",
"futures", "futures",
"lettre",
"memory-serve", "memory-serve",
"rand 0.10.1", "rand 0.10.1",
"recaptcha-verify",
"reqwest 0.12.28",
"ruma", "ruma",
"serde", "serde",
"serde_json",
"serde_urlencoded",
"thiserror", "thiserror",
"tower-http", "tower-http",
"tower-sec-fetch", "tower-sec-fetch",
"tower-sessions",
"tower-sessions-core",
"tracing", "tracing",
"url",
"validator", "validator",
] ]
@@ -1526,6 +1538,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c" checksum = "7cd812cc2bc1d69d4764bd80df88b4317eaef9e773c75226407d9bc0876b211c"
dependencies = [ dependencies = [
"powerfmt", "powerfmt",
"serde_core",
] ]
[[package]] [[package]]
@@ -4151,7 +4164,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma" name = "ruma"
version = "0.15.1" version = "0.15.1"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
dependencies = [ dependencies = [
"assign", "assign",
"js_int", "js_int",
@@ -4170,7 +4183,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-appservice-api" name = "ruma-appservice-api"
version = "0.15.0" version = "0.15.0"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@@ -4182,7 +4195,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-client-api" name = "ruma-client-api"
version = "0.23.1" version = "0.23.1"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"assign", "assign",
@@ -4204,7 +4217,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-common" name = "ruma-common"
version = "0.18.0" version = "0.18.0"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"base64 0.22.1", "base64 0.22.1",
@@ -4237,7 +4250,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-events" name = "ruma-events"
version = "0.33.0" version = "0.33.0"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"indexmap", "indexmap",
@@ -4258,7 +4271,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-federation-api" name = "ruma-federation-api"
version = "0.14.0" version = "0.14.0"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
dependencies = [ dependencies = [
"bytes", "bytes",
"headers", "headers",
@@ -4281,7 +4294,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identifiers-validation" name = "ruma-identifiers-validation"
version = "0.12.1" version = "0.12.1"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
dependencies = [ dependencies = [
"js_int", "js_int",
"thiserror", "thiserror",
@@ -4290,7 +4303,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-macros" name = "ruma-macros"
version = "0.18.0" version = "0.18.0"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"cfg-if", "cfg-if",
@@ -4306,7 +4319,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-push-gateway-api" name = "ruma-push-gateway-api"
version = "0.14.0" version = "0.14.0"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@@ -4318,7 +4331,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-signatures" name = "ruma-signatures"
version = "0.20.0" version = "0.20.0"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"ed25519-dalek", "ed25519-dalek",
@@ -4334,7 +4347,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-state-res" name = "ruma-state-res"
version = "0.16.0" version = "0.16.0"
source = "git+https://github.com/ruma/ruma.git?rev=3ecd80b92794d2d93f657a7b3db62d4be237526b#3ecd80b92794d2d93f657a7b3db62d4be237526b" source = "git+https://github.com/ruma/ruma.git?rev=9c9dccc93f054bbd28f23f630223fffa6289ecbc#9c9dccc93f054bbd28f23f630223fffa6289ecbc"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@@ -5543,6 +5556,22 @@ dependencies = [
"tracing", "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]] [[package]]
name = "tower-http" name = "tower-http"
version = "0.6.11" version = "0.6.11"
@@ -5591,6 +5620,44 @@ version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "8df9b6e13f2d32c91b9bd719c00d1958837bc7dec474d94952798cc8e69eeec3" 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]] [[package]]
name = "tracing" name = "tracing"
version = "0.1.44" version = "0.1.44"
+4 -2
View File
@@ -344,7 +344,7 @@ version = "1.1.1"
[workspace.dependencies.ruma] [workspace.dependencies.ruma]
# version = "0.14.1" # version = "0.14.1"
git = "https://github.com/ruma/ruma.git" git = "https://github.com/ruma/ruma.git"
rev = "3ecd80b92794d2d93f657a7b3db62d4be237526b" rev = "9c9dccc93f054bbd28f23f630223fffa6289ecbc"
features = [ features = [
"appservice-api-c", "appservice-api-c",
"client-api", "client-api",
@@ -379,7 +379,6 @@ features = [
"unstable-msc4293", "unstable-msc4293",
"unstable-msc4406", "unstable-msc4406",
"unstable-msc4439", "unstable-msc4439",
"unstable-msc4466",
"unstable-extensible-events", "unstable-extensible-events",
] ]
@@ -560,6 +559,9 @@ features = ["std"]
[workspace.dependencies.nonzero_ext] [workspace.dependencies.nonzero_ext]
version = "0.3.0" version = "0.3.0"
[workspace.dependencies.serde_urlencoded]
version = "0.7.1"
# #
# Patches # Patches
# #
+1
View File
@@ -0,0 +1 @@
Users may now be forbidden from deactivating their own accounts with the new `allow_deactivation` config option. Contributed by @ginger.
+1
View File
@@ -0,0 +1 @@
Added support for authenticating clients using the new OAuth 2.0 login API. Contributed by @ginger.
-1
View File
@@ -1 +0,0 @@
Added support for MSC4466, which allows clients to customize how changes to a user's global profile are propagated. Contributed by @ginger.
+37 -9
View File
@@ -521,17 +521,15 @@
# #
#recaptcha_private_site_key = #recaptcha_private_site_key =
# Policy documents, such as terms and conditions or a privacy policy, # Controls whether users are allowed to deactivate their own accounts
# which users must agree to when registering an account. # through the account management panel or their Matrix clients. Server
# admins can always deactivate users using the relevant admin commands.
# #
# Example: # Note that, in some jurisdictions, you may be legally required to honor
# ```ignore # users who request to deactivate their accounts if you set this option
# [global.registration_terms.privacy_policy] # to `false`.
# 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" }
# ```
# #
#registration_terms = {} #allow_deactivation = true
# Controls whether encrypted rooms and events are allowed. # Controls whether encrypted rooms and events are allowed.
# #
@@ -1987,3 +1985,33 @@
# `require_email_for_registration`. # `require_email_for_registration`.
# #
#require_email_for_token_registration = false #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)] #[derive(Debug, Parser)]
#[command(name = conduwuit_core::name(), version = conduwuit_core::version())] #[command(name = conduwuit_core::BRANDING, version = conduwuit_core::version())]
pub enum AdminCommand { pub enum AdminCommand {
#[command(subcommand)] #[command(subcommand)]
/// Commands for managing appservices /// Commands for managing appservices
+25 -2
View File
@@ -30,14 +30,37 @@ pub(super) async fn issue_token(&self, expires: super::TokenExpires) -> Result {
.issue_token(self.sender_or_service_user().into(), expires); .issue_token(self.sender_or_service_user().into(), expires);
self.write_str(&format!( self.write_str(&format!(
"New registration token issued: `{token}`. {}.", "New registration token issued: `{token}` . {}.",
if let Some(expires) = info.expires { if let Some(expires) = info.expires {
format!("{expires}") format!("{expires}")
} else { } else {
"Never expires".to_owned() "Never expires".to_owned()
} }
)) ))
.await .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(())
} }
#[admin_command] #[admin_command]
+13 -149
View File
@@ -1,13 +1,10 @@
use std::{ use std::collections::{BTreeMap, HashSet};
collections::{BTreeMap, HashSet},
fmt::Write as _,
};
use api::client::{ use api::client::{
full_user_deactivate, leave_room, recreate_push_rules_and_return, remote_leave_room, full_user_deactivate, leave_room, recreate_push_rules_and_return, remote_leave_room,
}; };
use conduwuit::{ use conduwuit::{
Err, Result, debug_warn, error, info, Err, Result, debug_warn, info,
matrix::{Event, pdu::PartialPdu}, matrix::{Event, pdu::PartialPdu},
utils::{self, ReadyExt}, utils::{self, ReadyExt},
warn, warn,
@@ -53,130 +50,22 @@ pub(super) async fn list_users(&self) -> Result {
#[admin_command] #[admin_command]
pub(super) async fn create_user(&self, username: String, password: Option<String>) -> Result { pub(super) async fn create_user(&self, username: String, password: Option<String>) -> Result {
// Validate user id // Validate user id
let user_id = parse_local_user_id(self.services, &username)?; let user_id = self
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
.create(&user_id, Some(HashedPassword::new(&password)?))
.await?;
// 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 .services
.server .users
.config .determine_registration_user_id(Some(username), None, None)
.new_user_displayname_suffix .await?;
.is_empty()
{ let password = HashedPassword::new(
write!(displayname, " {}", self.services.server.config.new_user_displayname_suffix)?; &password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH)),
} )?;
self.services self.services
.users .users
.set_displayname(&user_id, Some(displayname)); .create_local_account(&user_id, password, None)
.await;
// Initial account data self.write_str(&format!("Created user {user_id}")).await
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] #[admin_command]
@@ -302,31 +191,6 @@ pub(super) async fn reset_password(
Ok(()) 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] #[admin_command]
pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result { pub(super) async fn deactivate_all(&self, no_leave_rooms: bool, force: bool) -> Result {
if self.body.len() < 2 if self.body.len() < 2
-6
View File
@@ -29,12 +29,6 @@ pub enum UserCommand {
password: Option<String>, 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. /// Get a user's associated email address.
GetEmail { GetEmail {
user_id: String, user_id: String,
+2
View File
@@ -62,6 +62,8 @@ zstd_compression = [
"reqwest/zstd", "reqwest/zstd",
] ]
admin_api = []
[dependencies] [dependencies]
async-trait.workspace = true async-trait.workspace = true
axum-client-ip.workspace = true axum-client-ip.workspace = true
-1
View File
@@ -1 +0,0 @@
pub mod rooms;
-36
View File
@@ -1,36 +0,0 @@
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
@@ -1,2 +0,0 @@
pub mod ban;
pub mod list;
+43 -47
View File
@@ -24,10 +24,10 @@ use ruma::{
power_levels::RoomPowerLevelsEventContent, power_levels::RoomPowerLevelsEventContent,
}, },
}; };
use service::{mailer::messages, uiaa::Identity, users::HashedPassword}; use service::{mailer::messages, uiaa::UiaaInitiator, users::HashedPassword};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH}; use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::Ruma; use crate::{Ruma, router::ClientIdentity};
pub(crate) mod register; pub(crate) mod register;
pub(crate) mod threepid; pub(crate) mod threepid;
@@ -49,41 +49,16 @@ pub(crate) async fn get_register_available_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<get_username_availability::v3::Request>, body: Ruma<get_username_availability::v3::Request>,
) -> Result<get_username_availability::v3::Response> { ) -> Result<get_username_availability::v3::Response> {
// Validate user id let _ = services
let user_id = .users
match UserId::parse_with_server_name(&body.username, services.globals.server_name()) { .determine_registration_user_id(
| Ok(user_id) => { Some(body.username.clone()),
if let Err(e) = user_id.validate_strict() { None,
return Err!(Request(InvalidUsername(debug_warn!( body.identity
"Username {} contains disallowed characters or spaces: {e}", .as_ref()
body.username .and_then(ClientIdentity::appservice_info),
)))); )
} .await?;
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)) Ok(get_username_availability::v3::Response::new(true))
} }
@@ -111,7 +86,7 @@ pub(crate) async fn change_password_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<change_password::v3::Request>, body: Ruma<change_password::v3::Request>,
) -> Result<change_password::v3::Response> { ) -> Result<change_password::v3::Response> {
let identity = if let Some(ref user_id) = body.sender_user { let identity = if let Some(identity) = body.identity.as_ref() {
// A signed-in user is trying to change their password, prompt them for their // A signed-in user is trying to change their password, prompt them for their
// existing one // existing one
@@ -121,7 +96,7 @@ pub(crate) async fn change_password_route(
&body.auth, &body.auth,
vec![AuthFlow::new(vec![AuthType::Password])], vec![AuthFlow::new(vec![AuthType::Password])],
Box::default(), Box::default(),
Some(Identity::from_user_id(user_id)), Some(UiaaInitiator::new(identity.sender_user(), identity.sender_device())),
) )
.await? .await?
} else { } else {
@@ -157,7 +132,12 @@ pub(crate) async fn change_password_route(
services services
.users .users
.all_device_ids(&sender_user) .all_device_ids(&sender_user)
.ready_filter(|id| *id != body.sender_device()) .ready_filter(|id| {
body.identity
.as_ref()
.and_then(|identity| identity.sender_device())
.is_none_or(|sender_device| sender_device != *id)
})
.for_each(async |id| services.users.remove_device(&sender_user, &id).await) .for_each(async |id| services.users.remove_device(&sender_user, &id).await)
.await; .await;
@@ -173,7 +153,12 @@ pub(crate) async fn change_password_route(
.await .await
.ok() .ok()
.as_ref() .as_ref()
.is_some_and(|pusher_device| pusher_device != body.sender_device()) .is_some_and(|pusher_device| {
body.identity
.as_ref()
.and_then(|identity| identity.sender_device())
.is_none_or(|sender_device| sender_device != *pusher_device)
})
.then_some(pushkey) .then_some(pushkey)
}) })
.for_each(async |pushkey| { .for_each(async |pushkey| {
@@ -241,9 +226,11 @@ pub(crate) async fn whoami_route(
State(_): State<crate::State>, State(_): State<crate::State>,
body: Ruma<whoami::v3::Request>, body: Ruma<whoami::v3::Request>,
) -> Result<whoami::v3::Response> { ) -> Result<whoami::v3::Response> {
Ok(assign!(whoami::v3::Response::new(body.sender_user().to_owned(), false), { Ok(
device_id: body.sender_device, assign!(whoami::v3::Response::new(body.identity.sender_user().to_owned(), false), {
})) device_id: body.identity.sender_device().map(ToOwned::to_owned),
}),
)
} }
/// # `POST /_matrix/client/r0/account/deactivate` /// # `POST /_matrix/client/r0/account/deactivate`
@@ -265,15 +252,24 @@ pub(crate) async fn deactivate_route(
) -> Result<deactivate::v3::Response> { ) -> Result<deactivate::v3::Response> {
// Authentication for this endpoint is technically optional, // Authentication for this endpoint is technically optional,
// but we require the user to be logged in // but we require the user to be logged in
let sender_user = body let identity = body
.sender_user .identity
.as_ref() .as_ref()
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?; .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 // Prompt the user to confirm with their password using UIAA
let _ = services let _ = services
.uiaa .uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user))) .authenticate_password(&body.auth, sender_user, identity.sender_device(), None)
.await?; .await?;
// Remove profile pictures and display name // Remove profile pictures and display name
+57 -295
View File
@@ -1,17 +1,15 @@
use std::{collections::HashMap, fmt::Write}; use std::collections::HashMap;
use axum::extract::State; use axum::extract::State;
use axum_client_ip::ClientIp; use axum_client_ip::ClientIp;
use conduwuit::{ use conduwuit::{
Err, Result, debug_info, error, info, Err, Result, debug_info, info,
utils::{self}, utils::{self},
warn,
}; };
use conduwuit_service::Services; use conduwuit_service::Services;
use futures::{FutureExt, StreamExt}; use futures::StreamExt;
use lettre::{Address, message::Mailbox}; use lettre::{Address, message::Mailbox};
use ruma::{ use ruma::{
OwnedUserId, UserId,
api::client::{ api::client::{
account::{ account::{
register::{self, LoginType, RegistrationKind}, register::{self, LoginType, RegistrationKind},
@@ -20,11 +18,6 @@ use ruma::{
uiaa::{AuthFlow, AuthType}, uiaa::{AuthFlow, AuthType},
}, },
assign, assign,
events::{
GlobalAccountDataEventType, push_rules::PushRulesEvent,
room::message::RoomMessageEventContent,
},
push,
}; };
use serde_json::value::RawValue; use serde_json::value::RawValue;
use service::{mailer::messages, users::HashedPassword}; use service::{mailer::messages, users::HashedPassword};
@@ -32,8 +25,6 @@ use service::{mailer::messages, users::HashedPassword};
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH}; use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::Ruma; use crate::Ruma;
const RANDOM_USER_ID_LENGTH: usize = 10;
/// # `POST /_matrix/client/v3/register` /// # `POST /_matrix/client/v3/register`
/// ///
/// Register an account on this homeserver. /// Register an account on this homeserver.
@@ -52,14 +43,12 @@ pub(crate) async fn register_route(
return Err!(Request(GuestAccessForbidden("Guests may not register on this server."))); 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 // 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) // run (so the first user account can be created)
let allow_registration = let allow_registration =
services.config.allow_registration || services.firstrun.is_first_run(); services.config.allow_registration || services.firstrun.is_first_run();
if !allow_registration && body.appservice_info.is_none() { if !allow_registration && body.identity.is_none() {
info!( info!(
?body.username, ?body.username,
?body.initial_device_display_name, ?body.initial_device_display_name,
@@ -71,101 +60,59 @@ pub(crate) async fn register_route(
))); )));
} }
let identity = if body.appservice_info.is_some() { let user_id = if body.body.login_type == Some(LoginType::ApplicationService) {
// Appservices can skip auth let Some(appservice_info) = &body.identity else {
None 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
} else { } else {
// Perform UIAA to determine the user's identity // Perform UIAA to determine the user's identity
let (flows, params) = create_registration_uiaa_session(&services).await?; let (flows, params) = create_registration_uiaa_session(&services).await?;
Some( let identity = services
services .uiaa
.uiaa .authenticate(&body.auth, flows, params, None)
.authenticate(&body.auth, flows, params, None)
.await?,
)
};
// 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
}
});
let user_id =
determine_registration_user_id(&services, supplied_username, emergency_mode_enabled)
.await?; .await?;
if body.body.login_type == Some(LoginType::ApplicationService) { let password = if let Some(password) = &body.password {
// For appservice logins, make sure that the user ID is in the appservice's HashedPassword::new(password)?
// namespace } else {
return Err!(Request(InvalidParam("A password must be provided.")));
};
match body.appservice_info { let user_id = services
| Some(ref info) => .users
if !info.is_user_match(&user_id) && !emergency_mode_enabled { .determine_registration_user_id(body.username.clone(), identity.email.as_ref(), None)
return Err!(Request(Exclusive( .await?;
"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() { services
None .users
} else if let Some(password) = body.password.as_deref() { .create_local_account(&user_id, password, identity.email)
Some(HashedPassword::new(password)?) .await;
} else { services.users.join_auto_join_rooms(&user_id).await;
return Err!(Request(InvalidParam("A password must be provided"))); user_id
}; };
// 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 (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."
)));
}
// Generate new device id if the user didn't specify one
let device_id = body let device_id = body
.device_id .device_id
.clone() .clone()
@@ -181,6 +128,7 @@ pub(crate) async fn register_route(
&user_id, &user_id,
&device_id, &device_id,
&new_token, &new_token,
None,
body.initial_device_display_name.clone(), body.initial_device_display_name.clone(),
Some(client.to_string()), Some(client.to_string()),
) )
@@ -191,118 +139,7 @@ pub(crate) async fn register_route(
(None, None) (None, None)
}; };
debug_info!(%user_id, ?device, "User account was created"); debug_info!(%user_id, ?device, "New account created via legacy registration");
// 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), { Ok(assign!(register::v3::Response::new(user_id), {
access_token: token, access_token: token,
@@ -374,21 +211,21 @@ async fn create_registration_uiaa_session(
// Require all users to agree to the terms and conditions, if configured // Require all users to agree to the terms and conditions, if configured
let terms = &services.config.registration_terms; let terms = &services.config.registration_terms;
if !terms.is_empty() { if !terms.documents.is_empty() {
let mut terms = let mut terms_map = HashMap::new();
serde_json::to_value(terms.clone()).expect("failed to serialize terms");
// Insert a dummy `version` field for (id, document) in &terms.documents {
for (_, documents) in terms.as_object_mut().unwrap() { terms_map.insert(id.to_owned(), serde_json::json!({
let documents = documents.as_object_mut().unwrap(); terms.language.clone(): serde_json::to_value(document).expect("should be able to serialize document")
}));
documents.insert("version".to_owned(), "latest".into());
} }
terms_map.insert("version".to_owned(), "latest".into());
params.insert( params.insert(
AuthType::Terms.as_str().to_owned(), AuthType::Terms.as_str().to_owned(),
serde_json::json!({ serde_json::json!({
"policies": terms, "policies": terms_map,
}), }),
); );
@@ -421,81 +258,6 @@ async fn create_registration_uiaa_session(
Ok((flows, params)) 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` /// # `POST /_matrix/client/v3/register/email/requestToken`
/// ///
/// Requests a validation email for the purpose of registering a new account. /// Requests a validation email for the purpose of registering a new account.
+23 -13
View File
@@ -11,9 +11,9 @@ use ruma::{
}, },
thirdparty::{Medium, ThirdPartyIdentifierInit}, thirdparty::{Medium, ThirdPartyIdentifierInit},
}; };
use service::{mailer::messages, uiaa::Identity}; use service::mailer::messages;
use crate::Ruma; use crate::{Ruma, router::ClientIdentity};
/// # `GET _matrix/client/v3/account/3pid` /// # `GET _matrix/client/v3/account/3pid`
/// ///
@@ -22,7 +22,7 @@ pub(crate) async fn third_party_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_3pids::v3::Request>, body: Ruma<get_3pids::v3::Request>,
) -> Result<get_3pids::v3::Response> { ) -> Result<get_3pids::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let mut threepids = vec![]; let mut threepids = vec![];
if let Some(email) = services if let Some(email) = services
@@ -53,6 +53,14 @@ pub(crate) async fn request_3pid_management_token_via_email_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<request_3pid_management_token_via_email::v3::Request>, body: Ruma<request_3pid_management_token_via_email::v3::Request>,
) -> Result<request_3pid_management_token_via_email::v3::Response> { ) -> 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() { if !services.threepid.email_requirement().may_change() {
return Err!(Request(Forbidden("You may not change your email address."))); return Err!(Request(Forbidden("You may not change your email address.")));
} }
@@ -76,7 +84,7 @@ pub(crate) async fn request_3pid_management_token_via_email_route(
Mailbox::new(None, email), Mailbox::new(None, email),
|verification_link| messages::ChangeEmail { |verification_link| messages::ChangeEmail {
server_name: services.config.server_name.as_str(), server_name: services.config.server_name.as_str(),
user_id: body.sender_user.as_deref(), user_id: Some(sender_user),
verification_link, verification_link,
}, },
&body.client_secret, &body.client_secret,
@@ -107,8 +115,6 @@ pub(crate) async fn add_3pid_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<add_3pid::v3::Request>, body: Ruma<add_3pid::v3::Request>,
) -> Result<add_3pid::v3::Response> { ) -> Result<add_3pid::v3::Response> {
let sender_user = body.sender_user();
if !services.threepid.email_requirement().may_change() { if !services.threepid.email_requirement().may_change() {
return Err!(Request(Forbidden("You may not change your email address."))); return Err!(Request(Forbidden("You may not change your email address.")));
} }
@@ -116,18 +122,24 @@ pub(crate) async fn add_3pid_route(
// Require password auth to add an email // Require password auth to add an email
let _ = services let _ = services
.uiaa .uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user))) .authenticate_password(
&body.auth,
body.identity.sender_user(),
body.identity.sender_device(),
None,
)
.await?; .await?;
let email = services let email = services
.threepid .threepid
.consume_valid_session(&body.sid, &body.client_secret) .get_valid_session(&body.sid, &body.client_secret)
.await .await
.map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?; .map_err(|message| err!(Request(ThreepidAuthFailed("{message}"))))?
.consume();
services services
.threepid .threepid
.associate_localpart_email(sender_user.localpart(), &email) .associate_localpart_email(body.identity.sender_user().localpart(), &email)
.await?; .await?;
Ok(add_3pid::v3::Response::new()) Ok(add_3pid::v3::Response::new())
@@ -138,8 +150,6 @@ pub(crate) async fn delete_3pid_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_3pid::v3::Request>, body: Ruma<delete_3pid::v3::Request>,
) -> Result<delete_3pid::v3::Response> { ) -> Result<delete_3pid::v3::Response> {
let sender_user = body.sender_user();
if body.medium != Medium::Email { if body.medium != Medium::Email {
return Ok(delete_3pid::v3::Response::new(ThirdPartyIdRemovalStatus::NoSupport)); return Ok(delete_3pid::v3::Response::new(ThirdPartyIdRemovalStatus::NoSupport));
} }
@@ -150,7 +160,7 @@ pub(crate) async fn delete_3pid_route(
if services if services
.threepid .threepid
.disassociate_localpart_email(sender_user.localpart()) .disassociate_localpart_email(body.identity.sender_user().localpart())
.await .await
.is_none() .is_none()
{ {
+8 -8
View File
@@ -22,9 +22,9 @@ pub(crate) async fn set_global_account_data_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_global_account_data::v3::Request>, body: Ruma<set_global_account_data::v3::Request>,
) -> Result<set_global_account_data::v3::Response> { ) -> Result<set_global_account_data::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if sender_user != body.user_id && body.appservice_info.is_none() { if sender_user != body.user_id && !body.identity.is_appservice() {
return Err!(Request(Forbidden("You cannot set account data for other users."))); 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>, State(services): State<crate::State>,
body: Ruma<set_room_account_data::v3::Request>, body: Ruma<set_room_account_data::v3::Request>,
) -> Result<set_room_account_data::v3::Response> { ) -> Result<set_room_account_data::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if sender_user != body.user_id && body.appservice_info.is_none() { if sender_user != body.user_id && !body.identity.is_appservice() {
return Err!(Request(Forbidden("You cannot set account data for other users."))); 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>, State(services): State<crate::State>,
body: Ruma<get_global_account_data::v3::Request>, body: Ruma<get_global_account_data::v3::Request>,
) -> Result<get_global_account_data::v3::Response> { ) -> Result<get_global_account_data::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if sender_user != body.user_id && body.appservice_info.is_none() { if sender_user != body.user_id && !body.identity.is_appservice() {
return Err!(Request(Forbidden("You cannot get account data of other users."))); 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>, State(services): State<crate::State>,
body: Ruma<get_room_account_data::v3::Request>, body: Ruma<get_room_account_data::v3::Request>,
) -> Result<get_room_account_data::v3::Response> { ) -> Result<get_room_account_data::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if sender_user != body.user_id && body.appservice_info.is_none() { if sender_user != body.user_id && !body.identity.is_appservice() {
return Err!(Request(Forbidden("You cannot get account data of other users."))); return Err!(Request(Forbidden("You cannot get account data of other users.")));
} }
+89
View File
@@ -0,0 +1,89 @@
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))
}
+3 -1
View File
@@ -1,3 +1,5 @@
mod lock;
pub(crate) mod site;
mod suspend; mod suspend;
pub(crate) use self::suspend::*; pub(crate) use self::{lock::*, suspend::*};
+2
View File
@@ -0,0 +1,2 @@
pub(crate) mod rooms;
pub(crate) mod users;
@@ -6,14 +6,14 @@ use ruminuwuity::admin::continuwuity::rooms;
use crate::{Ruma, client::leave_room}; use crate::{Ruma, client::leave_room};
/// # `PUT /_continuwuity/admin/rooms/{roomID}/ban` /// # `PUT /_continuwuity/admin/v1/rooms/{roomID}/ban`
/// ///
/// Bans or unbans a room. /// Bans or unbans a room.
pub(crate) async fn ban_room( pub(crate) async fn ban_room(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<rooms::ban::v1::Request>, body: Ruma<rooms::ban::v1::Request>,
) -> Result<rooms::ban::v1::Response> { ) -> Result<rooms::ban::v1::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if !services.users.is_admin(sender_user).await { if !services.users.is_admin(sender_user).await {
return Err!(Request(Forbidden("Only server administrators can use this endpoint"))); return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
} }
+188
View File
@@ -0,0 +1,188 @@
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
@@ -0,0 +1,5 @@
mod ban;
mod list;
pub(crate) use ban::ban_room;
pub(crate) use list::*;
+119
View File
@@ -0,0 +1,119 @@
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
@@ -0,0 +1,48 @@
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
@@ -0,0 +1,5 @@
mod create;
mod list;
pub(crate) use create::*;
pub(crate) use list::*;
+14 -13
View File
@@ -1,7 +1,7 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Result}; use conduwuit::{Err, Result};
use futures::future::{join, join3}; use futures::future::{join, join3};
use ruminuwuity::admin::{get_suspended, set_suspended}; use ruma::api::client::admin::{is_user_suspended, suspend_user};
use crate::Ruma; use crate::Ruma;
@@ -10,12 +10,13 @@ use crate::Ruma;
/// Check the suspension status of a target user /// Check the suspension status of a target user
pub(crate) async fn get_suspended_status( pub(crate) async fn get_suspended_status(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_suspended::v1::Request>, body: Ruma<is_user_suspended::v1::Request>,
) -> Result<get_suspended::v1::Response> { ) -> Result<is_user_suspended::v1::Response> {
let sender_user = body.sender_user(); let (admin, active) = join(
services.users.is_admin(body.identity.sender_user()),
let (admin, active) = services.users.is_active(&body.user_id),
join(services.users.is_admin(sender_user), services.users.is_active(&body.user_id)).await; )
.await;
if !admin { if !admin {
return Err!(Request(Forbidden("Only server administrators can use this endpoint"))); return Err!(Request(Forbidden("Only server administrators can use this endpoint")));
} }
@@ -25,7 +26,7 @@ pub(crate) async fn get_suspended_status(
if !active { if !active {
return Err!(Request(NotFound("Unknown user"))); return Err!(Request(NotFound("Unknown user")));
} }
Ok(get_suspended::v1::Response::new( Ok(is_user_suspended::v1::Response::new(
services.users.is_suspended(&body.user_id).await?, services.users.is_suspended(&body.user_id).await?,
)) ))
} }
@@ -35,9 +36,9 @@ pub(crate) async fn get_suspended_status(
/// Set the suspension status of a target user /// Set the suspension status of a target user
pub(crate) async fn put_suspended_status( pub(crate) async fn put_suspended_status(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_suspended::v1::Request>, body: Ruma<suspend_user::v1::Request>,
) -> Result<set_suspended::v1::Response> { ) -> Result<suspend_user::v1::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let (sender_admin, active, target_admin) = join3( let (sender_admin, active, target_admin) = join3(
services.users.is_admin(sender_user), services.users.is_admin(sender_user),
@@ -63,7 +64,7 @@ pub(crate) async fn put_suspended_status(
} }
if services.users.is_suspended(&body.user_id).await? == body.suspended { if services.users.is_suspended(&body.user_id).await? == body.suspended {
// No change // No change
return Ok(set_suspended::v1::Response::new(body.suspended)); return Ok(suspend_user::v1::Response::new(body.suspended));
} }
let action = if body.suspended { let action = if body.suspended {
@@ -85,5 +86,5 @@ pub(crate) async fn put_suspended_status(
.await; .await;
} }
Ok(set_suspended::v1::Response::new(body.suspended)) Ok(suspend_user::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>, State(services): State<crate::State>,
body: Ruma<create_alias::v3::Request>, body: Ruma<create_alias::v3::Request>,
) -> Result<create_alias::v3::Response> { ) -> Result<create_alias::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
@@ -19,7 +19,7 @@ pub(crate) async fn create_alias_route(
services services
.rooms .rooms
.alias .alias
.appservice_checks(&body.room_alias, &body.appservice_info) .appservice_checks(&body.room_alias, body.identity.appservice_info())
.await?; .await?;
// this isn't apart of alias_checks or delete alias route because we should // 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>, State(services): State<crate::State>,
body: Ruma<delete_alias::v3::Request>, body: Ruma<delete_alias::v3::Request>,
) -> Result<delete_alias::v3::Response> { ) -> Result<delete_alias::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
@@ -67,7 +67,7 @@ pub(crate) async fn delete_alias_route(
services services
.rooms .rooms
.alias .alias
.appservice_checks(&body.room_alias, &body.appservice_info) .appservice_checks(&body.room_alias, body.identity.appservice_info())
.await?; .await?;
services services
+2 -4
View File
@@ -1,5 +1,5 @@
use axum::extract::State; use axum::extract::State;
use conduwuit::{Err, Result, err}; use conduwuit::{Err, Result};
use ruma::{ use ruma::{
api::{appservice::ping, client::appservice::request_ping}, api::{appservice::ping, client::appservice::request_ping},
assign, assign,
@@ -15,9 +15,7 @@ pub(crate) async fn appservice_ping(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<request_ping::v1::Request>, body: Ruma<request_ping::v1::Request>,
) -> Result<request_ping::v1::Response> { ) -> Result<request_ping::v1::Response> {
let appservice_info = body.appservice_info.as_ref().ok_or_else(|| { let appservice_info = &body.identity;
err!(Request(Forbidden("This endpoint can only be called by appservices.")))
})?;
if body.appservice_id != appservice_info.registration.id { if body.appservice_id != appservice_info.registration.id {
return Err!(Request(Forbidden( return Err!(Request(Forbidden(
+50 -26
View File
@@ -25,7 +25,7 @@ pub(crate) async fn create_backup_version_route(
) -> Result<create_backup_version::v3::Response> { ) -> Result<create_backup_version::v3::Response> {
let version = services let version = services
.key_backups .key_backups
.create_backup(body.sender_user(), &body.algorithm)?; .create_backup(body.identity.sender_user(), &body.algorithm)?;
Ok(create_backup_version::v3::Response::new(version)) 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> { ) -> Result<update_backup_version::v3::Response> {
services services
.key_backups .key_backups
.update_backup(body.sender_user(), &body.version, &body.algorithm) .update_backup(body.identity.sender_user(), &body.version, &body.algorithm)
.await?; .await?;
Ok(update_backup_version::v3::Response::new()) 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> { ) -> Result<get_latest_backup_info::v3::Response> {
let (version, algorithm) = services let (version, algorithm) = services
.key_backups .key_backups
.get_latest_backup(body.sender_user()) .get_latest_backup(body.identity.sender_user())
.await .await
.map_err(|_| err!(Request(NotFound("Key backup does not exist."))))?; .map_err(|_| err!(Request(NotFound("Key backup does not exist."))))?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &version).await; let (count, etag) = get_count_etag(&services, body.identity.sender_user(), &version).await;
Ok(get_latest_backup_info::v3::Response::new(algorithm, count, etag, version)) Ok(get_latest_backup_info::v3::Response::new(algorithm, count, etag, version))
} }
@@ -73,13 +73,14 @@ pub(crate) async fn get_backup_info_route(
) -> Result<get_backup_info::v3::Response> { ) -> Result<get_backup_info::v3::Response> {
let algorithm = services let algorithm = services
.key_backups .key_backups
.get_backup(body.sender_user(), &body.version) .get_backup(body.identity.sender_user(), &body.version)
.await .await
.map_err(|_| { .map_err(|_| {
err!(Request(NotFound("Key backup does not exist at version {:?}", body.version))) err!(Request(NotFound("Key backup does not exist at version {:?}", body.version)))
})?; })?;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await; let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
Ok(get_backup_info::v3::Response::new(algorithm, count, etag, body.version.clone())) Ok(get_backup_info::v3::Response::new(algorithm, count, etag, body.version.clone()))
} }
@@ -96,7 +97,7 @@ pub(crate) async fn delete_backup_version_route(
) -> Result<delete_backup_version::v3::Response> { ) -> Result<delete_backup_version::v3::Response> {
services services
.key_backups .key_backups
.delete_backup(body.sender_user(), &body.version) .delete_backup(body.identity.sender_user(), &body.version)
.await; .await;
Ok(delete_backup_version::v3::Response::new()) Ok(delete_backup_version::v3::Response::new())
@@ -116,7 +117,7 @@ pub(crate) async fn add_backup_keys_route(
) -> Result<add_backup_keys::v3::Response> { ) -> Result<add_backup_keys::v3::Response> {
if services if services
.key_backups .key_backups
.get_latest_backup_version(body.sender_user()) .get_latest_backup_version(body.identity.sender_user())
.await .await
.is_ok_and(|version| version != body.version) .is_ok_and(|version| version != body.version)
{ {
@@ -129,12 +130,19 @@ pub(crate) async fn add_backup_keys_route(
for (session_id, key_data) in &room.sessions { for (session_id, key_data) in &room.sessions {
services services
.key_backups .key_backups
.add_key(body.sender_user(), &body.version, room_id, session_id, key_data) .add_key(
body.identity.sender_user(),
&body.version,
room_id,
session_id,
key_data,
)
.await?; .await?;
} }
} }
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await; let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
Ok(add_backup_keys::v3::Response::new(etag, count)) Ok(add_backup_keys::v3::Response::new(etag, count))
} }
@@ -153,7 +161,7 @@ pub(crate) async fn add_backup_keys_for_room_route(
) -> Result<add_backup_keys_for_room::v3::Response> { ) -> Result<add_backup_keys_for_room::v3::Response> {
if services if services
.key_backups .key_backups
.get_latest_backup_version(body.sender_user()) .get_latest_backup_version(body.identity.sender_user())
.await .await
.is_ok_and(|version| version != body.version) .is_ok_and(|version| version != body.version)
{ {
@@ -165,11 +173,18 @@ pub(crate) async fn add_backup_keys_for_room_route(
for (session_id, key_data) in &body.sessions { for (session_id, key_data) in &body.sessions {
services services
.key_backups .key_backups
.add_key(body.sender_user(), &body.version, &body.room_id, session_id, key_data) .add_key(
body.identity.sender_user(),
&body.version,
&body.room_id,
session_id,
key_data,
)
.await?; .await?;
} }
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await; let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
Ok(add_backup_keys_for_room::v3::Response::new(etag, count)) Ok(add_backup_keys_for_room::v3::Response::new(etag, count))
} }
@@ -188,7 +203,7 @@ pub(crate) async fn add_backup_keys_for_session_route(
) -> Result<add_backup_keys_for_session::v3::Response> { ) -> Result<add_backup_keys_for_session::v3::Response> {
if services if services
.key_backups .key_backups
.get_latest_backup_version(body.sender_user()) .get_latest_backup_version(body.identity.sender_user())
.await .await
.is_ok_and(|version| version != body.version) .is_ok_and(|version| version != body.version)
{ {
@@ -201,7 +216,7 @@ pub(crate) async fn add_backup_keys_for_session_route(
let mut ok_to_replace = true; let mut ok_to_replace = true;
if let Some(old_key) = &services if let Some(old_key) = &services
.key_backups .key_backups
.get_session(body.sender_user(), &body.version, &body.room_id, &body.session_id) .get_session(body.identity.sender_user(), &body.version, &body.room_id, &body.session_id)
.await .await
.ok() .ok()
{ {
@@ -260,7 +275,7 @@ pub(crate) async fn add_backup_keys_for_session_route(
services services
.key_backups .key_backups
.add_key( .add_key(
body.sender_user(), body.identity.sender_user(),
&body.version, &body.version,
&body.room_id, &body.room_id,
&body.session_id, &body.session_id,
@@ -269,7 +284,8 @@ pub(crate) async fn add_backup_keys_for_session_route(
.await?; .await?;
} }
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await; let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
Ok(add_backup_keys_for_session::v3::Response::new(etag, count)) Ok(add_backup_keys_for_session::v3::Response::new(etag, count))
} }
@@ -283,7 +299,7 @@ pub(crate) async fn get_backup_keys_route(
) -> Result<get_backup_keys::v3::Response> { ) -> Result<get_backup_keys::v3::Response> {
let rooms = services let rooms = services
.key_backups .key_backups
.get_all(body.sender_user(), &body.version) .get_all(body.identity.sender_user(), &body.version)
.await; .await;
Ok(get_backup_keys::v3::Response::new(rooms)) Ok(get_backup_keys::v3::Response::new(rooms))
@@ -298,7 +314,7 @@ pub(crate) async fn get_backup_keys_for_room_route(
) -> Result<get_backup_keys_for_room::v3::Response> { ) -> Result<get_backup_keys_for_room::v3::Response> {
let sessions = services let sessions = services
.key_backups .key_backups
.get_room(body.sender_user(), &body.version, &body.room_id) .get_room(body.identity.sender_user(), &body.version, &body.room_id)
.await; .await;
Ok(get_backup_keys_for_room::v3::Response::new(sessions)) Ok(get_backup_keys_for_room::v3::Response::new(sessions))
@@ -313,7 +329,7 @@ pub(crate) async fn get_backup_keys_for_session_route(
) -> Result<get_backup_keys_for_session::v3::Response> { ) -> Result<get_backup_keys_for_session::v3::Response> {
let key_data = services let key_data = services
.key_backups .key_backups
.get_session(body.sender_user(), &body.version, &body.room_id, &body.session_id) .get_session(body.identity.sender_user(), &body.version, &body.room_id, &body.session_id)
.await .await
.map_err(|_| { .map_err(|_| {
err!(Request(NotFound(debug_error!("Backup key not found for this user's session.")))) err!(Request(NotFound(debug_error!("Backup key not found for this user's session."))))
@@ -331,10 +347,11 @@ pub(crate) async fn delete_backup_keys_route(
) -> Result<delete_backup_keys::v3::Response> { ) -> Result<delete_backup_keys::v3::Response> {
services services
.key_backups .key_backups
.delete_all_keys(body.sender_user(), &body.version) .delete_all_keys(body.identity.sender_user(), &body.version)
.await; .await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await; let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
Ok(delete_backup_keys::v3::Response::new(etag, count)) Ok(delete_backup_keys::v3::Response::new(etag, count))
} }
@@ -348,10 +365,11 @@ pub(crate) async fn delete_backup_keys_for_room_route(
) -> Result<delete_backup_keys_for_room::v3::Response> { ) -> Result<delete_backup_keys_for_room::v3::Response> {
services services
.key_backups .key_backups
.delete_room_keys(body.sender_user(), &body.version, &body.room_id) .delete_room_keys(body.identity.sender_user(), &body.version, &body.room_id)
.await; .await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await; let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
Ok(delete_backup_keys_for_room::v3::Response::new(etag, count)) Ok(delete_backup_keys_for_room::v3::Response::new(etag, count))
} }
@@ -365,10 +383,16 @@ pub(crate) async fn delete_backup_keys_for_session_route(
) -> Result<delete_backup_keys_for_session::v3::Response> { ) -> Result<delete_backup_keys_for_session::v3::Response> {
services services
.key_backups .key_backups
.delete_room_key(body.sender_user(), &body.version, &body.room_id, &body.session_id) .delete_room_key(
body.identity.sender_user(),
&body.version,
&body.room_id,
&body.session_id,
)
.await; .await;
let (count, etag) = get_count_etag(&services, body.sender_user(), &body.version).await; let (count, etag) =
get_count_etag(&services, body.identity.sender_user(), &body.version).await;
Ok(delete_backup_keys_for_session::v3::Response::new(etag, count)) Ok(delete_backup_keys_for_session::v3::Response::new(etag, count))
} }
+1 -5
View File
@@ -48,11 +48,7 @@ pub(crate) async fn get_capabilities_route(
json!({"enabled": services.config.forget_forced_upon_leave}), json!({"enabled": services.config.forget_forced_upon_leave}),
)?; )?;
if services if services.users.is_admin(body.identity.sender_user()).await {
.users
.is_admin(body.sender_user.as_ref().unwrap())
.await
{
// Advertise suspension API // Advertise suspension API
capabilities.set("uk.timedout.msc4323", json!({"suspend": true, "lock": false}))?; 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>, State(services): State<crate::State>,
body: Ruma<get_context::v3::Request>, body: Ruma<get_context::v3::Request>,
) -> Result<get_context::v3::Response> { ) -> Result<get_context::v3::Response> {
let sender = body.sender(); let sender_user = body.identity.sender_user();
let (sender_user, sender_device) = sender; let sender_device = body.identity.sender_device();
let room_id = &body.room_id; let room_id = &body.room_id;
let event_id = &body.event_id; let event_id = &body.event_id;
let filter = &body.filter; let filter = &body.filter;
@@ -143,7 +143,7 @@ pub(crate) async fn get_context_route(
let lazy_loading_context = lazy_loading::Context { let lazy_loading_context = lazy_loading::Context {
user_id: sender_user, user_id: sender_user,
device_id: Some(sender_device), device_id: sender_device,
room_id, room_id,
token: Some(base_count.into_unsigned()), token: Some(base_count.into_unsigned()),
options: Some(&filter.lazy_load_options), options: Some(&filter.lazy_load_options),
+5 -10
View File
@@ -25,16 +25,11 @@ pub(crate) async fn put_dehydrated_device_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<put_dehydrated_device::Request>, body: Ruma<put_dehydrated_device::Request>,
) -> Result<put_dehydrated_device::Response> { ) -> Result<put_dehydrated_device::Response> {
let sender_user = body let device_id = body.device_id.clone();
.sender_user
.as_deref()
.expect("AccessToken authentication required");
let device_id = body.body.device_id.clone();
services services
.users .users
.set_dehydrated_device(sender_user, body.body) .set_dehydrated_device(body.identity.sender_user(), body.body)
.await?; .await?;
Ok(put_dehydrated_device::Response::new(device_id)) Ok(put_dehydrated_device::Response::new(device_id))
@@ -49,7 +44,7 @@ pub(crate) async fn delete_dehydrated_device_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<delete_dehydrated_device::Request>, body: Ruma<delete_dehydrated_device::Request>,
) -> Result<delete_dehydrated_device::Response> { ) -> Result<delete_dehydrated_device::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let device_id = services.users.get_dehydrated_device_id(sender_user).await?; let device_id = services.users.get_dehydrated_device_id(sender_user).await?;
@@ -67,7 +62,7 @@ pub(crate) async fn get_dehydrated_device_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<get_dehydrated_device::Request>, body: Ruma<get_dehydrated_device::Request>,
) -> Result<get_dehydrated_device::Response> { ) -> Result<get_dehydrated_device::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let device = services.users.get_dehydrated_device(sender_user).await?; let device = services.users.get_dehydrated_device(sender_user).await?;
@@ -83,7 +78,7 @@ pub(crate) async fn get_dehydrated_events_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<get_events::Request>, body: Ruma<get_events::Request>,
) -> Result<get_events::Response> { ) -> Result<get_events::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let device_id = &body.body.device_id; let device_id = &body.body.device_id;
let existing_id = services.users.get_dehydrated_device_id(sender_user).await; let existing_id = services.users.get_dehydrated_device_id(sender_user).await;
+11 -13
View File
@@ -8,7 +8,6 @@ use ruma::{
self, delete_device, delete_devices, get_device, get_devices, update_device, self, delete_device, delete_devices, get_device, get_devices, update_device,
}, },
}; };
use service::uiaa::Identity;
use crate::{Ruma, client::DEVICE_ID_LENGTH}; use crate::{Ruma, client::DEVICE_ID_LENGTH};
@@ -21,7 +20,7 @@ pub(crate) async fn get_devices_route(
) -> Result<get_devices::v3::Response> { ) -> Result<get_devices::v3::Response> {
let devices: Vec<device::Device> = services let devices: Vec<device::Device> = services
.users .users
.all_devices_metadata(body.sender_user()) .all_devices_metadata(body.identity.sender_user())
.collect() .collect()
.await; .await;
@@ -37,7 +36,7 @@ pub(crate) async fn get_device_route(
) -> Result<get_device::v3::Response> { ) -> Result<get_device::v3::Response> {
let device = services let device = services
.users .users
.get_device_metadata(body.sender_user(), &body.body.device_id) .get_device_metadata(body.identity.sender_user(), &body.body.device_id)
.await .await
.map_err(|_| err!(Request(NotFound("Device not found."))))?; .map_err(|_| err!(Request(NotFound("Device not found."))))?;
@@ -53,8 +52,8 @@ pub(crate) async fn update_device_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<update_device::v3::Request>, body: Ruma<update_device::v3::Request>,
) -> Result<update_device::v3::Response> { ) -> Result<update_device::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let appservice = body.appservice_info.as_ref(); let appservice = body.identity.appservice_info();
match services match services
.users .users
@@ -95,6 +94,7 @@ pub(crate) async fn update_device_route(
&device_id, &device_id,
&appservice.registration.as_token, &appservice.registration.as_token,
None, None,
None,
Some(client.to_string()), Some(client.to_string()),
) )
.await?; .await?;
@@ -118,15 +118,14 @@ pub(crate) async fn delete_device_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_device::v3::Request>, body: Ruma<delete_device::v3::Request>,
) -> Result<delete_device::v3::Response> { ) -> Result<delete_device::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let appservice = body.appservice_info.as_ref();
// Appservices get to skip UIAA for this endpoint // Appservices get to skip UIAA for this endpoint
if appservice.is_none() { if let Some(sender_device) = body.identity.sender_device() {
// Prompt the user to confirm with their password using UIAA // Prompt the user to confirm with their password using UIAA
let _ = services let _ = services
.uiaa .uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user))) .authenticate_password(&body.auth, sender_user, Some(sender_device), None)
.await?; .await?;
} }
@@ -154,15 +153,14 @@ pub(crate) async fn delete_devices_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_devices::v3::Request>, body: Ruma<delete_devices::v3::Request>,
) -> Result<delete_devices::v3::Response> { ) -> Result<delete_devices::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let appservice = body.appservice_info.as_ref();
// Appservices get to skip UIAA for this endpoint // Appservices get to skip UIAA for this endpoint
if appservice.is_none() { if let Some(sender_device) = body.identity.sender_device() {
// Prompt the user to confirm with their password using UIAA // Prompt the user to confirm with their password using UIAA
let _ = services let _ = services
.uiaa .uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user))) .authenticate_password(&body.auth, sender_user, Some(sender_device), None)
.await?; .await?;
} }
+2 -2
View File
@@ -112,7 +112,7 @@ pub(crate) async fn set_room_visibility_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<set_room_visibility::v3::Request>, body: Ruma<set_room_visibility::v3::Request>,
) -> Result<set_room_visibility::v3::Response> { ) -> Result<set_room_visibility::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if !services.rooms.metadata.exists(&body.room_id).await { if !services.rooms.metadata.exists(&body.room_id).await {
// Return 404 if the room doesn't exist // Return 404 if the room doesn't exist
@@ -130,7 +130,7 @@ pub(crate) async fn set_room_visibility_route(
| room::Visibility::Public => { | room::Visibility::Public => {
if services.server.config.lockdown_public_room_directory if services.server.config.lockdown_public_room_directory
&& !services.users.is_admin(sender_user).await && !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none() && !body.identity.is_appservice()
{ {
info!( info!(
"Non-admin user {sender_user} tried to publish {0} to the room directory \ "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> { ) -> Result<get_filter::v3::Response> {
services services
.users .users
.get_filter(body.sender_user(), &body.filter_id) .get_filter(body.identity.sender_user(), &body.filter_id)
.await .await
.map(get_filter::v3::Response::new) .map(get_filter::v3::Response::new)
.map_err(|_| err!(Request(NotFound("Filter not found.")))) .map_err(|_| err!(Request(NotFound("Filter not found."))))
@@ -30,7 +30,7 @@ pub(crate) async fn create_filter_route(
) -> Result<create_filter::v3::Response> { ) -> Result<create_filter::v3::Response> {
let filter_id = services let filter_id = services
.users .users
.create_filter(body.sender_user(), &body.filter); .create_filter(body.identity.sender_user(), &body.filter);
Ok(create_filter::v3::Response::new(filter_id)) Ok(create_filter::v3::Response::new(filter_id))
} }
+13 -7
View File
@@ -26,7 +26,7 @@ use ruma::{
serde::Raw, serde::Raw,
}; };
use serde_json::json; use serde_json::json;
use service::uiaa::Identity; use service::oauth::OAuthTicket;
use crate::Ruma; use crate::Ruma;
@@ -41,7 +41,8 @@ pub(crate) async fn upload_keys_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<upload_keys::v3::Request>, body: Ruma<upload_keys::v3::Request>,
) -> Result<upload_keys::v3::Response> { ) -> Result<upload_keys::v3::Response> {
let (sender_user, sender_device) = body.sender(); let sender_user = body.identity.sender_user();
let sender_device = body.identity.expect_sender_device()?;
for (key_id, one_time_key) in &body.one_time_keys { for (key_id, one_time_key) in &body.one_time_keys {
if one_time_key if one_time_key
@@ -154,7 +155,7 @@ pub(crate) async fn get_keys_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_keys::v3::Request>, body: Ruma<get_keys::v3::Request>,
) -> Result<get_keys::v3::Response> { ) -> Result<get_keys::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
get_keys_helper( get_keys_helper(
&services, &services,
@@ -191,7 +192,7 @@ pub(crate) async fn upload_signing_keys_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<upload_signing_keys::v3::Request>, body: Ruma<upload_signing_keys::v3::Request>,
) -> Result<upload_signing_keys::v3::Response> { ) -> Result<upload_signing_keys::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if uiaa_needed_to_upload_keys( if uiaa_needed_to_upload_keys(
services, services,
@@ -204,7 +205,12 @@ pub(crate) async fn upload_signing_keys_route(
{ {
let _ = services let _ = services
.uiaa .uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user))) .authenticate_password(
&body.auth,
sender_user,
body.identity.sender_device(),
Some(OAuthTicket::CrossSigningReset),
)
.await?; .await?;
} }
@@ -287,7 +293,7 @@ pub(crate) async fn upload_signatures_route(
return Ok(upload_signatures::v3::Response::new()); return Ok(upload_signatures::v3::Response::new());
} }
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
for (user_id, keys) in &body.signed_keys { for (user_id, keys) in &body.signed_keys {
for (key_id, key) in keys { for (key_id, key) in keys {
@@ -340,7 +346,7 @@ pub(crate) async fn get_key_changes_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_key_changes::v3::Request>, body: Ruma<get_key_changes::v3::Request>,
) -> Result<get_key_changes::v3::Response> { ) -> Result<get_key_changes::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let mut device_list_updates = HashSet::new(); 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, ClientIp(client): ClientIp,
body: Ruma<create_content::v3::Request>, body: Ruma<create_content::v3::Request>,
) -> Result<create_content::v3::Response> { ) -> Result<create_content::v3::Response> {
let user = body.sender_user(); let user = body.identity.sender_user();
if services.users.is_suspended(user).await? { if services.users.is_suspended(user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); 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, ClientIp(client): ClientIp,
body: Ruma<get_content_thumbnail::v1::Request>, body: Ruma<get_content_thumbnail::v1::Request>,
) -> Result<get_content_thumbnail::v1::Response> { ) -> Result<get_content_thumbnail::v1::Response> {
let user = body.sender_user(); let user = body.identity.sender_user();
let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?; let dim = Dim::from_ruma(body.width, body.height, body.method.clone())?;
let mxc = Mxc { let mxc = Mxc {
@@ -142,7 +142,7 @@ pub(crate) async fn get_content_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<get_content::v1::Request>, body: Ruma<get_content::v1::Request>,
) -> Result<get_content::v1::Response> { ) -> Result<get_content::v1::Response> {
let user = body.sender_user(); let user = body.identity.sender_user();
let mxc = Mxc { let mxc = Mxc {
server_name: &body.server_name, server_name: &body.server_name,
@@ -189,7 +189,7 @@ pub(crate) async fn get_content_as_filename_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<get_content_as_filename::v1::Request>, body: Ruma<get_content_as_filename::v1::Request>,
) -> Result<get_content_as_filename::v1::Response> { ) -> Result<get_content_as_filename::v1::Response> {
let user = body.sender_user(); let user = body.identity.sender_user();
let mxc = Mxc { let mxc = Mxc {
server_name: &body.server_name, server_name: &body.server_name,
@@ -240,7 +240,7 @@ pub(crate) async fn get_media_preview_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<get_media_preview::v1::Request>, body: Ruma<get_media_preview::v1::Request>,
) -> Result<get_media_preview::v1::Response> { ) -> Result<get_media_preview::v1::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let url = &body.url; let url = &body.url;
let url = Url::parse(&body.url).map_err(|e| { 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, ClientIp(client): ClientIp,
body: Ruma<get_media_preview::v3::Request>, body: Ruma<get_media_preview::v3::Request>,
) -> Result<get_media_preview::v3::Response> { ) -> Result<get_media_preview::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let url = &body.url; let url = &body.url;
let url = Url::parse(&body.url).map_err(|e| { 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>, State(services): State<crate::State>,
body: Ruma<ban_user::v3::Request>, body: Ruma<ban_user::v3::Request>,
) -> Result<ban_user::v3::Response> { ) -> Result<ban_user::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if sender_user == body.user_id { if sender_user == body.user_id {
return Err!(Request(Forbidden("You cannot ban yourself."))); 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>, State(services): State<crate::State>,
body: Ruma<forget_room::v3::Request>, body: Ruma<forget_room::v3::Request>,
) -> Result<forget_room::v3::Response> { ) -> Result<forget_room::v3::Response> {
let user_id = body.sender_user(); let user_id = body.identity.sender_user();
let room_id = &body.room_id; let room_id = &body.room_id;
let joined = services.rooms.state_cache.is_joined(user_id, 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, ClientIp(client): ClientIp,
body: Ruma<invite_user::v3::Request>, body: Ruma<invite_user::v3::Request>,
) -> Result<invite_user::v3::Response> { ) -> Result<invite_user::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
+2 -2
View File
@@ -28,7 +28,7 @@ pub(crate) async fn join_room_by_id_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<join_room_by_id::v3::Request>, body: Ruma<join_room_by_id::v3::Request>,
) -> Result<join_room_by_id::v3::Response> { ) -> Result<join_room_by_id::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
@@ -97,7 +97,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<join_room_by_id_or_alias::v3::Request>, body: Ruma<join_room_by_id_or_alias::v3::Request>,
) -> Result<join_room_by_id_or_alias::v3::Response> { ) -> Result<join_room_by_id_or_alias::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let body = &body.body; let body = &body.body;
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
+1 -1
View File
@@ -15,7 +15,7 @@ pub(crate) async fn kick_user_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<kick_user::v3::Request>, body: Ruma<kick_user::v3::Request>,
) -> Result<kick_user::v3::Response> { ) -> Result<kick_user::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
+1 -1
View File
@@ -51,7 +51,7 @@ pub(crate) async fn knock_room_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<knock_room::v3::Request>, body: Ruma<knock_room::v3::Request>,
) -> Result<knock_room::v3::Response> { ) -> Result<knock_room::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let body = &body.body; let body = &body.body;
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
+1 -1
View File
@@ -32,7 +32,7 @@ pub(crate) async fn leave_room_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<leave_room::v3::Request>, body: Ruma<leave_room::v3::Request>,
) -> Result<leave_room::v3::Response> { ) -> Result<leave_room::v3::Response> {
leave_room(&services, body.sender_user(), &body.room_id, body.reason.clone()) leave_room(&services, body.identity.sender_user(), &body.room_id, body.reason.clone())
.boxed() .boxed()
.await .await
.map(|()| leave_room::v3::Response::new()) .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>, State(services): State<crate::State>,
body: Ruma<get_member_events::v3::Request>, body: Ruma<get_member_events::v3::Request>,
) -> Result<get_member_events::v3::Response> { ) -> Result<get_member_events::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let membership = body.membership.as_ref(); let membership = body.membership.as_ref();
let not_membership = body.not_membership.as_ref(); let not_membership = body.not_membership.as_ref();
@@ -72,7 +72,7 @@ pub(crate) async fn joined_members_route(
if !services if !services
.rooms .rooms
.state_accessor .state_accessor
.user_can_see_state_events(body.sender_user(), &body.room_id) .user_can_see_state_events(body.identity.sender_user(), &body.room_id)
.await .await
{ {
return Err!(Request(Forbidden("You don't have permission to view this room."))); 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 let joined_rooms = services
.rooms .rooms
.state_cache .state_cache
.rooms_joined(body.sender_user()) .rooms_joined(body.identity.sender_user())
.collect() .collect()
.await; .await;
+1 -1
View File
@@ -14,7 +14,7 @@ pub(crate) async fn unban_user_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<unban_user::v3::Request>, body: Ruma<unban_user::v3::Request>,
) -> Result<unban_user::v3::Response> { ) -> Result<unban_user::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
+4 -15
View File
@@ -23,7 +23,7 @@ use conduwuit_service::{
}; };
use futures::{FutureExt, StreamExt, TryFutureExt, future::OptionFuture, pin_mut}; use futures::{FutureExt, StreamExt, TryFutureExt, future::OptionFuture, pin_mut};
use ruma::{ use ruma::{
DeviceId, RoomId, UserId, RoomId, UserId,
api::{ api::{
Direction, Direction,
client::{filter::RoomEventFilter, message::get_message_events}, client::{filter::RoomEventFilter, message::get_message_events},
@@ -37,7 +37,6 @@ use ruma::{
serde::Raw, serde::Raw,
}; };
use ruminuwuity::invite_permission_config::FilterLevel; use ruminuwuity::invite_permission_config::FilterLevel;
use tracing::warn;
use crate::Ruma; use crate::Ruma;
@@ -76,8 +75,8 @@ pub(crate) async fn get_message_events_route(
ClientIp(client_ip): ClientIp, ClientIp(client_ip): ClientIp,
body: Ruma<get_message_events::v3::Request>, body: Ruma<get_message_events::v3::Request>,
) -> Result<get_message_events::v3::Response> { ) -> Result<get_message_events::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let sender_device = body.sender_device.as_deref(); let sender_device = body.identity.sender_device();
let room_id = &body.room_id; let room_id = &body.room_id;
let filter = &body.filter; let filter = &body.filter;
@@ -158,17 +157,7 @@ pub(crate) async fn get_message_events_route(
let lazy_loading_context = lazy_loading::Context { let lazy_loading_context = lazy_loading::Context {
user_id: sender_user, user_id: sender_user,
device_id: sender_device.or_else(|| { device_id: sender_device,
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, room_id,
token: Some(from.into_unsigned()), token: Some(from.into_unsigned()),
options: Some(&filter.lazy_load_options), options: Some(&filter.lazy_load_options),
+3
View File
@@ -16,6 +16,7 @@ pub(super) mod media_legacy;
pub(super) mod membership; pub(super) mod membership;
pub(super) mod message; pub(super) mod message;
pub(super) mod mutual_rooms; pub(super) mod mutual_rooms;
pub(super) mod oauth;
pub(super) mod openid; pub(super) mod openid;
pub(super) mod presence; pub(super) mod presence;
pub(super) mod profile; pub(super) mod profile;
@@ -61,6 +62,7 @@ pub(super) use membership::*;
pub use membership::{leave_all_rooms, leave_room, remote_leave_room}; pub use membership::{leave_all_rooms, leave_room, remote_leave_room};
pub(super) use message::*; pub(super) use message::*;
pub(super) use mutual_rooms::*; pub(super) use mutual_rooms::*;
pub(super) use oauth::*;
pub(super) use openid::*; pub(super) use openid::*;
pub(super) use presence::*; pub(super) use presence::*;
pub(super) use profile::*; pub(super) use profile::*;
@@ -73,6 +75,7 @@ pub(super) use report::*;
pub(super) use room::*; pub(super) use room::*;
pub(super) use search::*; pub(super) use search::*;
pub(super) use send::*; pub(super) use send::*;
pub use session::handle_login;
pub(super) use session::*; pub(super) use session::*;
pub(super) use space::*; pub(super) use space::*;
pub(super) use state::*; 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>, State(services): State<crate::State>,
body: Ruma<mutual_rooms::unstable::Request>, body: Ruma<mutual_rooms::unstable::Request>,
) -> Result<mutual_rooms::unstable::Response> { ) -> Result<mutual_rooms::unstable::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if sender_user == body.user_id { if sender_user == body.user_id {
return Err!(Request(Unknown("You cannot request rooms in common with yourself."))); return Err!(Request(Unknown("You cannot request rooms in common with yourself.")));
+56
View File
@@ -0,0 +1,56 @@
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
@@ -0,0 +1,28 @@
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
@@ -0,0 +1,62 @@
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
@@ -0,0 +1,23 @@
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>, State(services): State<crate::State>,
body: Ruma<account::request_openid_token::v3::Request>, body: Ruma<account::request_openid_token::v3::Request>,
) -> Result<account::request_openid_token::v3::Response> { ) -> Result<account::request_openid_token::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if sender_user != body.user_id { if sender_user != body.user_id {
return Err!(Request(InvalidParam( return Err!(Request(InvalidParam(
+9 -3
View File
@@ -20,13 +20,19 @@ pub(crate) async fn set_presence_route(
return Err!(Request(Forbidden("Presence is disabled on this server"))); return Err!(Request(Forbidden("Presence is disabled on this server")));
} }
if body.sender_user() != body.user_id && body.appservice_info.is_none() { if body.identity.sender_user() != body.user_id && !body.identity.is_appservice() {
return Err!(Request(InvalidParam("Not allowed to set presence of other users"))); return Err!(Request(InvalidParam("Not allowed to set presence of other users")));
} }
services services
.presence .presence
.set_presence(body.sender_user(), &body.presence, None, None, body.status_msg.clone()) .set_presence(
body.identity.sender_user(),
&body.presence,
None,
None,
body.status_msg.clone(),
)
.await?; .await?;
Ok(set_presence::v3::Response::new()) Ok(set_presence::v3::Response::new())
@@ -49,7 +55,7 @@ pub(crate) async fn get_presence_route(
let has_shared_rooms = services let has_shared_rooms = services
.rooms .rooms
.state_cache .state_cache
.user_sees_user(body.sender_user(), &body.user_id) .user_sees_user(body.identity.sender_user(), &body.user_id)
.await; .await;
if has_shared_rooms { if has_shared_rooms {
+59 -129
View File
@@ -8,12 +8,12 @@ use ruma::{
UserId, UserId,
api::{ api::{
client::profile::{ client::profile::{
PropagateTo, delete_profile_field, get_profile, get_profile_field, set_profile_field, delete_profile_field, get_profile, get_profile_field, set_profile_field,
}, },
federation, federation,
}, },
assign, assign,
events::room::member::MembershipState, events::room::member::{MembershipState, RoomMemberEventContent},
presence::PresenceState, presence::PresenceState,
profile::{ProfileFieldName, ProfileFieldValue}, profile::{ProfileFieldName, ProfileFieldValue},
}; };
@@ -51,9 +51,12 @@ pub(crate) async fn set_profile_field_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_profile_field::v3::Request>, body: Ruma<set_profile_field::v3::Request>,
) -> Result<set_profile_field::v3::Response> { ) -> Result<set_profile_field::v3::Response> {
if body.user_id != body.sender_user() if body.user_id != body.identity.sender_user()
&& !(body.appservice_info.is_some() && !(body.identity.is_appservice()
|| services.admin.user_is_admin(body.sender_user()).await) || services
.admin
.user_is_admin(body.identity.sender_user())
.await)
{ {
return Err!(Request(Forbidden("You may not change other users' profile data."))); return Err!(Request(Forbidden("You may not change other users' profile data.")));
} }
@@ -62,13 +65,8 @@ pub(crate) async fn set_profile_field_route(
return Err!(Request(InvalidParam("You may not change a remote user's profile data."))); return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
} }
set_profile_field( set_profile_field(&services, &body.user_id, ProfileFieldChange::Set(body.value.clone()))
&services, .await?;
&body.user_id,
ProfileFieldChange::Set(body.value.clone()),
body.propagate_to.clone(),
)
.await?;
Ok(set_profile_field::v3::Response::new()) Ok(set_profile_field::v3::Response::new())
} }
@@ -77,9 +75,12 @@ pub(crate) async fn delete_profile_field_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_profile_field::v3::Request>, body: Ruma<delete_profile_field::v3::Request>,
) -> Result<delete_profile_field::v3::Response> { ) -> Result<delete_profile_field::v3::Response> {
if body.user_id != body.sender_user() if body.user_id != body.identity.sender_user()
&& !(body.appservice_info.is_some() && !(body.identity.is_appservice()
|| services.admin.user_is_admin(body.sender_user()).await) || services
.admin
.user_is_admin(body.identity.sender_user())
.await)
{ {
return Err!(Request(Forbidden("You may not change other users' profile data."))); return Err!(Request(Forbidden("You may not change other users' profile data.")));
} }
@@ -88,13 +89,8 @@ pub(crate) async fn delete_profile_field_route(
return Err!(Request(InvalidParam("You may not change a remote user's profile data."))); return Err!(Request(InvalidParam("You may not change a remote user's profile data.")));
} }
set_profile_field( set_profile_field(&services, &body.user_id, ProfileFieldChange::Delete(body.field.clone()))
&services, .await?;
&body.user_id,
ProfileFieldChange::Delete(body.field.clone()),
body.propagate_to.clone(),
)
.await?;
Ok(delete_profile_field::v3::Response::new()) Ok(delete_profile_field::v3::Response::new())
} }
@@ -129,13 +125,7 @@ async fn fetch_full_profile(
continue; continue;
}; };
let _ = set_profile_field( let _ = set_profile_field(services, user_id, ProfileFieldChange::Set(value)).await;
services,
user_id,
ProfileFieldChange::Set(value),
PropagateTo::None,
)
.await;
} }
Some(BTreeMap::from_iter(response)) Some(BTreeMap::from_iter(response))
@@ -169,13 +159,8 @@ async fn fetch_profile_field(
if let Some(value) = response.get(field.as_str()).map(ToOwned::to_owned) { if let Some(value) = response.get(field.as_str()).map(ToOwned::to_owned) {
if let Ok(value) = ProfileFieldValue::new(field.as_str(), value) { if let Ok(value) = ProfileFieldValue::new(field.as_str(), value) {
let _ = set_profile_field( let _ = set_profile_field(services, user_id, ProfileFieldChange::Set(value.clone()))
services, .await;
user_id,
ProfileFieldChange::Set(value.clone()),
PropagateTo::None,
)
.await;
Ok(Some(value)) Ok(Some(value))
} else { } else {
@@ -184,13 +169,7 @@ async fn fetch_profile_field(
))) )))
} }
} else { } else {
let _ = set_profile_field( let _ = set_profile_field(services, user_id, ProfileFieldChange::Delete(field)).await;
services,
user_id,
ProfileFieldChange::Delete(field),
PropagateTo::None,
)
.await;
Ok(None) Ok(None)
} }
@@ -283,7 +262,6 @@ async fn set_profile_field(
services: &Services, services: &Services,
user_id: &UserId, user_id: &UserId,
change: ProfileFieldChange, change: ProfileFieldChange,
propagate_to: PropagateTo,
) -> Result<()> { ) -> Result<()> {
const MAX_KEY_LENGTH_BYTES: usize = 255; const MAX_KEY_LENGTH_BYTES: usize = 255;
const MAX_PROFILE_LENGTH_BYTES: usize = 65536; const MAX_PROFILE_LENGTH_BYTES: usize = 65536;
@@ -331,91 +309,6 @@ 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 { match change {
| ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => { | ProfileFieldChange::Set(ProfileFieldValue::DisplayName(displayname)) => {
services services
@@ -439,5 +332,42 @@ async fn set_profile_field(
.set_profile_key(user_id, other.field_name().as_str(), other.value()), .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(()) Ok(())
} }
+13 -12
View File
@@ -30,7 +30,7 @@ pub(crate) async fn get_pushrules_all_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_pushrules_all::v3::Request>, body: Ruma<get_pushrules_all::v3::Request>,
) -> Result<get_pushrules_all::v3::Response> { ) -> Result<get_pushrules_all::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let Some(content_value) = services let Some(content_value) = services
.account_data .account_data
@@ -101,7 +101,7 @@ pub(crate) async fn get_pushrules_global_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_pushrules_global_scope::v3::Request>, body: Ruma<get_pushrules_global_scope::v3::Request>,
) -> Result<get_pushrules_global_scope::v3::Response> { ) -> Result<get_pushrules_global_scope::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let Some(content_value) = services let Some(content_value) = services
.account_data .account_data
@@ -189,7 +189,7 @@ pub(crate) async fn get_pushrule_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_pushrule::v3::Request>, body: Ruma<get_pushrule::v3::Request>,
) -> Result<get_pushrule::v3::Response> { ) -> Result<get_pushrule::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.identity.sender_user();
// remove old deprecated mentions push rules as per MSC4210 // remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)] #[allow(deprecated)]
@@ -226,7 +226,7 @@ pub(crate) async fn set_pushrule_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_pushrule::v3::Request>, body: Ruma<set_pushrule::v3::Request>,
) -> Result<set_pushrule::v3::Response> { ) -> Result<set_pushrule::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let body = &body.body; let body = &body.body;
let mut account_data: PushRulesEvent = services let mut account_data: PushRulesEvent = services
.account_data .account_data
@@ -282,7 +282,7 @@ pub(crate) async fn get_pushrule_actions_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_pushrule_actions::v3::Request>, body: Ruma<get_pushrule_actions::v3::Request>,
) -> Result<get_pushrule_actions::v3::Response> { ) -> Result<get_pushrule_actions::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
// remove old deprecated mentions push rules as per MSC4210 // remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)] #[allow(deprecated)]
@@ -316,7 +316,7 @@ pub(crate) async fn set_pushrule_actions_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_pushrule_actions::v3::Request>, body: Ruma<set_pushrule_actions::v3::Request>,
) -> Result<set_pushrule_actions::v3::Response> { ) -> Result<set_pushrule_actions::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let mut account_data: PushRulesEvent = services let mut account_data: PushRulesEvent = services
.account_data .account_data
@@ -349,7 +349,7 @@ pub(crate) async fn get_pushrule_enabled_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_pushrule_enabled::v3::Request>, body: Ruma<get_pushrule_enabled::v3::Request>,
) -> Result<get_pushrule_enabled::v3::Response> { ) -> Result<get_pushrule_enabled::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
// remove old deprecated mentions push rules as per MSC4210 // remove old deprecated mentions push rules as per MSC4210
#[allow(deprecated)] #[allow(deprecated)]
@@ -383,7 +383,7 @@ pub(crate) async fn set_pushrule_enabled_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_pushrule_enabled::v3::Request>, body: Ruma<set_pushrule_enabled::v3::Request>,
) -> Result<set_pushrule_enabled::v3::Response> { ) -> Result<set_pushrule_enabled::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let mut account_data: PushRulesEvent = services let mut account_data: PushRulesEvent = services
.account_data .account_data
@@ -416,7 +416,7 @@ pub(crate) async fn delete_pushrule_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_pushrule::v3::Request>, body: Ruma<delete_pushrule::v3::Request>,
) -> Result<delete_pushrule::v3::Response> { ) -> Result<delete_pushrule::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let mut account_data: PushRulesEvent = services let mut account_data: PushRulesEvent = services
.account_data .account_data
@@ -458,7 +458,7 @@ pub(crate) async fn get_pushers_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_pushers::v3::Request>, body: Ruma<get_pushers::v3::Request>,
) -> Result<get_pushers::v3::Response> { ) -> Result<get_pushers::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
Ok(get_pushers::v3::Response::new(services.pusher.get_pushers(sender_user).await)) Ok(get_pushers::v3::Response::new(services.pusher.get_pushers(sender_user).await))
} }
@@ -472,11 +472,12 @@ pub(crate) async fn set_pushers_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<set_pusher::v3::Request>, body: Ruma<set_pusher::v3::Request>,
) -> Result<set_pusher::v3::Response> { ) -> Result<set_pusher::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let sender_device = body.identity.expect_sender_device()?;
services services
.pusher .pusher
.set_pusher(sender_user, body.sender_device(), &body.action) .set_pusher(sender_user, sender_device, &body.action)
.await?; .await?;
Ok(set_pusher::v3::Response::new()) 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>, State(services): State<crate::State>,
body: Ruma<set_read_marker::v3::Request>, body: Ruma<set_read_marker::v3::Request>,
) -> Result<set_read_marker::v3::Response> { ) -> Result<set_read_marker::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if let Some(event) = &body.fully_read { if let Some(event) = &body.fully_read {
let fully_read_event = FullyReadEvent::new(FullyReadEventContent::new(event.to_owned())); 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, ClientIp(client_ip): ClientIp,
body: Ruma<create_receipt::v3::Request>, body: Ruma<create_receipt::v3::Request>,
) -> Result<create_receipt::v3::Response> { ) -> Result<create_receipt::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
services services
.users .users
.update_device_last_seen(sender_user, body.sender_device.as_deref(), client_ip) .update_device_last_seen(sender_user, body.identity.sender_device(), client_ip)
.await; .await;
if matches!( if matches!(
+2 -2
View File
@@ -17,10 +17,10 @@ pub(crate) async fn redact_event_route(
ClientIp(client_ip): ClientIp, ClientIp(client_ip): ClientIp,
body: Ruma<redact_event::v3::Request>, body: Ruma<redact_event::v3::Request>,
) -> Result<redact_event::v3::Response> { ) -> Result<redact_event::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
services services
.users .users
.update_device_last_seen(sender_user, body.sender_device.as_deref(), client_ip) .update_device_last_seen(sender_user, body.identity.sender_device(), client_ip)
.await; .await;
let body = &body.body; let body = &body.body;
if services.users.is_suspended(sender_user).await? { 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> { ) -> Result<get_relating_events_with_rel_type_and_event_type::v1::Response> {
paginate_relations_with_filter( paginate_relations_with_filter(
&services, &services,
body.sender_user(), body.identity.sender_user(),
&body.room_id, &body.room_id,
&body.event_id, &body.event_id,
body.event_type.clone().into(), 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> { ) -> Result<get_relating_events_with_rel_type::v1::Response> {
paginate_relations_with_filter( paginate_relations_with_filter(
&services, &services,
body.sender_user(), body.identity.sender_user(),
&body.room_id, &body.room_id,
&body.event_id, &body.event_id,
None, None,
@@ -84,7 +84,7 @@ pub(crate) async fn get_relating_events_route(
) -> Result<get_relating_events::v1::Response> { ) -> Result<get_relating_events::v1::Response> {
paginate_relations_with_filter( paginate_relations_with_filter(
&services, &services,
body.sender_user(), body.identity.sender_user(),
&body.room_id, &body.room_id,
&body.event_id, &body.event_id,
None, None,
+4 -4
View File
@@ -36,7 +36,7 @@ pub(crate) async fn report_room_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<report_room::v3::Request>, body: Ruma<report_room::v3::Request>,
) -> Result<report_room::v3::Response> { ) -> Result<report_room::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
@@ -92,7 +92,7 @@ pub(crate) async fn report_event_route(
body: Ruma<report_content::v3::Request>, body: Ruma<report_content::v3::Request>,
) -> Result<report_content::v3::Response> { ) -> Result<report_content::v3::Response> {
// user authentication // user authentication
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
@@ -135,8 +135,8 @@ pub(crate) async fn report_user_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<report_user::v3::Request>, body: Ruma<report_user::v3::Request>,
) -> Result<report_user::v3::Response> { ) -> Result<report_user::v3::Response> {
// user authentication let sender_user = body.identity.sender_user();
let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
+1 -1
View File
@@ -15,7 +15,7 @@ pub(crate) async fn get_room_aliases_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<aliases::v3::Request>, body: Ruma<aliases::v3::Request>,
) -> Result<aliases::v3::Response> { ) -> Result<aliases::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if !services if !services
.rooms .rooms
+4 -4
View File
@@ -61,10 +61,10 @@ pub(crate) async fn create_room_route(
) -> Result<create_room::v3::Response> { ) -> Result<create_room::v3::Response> {
use create_room::v3::RoomPreset; use create_room::v3::RoomPreset;
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if !services.globals.allow_room_creation() if !services.globals.allow_room_creation()
&& body.appservice_info.is_none() && !body.identity.is_appservice()
&& !services.users.is_admin(sender_user).await && !services.users.is_admin(sender_user).await
{ {
return Err!(Request(Forbidden("Room creation has been disabled.",))); 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 if body.visibility == room::Visibility::Public
&& services.server.config.lockdown_public_room_directory && services.server.config.lockdown_public_room_directory
&& !services.users.is_admin(sender_user).await && !services.users.is_admin(sender_user).await
&& body.appservice_info.is_none() && !body.identity.is_appservice()
{ {
warn!( warn!(
"Non-admin user {sender_user} tried to publish {room_id:?} to the room directory \ "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() { let alias: Option<OwnedRoomAliasId> = match body.room_alias_name.as_ref() {
| Some(alias) => | Some(alias) =>
Some(room_alias_check(&services, alias, body.appservice_info.as_ref()).await?), Some(room_alias_check(&services, alias, body.identity.appservice_info()).await?),
| _ => None, | _ => None,
}; };
+4 -4
View File
@@ -24,25 +24,25 @@ pub(crate) async fn get_room_event_route(
let visible = services let visible = services
.rooms .rooms
.state_accessor .state_accessor
.user_can_see_event(body.sender_user(), room_id, event_id) .user_can_see_event(body.identity.sender_user(), room_id, event_id)
.map(Ok); .map(Ok);
let (mut event, visible) = try_join(event, visible).await?; let (mut event, visible) = try_join(event, visible).await?;
if !visible || is_ignored_pdu(services, &event, body.sender_user()).await? { if !visible || is_ignored_pdu(services, &event, body.identity.sender_user()).await? {
return Err!(Request(Forbidden("You don't have permission to view this event."))); return Err!(Request(Forbidden("You don't have permission to view this event.")));
} }
if let Err(e) = services if let Err(e) = services
.rooms .rooms
.pdu_metadata .pdu_metadata
.add_bundled_aggregations_to_pdu(body.sender_user(), &mut event) .add_bundled_aggregations_to_pdu(body.identity.sender_user(), &mut event)
.await .await
{ {
debug_warn!("Failed to add bundled aggregations to event: {e}"); debug_warn!("Failed to add bundled aggregations to event: {e}");
} }
event.set_unsigned(body.sender_user.as_deref()); event.set_unsigned(Some(body.identity.sender_user()));
Ok(get_room_event::v3::Response::new(event.into_format())) Ok(get_room_event::v3::Response::new(event.into_format()))
} }
+10 -12
View File
@@ -22,7 +22,7 @@ pub(crate) async fn room_initial_sync_route(
if !services if !services
.rooms .rooms
.state_accessor .state_accessor
.user_can_see_state_events(body.sender_user(), room_id) .user_can_see_state_events(body.identity.sender_user(), room_id)
.await .await
{ {
return Err!(Request(Forbidden("No room preview available."))); return Err!(Request(Forbidden("No room preview available.")));
@@ -31,7 +31,7 @@ pub(crate) async fn room_initial_sync_route(
let membership = services let membership = services
.rooms .rooms
.state_cache .state_cache
.user_membership(body.sender_user(), room_id) .user_membership(body.identity.sender_user(), room_id)
.map(Ok); .map(Ok);
let visibility = services.rooms.directory.visibility(room_id).map(Ok); let visibility = services.rooms.directory.visibility(room_id).map(Ok);
@@ -52,16 +52,14 @@ pub(crate) async fn room_initial_sync_route(
.pdus_rev(room_id, None) .pdus_rev(room_id, None)
.try_take(limit) .try_take(limit)
.and_then(async |mut pdu| { .and_then(async |mut pdu| {
pdu.1.set_unsigned(body.sender_user.as_deref()); pdu.1.set_unsigned(Some(body.identity.sender_user()));
if let Some(sender_user) = body.sender_user.as_deref() { if let Err(e) = services
if let Err(e) = services .rooms
.rooms .pdu_metadata
.pdu_metadata .add_bundled_aggregations_to_pdu(body.identity.sender_user(), &mut pdu.1)
.add_bundled_aggregations_to_pdu(sender_user, &mut pdu.1) .await
.await {
{ debug_warn!("Failed to add bundled aggregations: {e}");
debug_warn!("Failed to add bundled aggregations: {e}");
}
} }
Ok(pdu) Ok(pdu)
}) })
+6 -2
View File
@@ -4,7 +4,7 @@ use conduwuit::{Err, Result};
use ruma::api::client::room::get_summary; use ruma::api::client::room::get_summary;
use service::rooms::summary::Accessibility; use service::rooms::summary::Accessibility;
use crate::Ruma; use crate::{Ruma, router::ClientIdentity};
/// # `GET /_matrix/client/v1/room_summary/{roomIdOrAlias}` /// # `GET /_matrix/client/v1/room_summary/{roomIdOrAlias}`
/// ///
@@ -28,7 +28,11 @@ pub(crate) async fn get_room_summary(
let summary = services let summary = services
.rooms .rooms
.summary .summary
.get_room_summary_for_user(body.sender_user.as_deref(), &room_id, &servers) .get_room_summary_for_user(
body.identity.as_ref().map(ClientIdentity::sender_user),
&room_id,
&servers,
)
.await?; .await?;
match summary { match summary {
+1 -1
View File
@@ -277,7 +277,7 @@ pub(crate) async fn upgrade_room_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<upgrade_room::v3::Request>, body: Ruma<upgrade_room::v3::Request>,
) -> Result<upgrade_room::v3::Response> { ) -> Result<upgrade_room::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let (supported, forbid_unstable, is_unstable) = ( let (supported, forbid_unstable, is_unstable) = (
services.server.supported_room_version(&body.new_version), 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>, State(services): State<crate::State>,
body: Ruma<Request>, body: Ruma<Request>,
) -> Result<Response> { ) -> Result<Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let next_batch = body.next_batch.as_deref(); let next_batch = body.next_batch.as_deref();
let mut result_categories = ResultCategories::new(); let mut result_categories = ResultCategories::new();
+9 -5
View File
@@ -22,16 +22,16 @@ pub(crate) async fn send_message_event_route(
ClientIp(client_ip): ClientIp, ClientIp(client_ip): ClientIp,
body: Ruma<send_message_event::v3::Request>, body: Ruma<send_message_event::v3::Request>,
) -> Result<send_message_event::v3::Response> { ) -> Result<send_message_event::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let sender_device = body.sender_device.as_deref(); let sender_device = body.identity.sender_device();
let appservice_info = body.appservice_info.as_ref();
if services.users.is_suspended(sender_user).await? { if services.users.is_suspended(sender_user).await? {
return Err!(Request(UserSuspended("You cannot perform this action while suspended."))); return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
} }
services services
.users .users
.update_device_last_seen(sender_user, body.sender_device.as_deref(), client_ip) .update_device_last_seen(sender_user, sender_device, client_ip)
.await; .await;
// Forbid m.room.encrypted if encryption is disabled // Forbid m.room.encrypted if encryption is disabled
@@ -83,7 +83,11 @@ pub(crate) async fn send_message_event_route(
event_type: body.event_type.clone().into(), event_type: body.event_type.clone().into(),
content, content,
unsigned: Some(unsigned), unsigned: Some(unsigned),
timestamp: appservice_info.and(body.timestamp), timestamp: if body.identity.is_appservice() {
body.timestamp
} else {
None
},
..Default::default() ..Default::default()
}, },
sender_user, sender_user,
+37 -24
View File
@@ -21,7 +21,7 @@ use ruma::{
}, },
login::{ login::{
self, self,
v3::{DiscoveryInfo, HomeserverInfo}, v3::{DiscoveryInfo, HomeserverInfo, LoginInfo},
}, },
logout, logout_all, logout, logout_all,
}, },
@@ -29,7 +29,6 @@ use ruma::{
}, },
assign, assign,
}; };
use service::uiaa::Identity;
use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH}; use super::{DEVICE_ID_LENGTH, TOKEN_LENGTH};
use crate::Ruma; use crate::Ruma;
@@ -44,6 +43,12 @@ pub(crate) async fn get_login_types_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
_body: Ruma<get_login_types::v3::Request>, _body: Ruma<get_login_types::v3::Request>,
) -> Result<get_login_types::v3::Response> { ) -> 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![ Ok(get_login_types::v3::Response::new(vec![
get_login_types::v3::LoginType::Password(PasswordLoginType::default()), get_login_types::v3::LoginType::Password(PasswordLoginType::default()),
get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()), get_login_types::v3::LoginType::ApplicationService(ApplicationServiceLoginType::default()),
@@ -53,7 +58,7 @@ pub(crate) async fn get_login_types_route(
])) ]))
} }
pub(crate) async fn handle_login( pub async fn handle_login(
services: &Services, services: &Services,
identifier: Option<&UserIdentifier>, identifier: Option<&UserIdentifier>,
password: &str, password: &str,
@@ -87,10 +92,6 @@ pub(crate) async fn handle_login(
return Err!(Request(InvalidParam("User ID does not belong to this homeserver"))); 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 { if services.users.is_login_disabled(&user_id).await {
warn!(%user_id, "user attempted to log in with a login-disabled account"); 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."))); return Err!(Request(Forbidden("This account is not permitted to log in.")));
@@ -119,19 +120,29 @@ pub(crate) async fn login_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<login::v3::Request>, body: Ruma<login::v3::Request>,
) -> Result<login::v3::Response> { ) -> 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(); let emergency_mode_enabled = services.config.emergency_password.is_some();
// Validate login method // Validate login method
// TODO: Other login methods
let user_id = match &body.login_info { let user_id = match &body.login_info {
#[allow(deprecated)] #[allow(deprecated)]
| login::v3::LoginInfo::Password(login::v3::Password { | LoginInfo::Password(login::v3::Password { identifier, password, user, .. }) =>
identifier, handle_login(&services, identifier.as_ref(), password, user.as_ref()).await?,
password, | LoginInfo::Token(login::v3::Token { token, .. }) => {
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"); debug!("Got token login type");
if !services.server.config.login_via_existing_session { if !services.server.config.login_via_existing_session {
return Err!(Request(Unknown("Token login is not enabled."))); return Err!(Request(Unknown("Token login is not enabled.")));
@@ -139,14 +150,14 @@ pub(crate) async fn login_route(
services.users.find_from_login_token(token).await? services.users.find_from_login_token(token).await?
}, },
#[allow(deprecated)] #[allow(deprecated)]
| login::v3::LoginInfo::ApplicationService(login::v3::ApplicationService { | LoginInfo::ApplicationService(login::v3::ApplicationService {
identifier, identifier,
user, user,
.. ..
}) => { }) => {
debug!("Got appservice login type"); debug!("Got appservice login type");
let Some(ref info) = body.appservice_info else { let Some(ref info) = body.identity else {
return Err!(Request(MissingToken("Missing appservice token."))); return Err!(Request(MissingToken("Missing appservice token.")));
}; };
@@ -173,7 +184,6 @@ pub(crate) async fn login_route(
user_id user_id
}, },
| _ => { | _ => {
debug!("/login json_body: {:?}", &body.json_body);
return Err!(Request(Unknown( return Err!(Request(Unknown(
debug_warn!(?body.login_info, "Invalid or unsupported login type") debug_warn!(?body.login_info, "Invalid or unsupported login type")
))); )));
@@ -203,7 +213,7 @@ pub(crate) async fn login_route(
if device_exists { if device_exists {
services services
.users .users
.set_token(&user_id, &device_id, &token) .set_token(&user_id, &device_id, &token, None)
.await?; .await?;
} else { } else {
services services
@@ -212,6 +222,7 @@ pub(crate) async fn login_route(
&user_id, &user_id,
&device_id, &device_id,
&token, &token,
None,
body.initial_device_display_name.clone(), body.initial_device_display_name.clone(),
Some(client.to_string()), Some(client.to_string()),
) )
@@ -250,16 +261,16 @@ pub(crate) async fn login_token_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<get_login_token::v1::Request>, body: Ruma<get_login_token::v1::Request>,
) -> Result<get_login_token::v1::Response> { ) -> Result<get_login_token::v1::Response> {
if !services.server.config.login_via_existing_session { if !services.config.login_via_existing_session {
return Err!(Request(Forbidden("Login via an existing session is not enabled"))); return Err!(Request(Forbidden("Login via an existing session is not enabled")));
} }
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
// Prompt the user to confirm with their password using UIAA // Prompt the user to confirm with their password using UIAA
let _ = services let _ = services
.uiaa .uiaa
.authenticate_password(&body.auth, Some(Identity::from_user_id(sender_user))) .authenticate_password(&body.auth, sender_user, body.identity.sender_device(), None)
.await?; .await?;
let login_token = utils::random_string(TOKEN_LENGTH); let login_token = utils::random_string(TOKEN_LENGTH);
@@ -286,7 +297,9 @@ pub(crate) async fn logout_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<logout::v3::Request>, body: Ruma<logout::v3::Request>,
) -> Result<logout::v3::Response> { ) -> Result<logout::v3::Response> {
let (sender_user, sender_device) = body.sender(); let sender_user = body.identity.sender_user();
let sender_device = body.identity.expect_sender_device()?;
services services
.users .users
.remove_device(sender_user, sender_device) .remove_device(sender_user, sender_device)
@@ -332,7 +345,7 @@ pub(crate) async fn logout_all_route(
ClientIp(client): ClientIp, ClientIp(client): ClientIp,
body: Ruma<logout_all::v3::Request>, body: Ruma<logout_all::v3::Request>,
) -> Result<logout_all::v3::Response> { ) -> Result<logout_all::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
services services
.users .users
.all_device_ids(sender_user) .all_device_ids(sender_user)
+1 -1
View File
@@ -27,7 +27,7 @@ pub(crate) async fn get_hierarchy_route(
.rooms .rooms
.summary .summary
.get_room_hierarchy_for_user( .get_room_hierarchy_for_user(
body.sender_user(), body.identity.sender_user(),
body.room_id.clone(), body.room_id.clone(),
max_depth, max_depth,
body.suggested_only, body.suggested_only,
+5 -5
View File
@@ -38,10 +38,10 @@ pub(crate) async fn send_state_event_for_key_route(
ClientIp(ip): ClientIp, ClientIp(ip): ClientIp,
body: Ruma<send_state_event::v3::Request>, body: Ruma<send_state_event::v3::Request>,
) -> Result<send_state_event::v3::Response> { ) -> Result<send_state_event::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
services services
.users .users
.update_device_last_seen(sender_user, body.sender_device.as_deref(), ip) .update_device_last_seen(sender_user, body.identity.sender_device(), ip)
.await; .await;
if services.users.is_suspended(sender_user).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.event_type,
&body.body.body, &body.body.body,
&body.state_key, &body.state_key,
if body.appservice_info.is_some() { if body.identity.is_appservice() {
body.timestamp body.timestamp
} else { } else {
None None
@@ -91,7 +91,7 @@ pub(crate) async fn get_state_events_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_state_events::v3::Request>, body: Ruma<get_state_events::v3::Request>,
) -> Result<get_state_events::v3::Response> { ) -> Result<get_state_events::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if !services if !services
.rooms .rooms
@@ -125,7 +125,7 @@ pub(crate) async fn get_state_event_for_key_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_state_event_for_key::v3::Request>, body: Ruma<get_state_event_for_key::v3::Request>,
) -> Result<get_state_event_for_key::v3::Response> { ) -> Result<get_state_event_for_key::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
if !services if !services
.rooms .rooms
+4 -2
View File
@@ -181,7 +181,8 @@ pub(crate) async fn sync_events_route(
ClientIp(client_ip): ClientIp, ClientIp(client_ip): ClientIp,
body: Ruma<sync_events::v3::Request>, body: Ruma<sync_events::v3::Request>,
) -> Result<sync_events::v3::Response> { ) -> Result<sync_events::v3::Response> {
let (sender_user, sender_device) = body.sender(); let sender_user = body.identity.sender_user();
let sender_device = body.identity.expect_sender_device()?;
// Presence update // Presence update
if services.config.allow_local_presence { if services.config.allow_local_presence {
@@ -225,7 +226,8 @@ pub(crate) async fn build_sync_events(
services: &Services, services: &Services,
body: &Ruma<sync_events::v3::Request>, body: &Ruma<sync_events::v3::Request>,
) -> Result<sync_events::v3::Response> { ) -> Result<sync_events::v3::Response> {
let (syncing_user, syncing_device) = body.sender(); let syncing_user = body.identity.sender_user();
let syncing_device = body.identity.sender_device().expect("should have a device");
let current_count = services.globals.current_count()?; let current_count = services.globals.current_count()?;
+3 -4
View File
@@ -69,9 +69,8 @@ pub(crate) async fn sync_events_v5_route(
ClientIp(client_ip): ClientIp, ClientIp(client_ip): ClientIp,
body: Ruma<sync_events::v5::Request>, body: Ruma<sync_events::v5::Request>,
) -> Result<sync_events::v5::Response> { ) -> Result<sync_events::v5::Response> {
debug_assert!(DEFAULT_BUMP_TYPES.is_sorted(), "DEFAULT_BUMP_TYPES is not sorted"); let sender_user = body.identity.sender_user();
let ref sender_user = body.sender_user().to_owned(); let sender_device = body.identity.expect_sender_device()?;
let ref sender_device = body.sender_device().to_owned();
services services
.users .users
@@ -93,7 +92,7 @@ pub(crate) async fn sync_events_v5_route(
.and_then(|string| string.parse().ok()) .and_then(|string| string.parse().ok())
.unwrap_or(0); .unwrap_or(0);
let snake_key = into_snake_key(sender_user.as_ref(), sender_device.as_str(), conn_id); let snake_key = into_snake_key(sender_user, sender_device.as_str(), conn_id);
if globalsince != 0 && !services.sync.snake_connection_cached(&snake_key) { if globalsince != 0 && !services.sync.snake_connection_cached(&snake_key) {
return Err!(Request(UnknownPos( return Err!(Request(UnknownPos(
+3 -3
View File
@@ -21,7 +21,7 @@ pub(crate) async fn update_tag_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<create_tag::v3::Request>, body: Ruma<create_tag::v3::Request>,
) -> Result<create_tag::v3::Response> { ) -> Result<create_tag::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let mut tags_event = services let mut tags_event = services
.account_data .account_data
@@ -56,7 +56,7 @@ pub(crate) async fn delete_tag_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<delete_tag::v3::Request>, body: Ruma<delete_tag::v3::Request>,
) -> Result<delete_tag::v3::Response> { ) -> Result<delete_tag::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let mut tags_event = services let mut tags_event = services
.account_data .account_data
@@ -88,7 +88,7 @@ pub(crate) async fn get_tags_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<get_tags::v3::Request>, body: Ruma<get_tags::v3::Request>,
) -> Result<get_tags::v3::Response> { ) -> Result<get_tags::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let tags_event = services let tags_event = services
.account_data .account_data
+3 -3
View File
@@ -34,14 +34,14 @@ pub(crate) async fn get_threads_route(
let threads: Vec<(PduCount, PduEvent)> = services let threads: Vec<(PduCount, PduEvent)> = services
.rooms .rooms
.threads .threads
.threads_until(body.sender_user(), &body.room_id, from, &body.include) .threads_until(body.identity.sender_user(), &body.room_id, from, &body.include)
.await? .await?
.take(limit) .take(limit)
.filter_map(|(count, pdu)| async move { .filter_map(|(count, pdu)| async move {
services services
.rooms .rooms
.state_accessor .state_accessor
.user_can_see_event(body.sender_user(), &body.room_id, &pdu.event_id) .user_can_see_event(body.identity.sender_user(), &body.room_id, &pdu.event_id)
.await .await
.then_some((count, pdu)) .then_some((count, pdu))
}) })
@@ -49,7 +49,7 @@ pub(crate) async fn get_threads_route(
if let Err(e) = services if let Err(e) = services
.rooms .rooms
.pdu_metadata .pdu_metadata
.add_bundled_aggregations_to_pdu(body.sender_user(), &mut pdu) .add_bundled_aggregations_to_pdu(body.identity.sender_user(), &mut pdu)
.await .await
{ {
debug_warn!("Failed to add bundled aggregations to thread: {e}"); 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>, State(services): State<crate::State>,
body: Ruma<send_event_to_device::v3::Request>, body: Ruma<send_event_to_device::v3::Request>,
) -> Result<send_event_to_device::v3::Response> { ) -> Result<send_event_to_device::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let sender_device = body.sender_device.as_deref(); let sender_device = body.identity.sender_device();
// Check if this is a new transaction id // Check if this is a new transaction id
if services 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>, body: Ruma<create_typing_event::v3::Request>,
) -> Result<create_typing_event::v3::Response> { ) -> Result<create_typing_event::v3::Response> {
use create_typing_event::v3::Typing; use create_typing_event::v3::Typing;
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
services services
.users .users
.update_device_last_seen(sender_user, body.sender_device.as_deref(), ip) .update_device_last_seen(sender_user, body.identity.sender_device(), ip)
.await; .await;
if sender_user != body.user_id && body.appservice_info.is_none() { if sender_user != body.user_id && !body.identity.is_appservice() {
return Err!(Request(Forbidden("You cannot update typing status of other users."))); 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` /// `/_matrix/federation/v1/version`
pub(crate) async fn conduwuit_server_version() -> Result<impl IntoResponse> { pub(crate) async fn conduwuit_server_version() -> Result<impl IntoResponse> {
Ok(Json(serde_json::json!({ Ok(Json(serde_json::json!({
"name": conduwuit::version::name(), "name": conduwuit::BRANDING,
"version": conduwuit::version::version(), "version": conduwuit::version(),
}))) })))
} }
+1 -1
View File
@@ -26,7 +26,7 @@ pub(crate) async fn search_users_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<search_users::v3::Request>, body: Ruma<search_users::v3::Request>,
) -> Result<search_users::v3::Response> { ) -> Result<search_users::v3::Response> {
let sender_user = body.sender_user(); let sender_user = body.identity.sender_user();
let limit = usize::try_from(body.limit) let limit = usize::try_from(body.limit)
.map_or(LIMIT_DEFAULT, usize::from) .map_or(LIMIT_DEFAULT, usize::from)
.min(LIMIT_MAX); .min(LIMIT_MAX);
+3 -13
View File
@@ -2,15 +2,13 @@ use std::time::{Duration, SystemTime};
use axum::extract::State; use axum::extract::State;
use base64::{Engine as _, engine::general_purpose}; use base64::{Engine as _, engine::general_purpose};
use conduwuit::{Err, Result, utils}; use conduwuit::{Err, Result};
use hmac::{Hmac, KeyInit, Mac}; use hmac::{Hmac, KeyInit, Mac};
use ruma::{SecondsSinceUnixEpoch, UserId, api::client::voip::get_turn_server_info}; use ruma::{SecondsSinceUnixEpoch, api::client::voip::get_turn_server_info};
use sha1::Sha1; use sha1::Sha1;
use crate::Ruma; use crate::Ruma;
const RANDOM_USER_ID_LENGTH: usize = 10;
type HmacSha1 = Hmac<Sha1>; type HmacSha1 = Hmac<Sha1>;
/// # `GET /_matrix/client/r0/voip/turnServer` /// # `GET /_matrix/client/r0/voip/turnServer`
@@ -35,15 +33,7 @@ pub(crate) async fn turn_server_route(
) )
.expect("time is valid"); .expect("time is valid");
let user = body.sender_user.unwrap_or_else(|| { let username: String = format!("{}:{}", expiry.get(), body.identity.sender_user());
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()) let mut mac = HmacSha1::new_from_slice(turn_secret.as_bytes())
.expect("HMAC can take key of any size"); .expect("HMAC can take key of any size");
+2 -42
View File
@@ -3,8 +3,7 @@ use conduwuit::{Err, Result};
use ruma::{ use ruma::{
api::client::discovery::{ api::client::discovery::{
discover_homeserver::{self, HomeserverInfo}, discover_homeserver::{self, HomeserverInfo},
discover_policy_server, discover_policy_server, discover_support,
discover_support::{self, Contact, ContactRole},
}, },
assign, assign,
}; };
@@ -67,46 +66,7 @@ pub(crate) async fn well_known_support(
.as_ref() .as_ref()
.map(ToString::to_string); .map(ToString::to_string);
let email_address = services.config.well_known.support_email.clone(); let contacts = services.admin.get_support_contacts().await;
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() { if contacts.is_empty() && support_page.is_none() {
// No admin room, no configured contacts, and no support page // No admin room, no configured contacts, and no support page
+1 -2
View File
@@ -1,4 +1,5 @@
#![type_length_limit = "16384"] //TODO: reduce me #![type_length_limit = "16384"] //TODO: reduce me
#![recursion_limit = "256"] // My Giant Async Function
#![allow(clippy::toplevel_ref_arg)] #![allow(clippy::toplevel_ref_arg)]
extern crate conduwuit_core as conduwuit; extern crate conduwuit_core as conduwuit;
@@ -10,8 +11,6 @@ pub mod client;
pub mod router; pub mod router;
pub mod server; pub mod server;
pub mod admin;
pub(crate) use self::router::{Ruma, RumaResponse, State}; pub(crate) use self::router::{Ruma, RumaResponse, State};
conduwuit::mod_ctor! {} conduwuit::mod_ctor! {}
+22 -8
View File
@@ -10,16 +10,18 @@ use axum::{
response::{IntoResponse, Redirect}, response::{IntoResponse, Redirect},
routing::{any, get, post}, routing::{any, get, post},
}; };
use conduwuit::{Server, err}; use conduwuit::err;
pub(super) use conduwuit_service::state::State; pub(super) use conduwuit_service::state::State;
use http::{Uri, uri}; use http::{Uri, uri};
use self::handler::RouterExt; use self::handler::RouterExt;
pub(super) use self::{args::Args as Ruma, response::RumaResponse}; pub(super) use self::{args::Args as Ruma, auth::ClientIdentity, response::RumaResponse};
use crate::{admin, client, server}; #[cfg(feature = "admin_api")]
use crate::client::admin::site as admin_api;
use crate::{client, server};
pub fn build(router: Router<State>, server: &Server) -> Router<State> { pub fn build(router: Router<State>, state: State) -> Router<State> {
let config = &server.config; let config = &state.server.config;
let mut router = router let mut router = router
.ruma_route(&client::appservice_ping) .ruma_route(&client::appservice_ping)
.ruma_route(&client::get_supported_versions_route) .ruma_route(&client::get_supported_versions_route)
@@ -181,15 +183,17 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.ruma_route(&client::get_room_summary) .ruma_route(&client::get_room_summary)
.ruma_route(&client::get_suspended_status) .ruma_route(&client::get_suspended_status)
.ruma_route(&client::put_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_support)
.ruma_route(&client::well_known_client) .ruma_route(&client::well_known_client)
.ruma_route(&client::well_known_policy_server) .ruma_route(&client::well_known_policy_server)
.ruma_route(&client::get_rtc_transports) .ruma_route(&client::get_rtc_transports)
.ruma_route(&client::room_initial_sync_route) .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("/_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 { if config.allow_federation {
router = router router = router
@@ -275,6 +279,16 @@ pub fn build(router: Router<State>, server: &Server) -> Router<State> {
.route("/_matrix/media/r0/preview_url", any(redirect_legacy_preview)); .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 router
} }
+13 -68
View File
@@ -6,17 +6,14 @@ use axum::{
extract::{FromRequest, Path, Query}, extract::{FromRequest, Path, Query},
}; };
use conduwuit::{Error, Result, err}; use conduwuit::{Error, Result, err};
use ruma::{ use ruma::{CanonicalJsonObject, api::IncomingRequest};
CanonicalJsonObject, DeviceId, OwnedDeviceId, OwnedServerName, OwnedUserId, ServerName,
UserId, api::IncomingRequest,
};
use serde::Deserialize; use serde::Deserialize;
use crate::{State, router::auth::CheckAuth, service::appservice::RegistrationInfo}; use crate::{State, router::auth::CheckAuth};
/// Query parameters needed to authenticate requests /// Query parameters needed to authenticate requests
#[derive(Deserialize)] #[derive(Deserialize)]
pub(super) struct AuthQueryParams { pub(crate) struct AuthQueryParams {
pub(super) user_id: Option<String>, pub(super) user_id: Option<String>,
/// Device ID for appservice device masquerading (MSC3202/MSC4190). /// Device ID for appservice device masquerading (MSC3202/MSC4190).
/// Can be provided as `device_id` or `org.matrix.msc3202.device_id`. /// Can be provided as `device_id` or `org.matrix.msc3202.device_id`.
@@ -25,67 +22,22 @@ pub(super) struct AuthQueryParams {
} }
/// Extractor for Ruma request structs /// Extractor for Ruma request structs
pub(crate) struct Args<T> { pub(crate) struct Args<R: IncomingRequest<Authentication: CheckAuth> + Send + Sync + 'static> {
/// Request struct body /// Request struct body
pub(crate) body: T, pub(crate) body: R,
/// Federation server authentication: X-Matrix origin /// Parsed JSON body. None when body is not JSON.
/// 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>, pub(crate) json_body: Option<CanonicalJsonObject>,
/// Identity of the requesting entity
pub(crate) identity: <R::Authentication as CheckAuth>::Identity,
} }
impl<T> Args<T> impl<R> Deref for Args<R>
where where
T: IncomingRequest + Send + Sync + 'static, R: IncomingRequest<Authentication: CheckAuth> + Send + Sync + 'static,
{ {
#[inline] type Target = R;
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 } fn deref(&self) -> &Self::Target { &self.body }
} }
@@ -145,13 +97,6 @@ where
let body = R::try_from_http_request(request, &path) let body = R::try_from_http_request(request, &path)
.map_err(|e| err!(Request(BadJson(debug_warn!("{e}")))))?; .map_err(|e| err!(Request(BadJson(debug_warn!("{e}")))))?;
Ok(Self { Ok(Self { body, json_body, identity: auth })
body,
origin: auth.origin,
sender_user: auth.sender_user,
sender_device: auth.sender_device,
appservice_info: auth.appservice_info,
json_body,
})
} }
} }
+157 -98
View File
@@ -1,8 +1,9 @@
use std::any::{Any, TypeId}; use std::any::{Any, TypeId};
use conduwuit::{Err, Result, err}; use conduwuit::{Err, Error, Result, err};
use http::StatusCode;
use ruma::{ use ruma::{
OwnedDeviceId, OwnedServerName, OwnedUserId, UserId, DeviceId, OwnedDeviceId, OwnedServerName, OwnedUserId, UserId,
api::{ api::{
IncomingRequest, IncomingRequest,
auth_scheme::{ auth_scheme::{
@@ -10,30 +11,70 @@ use ruma::{
AuthScheme, NoAccessToken, NoAuthentication, AuthScheme, NoAccessToken, NoAuthentication,
}, },
client, client,
error::{ErrorKind, UnknownTokenErrorData},
federation::authentication::ServerSignatures, federation::authentication::ServerSignatures,
}, },
assign,
}; };
use service::{ use service::{
Services, Services,
server_keys::{PubKeyMap, PubKeys}, server_keys::{PubKeyMap, PubKeys},
users::AccessTokenStatus,
}; };
use crate::{router::args::AuthQueryParams, service::appservice::RegistrationInfo}; use crate::{router::args::AuthQueryParams, service::appservice::RegistrationInfo};
#[derive(Default)] pub(crate) enum ClientIdentity {
pub(super) struct Auth { User {
pub(super) origin: Option<OwnedServerName>, sender_user: OwnedUserId,
pub(super) sender_user: Option<OwnedUserId>, sender_device: OwnedDeviceId,
pub(super) sender_device: Option<OwnedDeviceId>, },
pub(super) appservice_info: Option<RegistrationInfo>, Appservice {
sender_user: OwnedUserId,
sender_device: Option<OwnedDeviceId>,
appservice_info: Box<RegistrationInfo>,
},
} }
pub(super) trait CheckAuth: AuthScheme { 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;
fn authenticate<R: IncomingRequest + Any, B: AsRef<[u8]> + Sync>( fn authenticate<R: IncomingRequest + Any, B: AsRef<[u8]> + Sync>(
services: &Services, services: &Services,
incoming_request: &hyper::Request<B>, incoming_request: &hyper::Request<B>,
query: AuthQueryParams, query: AuthQueryParams,
) -> impl Future<Output = Result<Auth>> + Send { ) -> impl Future<Output = Result<Self::Identity>> + Send {
async move { async move {
let route = TypeId::of::<R>(); let route = TypeId::of::<R>();
@@ -54,17 +95,19 @@ pub(super) trait CheckAuth: AuthScheme {
request: &hyper::Request<B>, request: &hyper::Request<B>,
query: AuthQueryParams, query: AuthQueryParams,
route: TypeId, route: TypeId,
) -> impl Future<Output = Result<Auth>> + Send; ) -> impl Future<Output = Result<Self::Identity>> + Send;
} }
impl CheckAuth for ServerSignatures { impl CheckAuth for ServerSignatures {
type Identity = OwnedServerName;
async fn verify<B: AsRef<[u8]> + Sync>( async fn verify<B: AsRef<[u8]> + Sync>(
services: &Services, services: &Services,
output: Self::Output, output: Self::Output,
request: &hyper::Request<B>, request: &hyper::Request<B>,
_query: AuthQueryParams, _query: AuthQueryParams,
_route: TypeId, _route: TypeId,
) -> Result<Auth> { ) -> Result<Self::Identity> {
let destination = services.globals.server_name(); let destination = services.globals.server_name();
if output if output
.destination .destination
@@ -96,10 +139,7 @@ impl CheckAuth for ServerSignatures {
))); )));
} }
Ok(Auth { Ok(output.origin)
origin: Some(output.origin.clone()),
..Default::default()
})
}, },
| Err(err) => | Err(err) =>
Err!(Request(Unauthorized(warn!("Failed to verify X-Matrix header: {err}")))), Err!(Request(Unauthorized(warn!("Failed to verify X-Matrix header: {err}")))),
@@ -108,162 +148,181 @@ impl CheckAuth for ServerSignatures {
} }
impl CheckAuth for AccessToken { impl CheckAuth for AccessToken {
type Identity = ClientIdentity;
async fn verify<B: AsRef<[u8]> + Sync>( async fn verify<B: AsRef<[u8]> + Sync>(
services: &Services, services: &Services,
output: Self::Output, output: Self::Output,
_request: &hyper::Request<B>, _request: &hyper::Request<B>,
query: AuthQueryParams, query: AuthQueryParams,
route: TypeId, route: TypeId,
) -> Result<Auth> { ) -> Result<Self::Identity> {
// Check for appservice tokens first 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,
));
}
let (sender_user, sender_device, appservice_info) = { // Locked users can only use /logout and /logout/all
if let Ok((sender_user, sender_device)) = if services
services.users.find_from_token(&output).await .users
.is_locked(&sender_user)
.await
.is_ok_and(std::convert::identity)
{ {
// Locked users can only use /logout and /logout/all if !(route == TypeId::of::<client::session::logout::v3::Request>()
if services || route == TypeId::of::<client::session::logout_all::v3::Request>())
.users
.is_locked(&sender_user)
.await
.is_ok_and(std::convert::identity)
{ {
if !(route == TypeId::of::<client::session::logout::v3::Request>() return Err!(Request(UserLocked("Your account is locked.")));
|| route == TypeId::of::<client::session::logout_all::v3::Request>())
{
return Err!(Request(Unauthorized("Your account is locked.")));
}
} }
}
(Some(sender_user), Some(sender_device), None) Ok(ClientIdentity::User { sender_user, sender_device })
} else if let Ok(appservice_info) = services.appservice.find_from_token(&output).await } else if let Ok(appservice_info) = services.appservice.find_from_token(&output).await {
{ let Ok(sender_user) = query.user_id.clone().map_or_else(
let Ok(sender_user) = query.user_id.clone().map_or_else( || {
|| { UserId::parse_with_server_name(
UserId::parse_with_server_name( appservice_info.registration.sender_localpart.as_str(),
appservice_info.registration.sender_localpart.as_str(), services.globals.server_name(),
services.globals.server_name(), )
) },
}, UserId::parse,
UserId::parse, ) else {
) else { return Err!(Request(InvalidUsername("Username is invalid.")));
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()
{
return Err!(Request(Forbidden(
"Device does not exist for user or appservice cannot masquerade as \
this device."
)));
}
Some(device_id.to_owned())
} else {
None
}; };
if !appservice_info.is_user_match(&sender_user) { Ok(ClientIdentity::Appservice {
return Err!(Request(Exclusive("User is not in namespace."))); sender_user,
} sender_device,
appservice_info: Box::new(appservice_info),
// MSC3202/MSC4190: Handle device_id masquerading for appservices. })
// The device_id can be provided via `device_id` or } else {
// `org.matrix.msc3202.device_id` query parameter. Err(Error::Request(
let sender_device = ErrorKind::UnknownToken(UnknownTokenErrorData::new()),
if let Some(device_id) = query.device_id.as_deref().map(Into::into) { "Invalid token".into(),
// Verify the device exists for this user StatusCode::UNAUTHORIZED,
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 { impl CheckAuth for AccessTokenOptional {
type Identity = Option<ClientIdentity>;
async fn verify<B: AsRef<[u8]> + Sync>( async fn verify<B: AsRef<[u8]> + Sync>(
services: &Services, services: &Services,
output: Self::Output, output: Self::Output,
request: &hyper::Request<B>, request: &hyper::Request<B>,
query: AuthQueryParams, query: AuthQueryParams,
route: TypeId, route: TypeId,
) -> Result<Auth> { ) -> Result<Self::Identity> {
match output { match output {
| Some(token) => | Some(token) =>
<AccessToken as CheckAuth>::verify(services, token, request, query, route).await, <AccessToken as CheckAuth>::verify(services, token, request, query, route)
| None => Ok(Auth::default()), .await
.map(Some),
| None => Ok(None),
} }
} }
} }
impl CheckAuth for AppserviceToken { impl CheckAuth for AppserviceToken {
type Identity = RegistrationInfo;
async fn verify<B: AsRef<[u8]> + Sync>( async fn verify<B: AsRef<[u8]> + Sync>(
services: &Services, services: &Services,
output: Self::Output, output: Self::Output,
_request: &hyper::Request<B>, _request: &hyper::Request<B>,
_query: AuthQueryParams, _query: AuthQueryParams,
_route: TypeId, _route: TypeId,
) -> Result<Auth> { ) -> Result<Self::Identity> {
let Ok(appservice_info) = services.appservice.find_from_token(&output).await else { let Ok(appservice_info) = services.appservice.find_from_token(&output).await else {
return Err!(Request(Unauthorized("Invalid appservice token."))); return Err!(Request(Unauthorized("Invalid appservice token.")));
}; };
Ok(Auth { Ok(appservice_info)
appservice_info: Some(appservice_info),
..Default::default()
})
} }
} }
impl CheckAuth for AppserviceTokenOptional { impl CheckAuth for AppserviceTokenOptional {
type Identity = Option<RegistrationInfo>;
async fn verify<B: AsRef<[u8]> + Sync>( async fn verify<B: AsRef<[u8]> + Sync>(
services: &Services, services: &Services,
output: Self::Output, output: Self::Output,
request: &hyper::Request<B>, request: &hyper::Request<B>,
query: AuthQueryParams, query: AuthQueryParams,
route: TypeId, route: TypeId,
) -> Result<Auth> { ) -> Result<Self::Identity> {
match output { match output {
| Some(token) => | Some(token) =>
<AppserviceToken as CheckAuth>::verify(services, token, request, query, route) <AppserviceToken as CheckAuth>::verify(services, token, request, query, route)
.await, .await
| None => Ok(Auth::default()), .map(Some),
| None => Ok(None),
} }
} }
} }
impl CheckAuth for NoAuthentication { impl CheckAuth for NoAuthentication {
type Identity = ();
async fn verify<B: AsRef<[u8]> + Sync>( async fn verify<B: AsRef<[u8]> + Sync>(
_services: &Services, _services: &Services,
_output: Self::Output, _output: Self::Output,
_request: &hyper::Request<B>, _request: &hyper::Request<B>,
_query: AuthQueryParams, _query: AuthQueryParams,
_route: TypeId, _route: TypeId,
) -> Result<Auth> { ) -> Result<Self::Identity> {
Ok(Auth::default()) Ok(())
} }
} }
impl CheckAuth for NoAccessToken { impl CheckAuth for NoAccessToken {
type Identity = Option<ClientIdentity>;
async fn verify<B: AsRef<[u8]> + Sync>( async fn verify<B: AsRef<[u8]> + Sync>(
services: &Services, services: &Services,
_output: Self::Output, _output: Self::Output,
request: &hyper::Request<B>, request: &hyper::Request<B>,
query: AuthQueryParams, query: AuthQueryParams,
route: TypeId, route: TypeId,
) -> Result<Auth> { ) -> Result<Self::Identity> {
// We handle these the same as AccessTokenOptional // We handle these the same as AccessTokenOptional
let token = AccessTokenOptional::extract_authentication(request).map_err(|err| { let token = AccessTokenOptional::extract_authentication(request).map_err(|err| {
err!(Request(Unauthorized(warn!("Failed to extract authorization: {}", 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> { ) -> Result<get_backfill::v1::Response> {
AccessCheck { AccessCheck {
services: &services, services: &services,
origin: body.origin(), origin: &body.identity,
room_id: &body.room_id, room_id: &body.room_id,
event_id: None, event_id: None,
} }
@@ -41,7 +41,7 @@ pub(crate) async fn get_backfill_route(
.await .await
{ {
info!( info!(
origin = body.origin().as_str(), origin = body.identity.as_str(),
"Refusing to serve backfill for room we aren't participating in" "Refusing to serve backfill for room we aren't participating in"
); );
return Err!(Request(NotFound("This server is not participating in that room."))); 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 Ok(services
.rooms .rooms
.state_accessor .state_accessor
.server_can_see_event(body.origin(), &pdu.room_id_or_hash(), &pdu.event_id) .server_can_see_event(&body.identity, &pdu.room_id_or_hash(), &pdu.event_id)
.await .await
.then_some(pdu)) .then_some(pdu))
}) })
+2 -2
View File
@@ -40,7 +40,7 @@ pub(crate) async fn get_event_route(
AccessCheck { AccessCheck {
services: &services, services: &services,
origin: body.origin(), origin: &body.identity,
room_id, room_id,
event_id: Some(&body.event_id), event_id: Some(&body.event_id),
} }
@@ -54,7 +54,7 @@ pub(crate) async fn get_event_route(
.await .await
{ {
info!( info!(
origin = body.origin().as_str(), origin = body.identity.as_str(),
"Refusing to serve state for room we aren't participating in" "Refusing to serve state for room we aren't participating in"
); );
return Err!(Request(NotFound("This server is not participating in that room."))); 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> { ) -> Result<get_event_authorization::v1::Response> {
AccessCheck { AccessCheck {
services: &services, services: &services,
origin: body.origin(), origin: &body.identity,
room_id: &body.room_id, room_id: &body.room_id,
event_id: None, event_id: None,
} }
@@ -42,7 +42,7 @@ pub(crate) async fn get_event_authorization_route(
.await .await
{ {
info!( info!(
origin = body.origin().as_str(), origin = body.identity.as_str(),
"Refusing to serve state for room we aren't participating in" "Refusing to serve state for room we aren't participating in"
); );
return Err!(Request(NotFound("This server is not participating in that room."))); 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> { ) -> Result<get_missing_events::v1::Response> {
AccessCheck { AccessCheck {
services: &services, services: &services,
origin: body.origin(), origin: &body.identity,
room_id: &body.room_id, room_id: &body.room_id,
event_id: None, event_id: None,
} }
@@ -36,7 +36,7 @@ pub(crate) async fn get_missing_events_route(
.await .await
{ {
info!( info!(
origin = body.origin().as_str(), origin = body.identity.as_str(),
"Refusing to serve state for room we aren't participating in" "Refusing to serve state for room we aren't participating in"
); );
return Err!(Request(NotFound("This server is not participating in that room."))); 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 if !services
.rooms .rooms
.state_accessor .state_accessor
.server_can_see_event(body.origin(), &body.room_id, pdu.event_id()) .server_can_see_event(&body.identity, &body.room_id, pdu.event_id())
.await .await
{ {
debug!(%next_event_id, origin = %body.origin(), "redacting event origin cannot see"); debug!(%next_event_id, origin = %body.identity, "redacting event origin cannot see");
pdu.redact(&room_version, json!({}))?; pdu.redact(&room_version, json!({}))?;
} }
+2 -2
View File
@@ -20,7 +20,7 @@ pub(crate) async fn get_hierarchy_route(
.await .await
{ {
info!( info!(
origin = body.origin().as_str(), origin = body.identity.as_str(),
"Refusing to serve state for room we aren't participating in" "Refusing to serve state for room we aren't participating in"
); );
return Err!(Request(NotFound("This server is not participating in that room."))); 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 let response = services
.rooms .rooms
.summary .summary
.get_local_room_summary_for_server(body.origin(), &body.room_id, body.suggested_only) .get_local_room_summary_for_server(&body.identity, &body.room_id, body.suggested_only)
.await; .await;
if let Accessibility::Accessible(response) = response { if let Accessibility::Accessible(response) = response {
+4 -5
View File
@@ -32,7 +32,7 @@ pub(crate) async fn create_invite_route(
services services
.rooms .rooms
.event_handler .event_handler
.acl_check(body.origin(), &body.room_id) .acl_check(&body.identity, &body.room_id)
.await?; .await?;
if !services.server.supported_room_version(&body.room_version) { if !services.server.supported_room_version(&body.room_version) {
@@ -54,12 +54,11 @@ pub(crate) async fn create_invite_route(
if services if services
.moderation .moderation
.is_remote_server_forbidden(body.origin()) .is_remote_server_forbidden(&body.identity)
{ {
warn!( warn!(
"Received federated/remote invite from banned server {} for room ID {}. Rejecting.", "Received federated/remote invite from banned server {} for room ID {}. Rejecting.",
body.origin(), body.identity, body.room_id
body.room_id
); );
return Err!(Request(Forbidden("Server is banned on this homeserver."))); return Err!(Request(Forbidden("Server is banned on this homeserver.")));
@@ -105,7 +104,7 @@ pub(crate) async fn create_invite_route(
.and_then(Result::ok) .and_then(Result::ok)
.ok_or_else(|| err!(Request(InvalidParam("Invalid sender property"))))?; .ok_or_else(|| err!(Request(InvalidParam("Invalid sender property"))))?;
if sender_user.server_name() != body.origin() { if sender_user.server_name() != body.identity {
return Err!(Request(Forbidden("Sender's server does not match the origin server.",))); return Err!(Request(Forbidden("Sender's server does not match the origin server.",)));
} }
+6 -8
View File
@@ -30,7 +30,7 @@ use crate::Ruma;
/// # `GET /_matrix/federation/v1/make_join/{roomId}/{userId}` /// # `GET /_matrix/federation/v1/make_join/{roomId}/{userId}`
/// ///
/// Creates a join template. /// Creates a join template.
#[tracing::instrument(skip_all, fields(room_id = %body.room_id, user_id = %body.user_id, origin = %body.origin()), level = "info")] #[tracing::instrument(skip_all, fields(room_id = %body.room_id, user_id = %body.user_id, origin = %body.identity), level = "info")]
pub(crate) async fn create_join_event_template_route( pub(crate) async fn create_join_event_template_route(
State(services): State<crate::State>, State(services): State<crate::State>,
body: Ruma<prepare_join_event::v1::Request>, body: Ruma<prepare_join_event::v1::Request>,
@@ -45,14 +45,14 @@ pub(crate) async fn create_join_event_template_route(
.await .await
{ {
info!( info!(
origin = body.origin().as_str(), origin = body.identity.as_str(),
room_id = %body.room_id, room_id = %body.room_id,
"Refusing to serve make_join for room we aren't participating in" "Refusing to serve make_join for room we aren't participating in"
); );
return Err!(Request(NotFound("This server is not participating in that room."))); return Err!(Request(NotFound("This server is not participating in that room.")));
} }
if body.user_id.server_name() != body.origin() { if body.user_id.server_name() != body.identity {
return Err!(Request(BadJson("Not allowed to join on behalf of another server/user."))); return Err!(Request(BadJson("Not allowed to join on behalf of another server/user.")));
} }
@@ -60,19 +60,17 @@ pub(crate) async fn create_join_event_template_route(
services services
.rooms .rooms
.event_handler .event_handler
.acl_check(body.origin(), &body.room_id) .acl_check(&body.identity, &body.room_id)
.await?; .await?;
if services if services
.moderation .moderation
.is_remote_server_forbidden(body.origin()) .is_remote_server_forbidden(&body.identity)
{ {
warn!( warn!(
"Server {} for remote user {} tried joining room ID {} which has a server name that \ "Server {} for remote user {} tried joining room ID {} which has a server name that \
is globally forbidden. Rejecting.", is globally forbidden. Rejecting.",
body.origin(), body.identity, &body.user_id, &body.room_id,
&body.user_id,
&body.room_id,
); );
return Err!(Request(Forbidden("Server is banned on this homeserver."))); return Err!(Request(Forbidden("Server is banned on this homeserver.")));
} }
+5 -7
View File
@@ -28,14 +28,14 @@ pub(crate) async fn create_knock_event_template_route(
.await .await
{ {
info!( info!(
origin = body.origin().as_str(), origin = body.identity.as_str(),
room_id = %body.room_id, room_id = %body.room_id,
"Refusing to serve make_knock for room we aren't participating in" "Refusing to serve make_knock for room we aren't participating in"
); );
return Err!(Request(NotFound("This server is not participating in that room."))); return Err!(Request(NotFound("This server is not participating in that room.")));
} }
if body.user_id.server_name() != body.origin() { if body.user_id.server_name() != body.identity {
return Err!(Request(BadJson("Not allowed to knock on behalf of another server/user."))); return Err!(Request(BadJson("Not allowed to knock on behalf of another server/user.")));
} }
@@ -43,19 +43,17 @@ pub(crate) async fn create_knock_event_template_route(
services services
.rooms .rooms
.event_handler .event_handler
.acl_check(body.origin(), &body.room_id) .acl_check(&body.identity, &body.room_id)
.await?; .await?;
if services if services
.moderation .moderation
.is_remote_server_forbidden(body.origin()) .is_remote_server_forbidden(&body.identity)
{ {
warn!( warn!(
"Server {} for remote user {} tried knocking room ID {} which has a server name \ "Server {} for remote user {} tried knocking room ID {} which has a server name \
that is globally forbidden. Rejecting.", that is globally forbidden. Rejecting.",
body.origin(), body.identity, &body.user_id, &body.room_id,
&body.user_id,
&body.room_id,
); );
return Err!(Request(Forbidden("Server is banned on this homeserver."))); 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 .await
{ {
info!( info!(
origin = body.origin().as_str(), origin = body.identity.as_str(),
"Refusing to serve make_leave for room we aren't participating in" "Refusing to serve make_leave for room we aren't participating in"
); );
return Err!(Request(NotFound("This server is not participating in that room."))); return Err!(Request(NotFound("This server is not participating in that room.")));
} }
if body.user_id.server_name() != body.origin() { if body.user_id.server_name() != body.identity {
return Err!(Request(Forbidden( return Err!(Request(Forbidden(
"Not allowed to leave on behalf of another server/user." "Not allowed to leave on behalf of another server/user."
))); )));
@@ -42,7 +42,7 @@ pub(crate) async fn create_leave_event_template_route(
services services
.rooms .rooms
.event_handler .event_handler
.acl_check(body.origin(), &body.room_id) .acl_check(&body.identity, &body.room_id)
.await?; .await?;
let room_version = services.rooms.state.get_room_version(&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, ClientIp(client): ClientIp,
body: Ruma<send_transaction_message::v1::Request>, body: Ruma<send_transaction_message::v1::Request>,
) -> Result<send_transaction_message::v1::Response> { ) -> Result<send_transaction_message::v1::Response> {
if body.origin() != body.body.origin { if body.identity != body.body.origin {
return Err!(Request(Forbidden( return Err!(Request(Forbidden(
"Not allowed to send transactions on behalf of other servers" "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.origin().to_owned(), body.transaction_id.clone()); let txn_key = (body.identity.clone(), body.transaction_id.clone());
// Atomically check cache, join active, or start new transaction // Atomically check cache, join active, or start new transaction
match services match services
@@ -136,7 +136,7 @@ async fn wait_for_result(
skip_all, skip_all,
fields( fields(
id = ?body.transaction_id.as_str(), id = ?body.transaction_id.as_str(),
origin = ?body.origin() origin = ?body.identity
) )
)] )]
async fn process_inbound_transaction( async fn process_inbound_transaction(
@@ -164,7 +164,7 @@ async fn process_inbound_transaction(
.stream(); .stream();
debug!(pdus = body.pdus.len(), edus = body.edus.len(), "Processing transaction",); debug!(pdus = body.pdus.len(), edus = body.edus.len(), "Processing transaction",);
let results = match handle(&services, &client, body.origin(), pdus, edus).await { let results = match handle(&services, &client, &body.identity, pdus, edus).await {
| Ok(results) => results, | Ok(results) => results,
| Err(err) => { | Err(err) => {
fail_federation_txn(services, &txn_key, &sender, 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