From fc93b29abe50e4f98fa33c9cd51214cdf78827a9 Mon Sep 17 00:00:00 2001
From: Matthias Ahouansou <matthias@ahouansou.cz>
Date: Thu, 8 Feb 2024 19:11:48 -0500
Subject: [PATCH] feat: forbid certain usernames & room aliases

squashed from https://gitlab.com/famedly/conduit/-/merge_requests/582

Signed-off-by: strawberry <strawberry@puppygock.gay>
---
 Cargo.lock                       | 23 +++++++++++++++-
 Cargo.toml                       |  3 +++
 src/api/client_server/account.rs | 24 +++++++++++++++++
 src/api/client_server/alias.rs   | 11 ++++++++
 src/api/client_server/room.rs    | 12 +++++++++
 src/config/mod.rs                | 16 +++++++++++
 src/database/mod.rs              | 46 ++++++++++++++++++++++++++++++++
 src/service/globals/mod.rs       |  9 +++++++
 8 files changed, 143 insertions(+), 1 deletion(-)

diff --git a/Cargo.lock b/Cargo.lock
index f653a2d0b..991c7e1b1 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -402,6 +402,7 @@ dependencies = [
  "hyperlocal",
  "image",
  "ipaddress",
+ "itertools 0.12.1",
  "jsonwebtoken",
  "lazy_static",
  "lru-cache",
@@ -422,6 +423,7 @@ dependencies = [
  "serde",
  "serde_html_form",
  "serde_json",
+ "serde_regex",
  "serde_yaml",
  "sha-1",
  "sha2",
@@ -1123,6 +1125,15 @@ dependencies = [
  "either",
 ]
 
+[[package]]
+name = "itertools"
+version = "0.12.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "ba291022dbbd398a455acf126c1e341954079855bc60dfdda641363bd6922569"
+dependencies = [
+ "either",
+]
+
 [[package]]
 name = "itoa"
 version = "1.0.10"
@@ -2191,7 +2202,7 @@ name = "ruma-state-res"
 version = "0.10.0"
 source = "git+https://github.com/ruma/ruma?rev=68c9bb0930f2195fa8672fbef9633ef62737df5d#68c9bb0930f2195fa8672fbef9633ef62737df5d"
 dependencies = [
- "itertools",
+ "itertools 0.11.0",
  "js_int",
  "ruma-common",
  "ruma-events",
@@ -2404,6 +2415,16 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "serde_regex"
+version = "1.1.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a8136f1a4ea815d7eac4101cfd0b16dc0cb5e1fe1b8609dfd728058656b7badf"
+dependencies = [
+ "regex",
+ "serde",
+]
+
 [[package]]
 name = "serde_spanned"
 version = "0.6.5"
diff --git a/Cargo.toml b/Cargo.toml
index 41a5a8b5b..3b79ad692 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -62,6 +62,9 @@ ring = "0.17.7"
 trust-dns-resolver = "0.23.2"
 # Used to find matching events for appservices
 regex = "1.10.3"
+# Used to load forbidden room/user regex from config
+serde_regex = "1.1.0"
+itertools = "0.12.1"
 # jwt jsonwebtokens
 jsonwebtoken = "9.2.0"
 # Performance measurements
diff --git a/src/api/client_server/account.rs b/src/api/client_server/account.rs
index 9ba82a58a..2f0a10aab 100644
--- a/src/api/client_server/account.rs
+++ b/src/api/client_server/account.rs
@@ -54,6 +54,17 @@ pub async fn get_register_available_route(
         ));
     }
 
+    if services()
+        .globals
+        .forbidden_usernames()
+        .is_match(user_id.localpart())
+    {
+        return Err(Error::BadRequest(
+            ErrorKind::Unknown,
+            "Username is forbidden.",
+        ));
+    }
+
     // TODO add check for appservice namespaces
 
     // If no if check is true we have an username that's available to be used.
@@ -120,12 +131,25 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
                 ErrorKind::InvalidUsername,
                 "Username is invalid.",
             ))?;
+
             if services().users.exists(&proposed_user_id)? {
                 return Err(Error::BadRequest(
                     ErrorKind::UserInUse,
                     "Desired user ID is already taken.",
                 ));
             }
+
+            if services()
+                .globals
+                .forbidden_usernames()
+                .is_match(proposed_user_id.localpart())
+            {
+                return Err(Error::BadRequest(
+                    ErrorKind::Unknown,
+                    "Username is forbidden.",
+                ));
+            }
+
             proposed_user_id
         }
         _ => loop {
diff --git a/src/api/client_server/alias.rs b/src/api/client_server/alias.rs
index bf9034455..1a236449b 100644
--- a/src/api/client_server/alias.rs
+++ b/src/api/client_server/alias.rs
@@ -26,6 +26,17 @@ pub async fn create_alias_route(
         ));
     }
 
+    if services()
+        .globals
+        .forbidden_room_names()
+        .is_match(body.room_alias.alias())
+    {
+        return Err(Error::BadRequest(
+            ErrorKind::Unknown,
+            "Room alias is forbidden.",
+        ));
+    }
+
     if services()
         .rooms
         .alias
diff --git a/src/api/client_server/room.rs b/src/api/client_server/room.rs
index 28647fa0c..dbd86b66f 100644
--- a/src/api/client_server/room.rs
+++ b/src/api/client_server/room.rs
@@ -166,6 +166,18 @@ pub async fn create_room_route(
                     ));
                 }
 
+                // check if room alias is forbidden
+                if services()
+                    .globals
+                    .forbidden_room_names()
+                    .is_match(localpart)
+                {
+                    return Err(Error::BadRequest(
+                        ErrorKind::Unknown,
+                        "Room alias name is forbidden.",
+                    ));
+                }
+
                 let alias = RoomAliasId::parse(format!(
                     "#{}:{}",
                     localpart,
diff --git a/src/config/mod.rs b/src/config/mod.rs
index d5cecfe27..4beccc680 100644
--- a/src/config/mod.rs
+++ b/src/config/mod.rs
@@ -7,6 +7,8 @@
 
 use figment::Figment;
 
+use itertools::Itertools;
+use regex::RegexSet;
 use ruma::{OwnedServerName, RoomVersionId};
 use serde::{de::IgnoredAny, Deserialize};
 use tracing::{error, warn};
@@ -132,6 +134,14 @@ pub struct Config {
     #[serde(default = "default_ip_range_denylist")]
     pub ip_range_denylist: Vec<String>,
 
+    #[serde(default = "RegexSet::empty")]
+    #[serde(with = "serde_regex")]
+    pub forbidden_room_names: RegexSet,
+
+    #[serde(default = "RegexSet::empty")]
+    #[serde(with = "serde_regex")]
+    pub forbidden_usernames: RegexSet,
+
     #[serde(flatten)]
     pub catchall: BTreeMap<String, IgnoredAny>,
 }
@@ -319,6 +329,12 @@ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
                 }
                 &lst.join(", ")
             }),
+            ("Forbidden usernames", {
+                &self.forbidden_usernames.patterns().iter().join(", ")
+            }),
+            ("Forbidden room names", {
+                &self.forbidden_room_names.patterns().iter().join(", ")
+            }),
         ];
 
         let mut msg: String = "Active config values:\n\n".to_owned();
diff --git a/src/database/mod.rs b/src/database/mod.rs
index 32921b132..e3026377d 100644
--- a/src/database/mod.rs
+++ b/src/database/mod.rs
@@ -8,6 +8,7 @@
 use abstraction::{KeyValueDatabaseEngine, KvTree};
 use argon2::{password_hash::SaltString, PasswordHasher, PasswordVerifier};
 use directories::ProjectDirs;
+use itertools::Itertools;
 use lru_cache::LruCache;
 use rand::thread_rng;
 use ruma::{
@@ -971,6 +972,51 @@ pub async fn load_or_create(config: Config) -> Result<()> {
                 latest_database_version
             );
 
+            {
+                let patterns = &services().globals.config.forbidden_usernames;
+                if !patterns.is_empty() {
+                    for user in services().users.iter() {
+                        let user_id = user?;
+                        let matches = patterns.matches(user_id.localpart());
+                        if matches.matched_any() {
+                            warn!(
+                                "User {} matches the following forbidden username patterns: {}",
+                                user_id.to_string(),
+                                matches
+                                    .into_iter()
+                                    .map(|x| &patterns.patterns()[x])
+                                    .join(", ")
+                            )
+                        }
+                    }
+                }
+            }
+
+            {
+                let patterns = &services().globals.config.forbidden_room_names;
+                if !patterns.is_empty() {
+                    for address in services().rooms.metadata.iter_ids() {
+                        let room_id = address?;
+                        let room_aliases = services().rooms.alias.local_aliases_for_room(&room_id);
+                        for room_alias_result in room_aliases {
+                            let room_alias = room_alias_result?;
+                            let matches = patterns.matches(room_alias.alias());
+                            if matches.matched_any() {
+                                warn!(
+                                "Room with alias {} ({}) matches the following forbidden room name patterns: {}",
+                                    room_alias,
+                                    &room_id,
+                                    matches
+                                        .into_iter()
+                                        .map(|x| &patterns.patterns()[x])
+                                        .join(", ")
+                            )
+                            }
+                        }
+                    }
+                }
+            }
+
             info!(
                 "Loaded {} database with version {}",
                 services().globals.config.database_backend,
diff --git a/src/service/globals/mod.rs b/src/service/globals/mod.rs
index 1f90684f7..434a8c4c2 100644
--- a/src/service/globals/mod.rs
+++ b/src/service/globals/mod.rs
@@ -1,6 +1,7 @@
 mod data;
 use argon2::Argon2;
 pub use data::Data;
+use regex::RegexSet;
 use ruma::{
     serde::Base64, OwnedDeviceId, OwnedEventId, OwnedRoomId, OwnedServerName,
     OwnedServerSigningKeyId, OwnedUserId,
@@ -389,6 +390,14 @@ pub fn emergency_password(&self) -> &Option<String> {
         &self.config.emergency_password
     }
 
+    pub fn forbidden_room_names(&self) -> &RegexSet {
+        &self.config.forbidden_room_names
+    }
+
+    pub fn forbidden_usernames(&self) -> &RegexSet {
+        &self.config.forbidden_usernames
+    }
+
     pub fn allow_local_presence(&self) -> bool {
         self.config.allow_local_presence
     }
-- 
GitLab