mirror of
https://forgejo.ellis.link/continuwuation/continuwuity/
synced 2026-07-05 21:31:52 +00:00
Compare commits
9 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| e5b11af3e8 | |||
| 71a26e433f | |||
| d353446488 | |||
| 77e8fd1744 | |||
| 7fa7b129c0 | |||
| 247bc15659 | |||
| 88a35e139d | |||
| 37574ef5cc | |||
| 1c816850ed |
+4
-11
@@ -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 }
|
||||
|
||||
@@ -0,0 +1 @@
|
||||
Implemented toggling the ability for an account to log in without mutating any of its data. Contributed by @nex.
|
||||
@@ -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]`
|
||||
|
||||
|
||||
@@ -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}
|
||||
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -45,7 +45,7 @@ pub(super) async fn revoke_token(&self, token: String) -> Result {
|
||||
let Some(token) = self
|
||||
.services
|
||||
.registration_tokens
|
||||
.validate_token(&token)
|
||||
.validate_token(token)
|
||||
.await
|
||||
else {
|
||||
return Err!("This token does not exist or has already expired.");
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -880,7 +880,7 @@ pub(crate) async fn check_registration_token_validity(
|
||||
|
||||
let valid = services
|
||||
.registration_tokens
|
||||
.validate_token(&body.token)
|
||||
.validate_token(body.token.clone())
|
||||
.await
|
||||
.is_some();
|
||||
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
@@ -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"]
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -23,18 +23,18 @@ struct Services {
|
||||
|
||||
/// A validated registration token which may be used to create an account.
|
||||
#[derive(Debug)]
|
||||
pub struct ValidToken<'token> {
|
||||
pub token: &'token str,
|
||||
pub struct ValidToken {
|
||||
pub token: String,
|
||||
pub source: ValidTokenSource,
|
||||
}
|
||||
|
||||
impl std::fmt::Display for ValidToken<'_> {
|
||||
impl std::fmt::Display for ValidToken {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
write!(f, "`{}` --- {}", self.token, &self.source)
|
||||
}
|
||||
}
|
||||
|
||||
impl PartialEq<str> for ValidToken<'_> {
|
||||
impl PartialEq<str> for ValidToken {
|
||||
fn eq(&self, other: &str) -> bool { self.token == other }
|
||||
}
|
||||
|
||||
@@ -85,11 +85,11 @@ pub fn issue_token(
|
||||
}
|
||||
|
||||
/// Get the registration token set in the config file, if it exists.
|
||||
pub fn get_config_file_token(&self) -> Option<ValidToken<'_>> {
|
||||
pub fn get_config_file_token(&self) -> Option<ValidToken> {
|
||||
self.services
|
||||
.config
|
||||
.registration_token
|
||||
.as_deref()
|
||||
.clone()
|
||||
.map(|token| ValidToken {
|
||||
token,
|
||||
source: ValidTokenSource::ConfigFile,
|
||||
@@ -97,7 +97,7 @@ pub fn get_config_file_token(&self) -> Option<ValidToken<'_>> {
|
||||
}
|
||||
|
||||
/// Validate a registration token.
|
||||
pub async fn validate_token<'token>(&self, token: &'token str) -> Option<ValidToken<'token>> {
|
||||
pub async fn validate_token(&self, token: String) -> Option<ValidToken> {
|
||||
// Check the registration token in the config first
|
||||
if self
|
||||
.get_config_file_token()
|
||||
@@ -110,7 +110,7 @@ pub async fn validate_token<'token>(&self, token: &'token str) -> Option<ValidTo
|
||||
}
|
||||
|
||||
// Now check the database
|
||||
if let Some(token_info) = self.db.lookup_token_info(token).await
|
||||
if let Some(token_info) = self.db.lookup_token_info(&token).await
|
||||
&& token_info.is_valid()
|
||||
{
|
||||
return Some(ValidToken {
|
||||
@@ -124,7 +124,7 @@ pub async fn validate_token<'token>(&self, token: &'token str) -> Option<ValidTo
|
||||
}
|
||||
|
||||
/// Mark a valid token as having been used to create a new account.
|
||||
pub fn mark_token_as_used(&self, ValidToken { token, source }: ValidToken<'_>) {
|
||||
pub fn mark_token_as_used(&self, ValidToken { token, source }: ValidToken) {
|
||||
match source {
|
||||
| ValidTokenSource::ConfigFile => {
|
||||
// we don't track uses of the config file token, do nothing
|
||||
@@ -132,7 +132,7 @@ pub fn mark_token_as_used(&self, ValidToken { token, source }: ValidToken<'_>) {
|
||||
| ValidTokenSource::Database(mut info) => {
|
||||
info.uses = info.uses.saturating_add(1);
|
||||
|
||||
self.db.save_token(token, &info);
|
||||
self.db.save_token(&token, &info);
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -141,7 +141,7 @@ pub fn mark_token_as_used(&self, ValidToken { token, source }: ValidToken<'_>) {
|
||||
///
|
||||
/// Note that some tokens (like the one set in the config file) cannot be
|
||||
/// revoked.
|
||||
pub fn revoke_token(&self, ValidToken { token, source }: ValidToken<'_>) -> Result {
|
||||
pub fn revoke_token(&self, ValidToken { token, source }: ValidToken) -> Result {
|
||||
match source {
|
||||
| ValidTokenSource::ConfigFile => {
|
||||
// the config file token cannot be revoked
|
||||
@@ -151,19 +151,19 @@ pub fn revoke_token(&self, ValidToken { token, source }: ValidToken<'_>) -> Resu
|
||||
)
|
||||
},
|
||||
| ValidTokenSource::Database(_) => {
|
||||
self.db.revoke_token(token);
|
||||
self.db.revoke_token(&token);
|
||||
Ok(())
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
/// Iterate over all valid registration tokens.
|
||||
pub fn iterate_tokens(&self) -> impl Stream<Item = ValidToken<'_>> + Send + '_ {
|
||||
pub fn iterate_tokens(&self) -> impl Stream<Item = ValidToken> + Send + '_ {
|
||||
let db_tokens = self
|
||||
.db
|
||||
.iterate_and_clean_tokens()
|
||||
.map(|(token, info)| ValidToken {
|
||||
token,
|
||||
token: token.to_owned(),
|
||||
source: ValidTokenSource::Database(info),
|
||||
});
|
||||
|
||||
|
||||
@@ -209,7 +209,7 @@ pub async fn try_auth(
|
||||
}
|
||||
},
|
||||
| AuthData::RegistrationToken(t) => {
|
||||
let token = t.token.trim();
|
||||
let token = t.token.trim().to_owned();
|
||||
|
||||
if let Some(valid_token) = self
|
||||
.services
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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]
|
||||
|
||||
@@ -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]
|
||||
|
||||
Reference in New Issue
Block a user