diff --git a/Cargo.toml b/Cargo.toml index 3cec668f4..0ef137530 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -363,6 +363,7 @@ features = [ "unstable-msc4310", "unstable-msc4311", "unstable-extensible-events", + "unstable-msc4380", ] [workspace.dependencies.rustls] diff --git a/src/api/client/membership/invite.rs b/src/api/client/membership/invite.rs index 1a3c330c4..216e8845a 100644 --- a/src/api/client/membership/invite.rs +++ b/src/api/client/membership/invite.rs @@ -1,11 +1,17 @@ use axum::extract::State; use axum_client_ip::InsecureClientIp; use futures::{FutureExt, join}; -use ruma::{api::client::membership::invite_user, events::room::member::MembershipState}; +use ruma::{ + api::client::{error::ErrorKind::InviteBlocked, membership::invite_user}, + events::room::member::MembershipState, +}; use tuwunel_core::{Err, Result}; use super::banned_room_check; -use crate::{Ruma, client::utils::invite_check}; +use crate::{ + Ruma, + client::utils::{invite_check, is_invite_blocked}, +}; /// # `POST /_matrix/client/r0/rooms/{roomId}/invite` /// @@ -28,6 +34,14 @@ pub(crate) async fn invite_user_route( return Err!(Request(ThreepidDenied("Third party identifiers are not implemented"))); }; + if is_invite_blocked(&services, user_id).await { + warn!( + "{user_id} has blocked invites and {sender_user} attempted to send an invite to \ + {room_id}" + ); + return Err!(Request(InviteBlocked)); + } + let sender_ignored_recipient = services .users .user_is_ignored(sender_user, user_id); diff --git a/src/api/client/room/create.rs b/src/api/client/room/create.rs index a037a6311..3f2ff2aee 100644 --- a/src/api/client/room/create.rs +++ b/src/api/client/room/create.rs @@ -37,7 +37,10 @@ use tuwunel_core::{ }; use tuwunel_service::{Services, appservice::RegistrationInfo, rooms::state::RoomMutexGuard}; -use crate::{Ruma, client::utils::invite_check}; +use crate::{ + Ruma, + client::utils::{invite_check, is_invite_blocked}, +}; /// # `POST /_matrix/client/v3/createRoom` /// @@ -391,6 +394,14 @@ pub(crate) async fn create_room_route( continue; } + if is_invite_blocked(&services, user_id).await { + warn!( + "{user_id} has blocked invites and {sender_user} attempted to send an \ + invite in createRoom for {room_id}" + ); + continue; + } + if let Err(e) = services .membership .invite(sender_user, user_id, &room_id, None, body.is_direct) diff --git a/src/api/client/utils.rs b/src/api/client/utils.rs index 17f27d341..f669b1fa1 100644 --- a/src/api/client/utils.rs +++ b/src/api/client/utils.rs @@ -1,12 +1,27 @@ -use ruma::{RoomId, UserId}; +use ruma::{RoomId, UserId, api::client::error::ErrorKind::InviteBlocked}; +use serde::Deserialize; use tuwunel_core::{Err, Result, warn}; use tuwunel_service::Services; +#[derive(Debug, Deserialize)] +pub(crate) struct InvitePermissionConfig { + #[serde(default)] + pub(crate) default_action: InviteDefaultAction, +} + +#[derive(Debug, Deserialize, PartialEq, Eq, Default)] +#[serde(rename_all = "snake_case")] +pub(crate) enum InviteDefaultAction { + #[default] + Allow, + Block, +} pub(crate) async fn invite_check( services: &Services, sender_user: &UserId, room_id: &RoomId, ) -> Result { + // when server admin block non admin invites, check if user is admin if !services.admin.user_is_admin(sender_user).await && services.config.block_non_admin_invites { warn!("{sender_user} is not an admin and attempted to send an invite to {room_id}"); @@ -15,3 +30,32 @@ pub(crate) async fn invite_check( Ok(()) } + +pub(crate) async fn is_invite_blocked(services: &Services, target_user: &UserId) -> bool { + // Check stable identifier + if let Ok(config) = services + .account_data + .get_global::(target_user, "m.invite_permission_config".into()) + .await + { + if config.default_action == InviteDefaultAction::Block { + return true; + } + } + // TODO: when MSC4380 is stable, remove this + // Check unstable identifier + if let Ok(config) = services + .account_data + .get_global::( + target_user, + "org.matrix.msc4380.invite_permission_config".into(), + ) + .await + { + if config.default_action == InviteDefaultAction::Block { + return true; + } + } + + false +} diff --git a/src/api/client/versions.rs b/src/api/client/versions.rs index 203d34d06..b5981555e 100644 --- a/src/api/client/versions.rs +++ b/src/api/client/versions.rs @@ -86,4 +86,6 @@ static UNSTABLE_FEATURES: [&str; 18] = [ "org.matrix.simplified_msc3575", // Allow room moderators to view redacted event content (https://github.com/matrix-org/matrix-spec-proposals/pull/2815) "fi.mau.msc2815", + // invite blocking (https://github.com/matrix-org/matrix-spec-proposals/pull/4380) + "org.matrix.msc4380", ]; diff --git a/src/api/server/invite.rs b/src/api/server/invite.rs index cc26f146c..d31e43c42 100644 --- a/src/api/server/invite.rs +++ b/src/api/server/invite.rs @@ -5,7 +5,7 @@ use futures::StreamExt; use ruma::{ CanonicalJsonValue, OwnedRoomId, OwnedUserId, RoomId, UserId, api::{ - client::error::ErrorKind, + client::error::{ErrorKind, ErrorKind::InviteBlocked}, federation::membership::{RawStrippedState, create_invite}, }, events::{ @@ -23,7 +23,7 @@ use tuwunel_core::{ utils::hash::sha256, }; -use crate::Ruma; +use crate::{Ruma, client::utils::is_invite_blocked}; /// # `PUT /_matrix/federation/v2/invite/{roomId}/{eventId}` /// @@ -158,6 +158,14 @@ pub(crate) async fn create_invite_route( return Err!(Request(Forbidden("This server does not allow room invites."))); } + if is_invite_blocked(&services, &invited_user).await { + warn!( + "{invited_user} has blocked invites and {sender} attempted to send a federation \ + invite for {room_id}" + ); + return Err!(Request(InviteBlocked)); + } + let mut invite_state: Vec<_> = body .invite_room_state .clone() diff --git a/src/core/error/response.rs b/src/core/error/response.rs index cc15038ee..aafbe9a08 100644 --- a/src/core/error/response.rs +++ b/src/core/error/response.rs @@ -81,6 +81,7 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode { | UserDeactivated | ThreepidDenied | WrongRoomKeysVersion { .. } + | InviteBlocked | Forbidden { .. } => StatusCode::FORBIDDEN, // 401