about summary refs log tree commit diff
diff options
context:
space:
mode:
authorVika <vika@fireburn.ru>2023-07-09 01:27:13 +0300
committerVika <vika@fireburn.ru>2023-07-09 01:27:13 +0300
commitdd10254f36409df57d7cd9ab30e7af139121a428 (patch)
tree0a2cc11aa6a9d63de2b955c9fc441f5dc4c0ad6b
parent82346b89c7d166817f3450759cc9f6df1ec7c74d (diff)
downloadkittybox-dd10254f36409df57d7cd9ab30e7af139121a428.tar.zst
frontend: filter out privacy-sensitive information from posts
This was the job of the database before. Now the frontend should do it
before passing the post to the templates.
-rw-r--r--kittybox-rs/src/frontend/mod.rs126
1 files changed, 124 insertions, 2 deletions
diff --git a/kittybox-rs/src/frontend/mod.rs b/kittybox-rs/src/frontend/mod.rs
index ed3932b..7a43532 100644
--- a/kittybox-rs/src/frontend/mod.rs
+++ b/kittybox-rs/src/frontend/mod.rs
@@ -70,10 +70,116 @@ impl std::error::Error for FrontendError {
 
 impl std::fmt::Display for FrontendError {
     fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
-        write!(f, "{}", self.msg)
+        write!(f, "{}", self.msg)?;
+        if let Some(err) = std::error::Error::source(&self) {
+            write!(f, ": {}", err)?;
+        }
+
+        Ok(())
     }
 }
 
+/// Filter the post according to the value of `user`.
+///
+/// Anonymous users cannot view private posts and protected locations;
+/// Logged-in users can only view private posts targeted at them;
+/// Logged-in users can't view private location data
+#[tracing::instrument(skip(post), fields(post = %post))]
+pub fn filter_post(
+    mut post: serde_json::Value,
+    user: Option<&str>,
+) -> Option<serde_json::Value> {
+    if post["properties"]["deleted"][0].is_string() {
+        tracing::debug!("Deleted post; returning tombstone instead");
+        return Some(serde_json::json!({
+            "type": post["type"],
+            "properties": {
+                "deleted": post["properties"]["deleted"]
+            }
+        }));
+    }
+    let empty_vec: Vec<serde_json::Value> = vec![];
+    let author_list = post["properties"]["author"]
+        .as_array()
+        .unwrap_or(&empty_vec)
+        .iter()
+        .map(|i| -> &str {
+            match i {
+                serde_json::Value::String(ref author) => author.as_str(),
+                mf2 => mf2["properties"]["uid"][0].as_str().unwrap()
+            }
+        }).collect::<Vec<&str>>();
+    let visibility = post["properties"]["visibility"][0]
+        .as_str()
+        .unwrap_or("public");
+    let audience = {
+        let mut audience = author_list.clone();
+        audience.extend(post["properties"]["audience"]
+            .as_array()
+            .unwrap_or(&empty_vec)
+            .iter()
+            .map(|i| i.as_str().unwrap()));
+
+        audience
+    };
+    tracing::debug!("post audience = {:?}", audience);
+    if (visibility == "private" && !audience.iter().any(|i| Some(*i) == user))
+        || (visibility == "protected" && user.is_none())
+    {
+        return None;
+    }
+    if post["properties"]["location"].is_array() {
+        let location_visibility = post["properties"]["location-visibility"][0]
+            .as_str()
+            .unwrap_or("private");
+        tracing::debug!("Post contains location, location privacy = {}", location_visibility);
+        let mut author = post["properties"]["author"]
+            .as_array()
+            .unwrap_or(&empty_vec)
+            .iter()
+            .map(|i| i.as_str().unwrap());
+        if (location_visibility == "private" && !author.any(|i| Some(i) == user))
+            || (location_visibility == "protected" && user.is_none())
+        {
+            post["properties"]
+                .as_object_mut()
+                .unwrap()
+                .remove("location");
+        }
+    }
+
+    match post["properties"]["author"].take() {
+        serde_json::Value::Array(children) => {
+            post["properties"]["author"] = serde_json::Value::Array(
+                children
+                    .into_iter()
+                    .filter_map(|post| if post.is_string() {
+                        Some(post)
+                    } else {
+                        filter_post(post, user)
+                    })
+                    .collect::<Vec<serde_json::Value>>()
+            );
+        },
+        serde_json::Value::Null => {},
+        other => post["properties"]["author"] = other
+    }
+
+    match post["children"].take() {
+        serde_json::Value::Array(children) => {
+            post["children"] = serde_json::Value::Array(
+                children
+                    .into_iter()
+                    .filter_map(|post| filter_post(post, user))
+                    .collect::<Vec<serde_json::Value>>()
+            );
+        },
+        serde_json::Value::Null => {},
+        other => post["children"] = other
+    }
+    Some(post)
+}
+
 async fn get_post_from_database<S: Storage>(
     db: &S,
     url: &str,
@@ -85,7 +191,23 @@ async fn get_post_from_database<S: Storage>(
         .await
     {
         Ok(result) => match result {
-            Some((post, cursor)) => Ok((post, cursor)),
+            Some((post, cursor)) => match filter_post(post, user.as_deref()) {
+                Some(post) => Ok((post, cursor)),
+                None => {
+                    // TODO: Authentication
+                    if user.is_some() {
+                        Err(FrontendError::with_code(
+                            StatusCode::FORBIDDEN,
+                            "User authenticated AND forbidden to access this resource",
+                        ))
+                    } else {
+                        Err(FrontendError::with_code(
+                            StatusCode::UNAUTHORIZED,
+                            "User needs to authenticate themselves",
+                        ))
+                    }
+                }
+            }
             None => Err(FrontendError::with_code(
                 StatusCode::NOT_FOUND,
                 "Post not found in the database",