Compare commits

...

14 Commits

Author SHA1 Message Date
timedout ce46b6869f chore: Bump dependencies to fix request errors 2026-01-05 20:10:30 +00:00
timedout a18b8254d0 chore: Add news fragment 2026-01-05 20:10:30 +00:00
timedout 279f7cbfe4 style: Fix failing lints 2026-01-05 20:10:29 +00:00
timedout 006c57face perf: Don't check accept_make_join twice for restricted make_join 2026-01-05 20:10:29 +00:00
timedout d52e0dc014 fix: Apply check_all_joins to make_join 2026-01-05 20:10:29 +00:00
timedout 4b873a1b95 fix: Apply spam checker to local restricted joins 2026-01-05 20:10:29 +00:00
timedout 76865e6f91 fix: Accept_may_join callback works again 2026-01-05 20:10:29 +00:00
timedout 99f16c2dfc fix: Call user_may_join_room later in the join process 2026-01-05 20:10:28 +00:00
timedout 5ac82f36f3 feat: Consolidate antispam checks into a service
Also adds support for the spam checker join rule, and Draupnir callbacks
2026-01-05 20:10:28 +00:00
timedout c249dd992e feat: Add support for automatically rejecting pending invites 2026-01-05 20:10:28 +00:00
timedout 0956779802 feat: Add Meowlnir invite interception support
Co-authored-by: Jade Ellis <jade@ellis.link>
2026-01-05 20:10:27 +00:00
timedout a83c1f1513 fix: Restrict suspend+lock commands to admin room
Also prevent locking the service user or admin users
2026-01-05 19:49:12 +00:00
timedout 8b5e4d8fe1 chore: Add news fragment 2026-01-05 19:34:21 +00:00
timedout 7502a944d7 feat: Add user locking and unlocking commands and functionality
Also corrects the response code returned by UserSuspended
2026-01-05 19:30:16 +00:00
21 changed files with 742 additions and 161 deletions
Generated
+33 -11
View File
@@ -1632,6 +1632,16 @@ dependencies = [
"litrs", "litrs",
] ]
[[package]]
name = "draupnir-antispam"
version = "0.1.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"ruma-common",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "dtor" name = "dtor"
version = "0.1.0" version = "0.1.0"
@@ -2982,6 +2992,16 @@ version = "2.7.6"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273" checksum = "f52b00d39961fc5b2736ea853c9cc86238e165017a493d1d5c8eac6bdc4cc273"
[[package]]
name = "meowlnir-antispam"
version = "0.1.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [
"ruma-common",
"serde",
"serde_json",
]
[[package]] [[package]]
name = "mime" name = "mime"
version = "0.3.17" version = "0.3.17"
@@ -4065,11 +4085,13 @@ checksum = "88f8660c1ff60292143c98d08fc6e2f654d722db50410e3f3797d40baaf9d8f3"
[[package]] [[package]]
name = "ruma" name = "ruma"
version = "0.10.1" version = "0.10.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [ dependencies = [
"assign", "assign",
"draupnir-antispam",
"js_int", "js_int",
"js_option", "js_option",
"meowlnir-antispam",
"ruma-appservice-api", "ruma-appservice-api",
"ruma-client-api", "ruma-client-api",
"ruma-common", "ruma-common",
@@ -4085,7 +4107,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-appservice-api" name = "ruma-appservice-api"
version = "0.10.0" version = "0.10.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@@ -4097,7 +4119,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-client-api" name = "ruma-client-api"
version = "0.18.0" version = "0.18.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"assign", "assign",
@@ -4120,7 +4142,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-common" name = "ruma-common"
version = "0.13.0" version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"base64 0.22.1", "base64 0.22.1",
@@ -4152,7 +4174,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-events" name = "ruma-events"
version = "0.28.1" version = "0.28.1"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [ dependencies = [
"as_variant", "as_variant",
"indexmap", "indexmap",
@@ -4177,7 +4199,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-federation-api" name = "ruma-federation-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [ dependencies = [
"bytes", "bytes",
"headers", "headers",
@@ -4199,7 +4221,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identifiers-validation" name = "ruma-identifiers-validation"
version = "0.9.5" version = "0.9.5"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [ dependencies = [
"js_int", "js_int",
"thiserror 2.0.17", "thiserror 2.0.17",
@@ -4208,7 +4230,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-identity-service-api" name = "ruma-identity-service-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@@ -4218,7 +4240,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-macros" name = "ruma-macros"
version = "0.13.0" version = "0.13.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [ dependencies = [
"cfg-if", "cfg-if",
"proc-macro-crate", "proc-macro-crate",
@@ -4233,7 +4255,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-push-gateway-api" name = "ruma-push-gateway-api"
version = "0.9.0" version = "0.9.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [ dependencies = [
"js_int", "js_int",
"ruma-common", "ruma-common",
@@ -4245,7 +4267,7 @@ dependencies = [
[[package]] [[package]]
name = "ruma-signatures" name = "ruma-signatures"
version = "0.15.0" version = "0.15.0"
source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=27abe0dcd33fd4056efc94bab3582646b31b6ce9#27abe0dcd33fd4056efc94bab3582646b31b6ce9" source = "git+https://forgejo.ellis.link/continuwuation/ruwuma?rev=79abd5d331bca596b7f37e367a9f2cebccd9f64d#79abd5d331bca596b7f37e367a9f2cebccd9f64d"
dependencies = [ dependencies = [
"base64 0.22.1", "base64 0.22.1",
"ed25519-dalek", "ed25519-dalek",
+62 -62
View File
@@ -33,11 +33,11 @@ features = ["serde"]
[workspace.dependencies.smallvec] [workspace.dependencies.smallvec]
version = "1.14.0" version = "1.14.0"
features = [ features = [
"const_generics", "const_generics",
"const_new", "const_new",
"serde", "serde",
"union", "union",
"write", "write",
] ]
[workspace.dependencies.smallstr] [workspace.dependencies.smallstr]
@@ -96,13 +96,13 @@ version = "1.11.1"
version = "0.7.9" version = "0.7.9"
default-features = false default-features = false
features = [ features = [
"form", "form",
"http1", "http1",
"http2", "http2",
"json", "json",
"matched-path", "matched-path",
"tokio", "tokio",
"tracing", "tracing",
] ]
[workspace.dependencies.axum-extra] [workspace.dependencies.axum-extra]
@@ -149,10 +149,10 @@ features = ["aws_lc_rs"]
version = "0.12.15" version = "0.12.15"
default-features = false default-features = false
features = [ features = [
"rustls-tls-native-roots", "rustls-tls-native-roots",
"socks", "socks",
"hickory-dns", "hickory-dns",
"http2", "http2",
] ]
[workspace.dependencies.serde] [workspace.dependencies.serde]
@@ -188,18 +188,18 @@ default-features = false
version = "0.25.5" version = "0.25.5"
default-features = false default-features = false
features = [ features = [
"jpeg", "jpeg",
"png", "png",
"gif", "gif",
"webp", "webp",
] ]
[workspace.dependencies.blurhash] [workspace.dependencies.blurhash]
version = "0.2.3" version = "0.2.3"
default-features = false default-features = false
features = [ features = [
"fast-linear-to-srgb", "fast-linear-to-srgb",
"image", "image",
] ]
# logging # logging
@@ -229,13 +229,13 @@ default-features = false
version = "4.5.35" version = "4.5.35"
default-features = false default-features = false
features = [ features = [
"derive", "derive",
"env", "env",
"error-context", "error-context",
"help", "help",
"std", "std",
"string", "string",
"usage", "usage",
] ]
[workspace.dependencies.futures] [workspace.dependencies.futures]
@@ -247,15 +247,15 @@ features = ["std", "async-await"]
version = "1.44.2" version = "1.44.2"
default-features = false default-features = false
features = [ features = [
"fs", "fs",
"net", "net",
"macros", "macros",
"sync", "sync",
"signal", "signal",
"time", "time",
"rt-multi-thread", "rt-multi-thread",
"io-util", "io-util",
"tracing", "tracing",
] ]
[workspace.dependencies.tokio-metrics] [workspace.dependencies.tokio-metrics]
@@ -280,18 +280,18 @@ default-features = false
version = "1.6.0" version = "1.6.0"
default-features = false default-features = false
features = [ features = [
"server", "server",
"http1", "http1",
"http2", "http2",
] ]
[workspace.dependencies.hyper-util] [workspace.dependencies.hyper-util]
version = "=0.1.17" version = "=0.1.17"
default-features = false default-features = false
features = [ features = [
"server-auto", "server-auto",
"server-graceful", "server-graceful",
"tokio", "tokio",
] ]
# to support multiple variations of setting a config option # to support multiple variations of setting a config option
@@ -310,9 +310,9 @@ features = ["env", "toml"]
version = "0.25.1" version = "0.25.1"
default-features = false default-features = false
features = [ features = [
"serde", "serde",
"system-config", "system-config",
"tokio", "tokio",
] ]
# Used for conduwuit::Error type # Used for conduwuit::Error type
@@ -351,7 +351,7 @@ version = "0.1.2"
# Used for matrix spec type definitions and helpers # Used for matrix spec type definitions and helpers
[workspace.dependencies.ruma] [workspace.dependencies.ruma]
git = "https://forgejo.ellis.link/continuwuation/ruwuma" git = "https://forgejo.ellis.link/continuwuation/ruwuma"
rev = "27abe0dcd33fd4056efc94bab3582646b31b6ce9" rev = "79abd5d331bca596b7f37e367a9f2cebccd9f64d"
features = [ features = [
"compat", "compat",
"rand", "rand",
@@ -381,13 +381,13 @@ features = [
"unstable-msc4095", "unstable-msc4095",
"unstable-msc4121", "unstable-msc4121",
"unstable-msc4125", "unstable-msc4125",
"unstable-msc4155", "unstable-msc4155",
"unstable-msc4186", "unstable-msc4186",
"unstable-msc4203", # sending to-device events to appservices "unstable-msc4203", # sending to-device events to appservices
"unstable-msc4210", # remove legacy mentions "unstable-msc4210", # remove legacy mentions
"unstable-extensible-events", "unstable-extensible-events",
"unstable-pdu", "unstable-pdu",
"unstable-msc4155" "unstable-msc4155"
] ]
[workspace.dependencies.rust-rocksdb] [workspace.dependencies.rust-rocksdb]
@@ -395,11 +395,11 @@ git = "https://forgejo.ellis.link/continuwuation/rust-rocksdb-zaidoon1"
rev = "61d9d23872197e9ace4a477f2617d5c9f50ecb23" rev = "61d9d23872197e9ace4a477f2617d5c9f50ecb23"
default-features = false default-features = false
features = [ features = [
"multi-threaded-cf", "multi-threaded-cf",
"mt_static", "mt_static",
"lz4", "lz4",
"zstd", "zstd",
"bzip2", "bzip2",
] ]
[workspace.dependencies.sha2] [workspace.dependencies.sha2]
@@ -458,16 +458,16 @@ git = "https://forgejo.ellis.link/continuwuation/jemallocator"
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8" rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
default-features = false default-features = false
features = [ features = [
"background_threads_runtime_support", "background_threads_runtime_support",
"unprefixed_malloc_on_supported_platforms", "unprefixed_malloc_on_supported_platforms",
] ]
[workspace.dependencies.tikv-jemallocator] [workspace.dependencies.tikv-jemallocator]
git = "https://forgejo.ellis.link/continuwuation/jemallocator" git = "https://forgejo.ellis.link/continuwuation/jemallocator"
rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8" rev = "82af58d6a13ddd5dcdc7d4e91eae3b63292995b8"
default-features = false default-features = false
features = [ features = [
"background_threads_runtime_support", "background_threads_runtime_support",
"unprefixed_malloc_on_supported_platforms", "unprefixed_malloc_on_supported_platforms",
] ]
[workspace.dependencies.tikv-jemalloc-ctl] [workspace.dependencies.tikv-jemalloc-ctl]
git = "https://forgejo.ellis.link/continuwuation/jemallocator" git = "https://forgejo.ellis.link/continuwuation/jemallocator"
@@ -491,9 +491,9 @@ default-features = false
version = "0.1.2" version = "0.1.2"
default-features = false default-features = false
features = [ features = [
"static", "static",
"gcc", "gcc",
"light", "light",
] ]
[workspace.dependencies.rustyline-async] [workspace.dependencies.rustyline-async]
+2
View File
@@ -0,0 +1,2 @@
Added support for invite and join anti-spam via Draupnir and Meowlnir, similar to that of synapse-http-antispam.
Contributed by @nex.
+1
View File
@@ -0,0 +1 @@
Implemented account locking functionality, to complement user suspension. Contributed by @nex.
+39 -1
View File
@@ -1647,7 +1647,7 @@
# Enable the tokio-console. This option is only relevant to developers. # Enable the tokio-console. This option is only relevant to developers.
# #
# For more information, see: # For more information, see:
# https://continuwuity.org/development.html#debugging-with-tokio-console # https://continuwuity.org/development.html#debugging-with-tokio-console
# #
#tokio_console = false #tokio_console = false
@@ -1923,3 +1923,41 @@
# example: "(objectClass=conduwuitAdmin)" or "(uid={username})" # example: "(objectClass=conduwuitAdmin)" or "(uid={username})"
# #
#admin_filter = "" #admin_filter = ""
[global.antispam.meowlnir]
# The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
#
# Example: "http://127.0.0.1:29339"
#
#base_url =
# The authentication secret defined in antispam->secret. Required for
# continuwuity to talk to Meowlnir.
#
#secret =
# The management room for which to send requests
#
#management_room =
# If enabled run all federated join attempts (both federated and local)
# through the Meowlnir anti-spam checks.
#
# By default, only join attempts for rooms with the `fi.mau.spam_checker`
# restricted join rule are checked.
#
#check_all_joins = false
[global.antispam.draupnir]
# The base URL on which to contact Draupnir (before /api/).
#
# Example: "http://127.0.0.1:29339"
#
#base_url =
# The authentication secret defined in
# web->synapseHTTPAntispam->authorization
#
#secret =
+43
View File
@@ -238,6 +238,7 @@ pub(super) async fn deactivate(&self, no_leave_rooms: bool, user_id: String) ->
#[admin_command] #[admin_command]
pub(super) async fn suspend(&self, user_id: String) -> Result { pub(super) async fn suspend(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?; let user_id = parse_local_user_id(self.services, &user_id)?;
if user_id == self.services.globals.server_user { if user_id == self.services.globals.server_user {
@@ -262,6 +263,7 @@ pub(super) async fn suspend(&self, user_id: String) -> Result {
#[admin_command] #[admin_command]
pub(super) async fn unsuspend(&self, user_id: String) -> Result { pub(super) async fn unsuspend(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?; let user_id = parse_local_user_id(self.services, &user_id)?;
if user_id == self.services.globals.server_user { if user_id == self.services.globals.server_user {
@@ -974,3 +976,44 @@ pub(super) async fn force_leave_remote_room(
self.write_str(&format!("{user_id} successfully left {room_id} via remote server.")) self.write_str(&format!("{user_id} successfully left {room_id} via remote server."))
.await .await
} }
#[admin_command]
pub(super) async fn lock(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if user_id == self.services.globals.server_user {
return Err!("Not allowed to lock the server service account.",);
}
if !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
if self.services.users.is_admin(&user_id).await {
return Err!("Admin users cannot be locked.");
}
self.services
.users
.lock_account(&user_id, self.sender_or_service_user())
.await;
self.write_str(&format!("User {user_id} has been locked."))
.await
}
#[admin_command]
pub(super) async fn unlock(&self, user_id: String) -> Result {
self.bail_restricted()?;
let user_id = parse_local_user_id(self.services, &user_id)?;
assert!(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
self.services.users.unlock_account(&user_id).await;
self.write_str(&format!("User {user_id} has been unlocked."))
.await
}
+20
View File
@@ -81,6 +81,26 @@ pub enum UserCommand {
user_id: String, user_id: String,
}, },
/// - Lock a user
///
/// Locked users are unable to use their accounts beyond logging out. This
/// is akin to a temporary deactivation that does not change the user's
/// password. This can be used to quickly prevent a user from accessing
/// their account.
Lock {
/// Username of the user to lock
user_id: String,
},
/// - Unlock a user
///
/// Reverses the effects of the `lock` command, allowing the user to use
/// their account again.
Unlock {
/// Username of the user to unlock
user_id: String,
},
/// - List local users in the database /// - List local users in the database
#[clap(alias = "list")] #[clap(alias = "list")]
ListUsers, ListUsers,
+13
View File
@@ -3,6 +3,7 @@ use axum_client_ip::InsecureClientIp;
use conduwuit::{ use conduwuit::{
Err, Result, debug_error, err, info, Err, Result, debug_error, err, info,
matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder}, matrix::{event::gen_event_id_canonical_json, pdu::PduBuilder},
warn,
}; };
use futures::FutureExt; use futures::FutureExt;
use ruma::{ use ruma::{
@@ -124,6 +125,18 @@ pub(crate) async fn invite_helper(
return Err!(Request(Forbidden("Invites are not allowed on this server."))); return Err!(Request(Forbidden("Invites are not allowed on this server.")));
} }
if let Err(e) = services
.antispam
.user_may_invite(sender_user.to_owned(), recipient_user.to_owned(), room_id.to_owned())
.await
{
warn!(
"Invite from {} to {} in room {} blocked by antispam: {e:?}",
sender_user, recipient_user, room_id
);
return Err!(Request(Forbidden("Invite blocked by antispam service.")));
}
if !services.globals.user_is_local(recipient_user) { if !services.globals.user_is_local(recipient_user) {
let (pdu, pdu_json, invite_room_state) = { let (pdu, pdu_json, invite_room_state) = {
let state_lock = services.rooms.state.mutex.lock(room_id).await; let state_lock = services.rooms.state.mutex.lock(room_id).await;
+85 -50
View File
@@ -33,7 +33,7 @@ use ruma::{
events::{ events::{
StateEventType, StateEventType,
room::{ room::{
join_rules::{AllowRule, JoinRule, RoomJoinRulesEventContent}, join_rules::{AllowRule, JoinRule},
member::{MembershipState, RoomMemberEventContent}, member::{MembershipState, RoomMemberEventContent},
}, },
}, },
@@ -288,6 +288,23 @@ pub async fn join_room_by_id_helper(
return Ok(join_room_by_id::v3::Response { room_id: room_id.into() }); return Ok(join_room_by_id::v3::Response { room_id: room_id.into() });
} }
if let Err(e) = services
.antispam
.user_may_join_room(
sender_user.to_owned(),
room_id.to_owned(),
services
.rooms
.state_cache
.is_invited(sender_user, room_id)
.await,
)
.await
{
warn!("Antispam prevented user {} from joining room {}: {}", sender_user, room_id, e);
return Err!(Request(Forbidden("You are not allowed to join this room.")));
}
let server_in_room = services let server_in_room = services
.rooms .rooms
.state_cache .state_cache
@@ -321,6 +338,17 @@ pub async fn join_room_by_id_helper(
))); )));
} }
if services.antispam.check_all_joins() {
if let Err(e) = services
.antispam
.meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned())
.await
{
warn!("Antispam prevented user {} from joining room {}: {}", sender_user, room_id, e);
return Err!(Request(Forbidden("Antispam rejected join request.")));
}
}
if server_in_room { if server_in_room {
join_room_by_id_helper_local( join_room_by_id_helper_local(
services, services,
@@ -347,7 +375,6 @@ pub async fn join_room_by_id_helper(
.boxed() .boxed()
.await?; .await?;
} }
Ok(join_room_by_id::v3::Response::new(room_id.to_owned())) Ok(join_room_by_id::v3::Response::new(room_id.to_owned()))
} }
@@ -720,45 +747,51 @@ async fn join_room_by_id_helper_local(
state_lock: RoomMutexGuard, state_lock: RoomMutexGuard,
) -> Result { ) -> Result {
debug_info!("We can join locally"); debug_info!("We can join locally");
let join_rules = services.rooms.state_accessor.get_join_rules(room_id).await;
let join_rules_event_content = services let mut restricted_join_authorized = None;
.rooms match join_rules {
.state_accessor | JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted) => {
.room_state_get_content::<RoomJoinRulesEventContent>( for restriction in restricted.allow {
room_id, match restriction {
&StateEventType::RoomJoinRules, | AllowRule::RoomMembership(membership) => {
"", if services
) .rooms
.await; .state_cache
.is_joined(sender_user, &membership.room_id)
let restriction_rooms = match join_rules_event_content { .await
| Ok(RoomJoinRulesEventContent { {
join_rule: JoinRule::Restricted(restricted) | JoinRule::KnockRestricted(restricted), restricted_join_authorized = Some(true);
}) => restricted break;
.allow }
.into_iter() },
.filter_map(|a| match a { | AllowRule::UnstableSpamChecker => {
| AllowRule::RoomMembership(r) => Some(r.room_id), match services
| _ => None, .antispam
}) .meowlnir_accept_make_join(room_id.to_owned(), sender_user.to_owned())
.collect(), .await
| _ => Vec::new(), {
}; | Ok(()) => {
restricted_join_authorized = Some(true);
let join_authorized_via_users_server: Option<OwnedUserId> = { break;
if restriction_rooms },
.iter() | Err(_) =>
.stream() return Err!(Request(Forbidden(
.any(|restriction_room_id| { "Antispam rejected join request."
trace!("Checking if {sender_user} is joined to {restriction_room_id}"); ))),
services }
.rooms },
.state_cache | _ => {},
.is_joined(sender_user, restriction_room_id) }
}) }
.await },
{ | _ => {},
services }
let join_authorized_via_users_server = if restricted_join_authorized.is_none() {
None
} else {
match restricted_join_authorized.unwrap() {
| true => services
.rooms .rooms
.state_cache .state_cache
.local_users_in_room(room_id) .local_users_in_room(room_id)
@@ -774,10 +807,14 @@ async fn join_room_by_id_helper_local(
.boxed() .boxed()
.next() .next()
.await .await
.map(ToOwned::to_owned) .map(ToOwned::to_owned),
} else { | false => {
trace!("No restriction rooms are joined by {sender_user}"); warn!(
None "Join authorization failed for restricted join in room {room_id} for user \
{sender_user}"
);
return Err!(Request(Forbidden("You are not authorized to join this room.")));
},
} }
}; };
@@ -805,16 +842,14 @@ async fn join_room_by_id_helper_local(
return Ok(()); return Ok(());
}; };
if restriction_rooms.is_empty() if servers.is_empty() || servers.len() == 1 && services.globals.server_is_ours(&servers[0]) {
&& (servers.is_empty()
|| servers.len() == 1 && services.globals.server_is_ours(&servers[0]))
{
return Err(error); return Err(error);
} }
warn!( warn!(
"We couldn't do the join locally, maybe federation can help to satisfy the restricted \ ?error,
join requirements" servers = %servers.len(),
"Could not join restricted room locally, attempting remote join",
); );
let Ok((make_join_response, remote_server)) = let Ok((make_join_response, remote_server)) =
make_join_request(services, sender_user, room_id, servers).await make_join_request(services, sender_user, room_id, servers).await
+24 -6
View File
@@ -137,12 +137,30 @@ pub(super) async fn auth(
| ( | (
AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None, AuthScheme::AccessToken | AuthScheme::AccessTokenOptional | AuthScheme::None,
Token::User((user_id, device_id)), Token::User((user_id, device_id)),
) => Ok(Auth { ) => {
origin: None, let is_locked = services.users.is_locked(&user_id).await.map_err(|e| {
sender_user: Some(user_id), err!(Request(Forbidden(warn!("Failed to check user lock status: {e}"))))
sender_device: Some(device_id), })?;
appservice_info: None, if is_locked {
}), // Only /logout and /logout/all are allowed for locked users
if !matches!(
metadata,
&ruma::api::client::session::logout::v3::Request::METADATA
| &ruma::api::client::session::logout_all::v3::Request::METADATA
) {
return Err(Error::BadRequest(
ErrorKind::UserLocked,
"This account has been locked.",
));
}
}
Ok(Auth {
origin: None,
sender_user: Some(user_id),
sender_device: Some(device_id),
appservice_info: None,
})
},
| (AuthScheme::ServerSignatures, Token::None) => | (AuthScheme::ServerSignatures, Token::None) =>
Ok(auth_server(services, request, json_body).await?), Ok(auth_server(services, request, json_body).await?),
| ( | (
+9
View File
@@ -148,6 +148,15 @@ pub(crate) async fn create_invite_route(
return Err!(Request(Forbidden("This server does not allow room invites."))); return Err!(Request(Forbidden("This server does not allow room invites.")));
} }
if let Err(e) = services
.antispam
.user_may_invite(sender_user.to_owned(), recipient_user.clone(), body.room_id.clone())
.await
{
warn!("Antispam rejected invite: {e:?}");
return Err!(Request(Forbidden("Invite rejected by antispam service.")));
}
let mut invite_state = body.invite_room_state.clone(); let mut invite_state = body.invite_room_state.clone();
let mut event: JsonObject = serde_json::from_str(body.event.get()) let mut event: JsonObject = serde_json::from_str(body.event.get())
+50 -22
View File
@@ -1,7 +1,7 @@
use std::borrow::ToOwned;
use axum::extract::State; use axum::extract::State;
use conduwuit::{ use conduwuit::{Err, Error, Result, debug, debug_info, info, matrix::pdu::PduBuilder, warn};
Err, Error, Result, debug_info, info, matrix::pdu::PduBuilder, utils::IterStream, warn,
};
use conduwuit_service::Services; use conduwuit_service::Services;
use futures::StreamExt; use futures::StreamExt;
use ruma::{ use ruma::{
@@ -122,6 +122,16 @@ pub(crate) async fn create_join_event_template_route(
None None
} }
}; };
if services.antispam.check_all_joins() && join_authorized_via_users_server.is_none() {
if services
.antispam
.meowlnir_accept_make_join(body.room_id.clone(), body.user_id.clone())
.await
.is_err()
{
return Err!(Request(Forbidden("Antispam rejected join request.")));
}
}
let (_pdu, mut pdu_json) = services let (_pdu, mut pdu_json) = services
.rooms .rooms
@@ -136,7 +146,6 @@ pub(crate) async fn create_join_event_template_route(
&state_lock, &state_lock,
) )
.await?; .await?;
drop(state_lock); drop(state_lock);
// room v3 and above removed the "event_id" field from remote PDU format // room v3 and above removed the "event_id" field from remote PDU format
@@ -192,25 +201,44 @@ pub(crate) async fn user_can_perform_restricted_join(
return Ok(false); return Ok(false);
} }
if r.allow for allow_rule in &r.allow {
.iter() match allow_rule {
.filter_map(|rule| { | AllowRule::RoomMembership(membership) => {
if let AllowRule::RoomMembership(membership) = rule { if services
Some(membership) .rooms
} else { .state_cache
None .is_joined(user_id, &membership.room_id)
} .await
}) {
.stream() debug!(
.any(|m| services.rooms.state_cache.is_joined(user_id, &m.room_id)) "User {} is allowed to join room {} via membership in room {}",
.await user_id, room_id, membership.room_id
{ );
Ok(true) return Ok(true);
} else { }
Err!(Request(UnableToAuthorizeJoin( },
"Joining user is not known to be in any required room." | AllowRule::UnstableSpamChecker =>
))) return match services
.antispam
.meowlnir_accept_make_join(room_id.to_owned(), user_id.to_owned())
.await
{
| Ok(()) => Ok(true),
| Err(_) => Err!(Request(Forbidden("Antispam rejected join request."))),
},
| _ => {
debug_info!(
"Unsupported allow rule in restricted join for room {}: {:?}",
room_id,
allow_rule
);
},
}
} }
Err!(Request(UnableToAuthorizeJoin(
"Joining user is not known to be in any required room."
)))
} }
pub(crate) fn maybe_strip_event_id( pub(crate) fn maybe_strip_event_id(
+59 -3
View File
@@ -18,7 +18,7 @@ use figment::providers::{Env, Format, Toml};
pub use figment::{Figment, value::Value as FigmentValue}; pub use figment::{Figment, value::Value as FigmentValue};
use regex::RegexSet; use regex::RegexSet;
use ruma::{ use ruma::{
OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId, OwnedRoomId, OwnedRoomOrAliasId, OwnedServerName, OwnedUserId, RoomVersionId,
api::client::discovery::discover_support::ContactRole, api::client::discovery::discover_support::ContactRole,
}; };
use serde::{Deserialize, de::IgnoredAny}; use serde::{Deserialize, de::IgnoredAny};
@@ -53,7 +53,8 @@ use crate::{Result, err, error::Error, utils::sys};
### For more information, see: ### For more information, see:
### https://continuwuity.org/configuration.html ### https://continuwuity.org/configuration.html
"#, "#,
ignore = "config_paths catchall well_known tls blurhashing allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure" ignore = "config_paths catchall well_known tls blurhashing \
allow_invalid_tls_certificates_yes_i_know_what_the_fuck_i_am_doing_with_this_and_i_know_this_is_insecure antispam"
)] )]
pub struct Config { pub struct Config {
// Paths to config file(s). Not supposed to be set manually in the config file, // Paths to config file(s). Not supposed to be set manually in the config file,
@@ -1887,7 +1888,7 @@ pub struct Config {
/// Enable the tokio-console. This option is only relevant to developers. /// Enable the tokio-console. This option is only relevant to developers.
/// ///
/// For more information, see: /// For more information, see:
/// https://continuwuity.org/development.html#debugging-with-tokio-console /// https://continuwuity.org/development.html#debugging-with-tokio-console
#[serde(default)] #[serde(default)]
pub tokio_console: bool, pub tokio_console: bool,
@@ -2024,6 +2025,10 @@ pub struct Config {
#[serde(default)] #[serde(default)]
pub ldap: LdapConfig, pub ldap: LdapConfig,
/// Configuration for antispam support
#[serde(default)]
pub antispam: Option<Antispam>,
// external structure; separate section // external structure; separate section
#[serde(default)] #[serde(default)]
pub blurhashing: BlurhashConfig, pub blurhashing: BlurhashConfig,
@@ -2240,6 +2245,57 @@ struct ListeningAddr {
addrs: Either<IpAddr, Vec<IpAddr>>, addrs: Either<IpAddr, Vec<IpAddr>>,
} }
#[derive(Clone, Debug, Deserialize)]
pub struct Antispam {
pub meowlnir: Option<MeowlnirConfig>,
pub draupnir: Option<DraupnirConfig>,
}
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.antispam.meowlnir"
)]
pub struct MeowlnirConfig {
/// The base URL on which to contact Meowlnir (before /_meowlnir/antispam).
///
/// Example: "http://127.0.0.1:29339"
pub base_url: Url,
/// The authentication secret defined in antispam->secret. Required for
/// continuwuity to talk to Meowlnir.
pub secret: String,
/// The management room for which to send requests
pub management_room: OwnedRoomId,
/// If enabled run all federated join attempts (both federated and local)
/// through the Meowlnir anti-spam checks.
///
/// By default, only join attempts for rooms with the `fi.mau.spam_checker`
/// restricted join rule are checked.
#[serde(default)]
pub check_all_joins: bool,
}
// TODO: the DraupnirConfig and MeowlnirConfig are basically identical.
// Maybe management_room could just become an Option<> and these structs merged?
#[derive(Clone, Debug, Deserialize)]
#[config_example_generator(
filename = "conduwuit-example.toml",
section = "global.antispam.draupnir"
)]
pub struct DraupnirConfig {
/// The base URL on which to contact Draupnir (before /api/).
///
/// Example: "http://127.0.0.1:29339"
pub base_url: Url,
/// The authentication secret defined in
/// web->synapseHTTPAntispam->authorization
pub secret: String,
}
const DEPRECATED_KEYS: &[&str; 9] = &[ const DEPRECATED_KEYS: &[&str; 9] = &[
"cache_capacity", "cache_capacity",
"conduit_cache_capacity_modifier", "conduit_cache_capacity_modifier",
+3 -1
View File
@@ -75,10 +75,12 @@ pub(super) fn bad_request_code(kind: &ErrorKind) -> StatusCode {
| ThreepidDenied | ThreepidDenied
| InviteBlocked | InviteBlocked
| WrongRoomKeysVersion { .. } | WrongRoomKeysVersion { .. }
| UserSuspended
| Forbidden { .. } => StatusCode::FORBIDDEN, | Forbidden { .. } => StatusCode::FORBIDDEN,
// 401 // 401
| UnknownToken { .. } | MissingToken | Unauthorized => StatusCode::UNAUTHORIZED, | UnknownToken { .. } | MissingToken | Unauthorized | UserLocked =>
StatusCode::UNAUTHORIZED,
// 400 // 400
| _ => StatusCode::BAD_REQUEST, | _ => StatusCode::BAD_REQUEST,
+4
View File
@@ -386,6 +386,10 @@ pub(super) static MAPS: &[Descriptor] = &[
name: "userid_suspension", name: "userid_suspension",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL
}, },
Descriptor {
name: "userid_lock",
..descriptor::RANDOM_SMALL
},
Descriptor { Descriptor {
name: "userid_presenceid", name: "userid_presenceid",
..descriptor::RANDOM_SMALL ..descriptor::RANDOM_SMALL
+182
View File
@@ -0,0 +1,182 @@
use std::{fmt::Debug, sync::Arc};
use async_trait::async_trait;
use conduwuit::{Result, config::Antispam, debug};
use ruma::{OwnedRoomId, OwnedUserId, draupnir_antispam, meowlnir_antispam};
use crate::{client, config, sending, service::Dep};
struct Services {
config: Dep<config::Service>,
client: Dep<client::Service>,
}
pub struct Service {
services: Services,
}
#[async_trait]
impl crate::Service for Service {
fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
Ok(Arc::new(Self {
services: Services {
client: args.depend::<client::Service>("client"),
config: args.depend::<config::Service>("config"),
},
}))
}
fn name(&self) -> &str { crate::service::make_name(std::module_path!()) }
}
impl Service {
async fn send_antispam_request<T>(
&self,
base_url: &str,
secret: &str,
request: T,
) -> Result<T::IncomingResponse>
where
T: ruma::api::OutgoingRequest + Debug + Send,
{
sending::antispam::send_antispam_request(
&self.services.client.appservice,
base_url,
secret,
request,
)
.await
}
/// Checks with the antispam service whether `inviter` may invite `invitee`
/// to `room_id`.
///
/// If no antispam service is configured, this always returns `Ok(())`.
/// If an error is returned, the invite should be blocked - the antispam
/// service was unreachable, or refused the invite.
pub async fn user_may_invite(
&self,
inviter: OwnedUserId,
invitee: OwnedUserId,
room_id: OwnedRoomId,
) -> Result<()> {
if let Some(config) = &self.services.config.antispam {
let result = if let Some(meowlnir) = &config.meowlnir {
debug!(?room_id, ?inviter, ?invitee, "Asking meowlnir for user_may_invite");
self.send_antispam_request(
meowlnir.base_url.as_str(),
&meowlnir.secret,
meowlnir_antispam::user_may_invite::v1::Request::new(
meowlnir.management_room.clone(),
inviter,
invitee,
room_id,
),
)
.await
.inspect(|_| debug!("meowlnir allowed the invite"))
.inspect_err(|e| debug!("meowlnir denied the invite: {e:?}"))
.map(|_| ())
} else if let Some(draupnir) = &config.draupnir {
debug!(?room_id, ?inviter, ?invitee, "Asking draupnir for user_may_invite");
self.send_antispam_request(
draupnir.base_url.as_str(),
&draupnir.secret,
draupnir_antispam::user_may_invite::v1::Request::new(
room_id, inviter, invitee,
),
)
.await
.inspect(|_| debug!("draupnir allowed the invite"))
.inspect_err(|e| debug!("draupnir denied the invite: {e:?}"))
.map(|_| ())
} else {
Ok(())
};
return result;
}
Ok(())
}
/// Checks with the antispam service whether `user_id` may join `room_id`.
pub async fn user_may_join_room(
&self,
user_id: OwnedUserId,
room_id: OwnedRoomId,
is_invited: bool,
) -> Result<()> {
if let Some(config) = &self.services.config.antispam {
let result = if let Some(meowlnir) = &config.meowlnir {
debug!(?room_id, ?user_id, ?is_invited, "Asking meowlnir for user_may_join_room");
self.send_antispam_request(
meowlnir.base_url.as_str(),
&meowlnir.secret,
meowlnir_antispam::user_may_join_room::v1::Request::new(
meowlnir.management_room.clone(),
user_id,
room_id,
is_invited,
),
)
.await
.inspect(|_| debug!("meowlnir allowed the join"))
.inspect_err(|e| debug!("meowlnir denied the join: {e:?}"))
.map(|_| ())
} else if let Some(draupnir) = &config.draupnir {
debug!(?room_id, ?user_id, ?is_invited, "Asking draupnir for user_may_join_room");
self.send_antispam_request(
draupnir.base_url.as_str(),
&draupnir.secret,
draupnir_antispam::user_may_join_room::v1::Request::new(
user_id, room_id, is_invited,
),
)
.await
.inspect(|_| debug!("draupnir allowed the join"))
.inspect_err(|e| debug!("draupnir denied the join: {e:?}"))
.map(|_| ())
} else {
Ok(())
};
return result;
}
Ok(())
}
/// Checks with Meowlnir whether the incoming federated `make_join` request
/// should be allowed. Applies the `fi.mau.spam_checker` join rule.
pub async fn meowlnir_accept_make_join(
&self,
room_id: OwnedRoomId,
user_id: OwnedUserId,
) -> Result<()> {
if let Some(Antispam { meowlnir: Some(meowlnir), .. }) = &self.services.config.antispam {
debug!(?room_id, ?user_id, "Asking meowlnir for accept_make_join");
self.send_antispam_request(
meowlnir.base_url.as_str(),
&meowlnir.secret,
meowlnir_antispam::accept_make_join::v1::Request::new(
meowlnir.management_room.clone(),
user_id,
room_id,
),
)
.await
.inspect(|_| debug!("meowlnir allowed the make_join"))
.inspect_err(|e| debug!("meowlnir denied the make_join: {e:?}"))
.map(|_| ())
} else {
Ok(())
}
}
/// Returns whether all joins should be checked with Meowlnir.
/// Is always false if Meowlnir is not configured.
pub fn check_all_joins(&self) -> bool {
if let Some(Antispam { meowlnir: Some(cfg), .. }) = &self.services.config.antispam {
cfg.check_all_joins
} else {
false
}
}
}
+3 -3
View File
@@ -1,6 +1,8 @@
#![type_length_limit = "8192"] #![type_length_limit = "8192"]
#![allow(refining_impl_trait)] #![allow(refining_impl_trait)]
extern crate conduwuit_core as conduwuit;
extern crate conduwuit_database as database;
mod manager; mod manager;
mod migrations; mod migrations;
mod service; mod service;
@@ -10,6 +12,7 @@ pub mod state;
pub mod account_data; pub mod account_data;
pub mod admin; pub mod admin;
pub mod announcements; pub mod announcements;
pub mod antispam;
pub mod appservice; pub mod appservice;
pub mod client; pub mod client;
pub mod config; pub mod config;
@@ -30,9 +33,6 @@ pub mod transaction_ids;
pub mod uiaa; pub mod uiaa;
pub mod users; pub mod users;
extern crate conduwuit_core as conduwuit;
extern crate conduwuit_database as database;
use ctor::{ctor, dtor}; use ctor::{ctor, dtor};
pub(crate) use service::{Args, Dep, Service}; pub(crate) use service::{Args, Dep, Service};
+65
View File
@@ -0,0 +1,65 @@
use std::{fmt::Debug, mem};
use bytes::BytesMut;
use conduwuit::{Err, Result, debug_error, err, utils, warn};
use reqwest::Client;
use ruma::api::{IncomingResponse, MatrixVersion, OutgoingRequest, SendAccessToken};
/// Sends a request to an antispam service
pub(crate) async fn send_antispam_request<T>(
client: &Client,
base_url: &str,
secret: &str,
request: T,
) -> Result<T::IncomingResponse>
where
T: OutgoingRequest + Debug + Send,
{
const VERSIONS: [MatrixVersion; 1] = [MatrixVersion::V1_15];
let http_request = request
.try_into_http_request::<BytesMut>(base_url, SendAccessToken::Always(secret), &VERSIONS)?
.map(BytesMut::freeze);
let reqwest_request = reqwest::Request::try_from(http_request)?;
let mut response = client.execute(reqwest_request).await.map_err(|e| {
warn!("Could not send request to antispam: {e:?}");
e
})?;
// reqwest::Response -> http::Response conversion
let status = response.status();
let mut http_response_builder = http::Response::builder()
.status(status)
.version(response.version());
mem::swap(
response.headers_mut(),
http_response_builder
.headers_mut()
.expect("http::response::Builder is usable"),
);
let body = response.bytes().await?; // TODO: handle timeout
if !status.is_success() {
debug_error!("Antispam response bytes: {:?}", utils::string_from_bytes(&body));
return match status {
| http::StatusCode::FORBIDDEN =>
Err!(Request(Forbidden("Request was rejected by antispam service.",))),
| _ => Err!(BadServerResponse(warn!(
"Antispam returned unsuccessful HTTP response {status}",
))),
};
}
let response = T::IncomingResponse::try_from_http_response(
http_response_builder
.body(body)
.expect("reqwest body is valid http body"),
);
response.map_err(|e| {
err!(BadServerResponse(warn!(
"Antispam returned invalid/malformed response bytes: {e}",
)))
})
}
+1
View File
@@ -1,3 +1,4 @@
pub mod antispam;
mod appservice; mod appservice;
mod data; mod data;
mod dest; mod dest;
+4 -2
View File
@@ -8,8 +8,8 @@ use futures::{Stream, StreamExt, TryStreamExt};
use tokio::sync::Mutex; use tokio::sync::Mutex;
use crate::{ use crate::{
account_data, admin, announcements, appservice, client, config, emergency, federation, account_data, admin, announcements, antispam, appservice, client, config, emergency,
globals, key_backups, federation, globals, key_backups,
manager::Manager, manager::Manager,
media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service, media, moderation, presence, pusher, resolver, rooms, sending, server_keys, service,
service::{Args, Map, Service}, service::{Args, Map, Service},
@@ -39,6 +39,7 @@ pub struct Services {
pub users: Arc<users::Service>, pub users: Arc<users::Service>,
pub moderation: Arc<moderation::Service>, pub moderation: Arc<moderation::Service>,
pub announcements: Arc<announcements::Service>, pub announcements: Arc<announcements::Service>,
pub antispam: Arc<antispam::Service>,
manager: Mutex<Option<Arc<Manager>>>, manager: Mutex<Option<Arc<Manager>>>,
pub(crate) service: Arc<Map>, pub(crate) service: Arc<Map>,
@@ -107,6 +108,7 @@ impl Services {
users: build!(users::Service), users: build!(users::Service),
moderation: build!(moderation::Service), moderation: build!(moderation::Service),
announcements: build!(announcements::Service), announcements: build!(announcements::Service),
antispam: build!(antispam::Service),
manager: Mutex::new(None), manager: Mutex::new(None),
service, service,
+40
View File
@@ -77,6 +77,7 @@ struct Data {
userid_origin: Arc<Map>, userid_origin: Arc<Map>,
userid_password: Arc<Map>, userid_password: Arc<Map>,
userid_suspension: Arc<Map>, userid_suspension: Arc<Map>,
userid_lock: Arc<Map>,
userid_selfsigningkeyid: Arc<Map>, userid_selfsigningkeyid: Arc<Map>,
userid_usersigningkeyid: Arc<Map>, userid_usersigningkeyid: Arc<Map>,
useridprofilekey_value: Arc<Map>, useridprofilekey_value: Arc<Map>,
@@ -115,6 +116,7 @@ impl crate::Service for Service {
userid_origin: args.db["userid_origin"].clone(), userid_origin: args.db["userid_origin"].clone(),
userid_password: args.db["userid_password"].clone(), userid_password: args.db["userid_password"].clone(),
userid_suspension: args.db["userid_suspension"].clone(), userid_suspension: args.db["userid_suspension"].clone(),
userid_lock: args.db["userid_lock"].clone(),
userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(), userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(), userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(),
useridprofilekey_value: args.db["useridprofilekey_value"].clone(), useridprofilekey_value: args.db["useridprofilekey_value"].clone(),
@@ -220,6 +222,26 @@ impl Service {
self.db.userid_suspension.remove(user_id); self.db.userid_suspension.remove(user_id);
} }
pub async fn lock_account(&self, user_id: &UserId, locking_user: &UserId) {
// NOTE: Locking is basically just suspension with a more severe effect,
// so we'll just re-use the suspension data structure to store the lock state.
let suspension = self
.db
.userid_lock
.get(user_id)
.await
.deserialized::<UserSuspension>()
.unwrap_or_else(|_| UserSuspension {
suspended: true,
suspended_at: MilliSecondsSinceUnixEpoch::now().get().into(),
suspended_by: locking_user.to_string(),
});
self.db.userid_lock.raw_put(user_id, Json(suspension));
}
pub async fn unlock_account(&self, user_id: &UserId) { self.db.userid_lock.remove(user_id); }
/// Check if a user has an account on this homeserver. /// Check if a user has an account on this homeserver.
#[inline] #[inline]
pub async fn exists(&self, user_id: &UserId) -> bool { pub async fn exists(&self, user_id: &UserId) -> bool {
@@ -255,6 +277,24 @@ impl Service {
} }
} }
pub async fn is_locked(&self, user_id: &UserId) -> Result<bool> {
match self
.db
.userid_lock
.get(user_id)
.await
.deserialized::<UserSuspension>()
{
| Ok(s) => Ok(s.suspended),
| Err(e) =>
if e.is_not_found() {
Ok(false)
} else {
Err(e)
},
}
}
/// Check if account is active, infallible /// Check if account is active, infallible
pub async fn is_active(&self, user_id: &UserId) -> bool { pub async fn is_active(&self, user_id: &UserId) -> bool {
!self.is_deactivated(user_id).await.unwrap_or(true) !self.is_deactivated(user_id).await.unwrap_or(true)