refactor+fix various issues with regs/logins and admin user commands

Signed-off-by: June Clementine Strawberry <june@3.dog>
This commit is contained in:
June Clementine Strawberry 2025-03-02 23:16:30 -05:00
parent 00cc23b649
commit af714d5778
No known key found for this signature in database
11 changed files with 309 additions and 220 deletions

View file

@ -2,7 +2,7 @@ use std::{collections::BTreeMap, fmt::Write as _};
use api::client::{full_user_deactivate, join_room_by_id_helper, leave_room};
use conduwuit::{
PduBuilder, Result, debug_warn, error, info, is_equal_to,
PduBuilder, Result, debug, debug_warn, error, info, is_equal_to,
utils::{self, ReadyExt},
warn,
};
@ -57,16 +57,16 @@ pub(super) async fn create_user(
// Validate user id
let user_id = parse_local_user_id(self.services, &username)?;
if self.services.users.exists(&user_id).await {
return Ok(RoomMessageEventContent::text_plain(format!(
"Userid {user_id} already exists"
)));
if let Err(e) = user_id.validate_strict() {
if self.services.config.emergency_password.is_none() {
return Ok(RoomMessageEventContent::text_plain(format!(
"Username {user_id} contains disallowed characters or spaces: {e}"
)));
}
}
if user_id.is_historical() {
return Ok(RoomMessageEventContent::text_plain(format!(
"User ID {user_id} does not conform to new Matrix identifier spec"
)));
if self.services.users.exists(&user_id).await {
return Ok(RoomMessageEventContent::text_plain(format!("User {user_id} already exists")));
}
let password = password.unwrap_or_else(|| utils::random_string(AUTO_GEN_PASSWORD_LENGTH));
@ -185,12 +185,12 @@ pub(super) async fn create_user(
.is_ok_and(is_equal_to!(1))
{
self.services.admin.make_user_admin(&user_id).await?;
warn!("Granting {user_id} admin privileges as the first user");
}
} else {
debug!("create_user admin command called without an admin room being available");
}
// Inhibit login does not work for guests
Ok(RoomMessageEventContent::text_plain(format!(
"Created user with user_id: {user_id} and password: `{password}`"
)))
@ -694,6 +694,19 @@ pub(super) async fn force_leave_room(
self.services.globals.user_is_local(&user_id),
"Parsed user_id must be a local user"
);
if !self
.services
.rooms
.state_cache
.is_joined(&user_id, &room_id)
.await
{
return Ok(RoomMessageEventContent::notice_markdown(format!(
"{user_id} is not joined in the room"
)));
}
leave_room(self.services, &user_id, &room_id, None).await?;
Ok(RoomMessageEventContent::notice_markdown(format!(

View file

@ -3,7 +3,8 @@ use std::fmt::Write;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{
Error, PduBuilder, Result, debug_info, error, info, is_equal_to, utils, utils::ReadyExt, warn,
Err, Error, PduBuilder, Result, debug_info, err, error, info, is_equal_to, utils,
utils::ReadyExt, warn,
};
use futures::{FutureExt, StreamExt};
use register::RegistrationKind;
@ -17,7 +18,6 @@ use ruma::{
request_3pid_management_token_via_email, request_3pid_management_token_via_msisdn,
whoami,
},
error::ErrorKind,
uiaa::{AuthFlow, AuthType, UiaaInfo},
},
events::{
@ -60,6 +60,14 @@ pub(crate) async fn get_register_available_route(
|| appservice.registration.id.contains("matrix_appservice_irc")
});
if services
.globals
.forbidden_usernames()
.is_match(&body.username)
{
return Err!(Request(Forbidden("Username is forbidden")));
}
// don't force the username lowercase if it's from matrix-appservice-irc
let body_username = if is_matrix_appservice_irc {
body.username.clone()
@ -68,30 +76,45 @@ pub(crate) async fn get_register_available_route(
};
// Validate user id
let user_id = UserId::parse_with_server_name(body_username, services.globals.server_name())
.ok()
.filter(|user_id| {
(!user_id.is_historical() || is_matrix_appservice_irc)
&& services.globals.user_is_local(user_id)
})
.ok_or(Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
let user_id =
match UserId::parse_with_server_name(&body_username, services.globals.server_name()) {
| Ok(user_id) => {
if let Err(e) = user_id.validate_strict() {
// unless the username is from the broken matrix appservice IRC bridge, we
// should follow synapse's behaviour on not allowing things like spaces
// and UTF-8 characters in usernames
if !is_matrix_appservice_irc {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} contains disallowed characters or spaces: \
{e}"
))));
}
}
user_id
},
| Err(e) => {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} is not valid: {e}"
))));
},
};
// Check if username is creative enough
if services.users.exists(&user_id).await {
return Err(Error::BadRequest(ErrorKind::UserInUse, "Desired user ID is already taken."));
return Err!(Request(UserInUse("User ID is not available.")));
}
if services
.globals
.forbidden_usernames()
.is_match(user_id.localpart())
{
return Err(Error::BadRequest(ErrorKind::Unknown, "Username is forbidden."));
if let Some(ref info) = body.appservice_info {
if !info.is_user_match(&user_id) {
return Err!(Request(Exclusive("Username is not in an appservice namespace.")));
}
};
if services.appservice.is_exclusive_user_id(&user_id).await {
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
// TODO add check for appservice namespaces
// If no if check is true we have an username that's available to be used.
Ok(get_username_availability::v3::Response { available: true })
}
@ -119,16 +142,27 @@ pub(crate) async fn register_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<register::v3::Request>,
) -> Result<register::v3::Response> {
if !services.globals.allow_registration() && body.appservice_info.is_none() {
info!(
"Registration disabled and request not from known appservice, rejecting \
registration attempt for username \"{}\"",
body.username.as_deref().unwrap_or("")
);
return Err(Error::BadRequest(ErrorKind::forbidden(), "Registration has been disabled."));
}
let is_guest = body.kind == RegistrationKind::Guest;
let emergency_mode_enabled = services.config.emergency_password.is_some();
if !services.globals.allow_registration() && body.appservice_info.is_none() {
match (body.username.as_ref(), body.initial_device_display_name.as_ref()) {
| (Some(username), Some(device_display_name)) => {
info!(%is_guest, user = %username, device_name = %device_display_name, "Rejecting registration attempt as registration is disabled");
},
| (Some(username), _) => {
info!(%is_guest, user = %username, "Rejecting registration attempt as registration is disabled");
},
| (_, Some(device_display_name)) => {
info!(%is_guest, device_name = %device_display_name, "Rejecting registration attempt as registration is disabled");
},
| (None, _) => {
info!(%is_guest, "Rejecting registration attempt as registration is disabled");
},
};
return Err!(Request(Forbidden("Registration has been disabled.")));
}
if is_guest
&& (!services.globals.allow_guest_registration()
@ -140,10 +174,7 @@ pub(crate) async fn register_route(
rejecting guest registration attempt, initial device name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err(Error::BadRequest(
ErrorKind::GuestAccessForbidden,
"Guest registration is disabled.",
));
return Err!(Request(GuestAccessForbidden("Guest registration is disabled.")));
}
// forbid guests from registering if there is not a real admin user yet. give
@ -154,13 +185,10 @@ pub(crate) async fn register_route(
rejecting registration. Guest's initial device name: \"{}\"",
body.initial_device_display_name.as_deref().unwrap_or("")
);
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Registration temporarily disabled.",
));
return Err!(Request(Forbidden("Registration is temporarily disabled.")));
}
let user_id = match (&body.username, is_guest) {
let user_id = match (body.username.as_ref(), is_guest) {
| (Some(username), false) => {
// workaround for https://github.com/matrix-org/matrix-appservice-irc/issues/1780 due to inactivity of fixing the issue
let is_matrix_appservice_irc =
@ -170,6 +198,12 @@ pub(crate) async fn register_route(
|| appservice.registration.id.contains("matrix_appservice_irc")
});
if services.globals.forbidden_usernames().is_match(username)
&& !emergency_mode_enabled
{
return Err!(Request(Forbidden("Username is forbidden")));
}
// don't force the username lowercase if it's from matrix-appservice-irc
let body_username = if is_matrix_appservice_irc {
username.clone()
@ -177,31 +211,34 @@ pub(crate) async fn register_route(
username.to_lowercase()
};
let proposed_user_id =
UserId::parse_with_server_name(body_username, services.globals.server_name())
.ok()
.filter(|user_id| {
(!user_id.is_historical() || is_matrix_appservice_irc)
&& services.globals.user_is_local(user_id)
})
.ok_or(Error::BadRequest(
ErrorKind::InvalidUsername,
"Username is invalid.",
))?;
let proposed_user_id = match UserId::parse_with_server_name(
&body_username,
services.globals.server_name(),
) {
| Ok(user_id) => {
if let Err(e) = user_id.validate_strict() {
// unless the username is from the broken matrix appservice IRC bridge, or
// we are in emergency mode, we should follow synapse's behaviour on
// not allowing things like spaces and UTF-8 characters in usernames
if !is_matrix_appservice_irc && !emergency_mode_enabled {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} contains disallowed characters or \
spaces: {e}"
))));
}
}
user_id
},
| Err(e) => {
return Err!(Request(InvalidUsername(debug_warn!(
"Username {body_username} is not valid: {e}"
))));
},
};
if services.users.exists(&proposed_user_id).await {
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."));
return Err!(Request(UserInUse("User ID is not available.")));
}
proposed_user_id
@ -221,21 +258,18 @@ pub(crate) async fn register_route(
if body.body.login_type == Some(LoginType::ApplicationService) {
match body.appservice_info {
| Some(ref info) =>
if !info.is_user_match(&user_id) {
return Err(Error::BadRequest(
ErrorKind::Exclusive,
"User is not in namespace.",
));
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
return Err!(Request(Exclusive(
"Username is not in an appservice namespace."
)));
},
| _ => {
return Err(Error::BadRequest(
ErrorKind::MissingToken,
"Missing appservice token.",
));
return Err!(Request(MissingToken("Missing appservice token.")));
},
}
} else if services.appservice.is_exclusive_user_id(&user_id).await {
return Err(Error::BadRequest(ErrorKind::Exclusive, "User ID reserved by appservice."));
} else if services.appservice.is_exclusive_user_id(&user_id).await && !emergency_mode_enabled
{
return Err!(Request(Exclusive("Username is reserved by an appservice.")));
}
// UIAA
@ -271,7 +305,7 @@ pub(crate) async fn register_route(
.uiaa
.try_auth(
&UserId::parse_with_server_name("", services.globals.server_name())
.expect("we know this is valid"),
.unwrap(),
"".into(),
auth,
&uiaainfo,
@ -287,7 +321,7 @@ pub(crate) async fn register_route(
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services.uiaa.create(
&UserId::parse_with_server_name("", services.globals.server_name())
.expect("we know this is valid"),
.unwrap(),
"".into(),
&uiaainfo,
&json,
@ -295,7 +329,7 @@ pub(crate) async fn register_route(
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
@ -407,7 +441,7 @@ pub(crate) async fn register_route(
// log in conduit admin channel if a guest registered
if body.appservice_info.is_none() && is_guest && services.globals.log_guest_registrations() {
info!("New guest user \"{user_id}\" registered on this server.");
debug_info!("New guest user \"{user_id}\" registered on this server.");
if !device_display_name.is_empty() {
if services.server.config.admin_room_notices {
@ -436,7 +470,8 @@ pub(crate) async fn register_route(
}
// If this is the first real user, grant them admin privileges except for guest
// users Note: the server user, @conduit:servername, is generated first
// users
// Note: the server user is generated first
if !is_guest {
if let Ok(admin_room) = services.admin.get_admin_room().await {
if services
@ -541,8 +576,8 @@ pub(crate) async fn change_password_route(
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))?;
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let sender_device = body.sender_device();
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
@ -566,16 +601,16 @@ pub(crate) async fn change_password_route(
// Success!
},
| _ => match body.json_body {
| Some(json) => {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json);
.create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
@ -589,7 +624,7 @@ pub(crate) async fn change_password_route(
services
.users
.all_device_ids(sender_user)
.ready_filter(|id| id != sender_device)
.ready_filter(|id| *id != sender_device)
.for_each(|id| services.users.remove_device(sender_user, id))
.await;
}
@ -651,8 +686,8 @@ pub(crate) async fn deactivate_route(
let sender_user = body
.sender_user
.as_ref()
.ok_or_else(|| Error::BadRequest(ErrorKind::MissingToken, "Missing access token."))?;
let sender_device = body.sender_device.as_ref().expect("user is authenticated");
.ok_or_else(|| err!(Request(MissingToken("Missing access token."))))?;
let sender_device = body.sender_device();
let mut uiaainfo = UiaaInfo {
flows: vec![AuthFlow { stages: vec![AuthType::Password] }],
@ -675,16 +710,16 @@ pub(crate) async fn deactivate_route(
// Success!
},
| _ => match body.json_body {
| Some(json) => {
| Some(ref json) => {
uiaainfo.session = Some(utils::random_string(SESSION_ID_LENGTH));
services
.uiaa
.create(sender_user, sender_device, &uiaainfo, &json);
.create(sender_user, sender_device, &uiaainfo, json);
return Err(Error::Uiaa(uiaainfo));
},
| _ => {
return Err(Error::BadRequest(ErrorKind::NotJson, "Not json."));
return Err!(Request(NotJson("JSON body is not valid")));
},
},
}
@ -743,10 +778,7 @@ pub(crate) async fn third_party_route(
pub(crate) async fn request_3pid_management_token_via_email_route(
_body: Ruma<request_3pid_management_token_via_email::v3::Request>,
) -> Result<request_3pid_management_token_via_email::v3::Response> {
Err(Error::BadRequest(
ErrorKind::ThreepidDenied,
"Third party identifier is not allowed",
))
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
}
/// # `POST /_matrix/client/v3/account/3pid/msisdn/requestToken`
@ -759,10 +791,7 @@ pub(crate) async fn request_3pid_management_token_via_email_route(
pub(crate) async fn request_3pid_management_token_via_msisdn_route(
_body: Ruma<request_3pid_management_token_via_msisdn::v3::Request>,
) -> Result<request_3pid_management_token_via_msisdn::v3::Response> {
Err(Error::BadRequest(
ErrorKind::ThreepidDenied,
"Third party identifier is not allowed",
))
Err!(Request(ThreepidDenied("Third party identifiers are not implemented")))
}
/// # `GET /_matrix/client/v1/register/m.login.registration_token/validity`
@ -776,10 +805,7 @@ pub(crate) async fn check_registration_token_validity(
body: Ruma<check_registration_token_validity::v1::Request>,
) -> Result<check_registration_token_validity::v1::Response> {
let Some(reg_token) = services.globals.registration_token.clone() else {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"Server does not allow token registration.",
));
return Err!(Request(Forbidden("Server does not allow token registration")));
};
Ok(check_registration_token_validity::v1::Response { valid: reg_token == body.token })

View file

@ -2,12 +2,11 @@ use std::time::Duration;
use axum::extract::State;
use axum_client_ip::InsecureClientIp;
use conduwuit::{Err, debug, err, info, utils::ReadyExt, warn};
use conduwuit::{Err, debug, err, info, utils::ReadyExt};
use futures::StreamExt;
use ruma::{
OwnedUserId, UserId,
UserId,
api::client::{
error::ErrorKind,
session::{
get_login_token,
get_login_types::{
@ -67,6 +66,8 @@ pub(crate) async fn login_route(
InsecureClientIp(client): InsecureClientIp,
body: Ruma<login::v3::Request>,
) -> Result<login::v3::Response> {
let emergency_mode_enabled = services.config.emergency_password.is_some();
// Validate login method
// TODO: Other login methods
let user_id = match &body.login_info {
@ -78,20 +79,22 @@ pub(crate) async fn login_route(
..
}) => {
debug!("Got password login type");
let user_id = if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) =
identifier
{
UserId::parse_with_server_name(
user_id.to_lowercase(),
services.globals.server_name(),
)
} else if let Some(user) = user {
OwnedUserId::parse(user)
} else {
warn!("Bad login type: {:?}", &body.login_info);
return Err!(Request(Forbidden("Bad login type.")));
}
.map_err(|_| Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid."))?;
let user_id =
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
UserId::parse_with_server_name(user_id, &services.config.server_name)
} else if let Some(user) = user {
UserId::parse_with_server_name(user, &services.config.server_name)
} else {
return Err!(Request(Unknown(
warn!(?body.login_info, "Invalid or unsupported login type")
)));
}
.map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
assert!(
services.globals.user_is_local(&user_id),
"User ID does not belong to this homeserver"
);
let hash = services
.users
@ -124,46 +127,40 @@ pub(crate) async fn login_route(
debug!("Got appservice login type");
let user_id =
if let Some(uiaa::UserIdentifier::UserIdOrLocalpart(user_id)) = identifier {
UserId::parse_with_server_name(
user_id.to_lowercase(),
services.globals.server_name(),
)
UserId::parse_with_server_name(user_id, &services.config.server_name)
} else if let Some(user) = user {
OwnedUserId::parse(user)
UserId::parse_with_server_name(user, &services.config.server_name)
} else {
warn!("Bad login type: {:?}", &body.login_info);
return Err(Error::BadRequest(ErrorKind::forbidden(), "Bad login type."));
return Err!(Request(Unknown(
warn!(?body.login_info, "Invalid or unsupported login type")
)));
}
.map_err(|e| {
warn!("Failed to parse username from appservice logging in: {e}");
Error::BadRequest(ErrorKind::InvalidUsername, "Username is invalid.")
})?;
.map_err(|e| err!(Request(InvalidUsername(warn!("Username is invalid: {e}")))))?;
assert!(
services.globals.user_is_local(&user_id),
"User ID does not belong to this homeserver"
);
match body.appservice_info {
| Some(ref info) =>
if !info.is_user_match(&user_id) {
return Err(Error::BadRequest(
ErrorKind::Exclusive,
"User is not in namespace.",
));
if !info.is_user_match(&user_id) && !emergency_mode_enabled {
return Err!(Request(Exclusive(
"Username is not in an appservice namespace."
)));
},
| _ => {
return Err(Error::BadRequest(
ErrorKind::MissingToken,
"Missing appservice token.",
));
return Err!(Request(MissingToken("Missing appservice token.")));
},
}
user_id
},
| _ => {
warn!("Unsupported or unknown login type: {:?}", &body.login_info);
debug!("JSON body: {:?}", &body.json_body);
return Err(Error::BadRequest(
ErrorKind::Unknown,
"Unsupported or unknown login type.",
));
debug!("/login json_body: {:?}", &body.json_body);
return Err!(Request(Unknown(
warn!(?body.login_info, "Invalid or unsupported login type")
)));
},
};
@ -216,9 +213,6 @@ pub(crate) async fn login_route(
info!("{user_id} logged in");
// home_server is deprecated but apparently must still be sent despite it being
// deprecated over 6 years ago. initially i thought this macro was unnecessary,
// but ruma uses this same macro for the same reason so...
#[allow(deprecated)]
Ok(login::v3::Response {
user_id,
@ -226,7 +220,7 @@ pub(crate) async fn login_route(
device_id,
well_known: client_discovery_info,
expires_in: None,
home_server: Some(services.globals.server_name().to_owned()),
home_server: Some(services.config.server_name.clone()),
refresh_token: None,
})
}

View file

@ -126,6 +126,14 @@ pub fn check(config: &Config) -> Result {
));
}
if config.emergency_password == Some(String::new()) {
return Err!(Config(
"emergency_password",
"Emergency password was set to an empty string, this is not valid. Unset \
emergency_password to disable it or set it to a real password."
));
}
// check if the user specified a registration token as `""`
if config.registration_token == Some(String::new()) {
return Err!(Config(

View file

@ -21,11 +21,11 @@ use crate::Services;
/// Create the admin room.
///
/// Users in this room are considered admins by conduit, and the room can be
/// Users in this room are considered admins by conduwuit, and the room can be
/// used to issue admin commands by talking to the server user inside it.
pub async fn create_admin_room(services: &Services) -> Result<()> {
pub async fn create_admin_room(services: &Services) -> Result {
let room_id = RoomId::new(services.globals.server_name());
let room_version = &services.server.config.default_room_version;
let room_version = &services.config.default_room_version;
let _short_id = services
.rooms
@ -36,14 +36,14 @@ pub async fn create_admin_room(services: &Services) -> Result<()> {
let state_lock = services.rooms.state.mutex.lock(&room_id).await;
// Create a user for the server
let server_user = &services.globals.server_user;
let server_user = services.globals.server_user.as_ref();
services.users.create(server_user, None)?;
let create_content = {
use RoomVersionId::*;
match room_version {
| V1 | V2 | V3 | V4 | V5 | V6 | V7 | V8 | V9 | V10 =>
RoomCreateEventContent::new_v1(server_user.clone()),
RoomCreateEventContent::new_v1(server_user.into()),
| _ => RoomCreateEventContent::new_v11(),
}
};
@ -71,7 +71,7 @@ pub async fn create_admin_room(services: &Services) -> Result<()> {
.timeline
.build_and_append_pdu(
PduBuilder::state(
server_user.to_string(),
String::from(server_user),
&RoomMemberEventContent::new(MembershipState::Join),
),
server_user,
@ -81,7 +81,7 @@ pub async fn create_admin_room(services: &Services) -> Result<()> {
.await?;
// 3. Power levels
let users = BTreeMap::from_iter([(server_user.clone(), 100.into())]);
let users = BTreeMap::from_iter([(server_user.into(), 69420.into())]);
services
.rooms
@ -140,7 +140,7 @@ pub async fn create_admin_room(services: &Services) -> Result<()> {
.await?;
// 5. Events implied by name and topic
let room_name = format!("{} Admin Room", services.globals.server_name());
let room_name = format!("{} Admin Room", services.config.server_name);
services
.rooms
.timeline
@ -157,7 +157,7 @@ pub async fn create_admin_room(services: &Services) -> Result<()> {
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomTopicEventContent {
topic: format!("Manage {}", services.globals.server_name()),
topic: format!("Manage {} | Run commands prefixed with `!admin` | Run `!admin -h` for help | Documentation: https://conduwuit.puppyirl.gay/", services.config.server_name),
}),
server_user,
&room_id,
@ -187,7 +187,7 @@ pub async fn create_admin_room(services: &Services) -> Result<()> {
.alias
.set_alias(alias, &room_id, server_user)?;
// 7. (ad-hoc) Disable room previews for everyone by default
// 7. (ad-hoc) Disable room URL previews for everyone by default
services
.rooms
.timeline

View file

@ -1,10 +1,10 @@
use std::collections::BTreeMap;
use conduwuit::{Result, error, implement};
use conduwuit::{Err, Result, debug_info, debug_warn, error, implement};
use ruma::{
RoomId, UserId,
events::{
RoomAccountDataEventType,
RoomAccountDataEventType, StateEventType,
room::{
member::{MembershipState, RoomMemberEventContent},
message::RoomMessageEventContent,
@ -20,55 +20,98 @@ use crate::pdu::PduBuilder;
///
/// This is equivalent to granting server admin privileges.
#[implement(super::Service)]
pub async fn make_user_admin(&self, user_id: &UserId) -> Result<()> {
pub async fn make_user_admin(&self, user_id: &UserId) -> Result {
let Ok(room_id) = self.get_admin_room().await else {
debug_warn!(
"make_user_admin was called without an admin room being available or created"
);
return Ok(());
};
let state_lock = self.services.state.mutex.lock(&room_id).await;
if self.services.state_cache.is_joined(user_id, &room_id).await {
return Err!(debug_warn!("User is already joined in the admin room"));
}
if self
.services
.state_cache
.is_invited(user_id, &room_id)
.await
{
return Err!(debug_warn!("User is already pending an invitation to the admin room"));
}
// Use the server user to grant the new admin's power level
let server_user = &self.services.globals.server_user;
let server_user = self.services.globals.server_user.as_ref();
// Invite and join the real user
self.services
.timeline
.build_and_append_pdu(
PduBuilder::state(
user_id.to_string(),
&RoomMemberEventContent::new(MembershipState::Invite),
),
server_user,
// if this is our local user, just forcefully join them in the room. otherwise,
// invite the remote user.
if self.services.globals.user_is_local(user_id) {
debug_info!("Inviting local user {user_id} to admin room {room_id}");
self.services
.timeline
.build_and_append_pdu(
PduBuilder::state(
String::from(user_id),
&RoomMemberEventContent::new(MembershipState::Invite),
),
server_user,
&room_id,
&state_lock,
)
.await?;
debug_info!("Force joining local user {user_id} to admin room {room_id}");
self.services
.timeline
.build_and_append_pdu(
PduBuilder::state(
String::from(user_id),
&RoomMemberEventContent::new(MembershipState::Join),
),
user_id,
&room_id,
&state_lock,
)
.await?;
} else {
debug_info!("Inviting remote user {user_id} to admin room {room_id}");
self.services
.timeline
.build_and_append_pdu(
PduBuilder::state(
user_id.to_string(),
&RoomMemberEventContent::new(MembershipState::Invite),
),
server_user,
&room_id,
&state_lock,
)
.await?;
}
// Set power levels
let mut room_power_levels = self
.services
.state_accessor
.room_state_get_content::<RoomPowerLevelsEventContent>(
&room_id,
&state_lock,
&StateEventType::RoomPowerLevels,
"",
)
.await?;
self.services
.timeline
.build_and_append_pdu(
PduBuilder::state(
user_id.to_string(),
&RoomMemberEventContent::new(MembershipState::Join),
),
user_id,
&room_id,
&state_lock,
)
.await?;
.await
.unwrap_or_default();
// Set power level
let users = BTreeMap::from_iter([
(server_user.clone(), 100.into()),
(user_id.to_owned(), 100.into()),
]);
room_power_levels
.users
.insert(server_user.into(), 69420.into());
room_power_levels.users.insert(user_id.into(), 100.into());
self.services
.timeline
.build_and_append_pdu(
PduBuilder::state(String::new(), &RoomPowerLevelsEventContent {
users,
..Default::default()
}),
PduBuilder::state(String::new(), &room_power_levels),
server_user,
&room_id,
&state_lock,
@ -76,15 +119,17 @@ pub async fn make_user_admin(&self, user_id: &UserId) -> Result<()> {
.await?;
// Set room tag
let room_tag = &self.services.server.config.admin_room_tag;
let room_tag = self.services.server.config.admin_room_tag.as_str();
if !room_tag.is_empty() {
if let Err(e) = self.set_room_tag(&room_id, user_id, room_tag).await {
error!(?room_id, ?user_id, ?room_tag, ?e, "Failed to set tag for admin grant");
error!(?room_id, ?user_id, ?room_tag, "Failed to set tag for admin grant: {e}");
}
}
if self.services.server.config.admin_room_notices {
let welcome_message = String::from("## Thank you for trying out conduwuit!\n\nconduwuit is technically a hard fork of Conduit, which is in Beta. The Beta status initially was inherited from Conduit, however overtime this Beta status is rapidly becoming less and less relevant as our codebase significantly diverges more and more. conduwuit is quite stable and very usable as a daily driver and for a low-medium sized homeserver. There is still a lot of more work to be done, but it is in a far better place than the project was in early 2024.\n\nHelpful links:\n> GitHub Repo: https://github.com/girlbossceo/conduwuit\n> Documentation: https://conduwuit.puppyirl.gay/\n> Report issues: https://github.com/girlbossceo/conduwuit/issues\n\nFor a list of available commands, send the following message in this room: `!admin --help`\n\nHere are some rooms you can join (by typing the command into your client) -\n\nconduwuit space: `/join #conduwuit-space:puppygock.gay`\nconduwuit main room (Ask questions and get notified on updates): `/join #conduwuit:puppygock.gay`\nconduwuit offtopic room: `/join #conduwuit-offtopic:puppygock.gay`");
let welcome_message = String::from(
"## Thank you for trying out conduwuit!\n\nconduwuit is technically a hard fork of Conduit, which is in Beta. The Beta status initially was inherited from Conduit, however overtime this Beta status is rapidly becoming less and less relevant as our codebase significantly diverges more and more. conduwuit is quite stable and very usable as a daily driver and for a low-medium sized homeserver. There is still a lot of more work to be done, but it is in a far better place than the project was in early 2024.\n\nHelpful links:\n> GitHub Repo: https://github.com/girlbossceo/conduwuit\n> Documentation: https://conduwuit.puppyirl.gay/\n> Report issues: https://github.com/girlbossceo/conduwuit/issues\n\nFor a list of available commands, send the following message in this room: `!admin --help`\n\nHere are some rooms you can join (by typing the command into your client) -\n\nconduwuit space: `/join #conduwuit-space:puppygock.gay`\nconduwuit main room (Ask questions and get notified on updates): `/join #conduwuit:puppygock.gay`\nconduwuit offtopic room: `/join #conduwuit-offtopic:puppygock.gay`",
);
// Send welcome message
self.services
@ -102,7 +147,7 @@ pub async fn make_user_admin(&self, user_id: &UserId) -> Result<()> {
}
#[implement(super::Service)]
async fn set_room_tag(&self, room_id: &RoomId, user_id: &UserId, tag: &str) -> Result<()> {
async fn set_room_tag(&self, room_id: &RoomId, user_id: &UserId, tag: &str) -> Result {
let mut event = self
.services
.account_data
@ -125,7 +170,5 @@ async fn set_room_tag(&self, room_id: &RoomId, user_id: &UserId, tag: &str) -> R
RoomAccountDataEventType::Tag,
&serde_json::to_value(event)?,
)
.await?;
Ok(())
.await
}

View file

@ -40,6 +40,7 @@ struct Services {
timeline: Dep<rooms::timeline::Service>,
state: Dep<rooms::state::Service>,
state_cache: Dep<rooms::state_cache::Service>,
state_accessor: Dep<rooms::state_accessor::Service>,
account_data: Dep<account_data::Service>,
services: StdRwLock<Option<Weak<crate::Services>>>,
}
@ -85,6 +86,8 @@ impl crate::Service for Service {
timeline: args.depend::<rooms::timeline::Service>("rooms::timeline"),
state: args.depend::<rooms::state::Service>("rooms::state"),
state_cache: args.depend::<rooms::state_cache::Service>("rooms::state_cache"),
state_accessor: args
.depend::<rooms::state_accessor::Service>("rooms::state_accessor"),
account_data: args.depend::<account_data::Service>("account_data"),
services: None.into(),
},
@ -357,8 +360,8 @@ impl Service {
}
// This will evaluate to false if the emergency password is set up so that
// the administrator can execute commands as conduit
let emergency_password_set = self.services.globals.emergency_password().is_some();
// the administrator can execute commands as the server user
let emergency_password_set = self.services.server.config.emergency_password.is_some();
let from_server = pdu.sender == *server_user && !emergency_password_set;
if from_server && self.is_admin_room(&pdu.room_id).await {
return false;

View file

@ -90,7 +90,7 @@ impl Service {
.write()
.await
.remove(appservice_id)
.ok_or(err!("Appservice not found"))?;
.ok_or_else(|| err!("Appservice not found"))?;
// remove the appservice from the database
self.db.id_appserviceregistrations.del(appservice_id);

View file

@ -9,7 +9,7 @@ use ruma::{
push::Ruleset,
};
use crate::{Dep, account_data, globals, users};
use crate::{Dep, account_data, config, globals, users};
pub struct Service {
services: Services,
@ -17,6 +17,7 @@ pub struct Service {
struct Services {
account_data: Dep<account_data::Service>,
config: Dep<config::Service>,
globals: Dep<globals::Service>,
users: Dep<users::Service>,
}
@ -27,6 +28,8 @@ impl crate::Service for Service {
Ok(Arc::new(Self {
services: Services {
account_data: args.depend::<account_data::Service>("account_data"),
config: args.depend::<config::Service>("config"),
globals: args.depend::<globals::Service>("globals"),
users: args.depend::<users::Service>("users"),
},
@ -54,9 +57,9 @@ impl Service {
self.services
.users
.set_password(server_user, self.services.globals.emergency_password().as_deref())?;
.set_password(server_user, self.services.config.emergency_password.as_deref())?;
let (ruleset, pwd_set) = match self.services.globals.emergency_password() {
let (ruleset, pwd_set) = match self.services.config.emergency_password {
| Some(_) => (Ruleset::server_default(server_user), true),
| None => (Ruleset::new(), false),
};

View file

@ -153,8 +153,6 @@ impl Service {
pub fn notification_push_path(&self) -> &String { &self.server.config.notification_push_path }
pub fn emergency_password(&self) -> &Option<String> { &self.server.config.emergency_password }
pub fn url_preview_domain_contains_allowlist(&self) -> &Vec<String> {
&self.server.config.url_preview_domain_contains_allowlist
}

View file

@ -363,7 +363,7 @@ impl super::Service {
let hostname = hostname.trim_end_matches('.');
match self.resolver.resolver.srv_lookup(hostname).await {
| Err(e) => Self::handle_resolve_error(&e, hostname)?,
| Ok(result) =>
| Ok(result) => {
return Ok(result.iter().next().map(|result| {
FedDest::Named(
result.target().to_string().trim_end_matches('.').to_owned(),
@ -372,7 +372,8 @@ impl super::Service {
.try_into()
.unwrap_or_else(|_| FedDest::default_port()),
)
})),
}));
},
}
}