ObjectValidator: Add validation for `Delete`s.
authorlain <lain@soykaf.club>
Wed, 29 Apr 2020 17:09:51 +0000 (19:09 +0200)
committerlain <lain@soykaf.club>
Wed, 29 Apr 2020 17:09:51 +0000 (19:09 +0200)
lib/pleroma/web/activity_pub/builder.ex
lib/pleroma/web/activity_pub/object_validator.ex
lib/pleroma/web/activity_pub/object_validators/common_validations.ex
lib/pleroma/web/activity_pub/object_validators/delete_validator.ex [new file with mode: 0644]
test/web/activity_pub/object_validator_test.exs

index 429a510b8118df4f136f614de281131fd5dd78b8..5cc46c3eab66cffbe08fa6de271ddae9ac83dbb9 100644 (file)
@@ -10,6 +10,22 @@ defmodule Pleroma.Web.ActivityPub.Builder do
   alias Pleroma.Web.ActivityPub.Utils
   alias Pleroma.Web.ActivityPub.Visibility
 
+  @spec delete(User.t(), String.t()) :: {:ok, map(), keyword()}
+  def delete(actor, object_id) do
+    object = Object.normalize(object_id)
+
+    to = (object.data["to"] || []) ++ (object.data["cc"] || [])
+
+    {:ok,
+     %{
+       "id" => Utils.generate_activity_id(),
+       "actor" => actor.ap_id,
+       "object" => object_id,
+       "to" => to,
+       "type" => "Delete"
+     }, []}
+  end
+
   @spec like(User.t(), Object.t()) :: {:ok, map(), keyword()}
   def like(actor, object) do
     object_actor = User.get_cached_by_ap_id(object.data["actor"])
index dc4bce0595a12c409475206b2aed0e7222b7ce18..f476c6f724a9eec5b8320794a6ef681f5df5b10e 100644 (file)
@@ -12,10 +12,21 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
   alias Pleroma.Object
   alias Pleroma.User
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
+  alias Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator
 
   @spec validate(map(), keyword()) :: {:ok, map(), keyword()} | {:error, any()}
   def validate(object, meta)
 
+  def validate(%{"type" => "Delete"} = object, meta) do
+    with {:ok, object} <-
+           object
+           |> DeleteValidator.cast_and_validate()
+           |> Ecto.Changeset.apply_action(:insert) do
+      object = stringify_keys(object)
+      {:ok, object, meta}
+    end
+  end
+
   def validate(%{"type" => "Like"} = object, meta) do
     with {:ok, object} <-
            object |> LikeValidator.cast_and_validate() |> Ecto.Changeset.apply_action(:insert) do
@@ -24,6 +35,12 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidator do
     end
   end
 
+  def stringify_keys(%{__struct__: _} = object) do
+    object
+    |> Map.from_struct()
+    |> stringify_keys
+  end
+
   def stringify_keys(object) do
     object
     |> Map.new(fn {key, val} -> {to_string(key), val} end)
index b479c391837f1ccf20dfabd5134b562054e4dbb8..e115d952616ffc499773835d4842b3d9e0e5c17c 100644 (file)
@@ -8,6 +8,26 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations do
   alias Pleroma.Object
   alias Pleroma.User
 
+  def validate_recipients_presence(cng, fields \\ [:to, :cc]) do
+    non_empty =
+      fields
+      |> Enum.map(fn field -> get_field(cng, field) end)
+      |> Enum.any?(fn
+        [] -> false
+        _ -> true
+      end)
+
+    if non_empty do
+      cng
+    else
+      fields
+      |> Enum.reduce(cng, fn field, cng ->
+        cng
+        |> add_error(field, "no recipients in any field")
+      end)
+    end
+  end
+
   def validate_actor_presence(cng, field_name \\ :actor) do
     cng
     |> validate_change(field_name, fn field_name, actor ->
diff --git a/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex b/lib/pleroma/web/activity_pub/object_validators/delete_validator.ex
new file mode 100644 (file)
index 0000000..8dd5c19
--- /dev/null
@@ -0,0 +1,64 @@
+# Pleroma: A lightweight social networking server
+# Copyright © 2017-2020 Pleroma Authors <https://pleroma.social/>
+# SPDX-License-Identifier: AGPL-3.0-only
+
+defmodule Pleroma.Web.ActivityPub.ObjectValidators.DeleteValidator do
+  use Ecto.Schema
+
+  alias Pleroma.Web.ActivityPub.ObjectValidators.Types
+
+  import Ecto.Changeset
+  import Pleroma.Web.ActivityPub.ObjectValidators.CommonValidations
+
+  @primary_key false
+
+  embedded_schema do
+    field(:id, Types.ObjectID, primary_key: true)
+    field(:type, :string)
+    field(:actor, Types.ObjectID)
+    field(:to, Types.Recipients, default: [])
+    field(:cc, Types.Recipients, default: [])
+    field(:object, Types.ObjectID)
+  end
+
+  def cast_data(data) do
+    %__MODULE__{}
+    |> cast(data, __schema__(:fields))
+  end
+
+  def validate_data(cng) do
+    cng
+    |> validate_required([:id, :type, :actor, :to, :cc, :object])
+    |> validate_inclusion(:type, ["Delete"])
+    |> validate_same_domain()
+    |> validate_object_presence()
+    |> validate_recipients_presence()
+  end
+
+  def validate_same_domain(cng) do
+    actor_domain =
+      cng
+      |> get_field(:actor)
+      |> URI.parse()
+      |> (& &1.host).()
+
+    object_domain =
+      cng
+      |> get_field(:object)
+      |> URI.parse()
+      |> (& &1.host).()
+
+    if object_domain != actor_domain do
+      cng
+      |> add_error(:actor, "is not allowed to delete object")
+    else
+      cng
+    end
+  end
+
+  def cast_and_validate(data) do
+    data
+    |> cast_data
+    |> validate_data
+  end
+end
index 3c5c3696e299c14e08febf8d52caf83f26ca58b6..64b9ee1ec3bb94b59d0488bf8031d74495e8d71e 100644 (file)
@@ -1,6 +1,7 @@
 defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
   use Pleroma.DataCase
 
+  alias Pleroma.Web.ActivityPub.Builder
   alias Pleroma.Web.ActivityPub.ObjectValidator
   alias Pleroma.Web.ActivityPub.ObjectValidators.LikeValidator
   alias Pleroma.Web.ActivityPub.Utils
@@ -8,6 +9,72 @@ defmodule Pleroma.Web.ActivityPub.ObjectValidatorTest do
 
   import Pleroma.Factory
 
+  describe "deletes" do
+    setup do
+      user = insert(:user)
+      {:ok, post_activity} = CommonAPI.post(user, %{"status" => "cancel me daddy"})
+
+      {:ok, valid_post_delete, _} = Builder.delete(user, post_activity.data["object"])
+
+      %{user: user, valid_post_delete: valid_post_delete}
+    end
+
+    test "it is valid for a post deletion", %{valid_post_delete: valid_post_delete} do
+      assert match?({:ok, _, _}, ObjectValidator.validate(valid_post_delete, []))
+    end
+
+    test "it's invalid if the id is missing", %{valid_post_delete: valid_post_delete} do
+      no_id =
+        valid_post_delete
+        |> Map.delete("id")
+
+      {:error, cng} = ObjectValidator.validate(no_id, [])
+
+      assert {:id, {"can't be blank", [validation: :required]}} in cng.errors
+    end
+
+    test "it's invalid if the object doesn't exist", %{valid_post_delete: valid_post_delete} do
+      missing_object =
+        valid_post_delete
+        |> Map.put("object", "http://does.not/exist")
+
+      {:error, cng} = ObjectValidator.validate(missing_object, [])
+
+      assert {:object, {"can't find object", []}} in cng.errors
+    end
+
+    test "it's invalid if the actor of the object and the actor of delete are from different domains",
+         %{valid_post_delete: valid_post_delete} do
+      valid_other_actor =
+        valid_post_delete
+        |> Map.put("actor", valid_post_delete["actor"] <> "1")
+
+      assert match?({:ok, _, _}, ObjectValidator.validate(valid_other_actor, []))
+
+      invalid_other_actor =
+        valid_post_delete
+        |> Map.put("actor", "https://gensokyo.2hu/users/raymoo")
+
+      {:error, cng} = ObjectValidator.validate(invalid_other_actor, [])
+
+      assert {:actor, {"is not allowed to delete object", []}} in cng.errors
+    end
+
+    test "it's invalid if all the recipient fields are empty", %{
+      valid_post_delete: valid_post_delete
+    } do
+      empty_recipients =
+        valid_post_delete
+        |> Map.put("to", [])
+        |> Map.put("cc", [])
+
+      {:error, cng} = ObjectValidator.validate(empty_recipients, [])
+
+      assert {:to, {"no recipients in any field", []}} in cng.errors
+      assert {:cc, {"no recipients in any field", []}} in cng.errors
+    end
+  end
+
   describe "likes" do
     setup do
       user = insert(:user)