Compare commits

...

9 Commits

Author SHA1 Message Date
Ginger e5b11af3e8 fix: Allow cargo_common_metadata clippy lint 2026-01-08 14:00:02 -05:00
Ginger 71a26e433f fix: Update package and crate metadata 2026-01-08 00:21:28 +00:00
Jade Ellis d353446488 fix: Incomplete rename 2026-01-07 23:48:04 +00:00
timedout 77e8fd1744 style: Use contains to check for row presence 2026-01-07 17:31:54 +00:00
timedout 7fa7b129c0 perf: Store empty value (row only needs to exist) 2026-01-07 17:31:54 +00:00
timedout 247bc15659 fix: Await future 2026-01-07 17:31:53 +00:00
timedout 88a35e139d fix: Correctly return M_USER_LOCKED during login 2026-01-07 17:31:53 +00:00
timedout 37574ef5cc chore: Add news fragment 2026-01-07 17:31:53 +00:00
timedout 1c816850ed feat: Allow admins to disable the login capability of an account
# Conflicts:
#	src/admin/user/commands.rs
2026-01-07 17:31:51 +00:00
21 changed files with 116 additions and 46 deletions
+4 -11
View File
@@ -1,26 +1,17 @@
#cargo-features = ["profile-rustflags"]
[workspace]
resolver = "2"
members = ["src/*", "xtask/*"]
default-members = ["src/*"]
[workspace.package]
authors = [
"June Clementine Strawberry <june@girlboss.ceo>",
"strawberry <strawberry@puppygock.gay>", # woof
"Jason Volk <jason@zemos.net>",
]
categories = ["network-programming"]
description = "a very cool Matrix chat homeserver written in Rust"
authors = ["Continuwuity Team and contributors <team@continuwuity.org>"]
description = "A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver."
edition = "2024"
homepage = "https://continuwuity.org/"
keywords = ["chat", "matrix", "networking", "server", "uwu"]
license = "Apache-2.0"
# See also `rust-toolchain.toml`
readme = "README.md"
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
rust-version = "1.86.0"
version = "0.5.1"
[workspace.metadata.crane]
@@ -848,6 +839,8 @@ unknown_lints = "allow"
###################
cargo = { level = "warn", priority = -1 }
# Nobody except for us should be consuming these crates, they don't need metadata
cargo_common_metadata = { level = "allow"}
## some sadness
multiple_crate_versions = { level = "allow", priority = 1 }
+1
View File
@@ -0,0 +1 @@
Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex.
+1 -1
View File
@@ -8,7 +8,7 @@ # Command-Line Help for `continuwuity`
## `continuwuity`
a very cool Matrix chat homeserver written in Rust
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver.
**Usage:** `continuwuity [OPTIONS]`
+2 -2
View File
@@ -4,7 +4,7 @@
Name: continuwuity
Version: {{{ git_repo_version }}}
Release: 1%{?dist}
Summary: Very cool Matrix chat homeserver written in Rust
Summary: A Matrix homeserver written in Rust.
License: Apache-2.0 AND MIT
@@ -23,7 +23,7 @@ Requires: glibc
Requires: libstdc++
%global _description %{expand:
A cool hard fork of Conduit, a Matrix homeserver written in Rust}
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver.}
%description %{_description}
-2
View File
@@ -1,9 +1,7 @@
[package]
name = "conduwuit_admin"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
+60 -2
View File
@@ -280,7 +280,12 @@ pub(super) async fn unsuspend(&self, user_id: String) -> Result {
}
#[admin_command]
pub(super) async fn reset_password(&self, username: String, password: Option<String>) -> Result {
pub(super) async fn reset_password(
&self,
logout: bool,
username: String,
password: Option<String>,
) -> Result {
let user_id = parse_local_user_id(self.services, &username)?;
if user_id == self.services.globals.server_user {
@@ -303,7 +308,18 @@ pub(super) async fn reset_password(&self, username: String, password: Option<Str
write!(self, "Successfully reset the password for user {user_id}: `{new_password}`")
},
}
.await
.await?;
if logout {
self.services
.users
.all_device_ids(&user_id)
.for_each(|device_id| self.services.users.remove_device(&user_id, device_id))
.await;
write!(self, "\nAll existing sessions have been logged out.").await?;
}
Ok(())
}
#[admin_command]
@@ -1044,3 +1060,45 @@ pub(super) async fn logout(&self, user_id: String) -> Result {
self.write_str(&format!("User {user_id} has been logged out from all devices."))
.await
}
#[admin_command]
pub(super) async fn disable_login(&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 disable login for 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 have their login disallowed.");
}
self.services.users.disable_login(&user_id);
self.write_str(&format!(
"{user_id} can no longer log in. Their existing sessions remain unaffected."
))
.await
}
#[admin_command]
pub(super) async fn enable_login(&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 !self.services.users.exists(&user_id).await {
return Err!("User {user_id} does not exist.");
}
self.services.users.enable_login(&user_id);
self.write_str(&format!("{user_id} can now log in.")).await
}
+19
View File
@@ -20,6 +20,9 @@ pub enum UserCommand {
/// - Reset user password
ResetPassword {
/// Log out existing sessions
#[arg(short, long)]
logout: bool,
/// Username of the user for whom the password should be reset
username: String,
/// New password for the user, if unspecified one is generated
@@ -113,6 +116,22 @@ pub enum UserCommand {
user_id: String,
},
/// - Enable login for a user
EnableLogin {
/// Username of the user to enable login for
user_id: String,
},
/// - Disable login for a user
///
/// Disables login for the specified user without deactivating or locking
/// their account. This prevents the user from obtaining new access tokens,
/// but does not invalidate existing sessions.
DisableLogin {
/// Username of the user to disable login for
user_id: String,
},
/// - List local users in the database
#[clap(alias = "list")]
ListUsers,
-2
View File
@@ -1,9 +1,7 @@
[package]
name = "conduwuit_api"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
+11
View File
@@ -5,6 +5,7 @@
use conduwuit::{
Err, Error, Result, debug, err, info,
utils::{self, ReadyExt, hash},
warn,
};
use conduwuit_core::{debug_error, debug_warn};
use conduwuit_service::{Services, uiaa::SESSION_ID_LENGTH};
@@ -12,6 +13,7 @@
use ruma::{
OwnedUserId, UserId,
api::client::{
error::ErrorKind,
session::{
get_login_token,
get_login_types::{
@@ -184,6 +186,15 @@ pub(crate) async fn handle_login(
return Err!(Request(Unknown("User ID does not belong to this homeserver")));
}
if services.users.is_locked(&user_id).await? {
return Err(Error::BadRequest(ErrorKind::UserLocked, "This account has been locked."));
}
if services.users.is_login_disabled(&user_id).await {
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.")));
}
if cfg!(feature = "ldap") && services.config.ldap.enable {
match Box::pin(ldap_login(services, &user_id, &lowercased_user_id, password)).await {
| Ok(user_id) => Ok(user_id),
-2
View File
@@ -1,9 +1,7 @@
[package]
name = "conduwuit_build_metadata"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
-2
View File
@@ -1,9 +1,7 @@
[package]
name = "conduwuit_core"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
-2
View File
@@ -1,9 +1,7 @@
[package]
name = "conduwuit_database"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
+4
View File
@@ -394,6 +394,10 @@ pub(super) fn open_list(db: &Arc<Engine>, maps: &[Descriptor]) -> Result<Maps> {
name: "userid_lock",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userid_logindisabled",
..descriptor::RANDOM_SMALL
},
Descriptor {
name: "userid_presenceid",
..descriptor::RANDOM_SMALL
-2
View File
@@ -1,9 +1,7 @@
[package]
name = "conduwuit_macros"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
+2 -6
View File
@@ -2,15 +2,12 @@
name = "conduwuit"
default-run = "conduwuit"
authors.workspace = true
categories.workspace = true
description.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
metadata.crane.workspace = true
@@ -23,14 +20,13 @@ crate-type = [
[package.metadata.deb]
name = "continuwuity"
maintainer = "continuwuity developers <contact@continuwuity.org>"
copyright = "2024, continuwuity developers"
maintainer = "Continuwuity Team and contributors <team@continuwuity.org>"
license-file = ["../../LICENSE", "3"]
depends = "$auto, ca-certificates"
breaks = ["conduwuit (<<0.5.0)"]
replaces = ["conduwuit (<<0.5.0)"]
extended-description = """\
a cool hard fork of Conduit, a Matrix homeserver written in Rust"""
A Matrix homeserver written in Rust, the official continuation of the conduwuit homeserver."""
section = "net"
priority = "optional"
conf-files = ["/etc/conduwuit/conduwuit.toml"]
-2
View File
@@ -1,9 +1,7 @@
[package]
name = "conduwuit_router"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
-2
View File
@@ -1,9 +1,7 @@
[package]
name = "conduwuit_service"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
+12
View File
@@ -78,6 +78,7 @@ struct Data {
userid_password: Arc<Map>,
userid_suspension: Arc<Map>,
userid_lock: Arc<Map>,
userid_logindisabled: Arc<Map>,
userid_selfsigningkeyid: Arc<Map>,
userid_usersigningkeyid: Arc<Map>,
useridprofilekey_value: Arc<Map>,
@@ -117,6 +118,7 @@ fn build(args: crate::Args<'_>) -> Result<Arc<Self>> {
userid_password: args.db["userid_password"].clone(),
userid_suspension: args.db["userid_suspension"].clone(),
userid_lock: args.db["userid_lock"].clone(),
userid_logindisabled: args.db["userid_logindisabled"].clone(),
userid_selfsigningkeyid: args.db["userid_selfsigningkeyid"].clone(),
userid_usersigningkeyid: args.db["userid_usersigningkeyid"].clone(),
useridprofilekey_value: args.db["useridprofilekey_value"].clone(),
@@ -295,6 +297,16 @@ pub async fn is_locked(&self, user_id: &UserId) -> Result<bool> {
}
}
pub fn disable_login(&self, user_id: &UserId) {
self.db.userid_logindisabled.insert(user_id, "");
}
pub fn enable_login(&self, user_id: &UserId) { self.db.userid_logindisabled.remove(user_id); }
pub async fn is_login_disabled(&self, user_id: &UserId) -> bool {
self.db.userid_logindisabled.contains(user_id).await
}
/// Check if account is active, infallible
pub async fn is_active(&self, user_id: &UserId) -> bool {
!self.is_deactivated(user_id).await.unwrap_or(true)
-2
View File
@@ -1,9 +1,7 @@
[package]
name = "conduwuit_web"
categories.workspace = true
description.workspace = true
edition.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
@@ -1,15 +1,12 @@
[package]
name = "xtask-generate-commands"
authors.workspace = true
categories.workspace = true
description.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[dependencies]
-3
View File
@@ -1,15 +1,12 @@
[package]
name = "xtask"
authors.workspace = true
categories.workspace = true
description.workspace = true
edition.workspace = true
homepage.workspace = true
keywords.workspace = true
license.workspace = true
readme.workspace = true
repository.workspace = true
rust-version.workspace = true
version.workspace = true
[dependencies]