use serde_json::{Map, Value};
use super::EventObject;
use crate::wire;
#[cfg(test)]
mod tests;
const PROTECTED_FIELDS: [&str; 16] = [
"sessionId",
"id",
"projectId",
"server",
"serverName",
"serverVersion",
"clientName",
"clientVersion",
"identifyActorGivenId",
"identifyActorName",
"identifyData",
"resourceName",
"eventType",
"actorId",
"tags",
"properties",
];
const RESTORED_FIELDS: [&str; 5] = ["id", "sessionId", "projectId", "eventType", "timestamp"];
const STRING_HOOK_FIELD: &str = "redactionFn";
const EVENT_HOOK_FIELD: &str = "eventRedactionFn";
#[derive(Clone, Copy, PartialEq, Eq)]
enum Level {
Event,
EmptyPath,
Nested,
}
pub(crate) fn redact_strings<E>(
event: &EventObject,
mut redact: impl FnMut(&str) -> Result<String, E>,
) -> Result<EventObject, E> {
redact_object(event, Level::Event, &mut redact)
}
pub(crate) fn visit_strings<'a>(event: &'a EventObject, mut visit: impl FnMut(&'a str)) {
visit_object(event, Level::Event, &mut visit);
}
fn walked(level: Level, key: &str, value: &Value) -> Option<Level> {
let top = level != Level::Nested;
let kept = (top && PROTECTED_FIELDS.contains(&key))
|| (level == Level::Event && key == "timestamp" && value.is_string());
if kept {
None
} else if top && key.is_empty() {
Some(Level::EmptyPath)
} else {
Some(Level::Nested)
}
}
fn redact_object<E, F>(
object: &Map<String, Value>,
level: Level,
redact: &mut F,
) -> Result<Map<String, Value>, E>
where
F: FnMut(&str) -> Result<String, E>,
{
let mut out = Map::with_capacity(object.len());
for (key, value) in wire::own_entries(object) {
let value = match walked(level, key, value) {
None => value.clone(),
Some(child) => redact_value(value, child, redact)?,
};
out.insert(key.clone(), value);
}
Ok(out)
}
fn redact_value<E, F>(value: &Value, level: Level, redact: &mut F) -> Result<Value, E>
where
F: FnMut(&str) -> Result<String, E>,
{
Ok(match value {
Value::String(text) => Value::String(redact(text)?),
Value::Array(items) => Value::Array(
items
.iter()
.map(|item| redact_value(item, Level::Nested, redact))
.collect::<Result<_, _>>()?,
),
Value::Object(map) => Value::Object(redact_object(map, level, redact)?),
other => other.clone(),
})
}
fn visit_object<'a, F>(object: &'a Map<String, Value>, level: Level, visit: &mut F)
where
F: FnMut(&'a str),
{
for (key, value) in wire::own_entries(object) {
if let Some(child) = walked(level, key, value) {
visit_value(value, child, visit);
}
}
}
fn visit_value<'a, F>(value: &'a Value, level: Level, visit: &mut F)
where
F: FnMut(&'a str),
{
match value {
Value::String(text) => visit(text),
Value::Array(items) => {
for item in items {
visit_value(item, Level::Nested, visit);
}
}
Value::Object(map) => visit_object(map, level, visit),
Value::Null | Value::Bool(_) | Value::Number(_) => {}
}
}
pub(crate) fn apply_event_redaction(
original: &EventObject,
hook_result: Option<EventObject>,
) -> Option<EventObject> {
let copied = || std::iter::once(STRING_HOOK_FIELD).chain(RESTORED_FIELDS);
let mut event: EventObject = hook_result?
.into_iter()
.filter(|(key, _)| {
key != EVENT_HOOK_FIELD && (original.contains_key(key) || !copied().any(|f| f == key))
})
.collect();
for field in copied() {
if let Some(value) = original.get(field) {
event.insert(field.to_owned(), value.clone());
}
}
Some(event)
}