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.
|
/// Redacts an event using the rules specified in the Matrix client-server specification.
|
||||||
///
|
///
|
||||||
/// Functionally equivalent to `redact`, only;
|
/// Functionally equivalent to `redact`, only this'll redact the event in-place.
|
||||||
/// * upon error, the event is not touched.
|
|
||||||
/// * this'll redact the event in-place.
|
|
||||||
pub fn redact_in_place(
|
pub fn redact_in_place(
|
||||||
event: &mut CanonicalJsonObject,
|
event: &mut CanonicalJsonObject,
|
||||||
version: &RoomVersionId,
|
version: &RoomVersionId,
|
||||||
@ -181,7 +179,7 @@ pub fn redact_in_place(
|
|||||||
) -> Result<(), RedactionError> {
|
) -> Result<(), RedactionError> {
|
||||||
// Get the content keys here even if they're only needed inside the branch below, because we
|
// 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")`.
|
// 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)) => {
|
Some(CanonicalJsonValue::String(event_type)) => {
|
||||||
allowed_content_keys_for(event_type, version)
|
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)),
|
_ => 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);
|
let mut old_event = mem::take(event);
|
||||||
@ -224,18 +222,58 @@ pub fn redact_content_in_place(
|
|||||||
object: &mut CanonicalJsonObject,
|
object: &mut CanonicalJsonObject,
|
||||||
version: &RoomVersionId,
|
version: &RoomVersionId,
|
||||||
event_type: impl AsRef<str>,
|
event_type: impl AsRef<str>,
|
||||||
) {
|
) -> Result<(), RedactionError> {
|
||||||
object_retain_keys(object, allowed_content_keys_for(event_type.as_ref(), version));
|
object_retain_keys(object, allowed_content_keys_for(event_type.as_ref(), version))
|
||||||
}
|
}
|
||||||
|
|
||||||
fn object_retain_keys(object: &mut CanonicalJsonObject, keys: &[&str]) {
|
fn object_retain_keys(
|
||||||
let mut old_content = mem::take(object);
|
object: &mut CanonicalJsonObject,
|
||||||
|
allowed_keys: &AllowedKeys,
|
||||||
for &key in keys {
|
) -> Result<(), RedactionError> {
|
||||||
if let Some(value) = old_content.remove(key) {
|
match *allowed_keys {
|
||||||
object.insert(key.to_owned(), value);
|
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.
|
/// The fields that are allowed to remain in an event during redaction.
|
||||||
@ -257,7 +295,78 @@ static ALLOWED_KEYS: &[&str] = &[
|
|||||||
"membership",
|
"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 {
|
match event_type {
|
||||||
"m.room.member" => match version {
|
"m.room.member" => match version {
|
||||||
RoomVersionId::V1
|
RoomVersionId::V1
|
||||||
@ -267,10 +376,10 @@ fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'stat
|
|||||||
| RoomVersionId::V5
|
| RoomVersionId::V5
|
||||||
| RoomVersionId::V6
|
| RoomVersionId::V6
|
||||||
| RoomVersionId::V7
|
| RoomVersionId::V7
|
||||||
| RoomVersionId::V8 => &["membership"],
|
| RoomVersionId::V8 => &ROOM_MEMBER_V1,
|
||||||
_ => &["membership", "join_authorised_via_users_server"],
|
_ => &ROOM_MEMBER_V9,
|
||||||
},
|
},
|
||||||
"m.room.create" => &["creator"],
|
"m.room.create" => &ROOM_CREATE_V1,
|
||||||
"m.room.join_rules" => match version {
|
"m.room.join_rules" => match version {
|
||||||
RoomVersionId::V1
|
RoomVersionId::V1
|
||||||
| RoomVersionId::V2
|
| RoomVersionId::V2
|
||||||
@ -278,35 +387,24 @@ fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'stat
|
|||||||
| RoomVersionId::V4
|
| RoomVersionId::V4
|
||||||
| RoomVersionId::V5
|
| RoomVersionId::V5
|
||||||
| RoomVersionId::V6
|
| RoomVersionId::V6
|
||||||
| RoomVersionId::V7 => &["join_rule"],
|
| RoomVersionId::V7 => &ROOM_JOIN_RULES_V1,
|
||||||
_ => &["join_rule", "allow"],
|
_ => &ROOM_JOIN_RULES_V8,
|
||||||
},
|
},
|
||||||
"m.room.power_levels" => &[
|
"m.room.power_levels" => &ROOM_POWER_LEVELS_V1,
|
||||||
"ban",
|
|
||||||
"events",
|
|
||||||
"events_default",
|
|
||||||
"kick",
|
|
||||||
"redact",
|
|
||||||
"state_default",
|
|
||||||
"users",
|
|
||||||
"users_default",
|
|
||||||
],
|
|
||||||
"m.room.aliases" => match version {
|
"m.room.aliases" => match version {
|
||||||
RoomVersionId::V1
|
RoomVersionId::V1
|
||||||
| RoomVersionId::V2
|
| RoomVersionId::V2
|
||||||
| RoomVersionId::V3
|
| RoomVersionId::V3
|
||||||
| RoomVersionId::V4
|
| RoomVersionId::V4
|
||||||
| RoomVersionId::V5 => &["aliases"],
|
| RoomVersionId::V5 => &ROOM_ALIASES_V1,
|
||||||
// All other room versions, including custom ones, are treated by version 6 rules.
|
// All other room versions, including custom ones, are treated by version 6 rules.
|
||||||
// TODO: Should we return an error for unknown versions instead?
|
// TODO: Should we return an error for unknown versions instead?
|
||||||
_ => &[],
|
_ => &AllowedKeys::None,
|
||||||
},
|
},
|
||||||
#[cfg(feature = "unstable-msc2870")]
|
#[cfg(feature = "unstable-msc2870")]
|
||||||
"m.room.server_acl" if version.as_str() == "org.matrix.msc2870" => {
|
"m.room.server_acl" if version.as_str() == "org.matrix.msc2870" => &ROOM_SERVER_ACL_MSC2870,
|
||||||
&["allow", "deny", "allow_ip_literals"]
|
"m.room.history_visibility" => &ROOM_HISTORY_VISIBILITY_V1,
|
||||||
}
|
_ => &AllowedKeys::None,
|
||||||
"m.room.history_visibility" => &["history_visibility"],
|
|
||||||
_ => &[],
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -314,10 +412,16 @@ fn allowed_content_keys_for(event_type: &str, version: &RoomVersionId) -> &'stat
|
|||||||
mod tests {
|
mod tests {
|
||||||
use std::collections::BTreeMap;
|
use std::collections::BTreeMap;
|
||||||
|
|
||||||
|
use assert_matches2::assert_matches;
|
||||||
use js_int::int;
|
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]
|
#[test]
|
||||||
fn serialize_canon() {
|
fn serialize_canon() {
|
||||||
@ -407,4 +511,117 @@ mod tests {
|
|||||||
|
|
||||||
assert_eq!(to_canonical_value(t).unwrap(), CanonicalJsonValue::Object(expected));
|
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