config option to auto-remediate bad users joining bad rooms or servers

also forgets all rooms upon leave_all_rooms

Signed-off-by: strawberry <strawberry@puppygock.gay>
This commit is contained in:
strawberry 2024-05-07 22:39:55 -04:00 committed by June
parent 6946eead28
commit d15e461303
5 changed files with 117 additions and 144 deletions

View file

@ -269,6 +269,19 @@ url_preview_check_root_domain = false
# Defaults to true # Defaults to true
allow_profile_lookup_federation_requests = true allow_profile_lookup_federation_requests = true
# Config option to automatically deactivate the account of any user who attempts to join a:
# - banned room
# - forbidden room alias
# - room alias or ID with a forbidden server name
#
# This may be useful if all your banned lists consist of toxic rooms or servers that no good faith user would ever attempt to join, and
# to automatically remediate the problem without any admin user intervention.
#
# This will also make the user leave all rooms. Federation (e.g. remote room invites) are ignored here.
#
# Defaults to false as rooms can be banned for non-moderation-related reasons
#auto_deactivate_banned_room_attempts = false
### Misc ### Misc

View file

@ -533,7 +533,7 @@ pub(crate) async fn deactivate_route(body: Ruma<deactivate::v3::Request>) -> Res
} }
// Make the user leave all rooms before deactivation // Make the user leave all rooms before deactivation
client_server::leave_all_rooms(sender_user).await?; client_server::leave_all_rooms(sender_user).await;
// Remove devices and mark account as deactivated // Remove devices and mark account as deactivated
services().users.deactivate_account(sender_user)?; services().users.deactivate_account(sender_user)?;

View file

@ -21,12 +21,13 @@ use ruma::{
room::{ room::{
join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent}, join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent},
member::{MembershipState, RoomMemberEventContent}, member::{MembershipState, RoomMemberEventContent},
message::RoomMessageEventContent,
}, },
StateEventType, TimelineEventType, StateEventType, TimelineEventType,
}, },
serde::Base64, serde::Base64,
state_res, CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, OwnedServerName, state_res, CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, OwnedServerName,
OwnedUserId, RoomId, RoomVersionId, UserId, OwnedUserId, RoomId, RoomVersionId, ServerName, UserId,
}; };
use serde_json::value::{to_raw_value, RawValue as RawJsonValue}; use serde_json::value::{to_raw_value, RawValue as RawJsonValue};
use tokio::sync::RwLock; use tokio::sync::RwLock;
@ -40,6 +41,91 @@ use crate::{
Error, PduEvent, Result, Ruma, Error, PduEvent, Result, Ruma,
}; };
/// Checks if the room is banned in any way possible and the sender user is not
/// an admin.
///
/// Performs automatic deactivation if `auto_deactivate_banned_room_attempts` is
/// enabled
#[tracing::instrument]
async fn banned_room_check(user_id: &UserId, room_id: Option<&RoomId>, server_name: Option<&ServerName>) -> Result<()> {
if !services().users.is_admin(user_id)? {
if let Some(room_id) = room_id {
if services().rooms.metadata.is_banned(room_id)?
|| services()
.globals
.config
.forbidden_remote_server_names
.contains(&room_id.server_name().unwrap().to_owned())
{
warn!(
"User {user_id} who is not an admin attempted to send an invite for or attempted to join a banned \
room or banned room server name: {room_id}."
);
if services()
.globals
.config
.auto_deactivate_banned_room_attempts
{
warn!("Automatically deactivating user {user_id} due to attempted banned room join");
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Automatically deactivating user {user_id} due to attempted banned room join"
)))
.await;
// ignore errors
leave_all_rooms(user_id).await;
_ = services().users.deactivate_account(user_id);
}
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This room is banned on this homeserver.",
));
}
} else if let Some(server_name) = server_name {
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&server_name.to_owned())
{
warn!(
"User {user_id} who is not an admin tried joining a room which has the server name {server_name} \
that is globally forbidden. Rejecting.",
);
if services()
.globals
.config
.auto_deactivate_banned_room_attempts
{
warn!("Automatically deactivating user {user_id} due to attempted banned room join");
services()
.admin
.send_message(RoomMessageEventContent::text_plain(format!(
"Automatically deactivating user {user_id} due to attempted banned room join"
)))
.await;
// ignore errors
leave_all_rooms(user_id).await;
_ = services().users.deactivate_account(user_id);
}
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This remote server is banned on this homeserver.",
));
}
}
}
Ok(())
}
/// # `POST /_matrix/client/r0/rooms/{roomId}/join` /// # `POST /_matrix/client/r0/rooms/{roomId}/join`
/// ///
/// Tries to join the sender user into a room. /// Tries to join the sender user into a room.
@ -53,32 +139,7 @@ pub(crate) async fn join_room_by_id_route(
) -> Result<join_room_by_id::v3::Response> { ) -> Result<join_room_by_id::v3::Response> {
let sender_user = body.sender_user.as_ref().expect("user is authenticated"); let sender_user = body.sender_user.as_ref().expect("user is authenticated");
if services().rooms.metadata.is_banned(&body.room_id)? && !services().users.is_admin(sender_user)? { banned_room_check(sender_user, Some(&body.room_id), body.room_id.server_name()).await?;
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This room is banned on this homeserver.",
));
}
if let Some(server) = body.room_id.server_name() {
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&server.to_owned())
&& !services().users.is_admin(sender_user)?
{
warn!(
"User {sender_user} tried joining room ID {} which has a server name that is globally forbidden. \
Rejecting.",
body.room_id
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This remote server is banned on this homeserver.",
));
}
}
// There is no body.server_name for /roomId/join // There is no body.server_name for /roomId/join
let mut servers = services() let mut servers = services()
@ -131,31 +192,7 @@ pub(crate) async fn join_room_by_id_or_alias_route(
let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias) { let (servers, room_id) = match OwnedRoomId::try_from(body.room_id_or_alias) {
Ok(room_id) => { Ok(room_id) => {
if services().rooms.metadata.is_banned(&room_id)? && !services().users.is_admin(sender_user)? { banned_room_check(sender_user, Some(&room_id), room_id.server_name()).await?;
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This room is banned on this homeserver.",
));
}
if let Some(server) = room_id.server_name() {
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&server.to_owned())
&& !services().users.is_admin(sender_user)?
{
warn!(
"User {sender_user} tried joining room ID {room_id} which has a server name that is globally \
forbidden. Rejecting.",
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This remote server is banned on this homeserver.",
));
}
}
let mut servers = body.server_name.clone(); let mut servers = body.server_name.clone();
servers.extend( servers.extend(
@ -186,69 +223,9 @@ pub(crate) async fn join_room_by_id_or_alias_route(
(servers, room_id) (servers, room_id)
}, },
Err(room_alias) => { Err(room_alias) => {
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&room_alias.server_name().to_owned())
&& !services().users.is_admin(sender_user)?
{
warn!(
"User {sender_user} tried joining room alias {room_alias} which has a server name that is \
globally forbidden. Rejecting.",
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This remote server is banned on this homeserver.",
));
}
let response = get_alias_helper(room_alias.clone(), Some(body.server_name.clone())).await?; let response = get_alias_helper(room_alias.clone(), Some(body.server_name.clone())).await?;
if services().rooms.metadata.is_banned(&response.room_id)? && !services().users.is_admin(sender_user)? { banned_room_check(sender_user, Some(&response.room_id), Some(room_alias.server_name())).await?;
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This room is banned on this homeserver.",
));
}
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&room_alias.server_name().to_owned())
&& !services().users.is_admin(sender_user)?
{
warn!(
"User {sender_user} tried joining room alias {room_alias} with room ID {}, which the alias has a \
server name that is globally forbidden. Rejecting.",
&response.room_id
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This remote server is banned on this homeserver.",
));
}
if let Some(server) = response.room_id.server_name() {
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&server.to_owned())
&& !services().users.is_admin(sender_user)?
{
warn!(
"User {sender_user} tried joining room alias {room_alias} with room ID {}, which has a server \
name that is globally forbidden. Rejecting.",
&response.room_id
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This remote server is banned on this homeserver.",
));
}
}
let mut servers = body.server_name; let mut servers = body.server_name;
servers.extend(response.servers); servers.extend(response.servers);
@ -321,30 +298,7 @@ pub(crate) async fn invite_user_route(body: Ruma<invite_user::v3::Request>) -> R
)); ));
} }
if services().rooms.metadata.is_banned(&body.room_id)? && !services().users.is_admin(sender_user)? { banned_room_check(sender_user, Some(&body.room_id), body.room_id.server_name()).await?;
info!(
"Local user {} who is not an admin attempted to send an invite for banned room {}.",
&sender_user, &body.room_id
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"This room is banned on this homeserver.",
));
}
if let Some(server) = body.room_id.server_name() {
if services()
.globals
.config
.forbidden_remote_server_names
.contains(&server.to_owned())
{
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Server is banned on this homeserver.",
));
}
}
if let invite_user::v3::InvitationRecipient::UserId { if let invite_user::v3::InvitationRecipient::UserId {
user_id, user_id,
@ -1606,8 +1560,9 @@ pub(crate) async fn invite_helper(
Ok(()) Ok(())
} }
// Make a user leave all their joined rooms // Make a user leave all their joined rooms, forgets all rooms, and ignores
pub(crate) async fn leave_all_rooms(user_id: &UserId) -> Result<()> { // errors
pub(crate) async fn leave_all_rooms(user_id: &UserId) {
let all_rooms = services() let all_rooms = services()
.rooms .rooms
.state_cache .state_cache
@ -1627,10 +1582,9 @@ pub(crate) async fn leave_all_rooms(user_id: &UserId) -> Result<()> {
}; };
// ignore errors // ignore errors
_ = services().rooms.state_cache.forget(&room_id, user_id);
_ = leave_room(user_id, &room_id, None).await; _ = leave_room(user_id, &room_id, None).await;
} }
Ok(())
} }
pub(crate) async fn leave_room(user_id: &UserId, room_id: &RoomId, reason: Option<String>) -> Result<()> { pub(crate) async fn leave_room(user_id: &UserId, room_id: &RoomId, reason: Option<String>) -> Result<()> {

View file

@ -207,6 +207,8 @@ pub(crate) struct Config {
#[serde(default = "Vec::new")] #[serde(default = "Vec::new")]
pub(crate) auto_join_rooms: Vec<OwnedRoomId>, pub(crate) auto_join_rooms: Vec<OwnedRoomId>,
#[serde(default)]
pub(crate) auto_deactivate_banned_room_attempts: bool,
#[serde(default = "default_rocksdb_log_level")] #[serde(default = "default_rocksdb_log_level")]
pub(crate) rocksdb_log_level: String, pub(crate) rocksdb_log_level: String,
@ -612,6 +614,10 @@ impl fmt::Display for Config {
"Allow incoming profile lookup federation requests", "Allow incoming profile lookup federation requests",
&self.allow_profile_lookup_federation_requests.to_string(), &self.allow_profile_lookup_federation_requests.to_string(),
), ),
(
"Auto deactivate banned room join attempts",
&self.auto_deactivate_banned_room_attempts.to_string(),
),
("Notification push path", &self.notification_push_path), ("Notification push path", &self.notification_push_path),
("Allow room creation", &self.allow_room_creation.to_string()), ("Allow room creation", &self.allow_room_creation.to_string()),
( (

View file

@ -167,7 +167,7 @@ pub(crate) async fn deactivate(
services().users.deactivate_account(&user_id)?; services().users.deactivate_account(&user_id)?;
if leave_rooms { if leave_rooms {
leave_all_rooms(&user_id).await?; leave_all_rooms(&user_id).await;
} }
Ok(RoomMessageEventContent::text_plain(format!( Ok(RoomMessageEventContent::text_plain(format!(
@ -282,7 +282,7 @@ pub(crate) async fn deactivate_all(body: Vec<&str>, leave_rooms: bool, force: bo
if leave_rooms { if leave_rooms {
for &user_id in &user_ids { for &user_id in &user_ids {
_ = leave_all_rooms(user_id).await; leave_all_rooms(user_id).await;
} }
} }