canonical-json: Allow to preserve all keys and nested keys
This commit is contained in:
parent
bb6edd26bb
commit
2ef75a572c
@ -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<str>,
|
||||
) {
|
||||
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);
|
||||
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(value) = old_content.remove(key) {
|
||||
object.insert(key.to_owned(), value);
|
||||
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",
|
||||
})
|
||||
);
|
||||
}
|
||||
}
|
||||
|
Loading…
x
Reference in New Issue
Block a user