ruma_common/canonical_json/
redaction.rs1use std::mem;
2
3mod serializer;
4
5pub use self::serializer::RedactingSerializer;
6use super::{
7 CanonicalJsonFieldError, CanonicalJsonObject, CanonicalJsonObjectExt, CanonicalJsonValue,
8};
9use crate::{room_version_rules::RedactionRules, serde::Raw};
10
11pub fn redact(
36 mut object: CanonicalJsonObject,
37 rules: &RedactionRules,
38 redacted_because: Option<RedactedBecause>,
39) -> Result<CanonicalJsonObject, CanonicalJsonFieldError> {
40 redact_in_place(&mut object, rules, redacted_because)?;
41 Ok(object)
42}
43
44pub fn redact_in_place(
48 event: &mut CanonicalJsonObject,
49 rules: &RedactionRules,
50 redacted_because: Option<RedactedBecause>,
51) -> Result<(), CanonicalJsonFieldError> {
52 retained_event_keys(event)?.apply(rules, event);
53
54 if let Some(redacted_because) = redacted_because {
55 let unsigned = CanonicalJsonObject::from_iter([(
56 "redacted_because".to_owned(),
57 redacted_because.0.into(),
58 )]);
59 event.insert("unsigned".to_owned(), unsigned.into());
60 }
61
62 Ok(())
63}
64
65pub fn redact_content_in_place(
70 content: &mut CanonicalJsonObject,
71 rules: &RedactionRules,
72 event_type: impl AsRef<str>,
73) {
74 retained_event_content_keys(event_type.as_ref(), rules).apply(rules, content);
75}
76
77#[derive(Clone, Debug)]
79pub struct RedactedBecause(CanonicalJsonObject);
80
81impl RedactedBecause {
82 pub fn from_json(obj: CanonicalJsonObject) -> Self {
84 Self(obj)
85 }
86
87 pub fn from_raw_event(ev: &Raw<impl RedactionEvent>) -> serde_json::Result<Self> {
91 ev.deserialize_as_unchecked().map(Self)
92 }
93}
94
95pub trait RedactionEvent {}
97
98type RetainKeyFn = dyn Fn(&RedactionRules, &str) -> RetainKey;
101
102enum RetainKey {
104 Yes {
106 child_retained_keys: Option<RetainedKeys>,
110 },
111
112 No,
114}
115
116impl From<bool> for RetainKey {
117 fn from(value: bool) -> Self {
118 if value { Self::Yes { child_retained_keys: None } } else { Self::No }
119 }
120}
121
122enum RetainedKeys {
124 All,
126
127 Some(Box<RetainKeyFn>),
129
130 None,
132}
133
134impl RetainedKeys {
135 fn some<F>(retain_key_fn: F) -> Self
137 where
138 F: Fn(&RedactionRules, &str) -> RetainKey + Clone + 'static,
139 {
140 Self::Some(Box::new(retain_key_fn))
141 }
142
143 fn should_retain_key(&self, rules: &RedactionRules, key: &str) -> RetainKey {
145 match self {
146 Self::All => true.into(),
147 Self::Some(retain_key_fn) => retain_key_fn(rules, key),
148 Self::None => false.into(),
149 }
150 }
151
152 fn apply(&self, rules: &RedactionRules, object: &mut CanonicalJsonObject) {
154 match self {
155 Self::All => {}
156 Self::Some(retain_key_fn) => {
157 let old_object = mem::take(object);
158
159 for (key, mut value) in old_object {
160 if let RetainKey::Yes { child_retained_keys } = retain_key_fn(rules, &key) {
161 if let Some(child_retained_keys) = child_retained_keys
162 && let CanonicalJsonValue::Object(child_object) = &mut value
163 {
164 child_retained_keys.apply(rules, child_object);
165 }
166
167 object.insert(key, value);
168 }
169 }
170 }
171 Self::None => object.clear(),
172 }
173 }
174}
175
176fn retained_event_keys(
178 event: &CanonicalJsonObject,
179) -> Result<RetainedKeys, CanonicalJsonFieldError> {
180 let event_type = event.get_as_required_string("type", "type")?.to_owned();
181
182 Ok(RetainedKeys::some(move |rules, key| match key {
183 "content" => RetainKey::Yes {
184 child_retained_keys: Some(retained_event_content_keys(&event_type, rules)),
185 },
186 "event_id" | "type" | "room_id" | "sender" | "state_key" | "hashes" | "signatures"
187 | "depth" | "prev_events" | "auth_events" | "origin_server_ts" => true.into(),
188 "origin" | "membership" | "prev_state" => rules.keep_origin_membership_prev_state.into(),
189 _ => false.into(),
190 }))
191}
192
193fn retained_event_content_keys(event_type: &str, rules: &RedactionRules) -> RetainedKeys {
195 match event_type {
196 "m.room.member" => RetainedKeys::some(is_room_member_content_key_retained),
197 "m.room.create" => room_create_content_retained_keys(rules),
198 "m.room.join_rules" => RetainedKeys::some(is_room_join_rules_content_key_retained),
199 "m.room.power_levels" => RetainedKeys::some(is_room_power_levels_content_key_retained),
200 "m.room.history_visibility" => {
201 RetainedKeys::some(|_rules, key| is_room_history_visibility_content_key_retained(key))
202 }
203 "m.room.redaction" => room_redaction_content_retained_keys(rules),
204 "m.room.aliases" => room_aliases_content_retained_keys(rules),
205 #[cfg(feature = "unstable-msc2870")]
206 "m.room.server_acl" => RetainedKeys::some(is_room_server_acl_content_key_retained),
207 _ => RetainedKeys::None,
208 }
209}
210
211fn is_room_member_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
213 match key {
214 "membership" => true.into(),
215 "join_authorised_via_users_server" => {
216 rules.keep_room_member_join_authorised_via_users_server.into()
217 }
218 "third_party_invite" if rules.keep_room_member_third_party_invite_signed => {
219 RetainKey::Yes {
220 child_retained_keys: Some(RetainedKeys::some(|_rules, key| {
221 (key == "signed").into()
222 })),
223 }
224 }
225 _ => false.into(),
226 }
227}
228
229fn room_create_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
231 if rules.keep_room_create_content {
232 RetainedKeys::All
233 } else {
234 RetainedKeys::some(|_rules, field| (field == "creator").into())
235 }
236}
237
238fn is_room_join_rules_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
241 match key {
242 "join_rule" => true,
243 "allow" => rules.keep_room_join_rules_allow,
244 _ => false,
245 }
246 .into()
247}
248
249fn is_room_power_levels_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
252 match key {
253 "ban" | "events" | "events_default" | "kick" | "redact" | "state_default" | "users"
254 | "users_default" => true,
255 "invite" => rules.keep_room_power_levels_invite,
256 _ => false,
257 }
258 .into()
259}
260
261fn is_room_history_visibility_content_key_retained(key: &str) -> RetainKey {
264 (key == "history_visibility").into()
265}
266
267fn room_redaction_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
269 if rules.keep_room_redaction_redacts {
270 RetainedKeys::some(|_rules, field| (field == "redacts").into())
271 } else {
272 RetainedKeys::None
273 }
274}
275
276fn room_aliases_content_retained_keys(rules: &RedactionRules) -> RetainedKeys {
278 if rules.keep_room_aliases_aliases {
279 RetainedKeys::some(|_rules, field| (field == "aliases").into())
280 } else {
281 RetainedKeys::None
282 }
283}
284
285#[cfg(feature = "unstable-msc2870")]
288fn is_room_server_acl_content_key_retained(rules: &RedactionRules, key: &str) -> RetainKey {
289 match key {
290 "allow" | "deny" | "allow_ip_literals" => {
291 rules.keep_room_server_acl_allow_deny_allow_ip_literals
292 }
293 _ => false,
294 }
295 .into()
296}