From 2ef75a572c597e2925d62ba429090f19723aab9a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?K=C3=A9vin=20Commaille?= Date: Fri, 11 Aug 2023 15:05:09 +0200 Subject: [PATCH] canonical-json: Allow to preserve all keys and nested keys --- crates/ruma-common/src/canonical_json.rs | 293 ++++++++++++++++++++--- 1 file changed, 255 insertions(+), 38 deletions(-) diff --git a/crates/ruma-common/src/canonical_json.rs b/crates/ruma-common/src/canonical_json.rs index f67849b5..41c6e2aa 100644 --- a/crates/ruma-common/src/canonical_json.rs +++ b/crates/ruma-common/src/canonical_json.rs @@ -171,9 +171,7 @@ pub fn redact( /// Redacts an event using the rules specified in the Matrix client-server specification. /// -/// Functionally equivalent to `redact`, only; -/// * upon error, the event is not touched. -/// * this'll redact the event in-place. +/// Functionally equivalent to `redact`, only this'll redact the event in-place. pub fn redact_in_place( event: &mut CanonicalJsonObject, version: &RoomVersionId, @@ -181,7 +179,7 @@ pub fn redact_in_place( ) -> Result<(), RedactionError> { // Get the content keys here even if they're only needed inside the branch below, because we // can't teach rust that this is a disjoint borrow with `get_mut("content")`. - let allowed_content_keys: &[&str] = match event.get("type") { + let allowed_content_keys = match event.get("type") { Some(CanonicalJsonValue::String(event_type)) => { allowed_content_keys_for(event_type, version) } @@ -195,7 +193,7 @@ pub fn redact_in_place( _ => return Err(RedactionError::not_of_type("content", JsonType::Object)), }; - object_retain_keys(content, allowed_content_keys); + object_retain_keys(content, allowed_content_keys)?; } let mut old_event = mem::take(event); @@ -224,18 +222,58 @@ pub fn redact_content_in_place( object: &mut CanonicalJsonObject, version: &RoomVersionId, event_type: impl AsRef, -) { - object_retain_keys(object, allowed_content_keys_for(event_type.as_ref(), version)); +) -> Result<(), RedactionError> { + object_retain_keys(object, allowed_content_keys_for(event_type.as_ref(), version)) } -fn object_retain_keys(object: &mut CanonicalJsonObject, keys: &[&str]) { - let mut old_content = mem::take(object); - - for &key in keys { - if let Some(value) = old_content.remove(key) { - object.insert(key.to_owned(), value); +fn object_retain_keys( + object: &mut CanonicalJsonObject, + allowed_keys: &AllowedKeys, +) -> Result<(), RedactionError> { + match *allowed_keys { + AllowedKeys::All => {} + AllowedKeys::Some { keys, nested } => { + object_retain_some_keys(object, keys, nested)?; + } + AllowedKeys::None => { + object.clear(); } } + + Ok(()) +} + +fn object_retain_some_keys( + object: &mut CanonicalJsonObject, + keys: &[&str], + nested: &[(&str, &AllowedKeys)], +) -> Result<(), RedactionError> { + let mut old_object = mem::take(object); + + for &(nested_key, nested_allowed_keys) in nested { + if let Some((key, mut nested_object_value)) = old_object.remove_entry(nested_key) { + let nested_object = match &mut nested_object_value { + CanonicalJsonValue::Object(map) => map, + _ => return Err(RedactionError::not_of_type(nested_key, JsonType::Object)), + }; + + object_retain_keys(nested_object, nested_allowed_keys)?; + + // If the object is empty, it means none of the nested fields were found so we + // don't want to keep the object. + if !nested_object.is_empty() { + object.insert(key, nested_object_value); + } + } + } + + for &key in keys { + if let Some((key, value)) = old_object.remove_entry(key) { + object.insert(key, value); + } + } + + Ok(()) } /// The fields that are allowed to remain in an event during redaction. @@ -257,7 +295,78 @@ static ALLOWED_KEYS: &[&str] = &[ "membership", ]; -fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'static [&'static str] { +/// List of keys to preserve on an object. +#[derive(Clone, Copy)] +enum AllowedKeys { + /// All keys are preserved. + All, + /// Some keys are preserved. + Some { + /// The keys to preserve on this object. + keys: &'static [&'static str], + + /// Keys to preserve on nested objects. + /// + /// A list of `(nested_object_key, nested_allowed_keys)`. + nested: &'static [(&'static str, &'static AllowedKeys)], + }, + /// No keys are preserved. + None, +} + +impl AllowedKeys { + /// Creates an new `AllowedKeys::Some` with the given keys at this level. + const fn some(keys: &'static [&'static str]) -> Self { + Self::Some { keys, nested: &[] } + } + + /// Creates an new `AllowedKeys::Some` with the given keys and nested keys. + const fn some_nested( + keys: &'static [&'static str], + nested: &'static [(&'static str, &'static AllowedKeys)], + ) -> Self { + Self::Some { keys, nested } + } +} + +/// Allowed keys in `m.room.member`'s content according to room version 1. +static ROOM_MEMBER_V1: AllowedKeys = AllowedKeys::some(&["membership"]); +/// Allowed keys in `m.room.member`'s content according to room version 9. +static ROOM_MEMBER_V9: AllowedKeys = + AllowedKeys::some(&["membership", "join_authorised_via_users_server"]); + +/// Allowed keys in `m.room.create`'s content according to room version 1. +static ROOM_CREATE_V1: AllowedKeys = AllowedKeys::some(&["creator"]); + +/// Allowed keys in `m.room.join_rules`'s content according to room version 1. +static ROOM_JOIN_RULES_V1: AllowedKeys = AllowedKeys::some(&["join_rule"]); +/// Allowed keys in `m.room.join_rules`'s content according to room version 8. +static ROOM_JOIN_RULES_V8: AllowedKeys = AllowedKeys::some(&["join_rule", "allow"]); + +/// Allowed keys in `m.room.power_levels`'s content according to room version 1. +static ROOM_POWER_LEVELS_V1: AllowedKeys = AllowedKeys::some(&[ + "ban", + "events", + "events_default", + "kick", + "redact", + "state_default", + "users", + "users_default", +]); + +/// Allowed keys in `m.room.aliases`'s content according to room version 1. +static ROOM_ALIASES_V1: AllowedKeys = AllowedKeys::some(&["aliases"]); + +/// Allowed keys in `m.room.server_acl`'s content according to MSC2870. +#[cfg(feature = "unstable-msc2870")] +static ROOM_SERVER_ACL_MSC2870: AllowedKeys = + AllowedKeys::some(&["allow", "deny", "allow_ip_literals"]); + +/// Allowed keys in `m.room.history_visibility`'s content according to room version 1. +static ROOM_HISTORY_VISIBILITY_V1: AllowedKeys = AllowedKeys::some(&["history_visibility"]); + +fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'static AllowedKeys { match event_type { "m.room.member" => match version { RoomVersionId::V1 @@ -267,10 +376,10 @@ fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'stat | RoomVersionId::V5 | RoomVersionId::V6 | RoomVersionId::V7 - | RoomVersionId::V8 => &["membership"], - _ => &["membership", "join_authorised_via_users_server"], + | RoomVersionId::V8 => &ROOM_MEMBER_V1, + _ => &ROOM_MEMBER_V9, }, - "m.room.create" => &["creator"], + "m.room.create" => &ROOM_CREATE_V1, "m.room.join_rules" => match version { RoomVersionId::V1 | RoomVersionId::V2 @@ -278,35 +387,24 @@ fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'stat | RoomVersionId::V4 | RoomVersionId::V5 | RoomVersionId::V6 - | RoomVersionId::V7 => &["join_rule"], - _ => &["join_rule", "allow"], + | RoomVersionId::V7 => &ROOM_JOIN_RULES_V1, + _ => &ROOM_JOIN_RULES_V8, }, - "m.room.power_levels" => &[ - "ban", - "events", - "events_default", - "kick", - "redact", - "state_default", - "users", - "users_default", - ], + "m.room.power_levels" => &ROOM_POWER_LEVELS_V1, "m.room.aliases" => match version { RoomVersionId::V1 | RoomVersionId::V2 | RoomVersionId::V3 | RoomVersionId::V4 - | RoomVersionId::V5 => &["aliases"], + | RoomVersionId::V5 => &ROOM_ALIASES_V1, // All other room versions, including custom ones, are treated by version 6 rules. // TODO: Should we return an error for unknown versions instead? - _ => &[], + _ => &AllowedKeys::None, }, #[cfg(feature = "unstable-msc2870")] - "m.room.server_acl" if version.as_str() == "org.matrix.msc2870" => { - &["allow", "deny", "allow_ip_literals"] - } - "m.room.history_visibility" => &["history_visibility"], - _ => &[], + "m.room.server_acl" if version.as_str() == "org.matrix.msc2870" => &ROOM_SERVER_ACL_MSC2870, + "m.room.history_visibility" => &ROOM_HISTORY_VISIBILITY_V1, + _ => &AllowedKeys::None, } } @@ -314,10 +412,16 @@ fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'stat mod tests { use std::collections::BTreeMap; + use assert_matches2::assert_matches; use js_int::int; - use serde_json::{from_str as from_json_str, json, to_string as to_json_string}; + use serde_json::{ + from_str as from_json_str, json, to_string as to_json_string, to_value as to_json_value, + }; - use super::{to_canonical_value, try_from_json_map, value::CanonicalJsonValue}; + use super::{ + redact_in_place, to_canonical_value, try_from_json_map, value::CanonicalJsonValue, + }; + use crate::RoomVersionId; #[test] fn serialize_canon() { @@ -407,4 +511,117 @@ mod tests { assert_eq!(to_canonical_value(t).unwrap(), CanonicalJsonValue::Object(expected)); } + + #[test] + fn redact_allowed_keys_some() { + let original_event = json!({ + "content": { + "ban": 50, + "events": { + "m.room.avatar": 50, + "m.room.canonical_alias": 50, + "m.room.history_visibility": 100, + "m.room.name": 50, + "m.room.power_levels": 100 + }, + "events_default": 0, + "invite": 0, + "kick": 50, + "redact": 50, + "state_default": 50, + "users": { + "@example:localhost": 100 + }, + "users_default": 0 + }, + "event_id": "$15139375512JaHAW:localhost", + "origin_server_ts": 45, + "sender": "@example:localhost", + "room_id": "!room:localhost", + "state_key": "", + "type": "m.room.power_levels", + "unsigned": { + "age": 45 + } + }); + + assert_matches!( + CanonicalJsonValue::try_from(original_event), + Ok(CanonicalJsonValue::Object(mut object)) + ); + + redact_in_place(&mut object, &RoomVersionId::V1, None).unwrap(); + + let redacted_event = to_json_value(&object).unwrap(); + + assert_eq!( + redacted_event, + json!({ + "content": { + "ban": 50, + "events": { + "m.room.avatar": 50, + "m.room.canonical_alias": 50, + "m.room.history_visibility": 100, + "m.room.name": 50, + "m.room.power_levels": 100 + }, + "events_default": 0, + "kick": 50, + "redact": 50, + "state_default": 50, + "users": { + "@example:localhost": 100 + }, + "users_default": 0 + }, + "event_id": "$15139375512JaHAW:localhost", + "origin_server_ts": 45, + "sender": "@example:localhost", + "room_id": "!room:localhost", + "state_key": "", + "type": "m.room.power_levels", + }) + ); + } + + #[test] + fn redact_allowed_keys_none() { + let original_event = json!({ + "content": { + "aliases": ["#somewhere:localhost"] + }, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1, + "sender": "@example:localhost", + "state_key": "room.com", + "room_id": "!room:room.com", + "type": "m.room.aliases", + "unsigned": { + "age": 1 + } + }); + + assert_matches!( + CanonicalJsonValue::try_from(original_event), + Ok(CanonicalJsonValue::Object(mut object)) + ); + + redact_in_place(&mut object, &RoomVersionId::V10, None).unwrap(); + + let redacted_event = to_json_value(&object).unwrap(); + + assert_eq!( + redacted_event, + json!({ + "content": {}, + "event_id": "$152037280074GZeOm:localhost", + "origin_server_ts": 1, + "sender": "@example:localhost", + "state_key": "room.com", + "room_id": "!room:room.com", + "type": "m.room.aliases", + }) + ); + } }