fix: avoid panics when admin room is not available

Co-authored-by: strawberry <strawberry@puppygock.gay>
Signed-off-by: strawberry <strawberry@puppygock.gay>
This commit is contained in:
Matthias Ahouansou 2024-03-16 14:11:03 -04:00 committed by June
parent 5473631e1d
commit c48535ef32
3 changed files with 233 additions and 241 deletions

View file

@ -275,10 +275,14 @@ pub async fn register_route(body: Ruma<register::v3::Request>) -> Result<registe
// If this is the first real user, grant them admin privileges except for guest // 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, @conduit:servername, is generated first
if services().users.count()? == 2 && !is_guest { if !is_guest {
services().admin.make_user_admin(&user_id, displayname).await?; if let Some(admin_room) = services().admin.get_admin_room()? {
if services().rooms.state_cache.room_joined_count(&admin_room)? == Some(1) {
services().admin.make_user_admin(&user_id, displayname).await?;
warn!("Granting {} admin privileges as the first user", user_id); warn!("Granting {} admin privileges as the first user", user_id);
}
}
} }
Ok(register::v3::Response { Ok(register::v3::Response {

View file

@ -473,60 +473,50 @@ impl Service {
let conduit_user = UserId::parse(format!("@conduit:{}", services().globals.server_name())) let conduit_user = UserId::parse(format!("@conduit:{}", services().globals.server_name()))
.expect("@conduit:server_name is valid"); .expect("@conduit:server_name is valid");
let conduit_room = services() if let Ok(Some(conduit_room)) = services().admin.get_admin_room() {
.rooms loop {
.alias tokio::select! {
.resolve_local_alias( Some(event) = receiver.recv() => {
format!("#admins:{}", services().globals.server_name()) let (mut message_content, reply) = match event {
.as_str() AdminRoomEvent::SendMessage(content) => (content, None),
.try_into() AdminRoomEvent::ProcessMessage(room_message, reply_id) => {
.expect("#admins:server_name is a valid room alias"), (self.process_admin_message(room_message).await, Some(reply_id))
) }
.expect("Database data for admin room alias must be valid") };
.expect("Admin room must exist");
loop { let mutex_state = Arc::clone(
tokio::select! { services().globals
Some(event) = receiver.recv() => { .roomid_mutex_state
let (mut message_content, reply) = match event { .write()
AdminRoomEvent::SendMessage(content) => (content, None), .await
AdminRoomEvent::ProcessMessage(room_message, reply_id) => { .entry(conduit_room.clone())
(self.process_admin_message(room_message).await, Some(reply_id)) .or_default(),
);
let state_lock = mutex_state.lock().await;
if let Some(reply) = reply {
message_content.relates_to = Some(Reply { in_reply_to: InReplyTo { event_id: reply.into() } });
} }
};
let mutex_state = Arc::clone( services().rooms.timeline.build_and_append_pdu(
services().globals PduBuilder {
.roomid_mutex_state event_type: TimelineEventType::RoomMessage,
.write() content: to_raw_value(&message_content)
.await .expect("event is valid, we just created it"),
.entry(conduit_room.clone()) unsigned: None,
.or_default(), state_key: None,
); redacts: None,
},
&conduit_user,
&conduit_room,
&state_lock)
.await
.unwrap();
let state_lock = mutex_state.lock().await;
if let Some(reply) = reply { drop(state_lock);
message_content.relates_to = Some(Reply { in_reply_to: InReplyTo { event_id: reply.into() } });
} }
services().rooms.timeline.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomMessage,
content: to_raw_value(&message_content)
.expect("event is valid, we just created it"),
unsigned: None,
state_key: None,
redacts: None,
},
&conduit_user,
&conduit_room,
&state_lock)
.await
.unwrap();
drop(state_lock);
} }
} }
} }
@ -1111,14 +1101,13 @@ impl Service {
format!("#admins:{}", services().globals.server_name()) format!("#admins:{}", services().globals.server_name())
.try_into() .try_into()
.expect("#admins:server_name is a valid alias name"); .expect("#admins:server_name is a valid alias name");
let admin_room_id = services()
.rooms
.alias
.resolve_local_alias(&admin_room_alias)?
.expect("Admin room must exist");
if room.to_string().eq(&admin_room_id) || room.to_string().eq(&admin_room_alias) { if let Some(admin_room_id) = services().admin.get_admin_room()? {
return Ok(RoomMessageEventContent::text_plain("Not allowed to ban the admin room.")); if room.to_string().eq(&admin_room_id) || room.to_string().eq(&admin_room_alias) {
return Ok(RoomMessageEventContent::text_plain(
"Not allowed to ban the admin room.",
));
}
} }
let room_id = if room.is_room_id() { let room_id = if room.is_room_id() {
@ -1282,23 +1271,15 @@ impl Service {
let mut room_ban_count = 0; let mut room_ban_count = 0;
let mut room_ids: Vec<&RoomId> = Vec::new(); let mut room_ids: Vec<&RoomId> = Vec::new();
let admin_room_alias: Box<RoomAliasId> =
format!("#admins:{}", services().globals.server_name())
.try_into()
.expect("#admins:server_name is a valid alias name");
let admin_room_id = services()
.rooms
.alias
.resolve_local_alias(&admin_room_alias)?
.expect("Admin room must exist");
for &room_id in &rooms_s { for &room_id in &rooms_s {
match <&RoomId>::try_from(room_id) { match <&RoomId>::try_from(room_id) {
Ok(owned_room_id) => { Ok(owned_room_id) => {
// silently ignore deleting admin room // silently ignore deleting admin room
if owned_room_id.eq(&admin_room_id) { if let Some(admin_room_id) = services().admin.get_admin_room()? {
info!("User specified admin room in bulk ban list, ignoring"); if owned_room_id.eq(&admin_room_id) {
continue; info!("User specified admin room in bulk ban list, ignoring");
continue;
}
} }
room_ids.push(owned_room_id); room_ids.push(owned_room_id);
@ -2443,105 +2424,113 @@ impl Service {
Ok(()) Ok(())
} }
/// Gets the room ID of the admin room
///
/// Errors are propagated from the database, and will have None if there is
/// no admin room
pub(crate) fn get_admin_room(&self) -> Result<Option<OwnedRoomId>> {
let admin_room_alias: Box<RoomAliasId> = format!("#admins:{}", services().globals.server_name())
.try_into()
.expect("#admins:server_name is a valid alias name");
services().rooms.alias.resolve_local_alias(&admin_room_alias)
}
/// Invite the user to the conduit admin room. /// Invite the user to the conduit admin room.
/// ///
/// In conduit, this is equivalent to granting admin privileges. /// In conduit, this is equivalent to granting admin privileges.
pub(crate) async fn make_user_admin(&self, user_id: &UserId, displayname: String) -> Result<()> { pub(crate) async fn make_user_admin(&self, user_id: &UserId, displayname: String) -> Result<()> {
let admin_room_alias: Box<RoomAliasId> = format!("#admins:{}", services().globals.server_name()) if let Some(room_id) = services().admin.get_admin_room()? {
.try_into() let mutex_state =
.expect("#admins:server_name is a valid alias name"); Arc::clone(services().globals.roomid_mutex_state.write().await.entry(room_id.clone()).or_default());
let room_id = services().rooms.alias.resolve_local_alias(&admin_room_alias)?.expect("Admin room must exist"); let state_lock = mutex_state.lock().await;
let mutex_state = // Use the server user to grant the new admin's power level
Arc::clone(services().globals.roomid_mutex_state.write().await.entry(room_id.clone()).or_default()); let conduit_user = UserId::parse_with_server_name("conduit", services().globals.server_name())
let state_lock = mutex_state.lock().await; .expect("@conduit:server_name is valid");
// Use the server user to grant the new admin's power level // Invite and join the real user
let conduit_user = UserId::parse_with_server_name("conduit", services().globals.server_name()) services()
.expect("@conduit:server_name is valid"); .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(user_id.to_string()),
redacts: None,
},
&conduit_user,
&room_id,
&state_lock,
)
.await?;
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: Some(displayname),
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(user_id.to_string()),
redacts: None,
},
user_id,
&room_id,
&state_lock,
)
.await?;
// Invite and join the real user // Set power level
services() let mut users = BTreeMap::new();
.rooms users.insert(conduit_user.clone(), 100.into());
.timeline users.insert(user_id.to_owned(), 100.into());
.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(user_id.to_string()),
redacts: None,
},
&conduit_user,
&room_id,
&state_lock,
)
.await?;
services()
.rooms
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomMember,
content: to_raw_value(&RoomMemberEventContent {
membership: MembershipState::Join,
displayname: Some(displayname),
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(user_id.to_string()),
redacts: None,
},
user_id,
&room_id,
&state_lock,
)
.await?;
// Set power level services()
let mut users = BTreeMap::new(); .rooms
users.insert(conduit_user.clone(), 100.into()); .timeline
users.insert(user_id.to_owned(), 100.into()); .build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&RoomPowerLevelsEventContent {
users,
..Default::default()
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
redacts: None,
},
&conduit_user,
&room_id,
&state_lock,
)
.await?;
services() // Send welcome message
.rooms services().rooms.timeline.build_and_append_pdu(
.timeline
.build_and_append_pdu(
PduBuilder {
event_type: TimelineEventType::RoomPowerLevels,
content: to_raw_value(&RoomPowerLevelsEventContent {
users,
..Default::default()
})
.expect("event is valid, we just created it"),
unsigned: None,
state_key: Some("".to_owned()),
redacts: None,
},
&conduit_user,
&room_id,
&state_lock,
)
.await?;
// Send welcome message
services().rooms.timeline.build_and_append_pdu(
PduBuilder { PduBuilder {
event_type: TimelineEventType::RoomMessage, event_type: TimelineEventType::RoomMessage,
content: to_raw_value(&RoomMessageEventContent::text_html( content: to_raw_value(&RoomMessageEventContent::text_html(
@ -2558,7 +2547,10 @@ impl Service {
&state_lock, &state_lock,
).await?; ).await?;
Ok(()) Ok(())
} else {
Ok(())
}
} }
} }

View file

@ -26,7 +26,7 @@ use ruma::{
state_res, state_res,
state_res::{Event, RoomVersion}, state_res::{Event, RoomVersion},
uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, OwnedServerName, uint, user_id, CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, OwnedRoomId, OwnedServerName,
RoomAliasId, RoomId, RoomVersionId, ServerName, UserId, RoomId, RoomVersionId, ServerName, UserId,
}; };
use serde::Deserialize; use serde::Deserialize;
use serde_json::value::{to_raw_value, RawValue as RawJsonValue}; use serde_json::value::{to_raw_value, RawValue as RawJsonValue};
@ -461,10 +461,6 @@ impl Service {
if let Some(body) = content.body { if let Some(body) = content.body {
services().rooms.search.index_pdu(shortroomid, &pdu_id, &body)?; services().rooms.search.index_pdu(shortroomid, &pdu_id, &body)?;
let admin_room = services().rooms.alias.resolve_local_alias(
<&RoomAliasId>::try_from(format!("#admins:{}", services().globals.server_name()).as_str())
.expect("#admins:server_name is a valid room alias"),
)?;
let server_user = format!("@conduit:{}", services().globals.server_name()); let server_user = format!("@conduit:{}", services().globals.server_name());
let to_conduit = body.starts_with(&format!("{server_user}: ")) let to_conduit = body.starts_with(&format!("{server_user}: "))
@ -477,8 +473,10 @@ impl Service {
// the administrator can execute commands as conduit // the administrator can execute commands as conduit
let from_conduit = pdu.sender == server_user && services().globals.emergency_password().is_none(); let from_conduit = pdu.sender == server_user && services().globals.emergency_password().is_none();
if to_conduit && !from_conduit && admin_room.as_ref() == Some(&pdu.room_id) { if let Some(admin_room) = services().admin.get_admin_room()? {
services().admin.process_message(body, pdu.event_id.clone()); if to_conduit && !from_conduit && admin_room == pdu.room_id {
services().admin.process_message(body, pdu.event_id.clone());
}
} }
} }
}, },
@ -720,84 +718,82 @@ impl Service {
) -> Result<Arc<EventId>> { ) -> Result<Arc<EventId>> {
let (pdu, pdu_json) = self.create_hash_and_sign_event(pdu_builder, sender, room_id, state_lock)?; let (pdu, pdu_json) = self.create_hash_and_sign_event(pdu_builder, sender, room_id, state_lock)?;
let admin_room = services().rooms.alias.resolve_local_alias( if let Some(admin_room) = services().admin.get_admin_room()? {
<&RoomAliasId>::try_from(format!("#admins:{}", services().globals.server_name()).as_str()) if admin_room == room_id {
.expect("#admins:server_name is a valid room alias"), match pdu.event_type() {
)?; TimelineEventType::RoomEncryption => {
if admin_room.filter(|v| v == room_id).is_some() { warn!("Encryption is not allowed in the admins room");
match pdu.event_type() { return Err(Error::BadRequest(
TimelineEventType::RoomEncryption => { ErrorKind::Forbidden,
warn!("Encryption is not allowed in the admins room"); "Encryption is not allowed in the admins room.",
return Err(Error::BadRequest( ));
ErrorKind::Forbidden, },
"Encryption is not allowed in the admins room.", TimelineEventType::RoomMember => {
)); #[derive(Deserialize)]
}, struct ExtractMembership {
TimelineEventType::RoomMember => { membership: MembershipState,
#[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!("@conduit:{server_name}");
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!("Conduit user cannot leave from admins room");
return Err(Error::BadRequest(
ErrorKind::Forbidden,
"Conduit user cannot leave from admins room.",
));
} }
let count = services() let target = pdu.state_key().filter(|v| v.starts_with('@')).unwrap_or(sender.as_str());
.rooms let server_name = services().globals.server_name();
.state_cache let server_user = format!("@conduit:{server_name}");
.room_members(room_id) let content = serde_json::from_str::<ExtractMembership>(pdu.content.get())
.filter_map(std::result::Result::ok) .map_err(|_| Error::bad_database("Invalid content in pdu."))?;
.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 content.membership == MembershipState::Leave {
if target == server_user { if target == server_user {
warn!("Conduit user cannot be banned in admins room"); warn!("Conduit user cannot leave from admins room");
return Err(Error::BadRequest( return Err(Error::BadRequest(
ErrorKind::Forbidden, ErrorKind::Forbidden,
"Conduit user cannot be banned in admins room.", "Conduit user cannot leave from admins room.",
)); ));
}
let count = services()
.rooms
.state_cache
.room_members(room_id)
.filter_map(std::result::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.",
));
}
} }
let count = services() if content.membership == MembershipState::Ban && pdu.state_key().is_some() {
.rooms if target == server_user {
.state_cache warn!("Conduit user cannot be banned in admins room");
.room_members(room_id) return Err(Error::BadRequest(
.filter_map(std::result::Result::ok) ErrorKind::Forbidden,
.filter(|m| m.server_name() == server_name) "Conduit user cannot be banned in admins room.",
.filter(|m| m != target) ));
.count(); }
if count < 2 {
warn!("Last admin cannot be banned in admins room"); let count = services()
return Err(Error::BadRequest( .rooms
ErrorKind::Forbidden, .state_cache
"Last admin cannot be banned in admins room.", .room_members(room_id)
)); .filter_map(std::result::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.",
));
}
} }
} },
}, _ => {},
_ => {}, }
} }
} }