Compare commits
20 commits
main
...
backport/v
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
bb6c513ee8 | ||
|
|
5241f89ebc | ||
|
|
e2349de270 | ||
|
|
7c51688251 | ||
|
|
80399a0be0 | ||
|
|
bdcd08dd01 | ||
|
|
55df764af2 | ||
|
|
6d2cac66b7 | ||
|
|
22835ae8ec | ||
|
|
cc4852076f | ||
|
|
94507285d8 | ||
|
|
138bbf23df | ||
|
|
3aaecb0c5c | ||
|
|
1c430c0fd4 | ||
|
|
93966e4d78 | ||
|
|
fa3424eb2e | ||
|
|
9092e5f69f | ||
|
|
bbd1316b80 | ||
|
|
209601a8ea | ||
|
|
4182a67df2 |
17 changed files with 684 additions and 257 deletions
|
|
@ -23,6 +23,8 @@ on:
|
|||
- "renovate.json"
|
||||
- "pkg/**"
|
||||
- "docs/**"
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
# Allows you to run this workflow manually from the Actions tab
|
||||
workflow_dispatch:
|
||||
|
||||
|
|
|
|||
24
Cargo.lock
generated
24
Cargo.lock
generated
|
|
@ -875,7 +875,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"conduwuit_admin",
|
||||
|
|
@ -907,7 +907,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_admin"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"conduwuit_api",
|
||||
|
|
@ -929,7 +929,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_api"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"axum",
|
||||
|
|
@ -962,14 +962,14 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_build_metadata"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
dependencies = [
|
||||
"built 0.8.0",
|
||||
]
|
||||
|
||||
[[package]]
|
||||
name = "conduwuit_core"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
dependencies = [
|
||||
"argon2",
|
||||
"arrayvec",
|
||||
|
|
@ -1030,7 +1030,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_database"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
dependencies = [
|
||||
"async-channel",
|
||||
"conduwuit_core",
|
||||
|
|
@ -1049,7 +1049,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_macros"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
dependencies = [
|
||||
"itertools 0.14.0",
|
||||
"proc-macro2",
|
||||
|
|
@ -1059,7 +1059,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_router"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
dependencies = [
|
||||
"axum",
|
||||
"axum-client-ip",
|
||||
|
|
@ -1094,7 +1094,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_service"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
dependencies = [
|
||||
"async-trait",
|
||||
"base64 0.22.1",
|
||||
|
|
@ -1134,7 +1134,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "conduwuit_web"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
dependencies = [
|
||||
"askama",
|
||||
"axum",
|
||||
|
|
@ -6565,7 +6565,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "xtask"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
dependencies = [
|
||||
"clap",
|
||||
"serde",
|
||||
|
|
@ -6574,7 +6574,7 @@ dependencies = [
|
|||
|
||||
[[package]]
|
||||
name = "xtask-generate-commands"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
dependencies = [
|
||||
"clap-markdown",
|
||||
"clap_builder",
|
||||
|
|
|
|||
|
|
@ -21,7 +21,7 @@ license = "Apache-2.0"
|
|||
readme = "README.md"
|
||||
repository = "https://forgejo.ellis.link/continuwuation/continuwuity"
|
||||
rust-version = "1.86.0"
|
||||
version = "0.5.0-rc.8"
|
||||
version = "0.5.0-rc.8.1"
|
||||
|
||||
[workspace.metadata.crane]
|
||||
name = "conduwuit"
|
||||
|
|
|
|||
4
docs/static/announcements.json
vendored
4
docs/static/announcements.json
vendored
|
|
@ -8,6 +8,10 @@
|
|||
{
|
||||
"id": 3,
|
||||
"message": "_taps microphone_ The Continuwuity 0.5.0-rc.7 release is now available, and it's better than ever! **177 commits**, **35 pull requests**, **11 contributors,** and a lot of new stuff!\n\nFor highlights, we've got:\n\n* 🕵️ Full Policy Server support to fight spam!\n* 🚀 Smarter room & space upgrades.\n* 🚫 User suspension tools for better moderation.\n* 🤖 reCaptcha support for safer open registration.\n* 🔍 Ability to disable read receipts & typing indicators.\n* ⚡ Sweeping performance improvements!\n\nGet the [full changelog and downloads on our Forgejo](https://forgejo.ellis.link/continuwuation/continuwuity/releases/tag/v0.5.0-rc.7) - and make sure you're in the [Announcements room](https://matrix.to/#/!releases:continuwuity.org/$hN9z6L2_dTAlPxFLAoXVfo_g8DyYXu4cpvWsSrWhmB0) to get stuff like this sooner."
|
||||
},
|
||||
{
|
||||
"id": 5,
|
||||
"message": "It's a bird! It's a plane! No, it's 0.5.0-rc.8.1!\n\nThis is a minor bugfix update to the rc8 which backports some important fixes from the latest main branch. If you still haven't updated to rc8, you should skip to main. Otherwise, you should upgrade to this bugfix release as soon as possible.\n\nBugfixes backported to this version:\n\n- Resolved several issues with state resolution v2.1 (room version 12)\n- Fixed issues with the `restricted` and `knock_restricted` join rules that would sometimes incorrectly disallow a valid join\n- Fixed the automatic support contact listing being a no-op\n- Fixed upgrading pre-v12 rooms to v12 rooms\n- Fixed policy servers sending the incorrect JSON objects (resulted in false positives)\n- Fixed debug build panic during MSC4133 migration\n\nIt is recommended, if you can and are comfortable with doing so, following updates to the main branch - we're in the run up to the full 0.5.0 release, and more and more bugfixes and new features are being pushed constantly. Please don't forget to join [#announcements:continuwuity.org](https://matrix.to/#/#announcements:continuwuity.org) to receive this news faster and be alerted to other important updates!"
|
||||
}
|
||||
]
|
||||
}
|
||||
|
|
|
|||
|
|
@ -2,7 +2,7 @@ use std::cmp::max;
|
|||
|
||||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Error, Event, Result, debug, err, info,
|
||||
Err, Error, Event, Result, RoomVersion, debug, err, info,
|
||||
matrix::{StateKey, pdu::PduBuilder},
|
||||
};
|
||||
use futures::{FutureExt, StreamExt};
|
||||
|
|
@ -68,37 +68,76 @@ pub(crate) async fn upgrade_room_route(
|
|||
return Err!(Request(UserSuspended("You cannot perform this action while suspended.")));
|
||||
}
|
||||
|
||||
// First, check if the user has permission to upgrade the room (send tombstone
|
||||
// event)
|
||||
let old_room_state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
|
||||
// Check tombstone permission by attempting to create (but not send) the event
|
||||
// Note that this does internally call the policy server with a fake room ID,
|
||||
// which may not be good?
|
||||
let tombstone_test_result = services
|
||||
.rooms
|
||||
.timeline
|
||||
.create_hash_and_sign_event(
|
||||
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: RoomId::new(services.globals.server_name()),
|
||||
}),
|
||||
sender_user,
|
||||
Some(&body.room_id),
|
||||
&old_room_state_lock,
|
||||
)
|
||||
.await;
|
||||
|
||||
if let Err(_e) = tombstone_test_result {
|
||||
return Err!(Request(Forbidden("User does not have permission to upgrade this room.")));
|
||||
}
|
||||
|
||||
drop(old_room_state_lock);
|
||||
|
||||
// Create a replacement room
|
||||
let replacement_room = RoomId::new(services.globals.server_name());
|
||||
let room_features = RoomVersion::new(&body.new_version)?;
|
||||
let replacement_room: Option<&RoomId> = if room_features.room_ids_as_hashes {
|
||||
None
|
||||
} else {
|
||||
Some(&RoomId::new(services.globals.server_name()))
|
||||
};
|
||||
let replacement_room_tmp = match replacement_room {
|
||||
| Some(v) => v,
|
||||
| None => &RoomId::new(services.globals.server_name()),
|
||||
};
|
||||
|
||||
let _short_id = services
|
||||
.rooms
|
||||
.short
|
||||
.get_or_create_shortroomid(&replacement_room)
|
||||
.get_or_create_shortroomid(replacement_room_tmp)
|
||||
.await;
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
|
||||
// Send a m.room.tombstone event to the old room to indicate that it is not
|
||||
// intended to be used any further Fail if the sender does not have the required
|
||||
// permissions
|
||||
let tombstone_event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: replacement_room.clone(),
|
||||
}),
|
||||
sender_user,
|
||||
Some(&body.room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Change lock to replacement room
|
||||
drop(state_lock);
|
||||
let state_lock = services.rooms.state.mutex.lock(&replacement_room).await;
|
||||
// For pre-v12 rooms, send tombstone before creating replacement room
|
||||
let tombstone_event_id = if !room_features.room_ids_as_hashes {
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
// Send a m.room.tombstone event to the old room to indicate that it is not
|
||||
// intended to be used any further
|
||||
let tombstone_event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: replacement_room.unwrap().to_owned(),
|
||||
}),
|
||||
sender_user,
|
||||
Some(&body.room_id),
|
||||
&state_lock,
|
||||
)
|
||||
.await?;
|
||||
// Change lock to replacement room
|
||||
drop(state_lock);
|
||||
Some(tombstone_event_id)
|
||||
} else {
|
||||
None
|
||||
};
|
||||
let state_lock = services.rooms.state.mutex.lock(replacement_room_tmp).await;
|
||||
|
||||
// Get the old room creation event
|
||||
let mut create_event_content: CanonicalJsonObject = services
|
||||
|
|
@ -111,7 +150,7 @@ pub(crate) async fn upgrade_room_route(
|
|||
// Use the m.room.tombstone event as the predecessor
|
||||
let predecessor = Some(ruma::events::room::create::PreviousRoom::new(
|
||||
body.room_id.clone(),
|
||||
Some(tombstone_event_id),
|
||||
tombstone_event_id,
|
||||
));
|
||||
|
||||
// Send a m.room.create event containing a predecessor field and the applicable
|
||||
|
|
@ -132,6 +171,7 @@ pub(crate) async fn upgrade_room_route(
|
|||
// "creator" key no longer exists in V11 rooms
|
||||
create_event_content.remove("creator");
|
||||
},
|
||||
// TODO(hydra): additional_creators
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -159,7 +199,7 @@ pub(crate) async fn upgrade_room_route(
|
|||
return Err(Error::BadRequest(ErrorKind::BadJson, "Error forming creation event"));
|
||||
}
|
||||
|
||||
services
|
||||
let create_event_id = services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
|
|
@ -173,11 +213,18 @@ pub(crate) async fn upgrade_room_route(
|
|||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
Some(&replacement_room),
|
||||
replacement_room,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
.await?;
|
||||
let create_id = create_event_id.as_str().replace('$', "!");
|
||||
let (replacement_room, state_lock) = if room_features.room_ids_as_hashes {
|
||||
let parsed_room_id = RoomId::parse(&create_id)?;
|
||||
(Some(parsed_room_id), services.rooms.state.mutex.lock(parsed_room_id).await)
|
||||
} else {
|
||||
(replacement_room, state_lock)
|
||||
};
|
||||
|
||||
// Join the new room
|
||||
services
|
||||
|
|
@ -204,7 +251,7 @@ pub(crate) async fn upgrade_room_route(
|
|||
timestamp: None,
|
||||
},
|
||||
sender_user,
|
||||
Some(&replacement_room),
|
||||
replacement_room,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
|
|
@ -243,7 +290,7 @@ pub(crate) async fn upgrade_room_route(
|
|||
..Default::default()
|
||||
},
|
||||
sender_user,
|
||||
Some(&replacement_room),
|
||||
replacement_room,
|
||||
&state_lock,
|
||||
)
|
||||
.boxed()
|
||||
|
|
@ -268,7 +315,7 @@ pub(crate) async fn upgrade_room_route(
|
|||
services
|
||||
.rooms
|
||||
.alias
|
||||
.set_alias(alias, &replacement_room, sender_user)?;
|
||||
.set_alias(alias, replacement_room.unwrap(), sender_user)?;
|
||||
}
|
||||
|
||||
// Get the old room power levels
|
||||
|
|
@ -310,6 +357,27 @@ pub(crate) async fn upgrade_room_route(
|
|||
|
||||
drop(state_lock);
|
||||
|
||||
// For v12 rooms, send tombstone AFTER creating replacement room
|
||||
if room_features.room_ids_as_hashes {
|
||||
let old_room_state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
// For v12 rooms, no event reference in predecessor due to cyclic dependency -
|
||||
// could best effort one maybe?
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
.build_and_append_pdu(
|
||||
PduBuilder::state(StateKey::new(), &RoomTombstoneEventContent {
|
||||
body: "This room has been replaced".to_owned(),
|
||||
replacement_room: replacement_room.unwrap().to_owned(),
|
||||
}),
|
||||
sender_user,
|
||||
Some(&body.room_id),
|
||||
&old_room_state_lock,
|
||||
)
|
||||
.await?;
|
||||
drop(old_room_state_lock);
|
||||
}
|
||||
|
||||
// Check if the old room has a space parent, and if so, whether we should update
|
||||
// it (m.space.parent, room_id)
|
||||
let parents = services
|
||||
|
|
@ -334,8 +402,9 @@ pub(crate) async fn upgrade_room_route(
|
|||
continue;
|
||||
};
|
||||
debug!(
|
||||
"Updating space {space_id} child event for room {} to {replacement_room}",
|
||||
&body.room_id
|
||||
"Updating space {space_id} child event for room {} to {}",
|
||||
&body.room_id,
|
||||
replacement_room.unwrap()
|
||||
);
|
||||
// First, drop the space's child event
|
||||
let state_lock = services.rooms.state.mutex.lock(space_id).await;
|
||||
|
|
@ -359,7 +428,10 @@ pub(crate) async fn upgrade_room_route(
|
|||
.await
|
||||
.ok();
|
||||
// Now, add a new child event for the replacement room
|
||||
debug!("Adding space child event for room {replacement_room} in space {space_id}");
|
||||
debug!(
|
||||
"Adding space child event for room {} in space {space_id}",
|
||||
replacement_room.unwrap()
|
||||
);
|
||||
services
|
||||
.rooms
|
||||
.timeline
|
||||
|
|
@ -372,7 +444,7 @@ pub(crate) async fn upgrade_room_route(
|
|||
suggested: child.suggested,
|
||||
})
|
||||
.expect("event is valid, we just created it"),
|
||||
state_key: Some(replacement_room.as_str().into()),
|
||||
state_key: Some(replacement_room.unwrap().as_str().into()),
|
||||
..Default::default()
|
||||
},
|
||||
sender_user,
|
||||
|
|
@ -383,12 +455,15 @@ pub(crate) async fn upgrade_room_route(
|
|||
.await
|
||||
.ok();
|
||||
debug!(
|
||||
"Finished updating space {space_id} child event for room {} to {replacement_room}",
|
||||
&body.room_id
|
||||
"Finished updating space {space_id} child event for room {} to {}",
|
||||
&body.room_id,
|
||||
replacement_room.unwrap()
|
||||
);
|
||||
drop(state_lock);
|
||||
}
|
||||
|
||||
// Return the replacement room id
|
||||
Ok(upgrade_room::v3::Response { replacement_room })
|
||||
Ok(upgrade_room::v3::Response {
|
||||
replacement_room: replacement_room.unwrap().to_owned(),
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -59,6 +59,7 @@ pub(crate) async fn get_supported_versions_route(
|
|||
("us.cloke.msc4175".to_owned(), true), /* Profile field for user time zone (https://github.com/matrix-org/matrix-spec-proposals/pull/4175) */
|
||||
("org.matrix.simplified_msc3575".to_owned(), true), /* Simplified Sliding sync (https://github.com/matrix-org/matrix-spec-proposals/pull/4186) */
|
||||
("uk.timedout.msc4323".to_owned(), true), /* agnostic suspend (https://github.com/matrix-org/matrix-spec-proposals/pull/4323) */
|
||||
("org.matrix.msc4155".to_owned(), true), /* invite filtering (https://github.com/matrix-org/matrix-spec-proposals/pull/4155) */
|
||||
]),
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -78,7 +78,7 @@ pub(crate) async fn well_known_support(
|
|||
while let Some(user_id) = stream.next().await {
|
||||
// Skip server user
|
||||
if *user_id == services.globals.server_user {
|
||||
break;
|
||||
continue;
|
||||
}
|
||||
contacts.push(Contact {
|
||||
role: role_value.clone(),
|
||||
|
|
|
|||
|
|
@ -1,6 +1,6 @@
|
|||
use axum::extract::State;
|
||||
use conduwuit::{
|
||||
Err, Error, Result, debug_info, matrix::pdu::PduBuilder, utils::IterStream, warn,
|
||||
Err, Error, Result, debug_info, info, matrix::pdu::PduBuilder, utils::IterStream, warn,
|
||||
};
|
||||
use conduwuit_service::Services;
|
||||
use futures::StreamExt;
|
||||
|
|
@ -22,6 +22,7 @@ use crate::Ruma;
|
|||
/// # `GET /_matrix/federation/v1/make_join/{roomId}/{userId}`
|
||||
///
|
||||
/// Creates a join template.
|
||||
#[tracing::instrument(skip_all, fields(room_id = %body.room_id, user_id = %body.user_id, origin = %body.origin()))]
|
||||
pub(crate) async fn create_join_event_template_route(
|
||||
State(services): State<crate::State>,
|
||||
body: Ruma<prepare_join_event::v1::Request>,
|
||||
|
|
@ -72,11 +73,16 @@ pub(crate) async fn create_join_event_template_route(
|
|||
}
|
||||
|
||||
let state_lock = services.rooms.state.mutex.lock(&body.room_id).await;
|
||||
|
||||
let is_invited = services
|
||||
.rooms
|
||||
.state_cache
|
||||
.is_invited(&body.user_id, &body.room_id)
|
||||
.await;
|
||||
let join_authorized_via_users_server: Option<OwnedUserId> = {
|
||||
use RoomVersionId::*;
|
||||
if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) {
|
||||
// room version does not support restricted join rules
|
||||
if matches!(room_version_id, V1 | V2 | V3 | V4 | V5 | V6 | V7) || is_invited {
|
||||
// room version does not support restricted join rules, or the user is currently
|
||||
// already invited
|
||||
None
|
||||
} else if user_can_perform_restricted_join(
|
||||
&services,
|
||||
|
|
@ -103,6 +109,10 @@ pub(crate) async fn create_join_event_template_route(
|
|||
.await
|
||||
.map(ToOwned::to_owned)
|
||||
else {
|
||||
info!(
|
||||
"No local user is able to authorize the join of {} into {}",
|
||||
&body.user_id, &body.room_id
|
||||
);
|
||||
return Err!(Request(UnableToGrantJoin(
|
||||
"No user on this server is able to assist in joining."
|
||||
)));
|
||||
|
|
@ -167,6 +177,7 @@ pub(crate) async fn user_can_perform_restricted_join(
|
|||
)
|
||||
.await
|
||||
else {
|
||||
// No join rules means there's nothing to authorise (defaults to invite)
|
||||
return Ok(false);
|
||||
};
|
||||
|
||||
|
|
|
|||
|
|
@ -200,11 +200,15 @@ where
|
|||
if incoming_event.room_id().is_some() {
|
||||
let Some(room_id_server_name) = incoming_event.room_id().unwrap().server_name()
|
||||
else {
|
||||
warn!("room ID has no servername");
|
||||
warn!("legacy room ID has no server name");
|
||||
return Ok(false);
|
||||
};
|
||||
if room_id_server_name != sender.server_name() {
|
||||
warn!("servername of room ID does not match servername of sender");
|
||||
warn!(
|
||||
expected = %sender.server_name(),
|
||||
received = %room_id_server_name,
|
||||
"server name of legacy room ID does not match server name of sender"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -215,12 +219,12 @@ where
|
|||
.room_version
|
||||
.is_some_and(|v| v.deserialize().is_err())
|
||||
{
|
||||
warn!("invalid room version found in m.room.create event");
|
||||
warn!("unsupported room version found in m.room.create event");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
if room_version.room_ids_as_hashes && incoming_event.room_id().is_some() {
|
||||
warn!("room create event incorrectly claims a room ID");
|
||||
warn!("room create event incorrectly claims to have a room ID when it should not");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
|
@ -229,7 +233,7 @@ where
|
|||
{
|
||||
// If content has no creator field, reject
|
||||
if content.creator.is_none() {
|
||||
warn!("no creator field found in m.room.create content");
|
||||
warn!("m.room.create event incorrectly omits 'creator' field");
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -282,16 +286,19 @@ where
|
|||
.room_version
|
||||
.is_some_and(|v| v.deserialize().is_err())
|
||||
{
|
||||
warn!("invalid room version found in m.room.create event");
|
||||
warn!(
|
||||
create_event_id = %room_create_event.event_id(),
|
||||
"unsupported room version found in m.room.create event"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
let expected_room_id = room_create_event.room_id_or_hash();
|
||||
|
||||
if incoming_event.room_id().unwrap() != expected_room_id {
|
||||
if incoming_event.room_id().expect("event must have a room ID") != expected_room_id {
|
||||
warn!(
|
||||
expected = %expected_room_id,
|
||||
received = %incoming_event.room_id().unwrap(),
|
||||
"room_id of incoming event ({}) does not match room_id of m.room.create event ({})",
|
||||
"room_id of incoming event ({}) does not match that of the m.room.create event ({})",
|
||||
incoming_event.room_id().unwrap(),
|
||||
expected_room_id,
|
||||
);
|
||||
|
|
@ -304,12 +311,15 @@ where
|
|||
.auth_events()
|
||||
.any(|id| id == room_create_event.event_id());
|
||||
if room_version.room_ids_as_hashes && claims_create_event {
|
||||
warn!("m.room.create event incorrectly found in auth events");
|
||||
warn!("event incorrectly references m.room.create event in auth events");
|
||||
return Ok(false);
|
||||
} else if !room_version.room_ids_as_hashes && !claims_create_event {
|
||||
// If the create event is not referenced in the event's auth events, and this is
|
||||
// a v11 room, reject
|
||||
warn!("no m.room.create event found in auth events");
|
||||
warn!(
|
||||
missing = %room_create_event.event_id(),
|
||||
"event incorrectly did not reference an m.room.create in its auth events"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
|
@ -318,7 +328,7 @@ where
|
|||
warn!(
|
||||
expected = %expected_room_id,
|
||||
received = %pe.room_id().unwrap(),
|
||||
"room_id of power levels event does not match room_id of m.room.create event"
|
||||
"room_id of referenced power levels event does not match that of the m.room.create event"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
|
@ -332,8 +342,9 @@ where
|
|||
&& room_create_event.sender().server_name() != incoming_event.sender().server_name()
|
||||
{
|
||||
warn!(
|
||||
"room is not federated and event's sender domain does not match create event's \
|
||||
sender domain"
|
||||
sender = %incoming_event.sender(),
|
||||
create_sender = %room_create_event.sender(),
|
||||
"room is not federated and event's sender domain does not match create event's sender domain"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
|
@ -416,7 +427,6 @@ where
|
|||
&user_for_join_auth_membership,
|
||||
&room_create_event,
|
||||
)? {
|
||||
warn!("membership change not valid for some reason");
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
|
@ -429,7 +439,7 @@ where
|
|||
let sender_member_event = match sender_member_event {
|
||||
| Some(mem) => mem,
|
||||
| None => {
|
||||
warn!("sender not found in room");
|
||||
warn!("sender has no membership event");
|
||||
return Ok(false);
|
||||
},
|
||||
};
|
||||
|
|
@ -440,7 +450,7 @@ where
|
|||
!= expected_room_id
|
||||
{
|
||||
warn!(
|
||||
"room_id of incoming event ({}) does not match room_id of m.room.create event ({})",
|
||||
"room_id of incoming event ({}) does not match that of the m.room.create event ({})",
|
||||
sender_member_event
|
||||
.room_id()
|
||||
.expect("event must have a room ID"),
|
||||
|
|
@ -453,8 +463,7 @@ where
|
|||
from_json_str(sender_member_event.content().get())?;
|
||||
let Some(membership_state) = sender_membership_event_content.membership else {
|
||||
warn!(
|
||||
sender_membership_event_content = format!("{sender_membership_event_content:?}"),
|
||||
event_id = format!("{}", incoming_event.event_id()),
|
||||
?sender_membership_event_content,
|
||||
"Sender membership event content missing membership field"
|
||||
);
|
||||
return Err(Error::InvalidPdu("Missing membership field".to_owned()));
|
||||
|
|
@ -462,7 +471,11 @@ where
|
|||
let membership_state = membership_state.deserialize()?;
|
||||
|
||||
if !matches!(membership_state, MembershipState::Join) {
|
||||
warn!("sender's membership is not join");
|
||||
warn!(
|
||||
%sender,
|
||||
?membership_state,
|
||||
"sender cannot send events without being joined to the room"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
|
@ -522,7 +535,12 @@ where
|
|||
};
|
||||
|
||||
if sender_power_level < invite_level {
|
||||
warn!("sender's cannot send invites in this room");
|
||||
warn!(
|
||||
%sender,
|
||||
has=?sender_power_level,
|
||||
required=?invite_level,
|
||||
"sender cannot send invites in this room"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
|
@ -534,7 +552,11 @@ where
|
|||
// level, reject If the event has a state_key that starts with an @ and does
|
||||
// not match the sender, reject.
|
||||
if !can_send_event(incoming_event, power_levels_event.as_ref(), sender_power_level) {
|
||||
warn!("user cannot send event");
|
||||
warn!(
|
||||
%sender,
|
||||
event_type=?incoming_event.kind(),
|
||||
"sender cannot send event"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
|
||||
|
|
@ -579,6 +601,12 @@ where
|
|||
};
|
||||
|
||||
if !check_redaction(room_version, incoming_event, sender_power_level, redact_level)? {
|
||||
warn!(
|
||||
%sender,
|
||||
?sender_power_level,
|
||||
?redact_level,
|
||||
"redaction event was not allowed"
|
||||
);
|
||||
return Ok(false);
|
||||
}
|
||||
}
|
||||
|
|
@ -587,15 +615,21 @@ where
|
|||
Ok(true)
|
||||
}
|
||||
|
||||
fn is_creator<EV>(v: &RoomVersion, c: &BTreeSet<OwnedUserId>, ce: &EV, user_id: &UserId) -> bool
|
||||
fn is_creator<EV>(
|
||||
v: &RoomVersion,
|
||||
c: &BTreeSet<OwnedUserId>,
|
||||
ce: &EV,
|
||||
user_id: &UserId,
|
||||
have_pls: bool,
|
||||
) -> bool
|
||||
where
|
||||
EV: Event + Send + Sync,
|
||||
{
|
||||
if v.explicitly_privilege_room_creators {
|
||||
c.contains(user_id)
|
||||
} else if v.use_room_create_sender {
|
||||
} else if v.use_room_create_sender && !have_pls {
|
||||
ce.sender() == user_id
|
||||
} else {
|
||||
} else if !have_pls {
|
||||
#[allow(deprecated)]
|
||||
let creator = from_json_str::<RoomCreateEventContent>(ce.content().get())
|
||||
.unwrap()
|
||||
|
|
@ -604,6 +638,8 @@ where
|
|||
.unwrap();
|
||||
|
||||
creator == user_id
|
||||
} else {
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -696,10 +732,11 @@ where
|
|||
}
|
||||
trace!(?creators, "creators for room");
|
||||
|
||||
let mut join_rules = JoinRule::Invite;
|
||||
if let Some(jr) = &join_rules_event {
|
||||
join_rules = from_json_str::<RoomJoinRulesEventContent>(jr.content().get())?.join_rule;
|
||||
}
|
||||
let join_rules = if let Some(jr) = &join_rules_event {
|
||||
from_json_str::<RoomJoinRulesEventContent>(jr.content().get())?.join_rule
|
||||
} else {
|
||||
JoinRule::Invite
|
||||
};
|
||||
|
||||
let power_levels_event_id = power_levels_event.as_ref().map(Event::event_id);
|
||||
let sender_membership_event_id = sender_membership_event.as_ref().map(Event::event_id);
|
||||
|
|
@ -725,8 +762,13 @@ where
|
|||
(int!(0), int!(0))
|
||||
};
|
||||
let user_joined = user_for_join_auth_membership == &MembershipState::Join;
|
||||
let okay_power = is_creator(room_version, &creators, create_room, user_for_join_auth)
|
||||
|| auth_user_pl >= invite_level;
|
||||
let okay_power = is_creator(
|
||||
room_version,
|
||||
&creators,
|
||||
create_room,
|
||||
user_for_join_auth,
|
||||
power_levels_event.as_ref().is_some(),
|
||||
) || auth_user_pl >= invite_level;
|
||||
trace!(
|
||||
auth_user_pl=?auth_user_pl,
|
||||
invite_level=?invite_level,
|
||||
|
|
@ -741,8 +783,20 @@ where
|
|||
trace!("No auth user given for join auth");
|
||||
false
|
||||
};
|
||||
let sender_creator = is_creator(room_version, &creators, create_room, sender);
|
||||
let target_creator = is_creator(room_version, &creators, create_room, target_user);
|
||||
let sender_creator = is_creator(
|
||||
room_version,
|
||||
&creators,
|
||||
create_room,
|
||||
sender,
|
||||
power_levels_event.as_ref().is_some(),
|
||||
);
|
||||
let target_creator = is_creator(
|
||||
room_version,
|
||||
&creators,
|
||||
create_room,
|
||||
target_user,
|
||||
power_levels_event.as_ref().is_some(),
|
||||
);
|
||||
|
||||
Ok(match target_membership {
|
||||
| MembershipState::Join => {
|
||||
|
|
@ -759,7 +813,7 @@ where
|
|||
|
||||
if prev_event_is_create_event && no_more_prev_events {
|
||||
trace!(
|
||||
sender = %sender,
|
||||
%sender,
|
||||
target_user = %target_user,
|
||||
?sender_creator,
|
||||
?target_creator,
|
||||
|
|
@ -779,22 +833,33 @@ where
|
|||
);
|
||||
if sender != target_user {
|
||||
// If the sender does not match state_key, reject.
|
||||
warn!("Can't make other user join");
|
||||
warn!(
|
||||
%sender,
|
||||
target_user = %target_user,
|
||||
"sender cannot join on behalf of another user"
|
||||
);
|
||||
false
|
||||
} else if target_user_current_membership == MembershipState::Ban {
|
||||
// If the sender is banned, reject.
|
||||
warn!(?target_user_membership_event_id, "Banned user can't join");
|
||||
warn!(
|
||||
%sender,
|
||||
membership_event_id = ?target_user_membership_event_id,
|
||||
"sender cannot join as they are banned from the room"
|
||||
);
|
||||
false
|
||||
} else {
|
||||
match join_rules {
|
||||
| JoinRule::Invite =>
|
||||
if !membership_allows_join {
|
||||
warn!(
|
||||
membership=?target_user_current_membership,
|
||||
"Join rule is invite but membership does not allow join"
|
||||
%sender,
|
||||
membership_event_id = ?target_user_membership_event_id,
|
||||
membership = ?target_user_current_membership,
|
||||
"sender cannot join as they are not invited to the invite-only room"
|
||||
);
|
||||
false
|
||||
} else {
|
||||
trace!(sender=%sender, "sender is invited to room, allowing join");
|
||||
true
|
||||
},
|
||||
| JoinRule::Knock if !room_version.allow_knocking => {
|
||||
|
|
@ -804,11 +869,14 @@ where
|
|||
| JoinRule::Knock =>
|
||||
if !membership_allows_join {
|
||||
warn!(
|
||||
%sender,
|
||||
membership_event_id = ?target_user_membership_event_id,
|
||||
membership=?target_user_current_membership,
|
||||
"Join rule is knock but membership does not allow join"
|
||||
"sender cannot join a knock room without being invited or already joined"
|
||||
);
|
||||
false
|
||||
} else {
|
||||
trace!(sender=%sender, "sender is invited or already joined to room, allowing join");
|
||||
true
|
||||
},
|
||||
| JoinRule::KnockRestricted(_) if !room_version.knock_restricted_join_rule =>
|
||||
|
|
@ -820,33 +888,55 @@ where
|
|||
},
|
||||
| JoinRule::KnockRestricted(_) => {
|
||||
if membership_allows_join || user_for_join_auth_is_valid {
|
||||
trace!(
|
||||
%sender,
|
||||
%membership_allows_join,
|
||||
%user_for_join_auth_is_valid,
|
||||
"sender is invited, already joined to, or authorised to join the room, allowing join"
|
||||
);
|
||||
true
|
||||
} else {
|
||||
warn!(
|
||||
%sender,
|
||||
membership_event_id = ?target_user_membership_event_id,
|
||||
membership=?target_user_current_membership,
|
||||
"Join rule is a restricted one, but no valid authorising user \
|
||||
was given and the sender's current membership does not permit \
|
||||
a join transition"
|
||||
%user_for_join_auth_is_valid,
|
||||
?user_for_join_auth,
|
||||
"sender cannot join as they are not invited nor already joined to the room, nor was a \
|
||||
valid authorising user given to permit the join"
|
||||
);
|
||||
false
|
||||
}
|
||||
},
|
||||
| JoinRule::Restricted(_) =>
|
||||
if membership_allows_join || user_for_join_auth_is_valid {
|
||||
trace!(
|
||||
%sender,
|
||||
%membership_allows_join,
|
||||
%user_for_join_auth_is_valid,
|
||||
"sender is invited, already joined to, or authorised to join the room, allowing join"
|
||||
);
|
||||
true
|
||||
} else {
|
||||
warn!(
|
||||
"Join rule is a restricted one but no valid authorising user \
|
||||
was given"
|
||||
%sender,
|
||||
membership_event_id = ?target_user_membership_event_id,
|
||||
membership=?target_user_current_membership,
|
||||
%user_for_join_auth_is_valid,
|
||||
?user_for_join_auth,
|
||||
"sender cannot join as they are not invited nor already joined to the room, nor was a \
|
||||
valid authorising user given to permit the join"
|
||||
);
|
||||
false
|
||||
},
|
||||
| JoinRule::Public => true,
|
||||
| JoinRule::Public => {
|
||||
trace!(%sender, "join rule is public, allowing join");
|
||||
true
|
||||
},
|
||||
| _ => {
|
||||
warn!(
|
||||
join_rule=?join_rules,
|
||||
membership=?target_user_current_membership,
|
||||
"Unknown join rule doesn't allow joining, or the rule's conditions were not met"
|
||||
"Join rule is unknown, or the rule's conditions were not met"
|
||||
);
|
||||
false
|
||||
},
|
||||
|
|
@ -873,16 +963,23 @@ where
|
|||
}
|
||||
allow
|
||||
},
|
||||
| _ => {
|
||||
if !sender_is_joined
|
||||
|| target_user_current_membership == MembershipState::Join
|
||||
|| target_user_current_membership == MembershipState::Ban
|
||||
{
|
||||
| _ =>
|
||||
if !sender_is_joined {
|
||||
warn!(
|
||||
%sender,
|
||||
?sender_membership_event_id,
|
||||
?sender_membership,
|
||||
"sender cannot produce an invite without being joined to the room",
|
||||
);
|
||||
false
|
||||
} else if matches!(
|
||||
target_user_current_membership,
|
||||
MembershipState::Join | MembershipState::Ban
|
||||
) {
|
||||
warn!(
|
||||
?target_user_membership_event_id,
|
||||
?sender_membership_event_id,
|
||||
"Can't invite user if sender not joined or the user is currently \
|
||||
joined or banned",
|
||||
?target_user_current_membership,
|
||||
"cannot invite a user who is banned or already joined",
|
||||
);
|
||||
false
|
||||
} else {
|
||||
|
|
@ -892,56 +989,124 @@ where
|
|||
.is_some();
|
||||
if !allow {
|
||||
warn!(
|
||||
?target_user_membership_event_id,
|
||||
?power_levels_event_id,
|
||||
"User does not have enough power to invite",
|
||||
%sender,
|
||||
has=?sender_power,
|
||||
required=?power_levels.invite,
|
||||
"sender does not have enough power to produce invites",
|
||||
);
|
||||
}
|
||||
trace!(
|
||||
%sender,
|
||||
?sender_membership_event_id,
|
||||
?sender_membership,
|
||||
?target_user_membership_event_id,
|
||||
?target_user_current_membership,
|
||||
sender_pl=?sender_power,
|
||||
required_pl=?power_levels.invite,
|
||||
"allowing invite"
|
||||
);
|
||||
allow
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
},
|
||||
| MembershipState::Leave =>
|
||||
| MembershipState::Leave => {
|
||||
let can_unban = if target_user_current_membership == MembershipState::Ban {
|
||||
sender_creator || sender_power.filter(|&p| p >= &power_levels.ban).is_some()
|
||||
} else {
|
||||
true
|
||||
};
|
||||
let can_kick = if !matches!(
|
||||
target_user_current_membership,
|
||||
MembershipState::Ban | MembershipState::Leave
|
||||
) {
|
||||
if sender_creator {
|
||||
// sender is a creator
|
||||
true
|
||||
} else if sender_power.filter(|&p| p >= &power_levels.kick).is_none() {
|
||||
// sender lacks kick power level
|
||||
false
|
||||
} else if let Some(sp) = sender_power {
|
||||
if let Some(tp) = target_power {
|
||||
// sender must have more power than target
|
||||
sp > tp
|
||||
} else {
|
||||
// target has default power level
|
||||
true
|
||||
}
|
||||
} else {
|
||||
// sender has default power level
|
||||
false
|
||||
}
|
||||
} else {
|
||||
true
|
||||
};
|
||||
if sender == target_user {
|
||||
let allow = target_user_current_membership == MembershipState::Join
|
||||
|| target_user_current_membership == MembershipState::Invite
|
||||
|| target_user_current_membership == MembershipState::Knock;
|
||||
// self-leave
|
||||
// let allow = target_user_current_membership == MembershipState::Join
|
||||
// || target_user_current_membership == MembershipState::Invite
|
||||
// || target_user_current_membership == MembershipState::Knock;
|
||||
let allow = matches!(
|
||||
target_user_current_membership,
|
||||
MembershipState::Join | MembershipState::Invite | MembershipState::Knock
|
||||
);
|
||||
if !allow {
|
||||
warn!(
|
||||
?target_user_membership_event_id,
|
||||
?target_user_current_membership,
|
||||
"Can't leave if sender is not already invited, knocked, or joined"
|
||||
%sender,
|
||||
current_membership_event_id=?target_user_membership_event_id,
|
||||
current_membership=?target_user_current_membership,
|
||||
"sender cannot leave as they are not already knocking on, invited to, or joined to the room"
|
||||
);
|
||||
}
|
||||
trace!(sender=%sender, "allowing leave");
|
||||
allow
|
||||
} else if !sender_is_joined
|
||||
|| target_user_current_membership == MembershipState::Ban
|
||||
&& (sender_creator
|
||||
|| sender_power.filter(|&p| p < &power_levels.ban).is_some())
|
||||
{
|
||||
} else if !sender_is_joined {
|
||||
warn!(
|
||||
?target_user_membership_event_id,
|
||||
%sender,
|
||||
?sender_membership_event_id,
|
||||
"Can't kick if sender not joined or user is already banned",
|
||||
"sender cannot kick another user as they are not joined to the room",
|
||||
);
|
||||
false
|
||||
} else if !(can_unban && can_kick) {
|
||||
// If the target is banned, only a room creator or someone with ban power
|
||||
// level can unban them
|
||||
warn!(
|
||||
%sender,
|
||||
?target_user_membership_event_id,
|
||||
?power_levels_event_id,
|
||||
"sender lacks the power level required to unban users",
|
||||
);
|
||||
false
|
||||
} else if !can_kick {
|
||||
warn!(
|
||||
%sender,
|
||||
%target_user,
|
||||
?target_user_membership_event_id,
|
||||
?target_user_current_membership,
|
||||
?power_levels_event_id,
|
||||
"sender does not have enough power to kick the target",
|
||||
);
|
||||
false
|
||||
} else {
|
||||
let allow = sender_creator
|
||||
|| (sender_power.filter(|&p| p >= &power_levels.kick).is_some()
|
||||
&& target_power < sender_power);
|
||||
if !allow {
|
||||
warn!(
|
||||
?target_user_membership_event_id,
|
||||
?power_levels_event_id,
|
||||
"User does not have enough power to kick",
|
||||
);
|
||||
}
|
||||
allow
|
||||
},
|
||||
trace!(
|
||||
%sender,
|
||||
%target_user,
|
||||
?target_user_membership_event_id,
|
||||
?target_user_current_membership,
|
||||
sender_pl=?sender_power,
|
||||
target_pl=?target_power,
|
||||
required_pl=?power_levels.kick,
|
||||
"allowing kick/unban",
|
||||
);
|
||||
true
|
||||
}
|
||||
},
|
||||
| MembershipState::Ban =>
|
||||
if !sender_is_joined {
|
||||
warn!(?sender_membership_event_id, "Can't ban user if sender is not joined");
|
||||
warn!(
|
||||
%sender,
|
||||
?sender_membership_event_id,
|
||||
"sender cannot ban another user as they are not joined to the room",
|
||||
);
|
||||
false
|
||||
} else {
|
||||
let allow = sender_creator
|
||||
|
|
@ -949,9 +1114,11 @@ where
|
|||
&& target_power < sender_power);
|
||||
if !allow {
|
||||
warn!(
|
||||
%sender,
|
||||
%target_user,
|
||||
?target_user_membership_event_id,
|
||||
?power_levels_event_id,
|
||||
"User does not have enough power to ban",
|
||||
"sender does not have enough power to ban the target",
|
||||
);
|
||||
}
|
||||
allow
|
||||
|
|
@ -977,9 +1144,9 @@ where
|
|||
} else if sender != target_user {
|
||||
// 3. If `sender` does not match `state_key`, reject.
|
||||
warn!(
|
||||
?sender,
|
||||
?target_user,
|
||||
"Can't make another user knock, sender did not match target"
|
||||
%sender,
|
||||
%target_user,
|
||||
"sender cannot knock on behalf of another user",
|
||||
);
|
||||
false
|
||||
} else if matches!(
|
||||
|
|
@ -991,15 +1158,25 @@ where
|
|||
// 5. Otherwise, reject.
|
||||
warn!(
|
||||
?target_user_membership_event_id,
|
||||
?sender_membership,
|
||||
"Knocking with a membership state of ban, invite or join is invalid",
|
||||
);
|
||||
false
|
||||
} else {
|
||||
trace!(%sender, "allowing knock");
|
||||
true
|
||||
}
|
||||
},
|
||||
| _ => {
|
||||
warn!("Unknown membership transition");
|
||||
warn!(
|
||||
%sender,
|
||||
?target_membership,
|
||||
%target_user,
|
||||
%target_user_current_membership,
|
||||
"Unknown or invalid membership transition {} -> {}",
|
||||
target_user_current_membership,
|
||||
target_membership
|
||||
);
|
||||
false
|
||||
},
|
||||
})
|
||||
|
|
@ -1029,6 +1206,13 @@ fn can_send_event(event: &impl Event, ple: Option<&impl Event>, user_level: Int)
|
|||
if event.state_key().is_some_and(|k| k.starts_with('@'))
|
||||
&& event.state_key() != Some(event.sender().as_str())
|
||||
{
|
||||
warn!(
|
||||
%user_level,
|
||||
required=?event_type_power_level,
|
||||
state_key=?event.state_key(),
|
||||
sender=%event.sender(),
|
||||
"state_key starts with @ but does not match sender",
|
||||
);
|
||||
return false; // permission required to post in this room
|
||||
}
|
||||
|
||||
|
|
@ -1113,7 +1297,14 @@ fn check_power_levels(
|
|||
|
||||
// If the current value is equal to the sender's current power level, reject
|
||||
if user != power_event.sender() && old_level == Some(&user_level) {
|
||||
warn!("m.room.power_level cannot remove ops == to own");
|
||||
warn!(
|
||||
?old_level,
|
||||
?new_level,
|
||||
?user,
|
||||
%user_level,
|
||||
sender=%power_event.sender(),
|
||||
"cannot alter the power level of a user with the same power level as sender's own"
|
||||
);
|
||||
return Some(false); // cannot remove ops level == to own
|
||||
}
|
||||
|
||||
|
|
@ -1121,8 +1312,26 @@ fn check_power_levels(
|
|||
// If the new value is higher than the sender's current power level, reject
|
||||
let old_level_too_big = old_level > Some(&user_level);
|
||||
let new_level_too_big = new_level > Some(&user_level);
|
||||
if old_level_too_big || new_level_too_big {
|
||||
warn!("m.room.power_level failed to add ops > than own");
|
||||
if old_level_too_big {
|
||||
warn!(
|
||||
?old_level,
|
||||
?new_level,
|
||||
?user,
|
||||
%user_level,
|
||||
sender=%power_event.sender(),
|
||||
"cannot alter the power level of a user with a higher power level than sender's own"
|
||||
);
|
||||
return Some(false); // cannot add ops greater than own
|
||||
}
|
||||
if new_level_too_big {
|
||||
warn!(
|
||||
?old_level,
|
||||
?new_level,
|
||||
?user,
|
||||
%user_level,
|
||||
sender=%power_event.sender(),
|
||||
"cannot set the power level of a user to a level higher than sender's own"
|
||||
);
|
||||
return Some(false); // cannot add ops greater than own
|
||||
}
|
||||
}
|
||||
|
|
@ -1139,8 +1348,26 @@ fn check_power_levels(
|
|||
// If the new value is higher than the sender's current power level, reject
|
||||
let old_level_too_big = old_level > Some(&user_level);
|
||||
let new_level_too_big = new_level > Some(&user_level);
|
||||
if old_level_too_big || new_level_too_big {
|
||||
warn!("m.room.power_level failed to add ops > than own");
|
||||
if old_level_too_big {
|
||||
warn!(
|
||||
?old_level,
|
||||
?new_level,
|
||||
?ev_type,
|
||||
%user_level,
|
||||
sender=%power_event.sender(),
|
||||
"cannot alter the power level of an event with a higher power level than sender's own"
|
||||
);
|
||||
return Some(false); // cannot add ops greater than own
|
||||
}
|
||||
if new_level_too_big {
|
||||
warn!(
|
||||
?old_level,
|
||||
?new_level,
|
||||
?ev_type,
|
||||
%user_level,
|
||||
sender=%power_event.sender(),
|
||||
"cannot set the power level of an event to a level higher than sender's own"
|
||||
);
|
||||
return Some(false); // cannot add ops greater than own
|
||||
}
|
||||
}
|
||||
|
|
@ -1155,7 +1382,13 @@ fn check_power_levels(
|
|||
let old_level_too_big = old_level > user_level;
|
||||
let new_level_too_big = new_level > user_level;
|
||||
if old_level_too_big || new_level_too_big {
|
||||
warn!("m.room.power_level failed to add ops > than own");
|
||||
warn!(
|
||||
?old_level,
|
||||
?new_level,
|
||||
%user_level,
|
||||
sender=%power_event.sender(),
|
||||
"cannot alter the power level of notifications greater than sender's own"
|
||||
);
|
||||
return Some(false); // cannot add ops greater than own
|
||||
}
|
||||
}
|
||||
|
|
@ -1179,7 +1412,14 @@ fn check_power_levels(
|
|||
let new_level_too_big = new_lvl > user_level;
|
||||
|
||||
if old_level_too_big || new_level_too_big {
|
||||
warn!("cannot add ops > than own");
|
||||
warn!(
|
||||
?old_lvl,
|
||||
?new_lvl,
|
||||
%user_level,
|
||||
sender=%power_event.sender(),
|
||||
action=%lvl_name,
|
||||
"cannot alter the power level of action greater than sender's own",
|
||||
);
|
||||
return Some(false);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ pub use self::{
|
|||
room_version::RoomVersion,
|
||||
};
|
||||
use crate::{
|
||||
debug, debug_error,
|
||||
debug, debug_error, err,
|
||||
matrix::{Event, StateKey},
|
||||
state_res::room_version::StateResolutionVersion,
|
||||
trace,
|
||||
|
|
@ -101,40 +101,40 @@ where
|
|||
debug!(version = ?stateres_version, "State resolution starting");
|
||||
|
||||
// Split non-conflicting and conflicting state
|
||||
let (clean, conflicting) = separate(state_sets.into_iter());
|
||||
let (unconflicted, conflicting) = separate(state_sets.into_iter());
|
||||
|
||||
debug!(count = clean.len(), "non-conflicting events");
|
||||
trace!(map = ?clean, "non-conflicting events");
|
||||
debug!(count = unconflicted.len(), "non-conflicting events");
|
||||
trace!(map = ?unconflicted, "non-conflicting events");
|
||||
|
||||
if conflicting.is_empty() {
|
||||
debug!("no conflicting state found");
|
||||
return Ok(clean);
|
||||
return Ok(unconflicted);
|
||||
}
|
||||
|
||||
debug!(count = conflicting.len(), "conflicting events");
|
||||
trace!(map = ?conflicting, "conflicting events");
|
||||
let conflicted_state_subgraph: HashSet<_> = match stateres_version {
|
||||
| StateResolutionVersion::V2_1 =>
|
||||
calculate_conflicted_subgraph(&conflicting, event_fetch)
|
||||
let (conflicted_state_subgraph, initial_state) =
|
||||
if stateres_version == StateResolutionVersion::V2_1 {
|
||||
let csg = calculate_conflicted_subgraph(&conflicting, event_fetch)
|
||||
.await
|
||||
.ok_or_else(|| {
|
||||
Error::InvalidPdu("Failed to calculate conflicted subgraph".to_owned())
|
||||
})?,
|
||||
| _ => HashSet::new(),
|
||||
};
|
||||
debug!(count = conflicted_state_subgraph.len(), "conflicted subgraph");
|
||||
trace!(set = ?conflicted_state_subgraph, "conflicted subgraph");
|
||||
|
||||
let conflicting_values = conflicting.into_values().flatten().stream();
|
||||
})?;
|
||||
debug!(count = csg.len(), "conflicted subgraph");
|
||||
trace!(set = ?csg, "conflicted subgraph");
|
||||
(csg, HashMap::new())
|
||||
} else {
|
||||
(HashSet::new(), unconflicted.clone())
|
||||
};
|
||||
|
||||
// `all_conflicted` contains unique items
|
||||
// synapse says `full_set = {eid for eid in full_conflicted_set if eid in
|
||||
// event_map}`
|
||||
// Hydra: Also consider the conflicted state subgraph
|
||||
let all_conflicted: HashSet<_> = get_auth_chain_diff(auth_chain_sets)
|
||||
.chain(conflicting_values)
|
||||
.chain(conflicted_state_subgraph.into_iter().stream())
|
||||
.chain(conflicting.into_values().flatten().stream())
|
||||
.broad_filter_map(async |id| event_exists(id.clone()).await.then_some(id))
|
||||
.chain(conflicted_state_subgraph.into_iter().stream())
|
||||
.collect()
|
||||
.await;
|
||||
|
||||
|
|
@ -169,9 +169,8 @@ where
|
|||
// Sequentially auth check each control event.
|
||||
let resolved_control = iterative_auth_check(
|
||||
&room_version,
|
||||
&stateres_version,
|
||||
sorted_control_levels.iter().stream().map(AsRef::as_ref),
|
||||
clean.clone(),
|
||||
initial_state,
|
||||
&event_fetch,
|
||||
)
|
||||
.await?;
|
||||
|
|
@ -201,7 +200,7 @@ where
|
|||
let power_levels_ty_sk = (StateEventType::RoomPowerLevels, StateKey::new());
|
||||
let power_event = resolved_control.get(&power_levels_ty_sk);
|
||||
|
||||
debug!(event_id = ?power_event, "power event");
|
||||
trace!(event_id = ?power_event, "power event");
|
||||
|
||||
let sorted_left_events =
|
||||
mainline_sort(&events_to_resolve, power_event.cloned(), &event_fetch).await?;
|
||||
|
|
@ -210,21 +209,14 @@ where
|
|||
|
||||
let mut resolved_state = iterative_auth_check(
|
||||
&room_version,
|
||||
&stateres_version,
|
||||
sorted_left_events.iter().stream().map(AsRef::as_ref),
|
||||
resolved_control.clone(), // The control events are added to the final resolved state
|
||||
resolved_control, // The control events are added to the final resolved state
|
||||
&event_fetch,
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Add unconflicted state to the resolved state
|
||||
// We priorities the unconflicting state
|
||||
resolved_state.extend(clean);
|
||||
if stateres_version == StateResolutionVersion::V2_1 {
|
||||
resolved_state.extend(resolved_control);
|
||||
// TODO(hydra): this feels disgusting and wrong but it allows
|
||||
// the state to resolve properly?
|
||||
}
|
||||
// Ensure unconflicting state is in the final state
|
||||
resolved_state.extend(unconflicted);
|
||||
|
||||
debug!("state resolution finished");
|
||||
trace!( map = ?resolved_state, "final resolved state" );
|
||||
|
|
@ -319,8 +311,19 @@ where
|
|||
path.pop();
|
||||
continue;
|
||||
}
|
||||
let evt = fetch_event(event_id.clone()).await?;
|
||||
stack.push(evt.auth_events().map(ToOwned::to_owned).collect());
|
||||
trace!(event_id = event_id.as_str(), "fetching event for its auth events");
|
||||
let evt = fetch_event(event_id.clone()).await;
|
||||
if evt.is_none() {
|
||||
err!("could not fetch event {} to calculate conflicted subgraph", event_id);
|
||||
path.pop();
|
||||
continue;
|
||||
}
|
||||
stack.push(
|
||||
evt.expect("checked")
|
||||
.auth_events()
|
||||
.map(ToOwned::to_owned)
|
||||
.collect(),
|
||||
);
|
||||
seen.insert(event_id);
|
||||
}
|
||||
Some(subgraph)
|
||||
|
|
@ -592,7 +595,6 @@ where
|
|||
#[tracing::instrument(level = "trace", skip_all)]
|
||||
async fn iterative_auth_check<'a, E, F, Fut, S>(
|
||||
room_version: &RoomVersion,
|
||||
stateres_version: &StateResolutionVersion,
|
||||
events_to_check: S,
|
||||
unconflicted_state: StateMap<OwnedEventId>,
|
||||
fetch_event: &F,
|
||||
|
|
@ -617,6 +619,10 @@ where
|
|||
.boxed()
|
||||
.await?;
|
||||
trace!(list = ?events_to_check, "events to check");
|
||||
if events_to_check.is_empty() {
|
||||
debug!("no events to check, returning unconflicted state");
|
||||
return Ok(unconflicted_state);
|
||||
}
|
||||
|
||||
let auth_event_ids: HashSet<OwnedEventId> = events_to_check
|
||||
.iter()
|
||||
|
|
@ -637,10 +643,11 @@ where
|
|||
trace!(map = ?auth_events.keys().collect::<Vec<_>>(), "fetched auth events");
|
||||
|
||||
let auth_events = &auth_events;
|
||||
let mut resolved_state = match stateres_version {
|
||||
| StateResolutionVersion::V2_1 => StateMap::new(),
|
||||
| _ => unconflicted_state,
|
||||
};
|
||||
// NOTE: in state resolution v2.1, auth checks should start with an empty state
|
||||
// map. It is the caller's job to do this. Previously, this function would
|
||||
// force an empty state map in this case, and this resulted in power events
|
||||
// going missing from the resolved state as they'd be discarded here.
|
||||
let mut resolved_state = unconflicted_state;
|
||||
for event in events_to_check {
|
||||
trace!(event_id = event.event_id().as_str(), "checking event");
|
||||
let state_key = event
|
||||
|
|
@ -1028,7 +1035,6 @@ mod tests {
|
|||
|
||||
let resolved_power = super::iterative_auth_check(
|
||||
&RoomVersion::V6,
|
||||
&StateResolutionVersion::V2,
|
||||
sorted_power_events.iter().map(AsRef::as_ref).stream(),
|
||||
HashMap::new(), // unconflicted events
|
||||
&fetcher,
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use conduwuit::{
|
|||
},
|
||||
warn,
|
||||
};
|
||||
use database::Json;
|
||||
use futures::{FutureExt, StreamExt, TryStreamExt};
|
||||
use itertools::Itertools;
|
||||
use ruma::{
|
||||
|
|
@ -606,7 +607,7 @@ async fn fix_corrupt_msc4133_fields(services: &Services) -> Result {
|
|||
);
|
||||
};
|
||||
|
||||
useridprofilekey_value.put((user, key), new_value);
|
||||
useridprofilekey_value.put((user, key), Json(new_value));
|
||||
fixed = fixed.saturating_add(1);
|
||||
}
|
||||
total = total.saturating_add(1);
|
||||
|
|
|
|||
|
|
@ -4,9 +4,8 @@ use std::{
|
|||
};
|
||||
|
||||
use conduwuit::{
|
||||
Event, PduEvent, debug, debug_error, debug_warn, implement,
|
||||
matrix::event::gen_event_id_canonical_json, trace, utils::continue_exponential_backoff_secs,
|
||||
warn,
|
||||
Event, PduEvent, debug, debug_warn, implement, matrix::event::gen_event_id_canonical_json,
|
||||
trace, utils::continue_exponential_backoff_secs, warn,
|
||||
};
|
||||
use ruma::{
|
||||
CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName,
|
||||
|
|
@ -52,12 +51,14 @@ where
|
|||
};
|
||||
|
||||
let mut events_with_auth_events = Vec::with_capacity(events.clone().count());
|
||||
trace!("Fetching {} outlier pdus", events.clone().count());
|
||||
|
||||
for id in events {
|
||||
// a. Look in the main timeline (pduid_pdu tree)
|
||||
// b. Look at outlier pdu tree
|
||||
// (get_pdu_json checks both)
|
||||
if let Ok(local_pdu) = self.services.timeline.get_pdu(id).await {
|
||||
trace!("Found {id} in main timeline or outlier tree");
|
||||
events_with_auth_events.push((id.to_owned(), Some(local_pdu), vec![]));
|
||||
continue;
|
||||
}
|
||||
|
|
@ -104,7 +105,7 @@ where
|
|||
continue;
|
||||
}
|
||||
|
||||
debug!("Fetching {next_id} over federation.");
|
||||
debug!("Fetching {next_id} over federation from {origin}.");
|
||||
match self
|
||||
.services
|
||||
.sending
|
||||
|
|
@ -115,7 +116,7 @@ where
|
|||
.await
|
||||
{
|
||||
| Ok(res) => {
|
||||
debug!("Got {next_id} over federation");
|
||||
debug!("Got {next_id} over federation from {origin}");
|
||||
let Ok(room_version_id) = get_room_version_id(create_event) else {
|
||||
back_off((*next_id).to_owned());
|
||||
continue;
|
||||
|
|
@ -145,6 +146,9 @@ where
|
|||
auth_event.clone().into(),
|
||||
) {
|
||||
| Ok(auth_event) => {
|
||||
trace!(
|
||||
"Found auth event id {auth_event} for event {next_id}"
|
||||
);
|
||||
todo_auth_events.push_back(auth_event);
|
||||
},
|
||||
| _ => {
|
||||
|
|
@ -160,7 +164,7 @@ where
|
|||
events_all.insert(next_id);
|
||||
},
|
||||
| Err(e) => {
|
||||
debug_error!("Failed to fetch event {next_id}: {e}");
|
||||
warn!("Failed to fetch auth event {next_id} from {origin}: {e}");
|
||||
back_off((*next_id).to_owned());
|
||||
},
|
||||
}
|
||||
|
|
@ -175,7 +179,7 @@ where
|
|||
// b. Look at outlier pdu tree
|
||||
// (get_pdu_json checks both)
|
||||
if let Some(local_pdu) = local_pdu {
|
||||
trace!("Found {id} in db");
|
||||
trace!("Found {id} in main timeline or outlier tree");
|
||||
pdus.push((local_pdu.clone(), None));
|
||||
}
|
||||
|
||||
|
|
@ -201,6 +205,7 @@ where
|
|||
}
|
||||
}
|
||||
|
||||
trace!("Handling outlier {next_id}");
|
||||
match Box::pin(self.handle_outlier_pdu(
|
||||
origin,
|
||||
create_event,
|
||||
|
|
@ -213,6 +218,7 @@ where
|
|||
{
|
||||
| Ok((pdu, json)) =>
|
||||
if next_id == *id {
|
||||
trace!("Handled outlier {next_id} (original request)");
|
||||
pdus.push((pdu, Some(json)));
|
||||
},
|
||||
| Err(e) => {
|
||||
|
|
@ -222,6 +228,6 @@ where
|
|||
}
|
||||
}
|
||||
}
|
||||
|
||||
trace!("Fetched and handled {} outlier pdus", pdus.len());
|
||||
pdus
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,11 +1,12 @@
|
|||
use std::collections::{BTreeMap, HashMap, hash_map};
|
||||
|
||||
use conduwuit::{
|
||||
Err, Event, PduEvent, Result, debug, debug_info, err, implement, state_res, trace, warn,
|
||||
Err, Event, PduEvent, Result, debug, debug_info, debug_warn, err, implement, state_res, trace,
|
||||
};
|
||||
use futures::future::ready;
|
||||
use ruma::{
|
||||
CanonicalJsonObject, CanonicalJsonValue, EventId, RoomId, ServerName, events::StateEventType,
|
||||
CanonicalJsonObject, CanonicalJsonValue, EventId, OwnedEventId, RoomId, ServerName,
|
||||
events::StateEventType,
|
||||
};
|
||||
|
||||
use super::{check_room_id, get_room_version_id, to_room_version};
|
||||
|
|
@ -74,36 +75,73 @@ where
|
|||
|
||||
check_room_id(room_id, &pdu_event)?;
|
||||
|
||||
if !auth_events_known {
|
||||
// 4. fetch any missing auth events doing all checks listed here starting at 1.
|
||||
// These are not timeline events
|
||||
// 5. Reject "due to auth events" if can't get all the auth events or some of
|
||||
// the auth events are also rejected "due to auth events"
|
||||
// NOTE: Step 5 is not applied anymore because it failed too often
|
||||
debug!("Fetching auth events");
|
||||
Box::pin(self.fetch_and_handle_outliers(
|
||||
origin,
|
||||
pdu_event.auth_events(),
|
||||
create_event,
|
||||
room_id,
|
||||
))
|
||||
.await;
|
||||
// Fetch all auth events
|
||||
let mut auth_events: HashMap<OwnedEventId, PduEvent> = HashMap::new();
|
||||
|
||||
for aid in pdu_event.auth_events() {
|
||||
if let Ok(auth_event) = self.services.timeline.get_pdu(aid).await {
|
||||
check_room_id(room_id, &auth_event)?;
|
||||
trace!("Found auth event {aid} for outlier event {event_id} locally");
|
||||
auth_events.insert(aid.to_owned(), auth_event);
|
||||
} else {
|
||||
debug_warn!("Could not find auth event {aid} for outlier event {event_id} locally");
|
||||
}
|
||||
}
|
||||
|
||||
// Fetch any missing ones & reject invalid ones
|
||||
let missing_auth_events = if auth_events_known {
|
||||
pdu_event
|
||||
.auth_events()
|
||||
.filter(|id| !auth_events.contains_key(*id))
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
pdu_event.auth_events().collect::<Vec<_>>()
|
||||
};
|
||||
if !missing_auth_events.is_empty() || !auth_events_known {
|
||||
debug_info!(
|
||||
"Fetching {} missing auth events for outlier event {event_id}",
|
||||
missing_auth_events.len()
|
||||
);
|
||||
for (pdu, _) in self
|
||||
.fetch_and_handle_outliers(
|
||||
origin,
|
||||
missing_auth_events.iter().copied(),
|
||||
create_event,
|
||||
room_id,
|
||||
)
|
||||
.await
|
||||
{
|
||||
auth_events.insert(pdu.event_id().to_owned(), pdu);
|
||||
}
|
||||
} else {
|
||||
debug!("No missing auth events for outlier event {event_id}");
|
||||
}
|
||||
// reject if we are still missing some
|
||||
let still_missing = pdu_event
|
||||
.auth_events()
|
||||
.filter(|id| !auth_events.contains_key(*id))
|
||||
.collect::<Vec<_>>();
|
||||
if !still_missing.is_empty() {
|
||||
return Err!(Request(InvalidParam(
|
||||
"Could not fetch all auth events for outlier event {event_id}, still missing: \
|
||||
{still_missing:?}"
|
||||
)));
|
||||
}
|
||||
|
||||
// 6. Reject "due to auth events" if the event doesn't pass auth based on the
|
||||
// auth events
|
||||
debug!("Checking based on auth events");
|
||||
let mut auth_events_by_key: HashMap<_, _> = HashMap::with_capacity(auth_events.len());
|
||||
// Build map of auth events
|
||||
let mut auth_events = HashMap::with_capacity(pdu_event.auth_events().count());
|
||||
for id in pdu_event.auth_events() {
|
||||
let Ok(auth_event) = self.services.timeline.get_pdu(id).await else {
|
||||
warn!("Could not find auth event {id}");
|
||||
continue;
|
||||
};
|
||||
let auth_event = auth_events
|
||||
.get(id)
|
||||
.expect("we just checked that we have all auth events")
|
||||
.to_owned();
|
||||
|
||||
check_room_id(room_id, &auth_event)?;
|
||||
|
||||
match auth_events.entry((
|
||||
match auth_events_by_key.entry((
|
||||
auth_event.kind.to_string().into(),
|
||||
auth_event
|
||||
.state_key
|
||||
|
|
@ -123,7 +161,7 @@ where
|
|||
|
||||
// The original create event must be in the auth events
|
||||
if !matches!(
|
||||
auth_events.get(&(StateEventType::RoomCreate, String::new().into())),
|
||||
auth_events_by_key.get(&(StateEventType::RoomCreate, String::new().into())),
|
||||
Some(_) | None
|
||||
) {
|
||||
return Err!(Request(InvalidParam("Incoming event refers to wrong create event.")));
|
||||
|
|
@ -131,7 +169,7 @@ where
|
|||
|
||||
let state_fetch = |ty: &StateEventType, sk: &str| {
|
||||
let key = (ty.to_owned(), sk.into());
|
||||
ready(auth_events.get(&key).map(ToOwned::to_owned))
|
||||
ready(auth_events_by_key.get(&key).map(ToOwned::to_owned))
|
||||
};
|
||||
|
||||
let auth_check = state_res::event_auth::auth_check(
|
||||
|
|
|
|||
|
|
@ -5,9 +5,9 @@
|
|||
|
||||
use std::time::Duration;
|
||||
|
||||
use conduwuit::{Err, Event, PduEvent, Result, debug, implement, warn};
|
||||
use conduwuit::{Err, Event, PduEvent, Result, debug, debug_info, implement, trace, warn};
|
||||
use ruma::{
|
||||
RoomId, ServerName,
|
||||
CanonicalJsonObject, RoomId, ServerName,
|
||||
api::federation::room::policy::v1::Request as PolicyRequest,
|
||||
events::{StateEventType, room::policy::RoomPolicyEventContent},
|
||||
};
|
||||
|
|
@ -25,7 +25,12 @@ use ruma::{
|
|||
/// fail-open operation.
|
||||
#[implement(super::Service)]
|
||||
#[tracing::instrument(skip_all, level = "debug")]
|
||||
pub async fn ask_policy_server(&self, pdu: &PduEvent, room_id: &RoomId) -> Result<bool> {
|
||||
pub async fn ask_policy_server(
|
||||
&self,
|
||||
pdu: &PduEvent,
|
||||
pdu_json: &CanonicalJsonObject,
|
||||
room_id: &RoomId,
|
||||
) -> Result<bool> {
|
||||
if *pdu.event_type() == StateEventType::RoomPolicy.into() {
|
||||
debug!(
|
||||
room_id = %room_id,
|
||||
|
|
@ -47,12 +52,12 @@ pub async fn ask_policy_server(&self, pdu: &PduEvent, room_id: &RoomId) -> Resul
|
|||
let via = match policyserver.via {
|
||||
| Some(ref via) => ServerName::parse(via)?,
|
||||
| None => {
|
||||
debug!("No policy server configured for room {room_id}");
|
||||
trace!("No policy server configured for room {room_id}");
|
||||
return Ok(true);
|
||||
},
|
||||
};
|
||||
if via.is_empty() {
|
||||
debug!("Policy server is empty for room {room_id}, skipping spam check");
|
||||
trace!("Policy server is empty for room {room_id}, skipping spam check");
|
||||
return Ok(true);
|
||||
}
|
||||
if !self.services.state_cache.server_in_room(via, room_id).await {
|
||||
|
|
@ -66,12 +71,12 @@ pub async fn ask_policy_server(&self, pdu: &PduEvent, room_id: &RoomId) -> Resul
|
|||
let outgoing = self
|
||||
.services
|
||||
.sending
|
||||
.convert_to_outgoing_federation_event(pdu.to_canonical_object())
|
||||
.convert_to_outgoing_federation_event(pdu_json.clone())
|
||||
.await;
|
||||
debug!(
|
||||
debug_info!(
|
||||
room_id = %room_id,
|
||||
via = %via,
|
||||
outgoing = ?outgoing,
|
||||
outgoing = ?pdu_json,
|
||||
"Checking event for spam with policy server"
|
||||
);
|
||||
let response = tokio::time::timeout(
|
||||
|
|
@ -85,7 +90,10 @@ pub async fn ask_policy_server(&self, pdu: &PduEvent, room_id: &RoomId) -> Resul
|
|||
)
|
||||
.await;
|
||||
let response = match response {
|
||||
| Ok(Ok(response)) => response,
|
||||
| Ok(Ok(response)) => {
|
||||
debug!("Response from policy server: {:?}", response);
|
||||
response
|
||||
},
|
||||
| Ok(Err(e)) => {
|
||||
warn!(
|
||||
via = %via,
|
||||
|
|
@ -97,16 +105,18 @@ pub async fn ask_policy_server(&self, pdu: &PduEvent, room_id: &RoomId) -> Resul
|
|||
// default.
|
||||
return Err(e);
|
||||
},
|
||||
| Err(_) => {
|
||||
| Err(elapsed) => {
|
||||
warn!(
|
||||
via = %via,
|
||||
%via,
|
||||
event_id = %pdu.event_id(),
|
||||
room_id = %room_id,
|
||||
%room_id,
|
||||
%elapsed,
|
||||
"Policy server request timed out after 10 seconds"
|
||||
);
|
||||
return Err!("Request to policy server timed out");
|
||||
},
|
||||
};
|
||||
trace!("Recommendation from policy server was {}", response.recommendation);
|
||||
if response.recommendation == "spam" {
|
||||
warn!(
|
||||
via = %via,
|
||||
|
|
|
|||
|
|
@ -255,7 +255,10 @@ where
|
|||
// 14-pre. If the event is not a state event, ask the policy server about it
|
||||
if incoming_pdu.state_key.is_none() {
|
||||
debug!(event_id = %incoming_pdu.event_id, "Checking policy server for event");
|
||||
match self.ask_policy_server(&incoming_pdu, room_id).await {
|
||||
match self
|
||||
.ask_policy_server(&incoming_pdu, &incoming_pdu.to_canonical_object(), room_id)
|
||||
.await
|
||||
{
|
||||
| Ok(false) => {
|
||||
warn!(
|
||||
event_id = %incoming_pdu.event_id,
|
||||
|
|
|
|||
|
|
@ -1,9 +1,10 @@
|
|||
use conduwuit::{Err, Result, implement, matrix::Event, pdu::PduBuilder};
|
||||
use conduwuit::{Err, Result, RoomVersion, implement, matrix::Event, pdu::PduBuilder};
|
||||
use ruma::{
|
||||
EventId, RoomId, UserId,
|
||||
events::{
|
||||
StateEventType, TimelineEventType,
|
||||
room::{
|
||||
create::RoomCreateEventContent,
|
||||
history_visibility::{HistoryVisibility, RoomHistoryVisibilityEventContent},
|
||||
member::{MembershipState, RoomMemberEventContent},
|
||||
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
|
||||
|
|
@ -44,6 +45,23 @@ pub async fn user_can_redact(
|
|||
)));
|
||||
}
|
||||
|
||||
let room_create = self
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||
.await?;
|
||||
let create_content: RoomCreateEventContent =
|
||||
serde_json::from_str(room_create.content().get())?;
|
||||
let room_features = RoomVersion::new(&create_content.room_version)?;
|
||||
if room_features.explicitly_privilege_room_creators {
|
||||
let sender_owned = sender.to_owned();
|
||||
if sender == room_create.sender()
|
||||
|| create_content
|
||||
.additional_creators
|
||||
.is_some_and(|cs| cs.contains(&sender_owned))
|
||||
{
|
||||
return Ok(true);
|
||||
}
|
||||
}
|
||||
|
||||
match self
|
||||
.room_state_get_content::<RoomPowerLevelsEventContent>(
|
||||
room_id,
|
||||
|
|
@ -68,18 +86,10 @@ pub async fn user_can_redact(
|
|||
},
|
||||
| _ => {
|
||||
// Falling back on m.room.create to judge power level
|
||||
match self
|
||||
.room_state_get(room_id, &StateEventType::RoomCreate, "")
|
||||
.await
|
||||
{
|
||||
| Ok(room_create) => Ok(room_create.sender() == sender
|
||||
|| redacting_event
|
||||
.as_ref()
|
||||
.is_ok_and(|redacting_event| redacting_event.sender() == sender)),
|
||||
| _ => Err!(Database(
|
||||
"No m.room.power_levels or m.room.create events in database for room"
|
||||
)),
|
||||
}
|
||||
Ok(room_create.sender() == sender
|
||||
|| redacting_event
|
||||
.as_ref()
|
||||
.is_ok_and(|redacting_event| redacting_event.sender() == sender))
|
||||
},
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ use conduwuit_core::{
|
|||
state_res::{self, RoomVersion},
|
||||
},
|
||||
utils::{self, IterStream, ReadyExt, stream::TryIgnore},
|
||||
warn,
|
||||
};
|
||||
use futures::{StreamExt, TryStreamExt, future, future::ready};
|
||||
use ruma::{
|
||||
|
|
@ -19,7 +20,6 @@ use ruma::{
|
|||
uint,
|
||||
};
|
||||
use serde_json::value::{RawValue, to_raw_value};
|
||||
use tracing::warn;
|
||||
|
||||
use super::RoomMutexGuard;
|
||||
|
||||
|
|
@ -267,15 +267,35 @@ pub async fn create_hash_and_sign_event(
|
|||
| _ => Err!(Request(Unknown(warn!("Signing event failed: {e}")))),
|
||||
};
|
||||
}
|
||||
|
||||
// Check with the policy server
|
||||
// Generate event id
|
||||
pdu.event_id = gen_event_id(&pdu_json, &room_version_id)?;
|
||||
|
||||
pdu_json.insert("event_id".into(), CanonicalJsonValue::String(pdu.event_id.clone().into()));
|
||||
if room_id.is_some() {
|
||||
trace!(
|
||||
"Checking event in room {} with policy server",
|
||||
pdu.room_id.as_ref().map_or("None", |id| id.as_str())
|
||||
);
|
||||
match self
|
||||
.services
|
||||
.event_handler
|
||||
.ask_policy_server(&pdu, &pdu_json, pdu.room_id().expect("has room ID"))
|
||||
.await
|
||||
{
|
||||
| Ok(true) => {},
|
||||
| Ok(false) => {
|
||||
return Err!(Request(Forbidden(debug_warn!(
|
||||
"Policy server marked this event as spam"
|
||||
))));
|
||||
},
|
||||
| Err(e) => {
|
||||
// fail open
|
||||
warn!("Failed to check event with policy server: {e}");
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
// Check with the policy server
|
||||
// TODO(hydra): Skip this check for create events (why didnt we do this
|
||||
// already?)
|
||||
if room_id.is_some() {
|
||||
trace!(
|
||||
"Checking event {} in room {} with policy server",
|
||||
|
|
@ -285,7 +305,7 @@ pub async fn create_hash_and_sign_event(
|
|||
match self
|
||||
.services
|
||||
.event_handler
|
||||
.ask_policy_server(&pdu, &pdu.room_id_or_hash())
|
||||
.ask_policy_server(&pdu, &pdu_json, &pdu.room_id_or_hash())
|
||||
.await
|
||||
{
|
||||
| Ok(true) => {},
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue