mirror of
https://gitlab.computer.surgery/matrix/grapevine.git
synced 2026-02-06 08:41:23 +01:00
Merge branch 'charles/cfg-admin-bot-localpart' into 'main'
allow configuring admin bot localpart See merge request matrix/grapevine!118
This commit is contained in:
commit
37ef8b6f69
8 changed files with 428 additions and 132 deletions
|
|
@ -10,7 +10,7 @@ use std::{
|
||||||
use reqwest::Url;
|
use reqwest::Url;
|
||||||
use ruma::{
|
use ruma::{
|
||||||
api::federation::discovery::OldVerifyKey, OwnedServerName,
|
api::federation::discovery::OldVerifyKey, OwnedServerName,
|
||||||
OwnedServerSigningKeyId, RoomVersionId,
|
OwnedServerSigningKeyId, RoomVersionId, UserId,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
use strum::{Display, EnumIter, IntoEnumIterator};
|
use strum::{Display, EnumIter, IntoEnumIterator};
|
||||||
|
|
@ -71,6 +71,7 @@ pub(crate) struct Config {
|
||||||
#[serde(default)]
|
#[serde(default)]
|
||||||
pub(crate) turn: TurnConfig,
|
pub(crate) turn: TurnConfig,
|
||||||
|
|
||||||
|
pub(crate) admin_bot_localpart: String,
|
||||||
pub(crate) emergency_password: Option<String>,
|
pub(crate) emergency_password: Option<String>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -518,5 +519,15 @@ where
|
||||||
return Err(Error::RegistrationTokenEmpty);
|
return Err(Error::RegistrationTokenEmpty);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let admin_bot_id = UserId::parse(format!(
|
||||||
|
"@{}:{}",
|
||||||
|
config.admin_bot_localpart, config.server_name
|
||||||
|
))
|
||||||
|
.map_err(Error::InvalidAdminBotId)?;
|
||||||
|
|
||||||
|
if admin_bot_id.is_historical() {
|
||||||
|
return Err(Error::HistoricalAdminBotLocalpart);
|
||||||
|
}
|
||||||
|
|
||||||
Ok(config)
|
Ok(config)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -997,6 +997,13 @@ impl KeyValueDatabase {
|
||||||
Ok(())
|
Ok(())
|
||||||
})?;
|
})?;
|
||||||
|
|
||||||
|
// Ensure the admin bot localpart has been written once
|
||||||
|
if services().globals.saved_admin_bot_localpart()?.is_none() {
|
||||||
|
services().globals.save_admin_bot_localpart(
|
||||||
|
services().globals.admin_bot_user_id.localpart(),
|
||||||
|
)?;
|
||||||
|
}
|
||||||
|
|
||||||
assert_eq!(
|
assert_eq!(
|
||||||
services().globals.database_version().unwrap(),
|
services().globals.database_version().unwrap(),
|
||||||
latest_database_version,
|
latest_database_version,
|
||||||
|
|
@ -1008,6 +1015,21 @@ impl KeyValueDatabase {
|
||||||
version = latest_database_version,
|
version = latest_database_version,
|
||||||
"Loaded database",
|
"Loaded database",
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// TODO: Return this error directly instead of map_err hacks
|
||||||
|
services().admin.apply_admin_bot_localpart().await.map_err(
|
||||||
|
|e| {
|
||||||
|
error!(
|
||||||
|
error = %crate::error::DisplayWithSources{
|
||||||
|
error: &e,
|
||||||
|
infix: ": ",
|
||||||
|
},
|
||||||
|
"failed to apply admin bot localpart"
|
||||||
|
);
|
||||||
|
|
||||||
|
Error::BadConfig("failed to apply admin bot localpart")
|
||||||
|
},
|
||||||
|
)?;
|
||||||
} else {
|
} else {
|
||||||
services()
|
services()
|
||||||
.globals
|
.globals
|
||||||
|
|
|
||||||
|
|
@ -3,7 +3,7 @@ use futures_util::{stream::FuturesUnordered, StreamExt};
|
||||||
use ruma::{
|
use ruma::{
|
||||||
api::federation::discovery::{OldVerifyKey, ServerSigningKeys},
|
api::federation::discovery::{OldVerifyKey, ServerSigningKeys},
|
||||||
signatures::Ed25519KeyPair,
|
signatures::Ed25519KeyPair,
|
||||||
DeviceId, ServerName, UserId,
|
DeviceId, OwnedServerName, ServerName, UserId,
|
||||||
};
|
};
|
||||||
|
|
||||||
use crate::{
|
use crate::{
|
||||||
|
|
@ -291,4 +291,50 @@ impl service::globals::Data for KeyValueDatabase {
|
||||||
self.global.insert(b"version", &new_version.to_be_bytes())?;
|
self.global.insert(b"version", &new_version.to_be_bytes())?;
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
fn set_server_name(&self, server_name: &ServerName) -> Result<()> {
|
||||||
|
self.global.insert(b"server_name", server_name.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn server_name(&self) -> Result<Option<OwnedServerName>> {
|
||||||
|
let opt_bytes = self
|
||||||
|
.global
|
||||||
|
.get(b"server_name")
|
||||||
|
.map_err(|_| Error::bad_database("Failed to read from globals"))?;
|
||||||
|
|
||||||
|
// `server_name` has not been set yet
|
||||||
|
let Some(bytes) = opt_bytes else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let utf8 = String::from_utf8(bytes)
|
||||||
|
.map_err(|_| Error::bad_database("Invalid UTF-8 in server_name"))?;
|
||||||
|
|
||||||
|
let server_name = OwnedServerName::try_from(utf8)
|
||||||
|
.map_err(|_| Error::bad_database("Invalid server_name"))?;
|
||||||
|
|
||||||
|
Ok(Some(server_name))
|
||||||
|
}
|
||||||
|
|
||||||
|
fn set_admin_bot_localpart(&self, localpart: &str) -> Result<()> {
|
||||||
|
self.global.insert(b"admin_bot_localpart", localpart.as_bytes())
|
||||||
|
}
|
||||||
|
|
||||||
|
fn admin_bot_localpart(&self) -> Result<Option<String>> {
|
||||||
|
let opt_bytes = self
|
||||||
|
.global
|
||||||
|
.get(b"admin_bot_localpart")
|
||||||
|
.map_err(|_| Error::bad_database("Failed to read from globals"))?;
|
||||||
|
|
||||||
|
// `admin_bot_localpart` has not been set yet
|
||||||
|
let Some(bytes) = opt_bytes else {
|
||||||
|
return Ok(None);
|
||||||
|
};
|
||||||
|
|
||||||
|
let localpart = String::from_utf8(bytes).map_err(|_| {
|
||||||
|
Error::bad_database("Invalid UTF-8 in admin_bot_localpart")
|
||||||
|
})?;
|
||||||
|
|
||||||
|
Ok(Some(localpart))
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
|
||||||
75
src/error.rs
75
src/error.rs
|
|
@ -2,6 +2,7 @@
|
||||||
|
|
||||||
use std::{fmt, iter, path::PathBuf};
|
use std::{fmt, iter, path::PathBuf};
|
||||||
|
|
||||||
|
use ruma::{OwnedServerName, OwnedUserId};
|
||||||
use thiserror::Error;
|
use thiserror::Error;
|
||||||
|
|
||||||
use crate::config::ListenConfig;
|
use crate::config::ListenConfig;
|
||||||
|
|
@ -91,14 +92,29 @@ pub(crate) enum CheckConfigCommand {
|
||||||
#[allow(missing_docs)]
|
#[allow(missing_docs)]
|
||||||
#[derive(Error, Debug)]
|
#[derive(Error, Debug)]
|
||||||
pub(crate) enum ServerNameChanged {
|
pub(crate) enum ServerNameChanged {
|
||||||
|
#[error("failed to read saved server_name")]
|
||||||
|
ReadSavedServerName(#[source] crate::utils::error::Error),
|
||||||
|
|
||||||
#[error("failed to check if there are any users")]
|
#[error("failed to check if there are any users")]
|
||||||
NonZeroUsers(#[source] crate::utils::error::Error),
|
NonZeroUsers(#[source] crate::utils::error::Error),
|
||||||
|
|
||||||
#[error("failed to check if the admin bot exists")]
|
#[error("failed to check if the admin bot exists")]
|
||||||
AdminBotExists(#[source] crate::utils::error::Error),
|
AdminBotExists(#[source] crate::utils::error::Error),
|
||||||
|
|
||||||
#[error("`server_name` in the database and config file differ")]
|
#[error(
|
||||||
Renamed,
|
"`server_name` in the database ({0}) and config file ({1}) differ"
|
||||||
|
)]
|
||||||
|
Renamed(OwnedServerName, OwnedServerName),
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"couldn't find {0} in the database, either the `server_name` changed \
|
||||||
|
or the admin bot localpart was reconfigured without running the \
|
||||||
|
server at least once with the original localpart"
|
||||||
|
)]
|
||||||
|
MissingAdminBot(OwnedUserId),
|
||||||
|
|
||||||
|
#[error("failed to save the configured server_name")]
|
||||||
|
SaveServerName(#[source] crate::utils::error::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Observability initialization errors
|
/// Observability initialization errors
|
||||||
|
|
@ -136,6 +152,14 @@ pub(crate) enum Config {
|
||||||
|
|
||||||
#[error("registration token must not be empty")]
|
#[error("registration token must not be empty")]
|
||||||
RegistrationTokenEmpty,
|
RegistrationTokenEmpty,
|
||||||
|
|
||||||
|
#[error("invalid admin bot ID")]
|
||||||
|
InvalidAdminBotId(#[source] ruma::IdParseError),
|
||||||
|
|
||||||
|
#[error(
|
||||||
|
"admin bot ID is historical (i.e. contains deprecated characters)"
|
||||||
|
)]
|
||||||
|
HistoricalAdminBotLocalpart,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Errors that can occur while searching for a config file
|
/// Errors that can occur while searching for a config file
|
||||||
|
|
@ -183,3 +207,50 @@ pub(crate) enum Serve {
|
||||||
)]
|
)]
|
||||||
FederationSelfTestFailed(#[source] crate::Error),
|
FederationSelfTestFailed(#[source] crate::Error),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Errors applying the admin bot user localpart
|
||||||
|
// Missing docs are allowed here since that kind of information should be
|
||||||
|
// encoded in the error messages themselves anyway.
|
||||||
|
#[allow(missing_docs)]
|
||||||
|
#[derive(Error, Debug)]
|
||||||
|
pub(crate) enum ApplyAdminBotLocalpart {
|
||||||
|
#[error("failed to resolve the admin room ID")]
|
||||||
|
ResolveAdminRoomId(#[source] crate::utils::error::Error),
|
||||||
|
|
||||||
|
#[error("admin room alias does not exist")]
|
||||||
|
MissingAdminRoomAlias,
|
||||||
|
|
||||||
|
#[error("failed to check if {1} is a member of the room")]
|
||||||
|
CheckMembership(#[source] crate::utils::error::Error, OwnedUserId),
|
||||||
|
|
||||||
|
#[error("failed to read saved admin bot localpart")]
|
||||||
|
ReadSavedAdminBotLocalpart(#[source] crate::utils::error::Error),
|
||||||
|
|
||||||
|
#[error("failed to create {1}")]
|
||||||
|
CreateUser(#[source] crate::utils::error::Error, OwnedUserId),
|
||||||
|
|
||||||
|
#[error("{1} failed to invite {2} to the room")]
|
||||||
|
PreviousInviteNew(
|
||||||
|
#[source] crate::utils::error::Error,
|
||||||
|
OwnedUserId,
|
||||||
|
OwnedUserId,
|
||||||
|
),
|
||||||
|
|
||||||
|
#[error("{1} failed to join the room")]
|
||||||
|
NewJoin(#[source] crate::utils::error::Error, OwnedUserId),
|
||||||
|
|
||||||
|
#[error("failed to get the room state")]
|
||||||
|
GetState(#[source] crate::utils::error::Error),
|
||||||
|
|
||||||
|
#[error("failed parse power level state")]
|
||||||
|
ParsePowerLevels(#[source] serde_json::Error),
|
||||||
|
|
||||||
|
#[error("{1} failed to set the room state")]
|
||||||
|
SetState(#[source] crate::utils::error::Error, OwnedUserId),
|
||||||
|
|
||||||
|
#[error("{1} failed to leave the room")]
|
||||||
|
PreviousLeave(#[source] crate::utils::error::Error, OwnedUserId),
|
||||||
|
|
||||||
|
#[error("failed to save the new admin bot localpart")]
|
||||||
|
SaveAdminBotLocalpart(#[source] crate::utils::error::Error),
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -20,7 +20,7 @@ use ruma::{
|
||||||
power_levels::RoomPowerLevelsEventContent,
|
power_levels::RoomPowerLevelsEventContent,
|
||||||
topic::RoomTopicEventContent,
|
topic::RoomTopicEventContent,
|
||||||
},
|
},
|
||||||
TimelineEventType,
|
StateEventType, TimelineEventType,
|
||||||
},
|
},
|
||||||
signatures::verify_json,
|
signatures::verify_json,
|
||||||
EventId, MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedRoomId,
|
EventId, MilliSecondsSinceUnixEpoch, OwnedMxcUri, OwnedRoomId,
|
||||||
|
|
@ -1330,6 +1330,8 @@ impl Service {
|
||||||
|
|
||||||
/// Create the admin room.
|
/// Create the admin room.
|
||||||
///
|
///
|
||||||
|
/// This function should only be run on a fresh database.
|
||||||
|
///
|
||||||
/// Users in this room are considered admins by grapevine, and the room can
|
/// Users in this room are considered admins by grapevine, and the room can
|
||||||
/// be used to issue admin commands by talking to the server user inside
|
/// be used to issue admin commands by talking to the server user inside
|
||||||
/// it.
|
/// it.
|
||||||
|
|
@ -1347,6 +1349,9 @@ impl Service {
|
||||||
.await;
|
.await;
|
||||||
|
|
||||||
services().users.create(&services().globals.admin_bot_user_id, None)?;
|
services().users.create(&services().globals.admin_bot_user_id, None)?;
|
||||||
|
services().globals.save_admin_bot_localpart(
|
||||||
|
services().globals.admin_bot_user_id.localpart(),
|
||||||
|
)?;
|
||||||
|
|
||||||
let room_version_id = services().globals.default_room_version();
|
let room_version_id = services().globals.default_room_version();
|
||||||
let room_version = RoomVersion::try_from(&room_version_id)?;
|
let room_version = RoomVersion::try_from(&room_version_id)?;
|
||||||
|
|
@ -1570,6 +1575,200 @@ impl Service {
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[allow(clippy::too_many_lines)]
|
||||||
|
pub(crate) async fn apply_admin_bot_localpart(
|
||||||
|
&self,
|
||||||
|
) -> Result<(), crate::error::ApplyAdminBotLocalpart> {
|
||||||
|
use crate::error::ApplyAdminBotLocalpart as Error;
|
||||||
|
|
||||||
|
let cur_user_id = &services().globals.admin_bot_user_id;
|
||||||
|
|
||||||
|
let admin_room_id = self
|
||||||
|
.get_admin_room()
|
||||||
|
.map_err(Error::ResolveAdminRoomId)?
|
||||||
|
.ok_or(Error::MissingAdminRoomAlias)?;
|
||||||
|
|
||||||
|
let cur_in_room = services()
|
||||||
|
.rooms
|
||||||
|
.state_cache
|
||||||
|
.is_joined(cur_user_id, &admin_room_id)
|
||||||
|
.map_err(|e| Error::CheckMembership(e, cur_user_id.clone()))?;
|
||||||
|
|
||||||
|
let previous_user_id = {
|
||||||
|
let localpart = services()
|
||||||
|
.globals
|
||||||
|
.saved_admin_bot_localpart()
|
||||||
|
.map_err(Error::ReadSavedAdminBotLocalpart)?
|
||||||
|
.expect("admin bot localpart should have been saved by now");
|
||||||
|
|
||||||
|
let server_name = services().globals.server_name();
|
||||||
|
|
||||||
|
UserId::parse(format!("@{localpart}:{server_name}")).expect(
|
||||||
|
"localpart and server_name should have been validated by now",
|
||||||
|
)
|
||||||
|
};
|
||||||
|
|
||||||
|
if cur_in_room {
|
||||||
|
// Don't need to make any changes
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rename variable because now we know it's a new value
|
||||||
|
let new_user_id = cur_user_id;
|
||||||
|
|
||||||
|
// Create new
|
||||||
|
services()
|
||||||
|
.users
|
||||||
|
.create(new_user_id, None)
|
||||||
|
.map_err(|e| Error::CreateUser(e, new_user_id.clone()))?;
|
||||||
|
|
||||||
|
// Acquire lock
|
||||||
|
let room_token = services()
|
||||||
|
.globals
|
||||||
|
.roomid_mutex_state
|
||||||
|
.lock_key(admin_room_id.clone())
|
||||||
|
.await;
|
||||||
|
|
||||||
|
// Make previous invite new
|
||||||
|
services()
|
||||||
|
.rooms
|
||||||
|
.timeline
|
||||||
|
.build_and_append_pdu(
|
||||||
|
PduBuilder {
|
||||||
|
event_type: TimelineEventType::RoomMember,
|
||||||
|
content: to_raw_value(&RoomMemberEventContent {
|
||||||
|
membership: MembershipState::Invite,
|
||||||
|
displayname: None,
|
||||||
|
avatar_url: None,
|
||||||
|
is_direct: None,
|
||||||
|
third_party_invite: None,
|
||||||
|
blurhash: None,
|
||||||
|
reason: None,
|
||||||
|
join_authorized_via_users_server: None,
|
||||||
|
})
|
||||||
|
.expect("event is valid, we just created it"),
|
||||||
|
unsigned: None,
|
||||||
|
state_key: Some(new_user_id.to_string()),
|
||||||
|
redacts: None,
|
||||||
|
},
|
||||||
|
&previous_user_id,
|
||||||
|
&room_token,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| {
|
||||||
|
Error::PreviousInviteNew(
|
||||||
|
e,
|
||||||
|
previous_user_id.clone(),
|
||||||
|
new_user_id.clone(),
|
||||||
|
)
|
||||||
|
})?;
|
||||||
|
|
||||||
|
// Make new join
|
||||||
|
services()
|
||||||
|
.rooms
|
||||||
|
.timeline
|
||||||
|
.build_and_append_pdu(
|
||||||
|
PduBuilder {
|
||||||
|
event_type: TimelineEventType::RoomMember,
|
||||||
|
content: to_raw_value(&RoomMemberEventContent {
|
||||||
|
membership: MembershipState::Join,
|
||||||
|
displayname: None,
|
||||||
|
avatar_url: None,
|
||||||
|
is_direct: None,
|
||||||
|
third_party_invite: None,
|
||||||
|
blurhash: None,
|
||||||
|
reason: None,
|
||||||
|
join_authorized_via_users_server: None,
|
||||||
|
})
|
||||||
|
.expect("event is valid, we just created it"),
|
||||||
|
unsigned: None,
|
||||||
|
state_key: Some(new_user_id.to_string()),
|
||||||
|
redacts: None,
|
||||||
|
},
|
||||||
|
new_user_id,
|
||||||
|
&room_token,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::NewJoin(e, new_user_id.clone()))?;
|
||||||
|
|
||||||
|
// Get current power_levels
|
||||||
|
let mut power_levels: RoomPowerLevelsEventContent = services()
|
||||||
|
.rooms
|
||||||
|
.state_accessor
|
||||||
|
.room_state_get(
|
||||||
|
&admin_room_id,
|
||||||
|
&StateEventType::RoomPowerLevels,
|
||||||
|
"",
|
||||||
|
)
|
||||||
|
.map_err(Error::GetState)?
|
||||||
|
.map(|ev| serde_json::from_str(ev.content.get()))
|
||||||
|
.transpose()
|
||||||
|
.map_err(Error::ParsePowerLevels)?
|
||||||
|
.unwrap_or_default();
|
||||||
|
|
||||||
|
// Make previous promote new and demote self
|
||||||
|
services()
|
||||||
|
.rooms
|
||||||
|
.timeline
|
||||||
|
.build_and_append_pdu(
|
||||||
|
PduBuilder {
|
||||||
|
event_type: TimelineEventType::RoomPowerLevels,
|
||||||
|
content: to_raw_value({
|
||||||
|
power_levels.users.remove(&previous_user_id);
|
||||||
|
power_levels
|
||||||
|
.users
|
||||||
|
.insert(new_user_id.clone(), 100.into());
|
||||||
|
|
||||||
|
&power_levels
|
||||||
|
})
|
||||||
|
.expect("event is valid, we just created it"),
|
||||||
|
unsigned: None,
|
||||||
|
state_key: Some(String::new()),
|
||||||
|
redacts: None,
|
||||||
|
},
|
||||||
|
&previous_user_id,
|
||||||
|
&room_token,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::SetState(e, previous_user_id.clone()))?;
|
||||||
|
|
||||||
|
// Make previous leave
|
||||||
|
services()
|
||||||
|
.rooms
|
||||||
|
.timeline
|
||||||
|
.build_and_append_pdu(
|
||||||
|
PduBuilder {
|
||||||
|
event_type: TimelineEventType::RoomMember,
|
||||||
|
content: to_raw_value(&RoomMemberEventContent {
|
||||||
|
membership: MembershipState::Leave,
|
||||||
|
displayname: None,
|
||||||
|
avatar_url: None,
|
||||||
|
is_direct: None,
|
||||||
|
third_party_invite: None,
|
||||||
|
blurhash: None,
|
||||||
|
reason: None,
|
||||||
|
join_authorized_via_users_server: None,
|
||||||
|
})
|
||||||
|
.expect("event is valid, we just created it"),
|
||||||
|
unsigned: None,
|
||||||
|
state_key: Some(previous_user_id.to_string()),
|
||||||
|
redacts: None,
|
||||||
|
},
|
||||||
|
&previous_user_id,
|
||||||
|
&room_token,
|
||||||
|
)
|
||||||
|
.await
|
||||||
|
.map_err(|e| Error::PreviousLeave(e, previous_user_id.clone()))?;
|
||||||
|
|
||||||
|
// Save the new localpart
|
||||||
|
services()
|
||||||
|
.globals
|
||||||
|
.save_admin_bot_localpart(new_user_id.localpart())
|
||||||
|
.map_err(Error::SaveAdminBotLocalpart)?;
|
||||||
|
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
/// Gets the room ID of the admin room
|
/// Gets the room ID of the admin room
|
||||||
///
|
///
|
||||||
/// Errors are propagated from the database, and will have None if there is
|
/// Errors are propagated from the database, and will have None if there is
|
||||||
|
|
|
||||||
|
|
@ -236,14 +236,9 @@ impl Service {
|
||||||
|
|
||||||
let admin_bot_user_id = UserId::parse(format!(
|
let admin_bot_user_id = UserId::parse(format!(
|
||||||
"@{}:{}",
|
"@{}:{}",
|
||||||
if config.conduit_compat {
|
config.admin_bot_localpart, config.server_name,
|
||||||
"conduit"
|
|
||||||
} else {
|
|
||||||
"grapevine"
|
|
||||||
},
|
|
||||||
config.server_name,
|
|
||||||
))
|
))
|
||||||
.expect("admin bot user ID should be valid");
|
.map_err(|_| Error::BadConfig("Invalid admin bot localpart"))?;
|
||||||
|
|
||||||
let admin_bot_room_alias_id =
|
let admin_bot_room_alias_id =
|
||||||
RoomAliasId::parse(format!("#admins:{}", config.server_name))
|
RoomAliasId::parse(format!("#admins:{}", config.server_name))
|
||||||
|
|
@ -309,6 +304,10 @@ impl Service {
|
||||||
|
|
||||||
/// Check if `server_name` in the DB and config differ, return error if so
|
/// Check if `server_name` in the DB and config differ, return error if so
|
||||||
///
|
///
|
||||||
|
/// This function will save the currently configured `server_name` if the
|
||||||
|
/// check passes, so that future calls to this function will continue to
|
||||||
|
/// check against the first-configured value.
|
||||||
|
///
|
||||||
/// Matrix resource ownership is based on the server name; changing it
|
/// Matrix resource ownership is based on the server name; changing it
|
||||||
/// requires recreating the database from scratch. This check needs to be
|
/// requires recreating the database from scratch. This check needs to be
|
||||||
/// done before background tasks are started to avoid data races.
|
/// done before background tasks are started to avoid data races.
|
||||||
|
|
@ -319,22 +318,43 @@ impl Service {
|
||||||
) -> Result<(), crate::error::ServerNameChanged> {
|
) -> Result<(), crate::error::ServerNameChanged> {
|
||||||
use crate::error::ServerNameChanged as Error;
|
use crate::error::ServerNameChanged as Error;
|
||||||
|
|
||||||
if services()
|
let config = &*services().globals.config.server_name;
|
||||||
|
|
||||||
|
let opt_saved =
|
||||||
|
self.saved_server_name().map_err(Error::ReadSavedServerName)?;
|
||||||
|
|
||||||
|
// Check against saved server name
|
||||||
|
if let Some(saved) = opt_saved {
|
||||||
|
if saved == config {
|
||||||
|
return Ok(());
|
||||||
|
}
|
||||||
|
|
||||||
|
return Err(Error::Renamed(saved.clone(), config.to_owned()));
|
||||||
|
}
|
||||||
|
|
||||||
|
let non_zero_users = services()
|
||||||
.users
|
.users
|
||||||
.count()
|
.count()
|
||||||
.map(|x| x > 0)
|
.map(|x| x > 0)
|
||||||
.map_err(Error::NonZeroUsers)?
|
.map_err(Error::NonZeroUsers)?;
|
||||||
{
|
|
||||||
let admin_bot = self.admin_bot_user_id.as_ref();
|
let admin_bot_exists = services()
|
||||||
if !services()
|
.users
|
||||||
.users
|
.exists(&self.admin_bot_user_id)
|
||||||
.exists(admin_bot)
|
.map_err(Error::AdminBotExists)?;
|
||||||
.map_err(Error::AdminBotExists)?
|
|
||||||
{
|
// Fall back to checking against the admin bot user ID
|
||||||
return Err(Error::Renamed);
|
if non_zero_users && !admin_bot_exists {
|
||||||
}
|
return Err(Error::MissingAdminBot(self.admin_bot_user_id.clone()));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// If the server_name wasn't saved and the admin bot user ID check
|
||||||
|
// didn't fail, save the current server_name
|
||||||
|
services()
|
||||||
|
.globals
|
||||||
|
.save_server_name(config)
|
||||||
|
.map_err(Error::SaveServerName)?;
|
||||||
|
|
||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -621,6 +641,29 @@ impl Service {
|
||||||
self.shutdown.store(true, atomic::Ordering::Relaxed);
|
self.shutdown.store(true, atomic::Ordering::Relaxed);
|
||||||
self.rotate.fire();
|
self.rotate.fire();
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub(crate) fn save_server_name(
|
||||||
|
&self,
|
||||||
|
server_name: &ServerName,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.db.set_server_name(server_name)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Named this way to avoid conflicts with the existing `fn server_name`
|
||||||
|
pub(crate) fn saved_server_name(&self) -> Result<Option<OwnedServerName>> {
|
||||||
|
self.db.server_name()
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn save_admin_bot_localpart(
|
||||||
|
&self,
|
||||||
|
localpart: &str,
|
||||||
|
) -> Result<()> {
|
||||||
|
self.db.set_admin_bot_localpart(localpart)
|
||||||
|
}
|
||||||
|
|
||||||
|
pub(crate) fn saved_admin_bot_localpart(&self) -> Result<Option<String>> {
|
||||||
|
self.db.admin_bot_localpart()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
fn reqwest_client_builder(config: &Config) -> Result<reqwest::ClientBuilder> {
|
fn reqwest_client_builder(config: &Config) -> Result<reqwest::ClientBuilder> {
|
||||||
|
|
|
||||||
|
|
@ -8,7 +8,7 @@ use ruma::{
|
||||||
api::federation::discovery::{OldVerifyKey, ServerSigningKeys, VerifyKey},
|
api::federation::discovery::{OldVerifyKey, ServerSigningKeys, VerifyKey},
|
||||||
serde::Base64,
|
serde::Base64,
|
||||||
signatures::Ed25519KeyPair,
|
signatures::Ed25519KeyPair,
|
||||||
DeviceId, MilliSecondsSinceUnixEpoch, ServerName, UserId,
|
DeviceId, MilliSecondsSinceUnixEpoch, OwnedServerName, ServerName, UserId,
|
||||||
};
|
};
|
||||||
use serde::Deserialize;
|
use serde::Deserialize;
|
||||||
|
|
||||||
|
|
@ -117,4 +117,8 @@ pub(crate) trait Data: Send + Sync {
|
||||||
) -> Result<Option<SigningKeys>>;
|
) -> Result<Option<SigningKeys>>;
|
||||||
fn database_version(&self) -> Result<u64>;
|
fn database_version(&self) -> Result<u64>;
|
||||||
fn bump_database_version(&self, new_version: u64) -> Result<()>;
|
fn bump_database_version(&self, new_version: u64) -> Result<()>;
|
||||||
|
fn set_server_name(&self, server_name: &ServerName) -> Result<()>;
|
||||||
|
fn server_name(&self) -> Result<Option<OwnedServerName>>;
|
||||||
|
fn set_admin_bot_localpart(&self, localpart: &str) -> Result<()>;
|
||||||
|
fn admin_bot_localpart(&self) -> Result<Option<String>>;
|
||||||
}
|
}
|
||||||
|
|
|
||||||
|
|
@ -957,114 +957,14 @@ impl Service {
|
||||||
let (pdu, pdu_json) =
|
let (pdu, pdu_json) =
|
||||||
self.create_hash_and_sign_event(pdu_builder, sender, room_id)?;
|
self.create_hash_and_sign_event(pdu_builder, sender, room_id)?;
|
||||||
|
|
||||||
if let Some(admin_room) = services().admin.get_admin_room()? {
|
if services().admin.get_admin_room()?.is_some_and(|x| x == **room_id)
|
||||||
if admin_room == **room_id {
|
&& pdu.event_type() == &TimelineEventType::RoomEncryption
|
||||||
match pdu.event_type() {
|
{
|
||||||
TimelineEventType::RoomEncryption => {
|
warn!("Encryption is not allowed in the admins room");
|
||||||
warn!("Encryption is not allowed in the admins room");
|
return Err(Error::BadRequest(
|
||||||
return Err(Error::BadRequest(
|
ErrorKind::forbidden(),
|
||||||
ErrorKind::forbidden(),
|
"Encryption is not allowed in the admins room.",
|
||||||
"Encryption is not allowed in the admins room.",
|
));
|
||||||
));
|
|
||||||
}
|
|
||||||
TimelineEventType::RoomMember => {
|
|
||||||
#[derive(Deserialize)]
|
|
||||||
struct ExtractMembership {
|
|
||||||
membership: MembershipState,
|
|
||||||
}
|
|
||||||
|
|
||||||
let target = pdu
|
|
||||||
.state_key()
|
|
||||||
.filter(|v| v.starts_with('@'))
|
|
||||||
.unwrap_or(sender.as_str());
|
|
||||||
let server_name = services().globals.server_name();
|
|
||||||
let server_user = format!(
|
|
||||||
"@{}:{server_name}",
|
|
||||||
if services().globals.config.conduit_compat {
|
|
||||||
"conduit"
|
|
||||||
} else {
|
|
||||||
"grapevine"
|
|
||||||
},
|
|
||||||
);
|
|
||||||
let content =
|
|
||||||
serde_json::from_str::<ExtractMembership>(
|
|
||||||
pdu.content.get(),
|
|
||||||
)
|
|
||||||
.map_err(|_| {
|
|
||||||
Error::bad_database("Invalid content in pdu.")
|
|
||||||
})?;
|
|
||||||
|
|
||||||
if content.membership == MembershipState::Leave {
|
|
||||||
if target == server_user {
|
|
||||||
warn!(
|
|
||||||
"Grapevine user cannot leave from admins \
|
|
||||||
room"
|
|
||||||
);
|
|
||||||
return Err(Error::BadRequest(
|
|
||||||
ErrorKind::forbidden(),
|
|
||||||
"Grapevine user cannot leave from admins \
|
|
||||||
room.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let count = services()
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.room_members(room_id)
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.filter(|m| m.server_name() == server_name)
|
|
||||||
.filter(|m| m != target)
|
|
||||||
.count();
|
|
||||||
if count < 2 {
|
|
||||||
warn!(
|
|
||||||
"Last admin cannot leave from admins room"
|
|
||||||
);
|
|
||||||
return Err(Error::BadRequest(
|
|
||||||
ErrorKind::forbidden(),
|
|
||||||
"Last admin cannot leave from admins room.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if content.membership == MembershipState::Ban
|
|
||||||
&& pdu.state_key().is_some()
|
|
||||||
{
|
|
||||||
if target == server_user {
|
|
||||||
warn!(
|
|
||||||
"Grapevine user cannot be banned in \
|
|
||||||
admins room"
|
|
||||||
);
|
|
||||||
return Err(Error::BadRequest(
|
|
||||||
ErrorKind::forbidden(),
|
|
||||||
"Grapevine user cannot be banned in \
|
|
||||||
admins room.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
|
|
||||||
let count = services()
|
|
||||||
.rooms
|
|
||||||
.state_cache
|
|
||||||
.room_members(room_id)
|
|
||||||
.filter_map(Result::ok)
|
|
||||||
.filter(|m| m.server_name() == server_name)
|
|
||||||
.filter(|m| m != target)
|
|
||||||
.count();
|
|
||||||
if count < 2 {
|
|
||||||
warn!(
|
|
||||||
"Last admin cannot be banned in admins \
|
|
||||||
room"
|
|
||||||
);
|
|
||||||
return Err(Error::BadRequest(
|
|
||||||
ErrorKind::forbidden(),
|
|
||||||
"Last admin cannot be banned in admins \
|
|
||||||
room.",
|
|
||||||
));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => {}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If redaction event is not authorized, do not append it to the
|
// If redaction event is not authorized, do not append it to the
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue