Refactor redaction permission checks

This commit is contained in:
Olivia Lee 2025-07-19 22:26:49 -07:00 committed by Charles Hall
parent 61d2ff7183
commit aade2e17c2
4 changed files with 83 additions and 102 deletions

View file

@ -20,7 +20,7 @@ use serde_json::{
}; };
use tracing::warn; use tracing::warn;
use crate::Error; use crate::{utils::room_version::RoomVersion, Error};
/// Content hashes of a PDU. /// Content hashes of a PDU.
#[derive(Clone, Debug, Deserialize, Serialize)] #[derive(Clone, Debug, Deserialize, Serialize)]
@ -104,6 +104,35 @@ impl PduEvent {
unsigned.redacted_because.is_some() unsigned.redacted_because.is_some()
} }
/// Returns the redaction target of an event, if present.
///
/// For versions <v11, the redaction target is a top-level property. For
/// versions >=v11, it is part of the room content.
// Allowed because we don't use state_res::Event anywhere in grapevine, it's
// just implemented so we can pass Pdu to ruma stateres.
#[expect(clippy::same_name_method)]
pub(crate) fn redacts(
&self,
room_version: &RoomVersion,
) -> Option<Cow<'_, EventId>> {
if room_version.redaction_event_redacts_in_content {
#[derive(Deserialize)]
struct ExtractRedacts<'a> {
#[serde(borrow)]
redacts: Option<Cow<'a, EventId>>,
}
let extract =
serde_json::from_str::<ExtractRedacts<'_>>(self.content.get())
.map_err(|_| {
Error::bad_database("Invalid content in redaction pdu.")
})
.ok()?;
extract.redacts
} else {
self.redacts.as_ref().map(|e| Cow::Borrowed(&**e))
}
}
pub(crate) fn remove_transaction_id(&mut self) -> crate::Result<()> { pub(crate) fn remove_transaction_id(&mut self) -> crate::Result<()> {
if let Some(unsigned) = &self.unsigned { if let Some(unsigned) = &self.unsigned {
let mut unsigned: BTreeMap<String, Box<RawJsonValue>> = let mut unsigned: BTreeMap<String, Box<RawJsonValue>> =

View file

@ -19,11 +19,8 @@ use ruma::{
}, },
}, },
events::{ events::{
room::{ room::server_acl::RoomServerAclEventContent, StateEventType,
redaction::RoomRedactionEventContent, TimelineEventType,
server_acl::RoomServerAclEventContent,
},
StateEventType, TimelineEventType,
}, },
int, int,
state_res::{self, StateMap}, state_res::{self, StateMap},
@ -43,7 +40,7 @@ use super::{
use crate::{ use crate::{
service::{globals::SigningKeys, pdu, rooms::state::ExtractVersion}, service::{globals::SigningKeys, pdu, rooms::state::ExtractVersion},
services, services,
utils::{debug_slice_truncated, room_version::RoomVersion}, utils::debug_slice_truncated,
Error, PduEvent, Result, Error, PduEvent, Result,
}; };
@ -568,7 +565,6 @@ impl Service {
"Upgrading event to timeline pdu", "Upgrading event to timeline pdu",
); );
let room_version = RoomVersion::try_from(room_version_id)?;
let ruma_room_version = state_res::RoomVersion::new(room_version_id) let ruma_room_version = state_res::RoomVersion::new(room_version_id)
.map_err(|_| { .map_err(|_| {
Error::UnsupportedRoomVersion(room_version_id.clone()) Error::UnsupportedRoomVersion(room_version_id.clone())
@ -925,33 +921,10 @@ impl Service {
Error::BadRequest(ErrorKind::InvalidParam, "Auth check failed.") Error::BadRequest(ErrorKind::InvalidParam, "Auth check failed.")
})? || incoming_pdu.kind })? || incoming_pdu.kind
== TimelineEventType::RoomRedaction == TimelineEventType::RoomRedaction
&& if room_version.redaction_event_redacts_in_content { && services()
let content = .rooms
serde_json::from_str::<RoomRedactionEventContent>( .state_accessor
incoming_pdu.content.get(), .redaction_event_allowed(&incoming_pdu)?;
)
.map_err(|_| {
Error::bad_database("Invalid content in redaction pdu.")
})?;
if let Some(redact_id) = &content.redacts {
!services().rooms.state_accessor.user_can_redact(
redact_id,
&incoming_pdu.sender,
&incoming_pdu.room_id,
)?
} else {
false
}
} else if let Some(redact_id) = &incoming_pdu.redacts {
!services().rooms.state_accessor.user_can_redact(
redact_id,
&incoming_pdu.sender,
&incoming_pdu.room_id,
)?
} else {
false
};
// 13. Use state resolution to find new room state // 13. Use state resolution to find new room state

View file

@ -14,7 +14,7 @@ use ruma::{
name::RoomNameEventContent, name::RoomNameEventContent,
power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent}, power_levels::{RoomPowerLevels, RoomPowerLevelsEventContent},
}, },
StateEventType, StateEventType, TimelineEventType,
}, },
state_res::Event, state_res::Event,
EventId, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, ServerName, EventId, OwnedRoomId, OwnedServerName, OwnedUserId, RoomId, ServerName,
@ -26,9 +26,9 @@ use tracing::{error, warn};
use super::short::{ShortStateHash, ShortStateKey}; use super::short::{ShortStateHash, ShortStateKey};
use crate::{ use crate::{
observability::{FoundIn, Lookup, METRICS}, observability::{FoundIn, Lookup, METRICS},
service::{globals::marker, pdu::PduBuilder}, service::{globals::marker, pdu::PduBuilder, rooms::state::ExtractVersion},
services, services,
utils::on_demand_hashmap::KeyToken, utils::{on_demand_hashmap::KeyToken, room_version::RoomVersion},
Error, PduEvent, Result, Error, PduEvent, Result,
}; };
@ -521,7 +521,7 @@ impl Service {
.expect("Event content always serializes"); .expect("Event content always serializes");
let new_event = PduBuilder { let new_event = PduBuilder {
event_type: ruma::events::TimelineEventType::RoomMember, event_type: TimelineEventType::RoomMember,
content, content,
unsigned: None, unsigned: None,
state_key: Some(target_user.into()), state_key: Some(target_user.into()),
@ -592,4 +592,31 @@ impl Service {
}, },
) )
} }
/// Checks whether a redaction event is authorized against the current state
/// of it's room.
///
/// Panics if `event` is not a `m.room.redaction` event.
pub(crate) fn redaction_event_allowed(
&self,
event: &PduEvent,
) -> Result<bool> {
assert_eq!(
event.kind,
TimelineEventType::RoomRedaction,
"event must be a redaction event"
);
let room_version_id = services()
.rooms
.state
.get_create_content::<ExtractVersion>(&event.room_id)?;
let room_version = RoomVersion::try_from(&room_version_id)?;
let Some(redacted_id) = event.redacts(&room_version) else {
return Ok(false);
};
self.user_can_redact(&redacted_id, &event.sender, &event.room_id)
}
} }

View file

@ -14,7 +14,6 @@ use ruma::{
create::RoomCreateEventContent, encrypted::Relation, create::RoomCreateEventContent, encrypted::Relation,
member::MembershipState, message::RoomMessageEventContent, member::MembershipState, message::RoomMessageEventContent,
power_levels::RoomPowerLevelsEventContent, power_levels::RoomPowerLevelsEventContent,
redaction::RoomRedactionEventContent,
}, },
StateEventType, TimelineEventType, StateEventType, TimelineEventType,
}, },
@ -472,29 +471,14 @@ impl Service {
.state .state
.get_create_content::<ExtractVersion>(&pdu.room_id)?; .get_create_content::<ExtractVersion>(&pdu.room_id)?;
let room_version = RoomVersion::try_from(&room_version_id)?; let room_version = RoomVersion::try_from(&room_version_id)?;
if room_version.redaction_event_redacts_in_content {
let content = serde_json::from_str::< if let Some(redact_id) = pdu.redacts(&room_version) {
RoomRedactionEventContent, if services()
>(pdu.content.get()) .rooms
.map_err(|_| { .state_accessor
Error::bad_database("Invalid content in redaction pdu.") .redaction_event_allowed(pdu)?
})?; {
if let Some(redact_id) = &content.redacts { self.redact_pdu(&redact_id, pdu, shortroomid)?;
if services().rooms.state_accessor.user_can_redact(
redact_id,
&pdu.sender,
&pdu.room_id,
)? {
self.redact_pdu(redact_id, pdu, shortroomid)?;
}
}
} else if let Some(redact_id) = &pdu.redacts {
if services().rooms.state_accessor.user_can_redact(
redact_id,
&pdu.sender,
&pdu.room_id,
)? {
self.redact_pdu(redact_id, pdu, shortroomid)?;
} }
} }
} }
@ -1056,45 +1040,13 @@ impl Service {
// If redaction event is not authorized, do not append it to the // If redaction event is not authorized, do not append it to the
// timeline // timeline
if pdu.kind == TimelineEventType::RoomRedaction { if pdu.kind == TimelineEventType::RoomRedaction
let room_version_id = services() && !services().rooms.state_accessor.redaction_event_allowed(&pdu)?
.rooms {
.state return Err(Error::BadRequest(
.get_create_content::<ExtractVersion>(&pdu.room_id)?; ErrorKind::forbidden(),
let room_version = RoomVersion::try_from(&room_version_id)?; "User cannot redact this event.",
if room_version.redaction_event_redacts_in_content { ));
let content =
serde_json::from_str::<RoomRedactionEventContent>(
pdu.content.get(),
)
.map_err(|_| {
Error::bad_database("Invalid content in redaction pdu.")
})?;
if let Some(redact_id) = &content.redacts {
if !services().rooms.state_accessor.user_can_redact(
redact_id,
&pdu.sender,
&pdu.room_id,
)? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"User cannot redact this event.",
));
}
}
} else if let Some(redact_id) = &pdu.redacts {
if !services().rooms.state_accessor.user_can_redact(
redact_id,
&pdu.sender,
&pdu.room_id,
)? {
return Err(Error::BadRequest(
ErrorKind::forbidden(),
"User cannot redact this event.",
));
}
}
} }
// We append to state before appending the pdu, so we don't have a // We append to state before appending the pdu, so we don't have a