allow configuring the admin bot localpart

This commit is contained in:
Charles Hall 2024-09-27 10:51:29 -07:00
parent 9589382cb8
commit bb52753a92
No known key found for this signature in database
GPG key ID: 7B8E0645816E07CF
5 changed files with 279 additions and 9 deletions

View file

@ -10,7 +10,7 @@ use once_cell::sync::Lazy;
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};
@ -73,6 +73,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>,
} }
@ -451,5 +452,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)
} }

View file

@ -1089,6 +1089,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

View file

@ -136,6 +136,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 +191,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),
}

View file

@ -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,
@ -1528,6 +1528,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

View file

@ -234,14 +234,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))