Make sure only group moderators can update/delete events, posts

Signed-off-by: Thomas Citharel <tcit@tcit.fr>
This commit is contained in:
Thomas Citharel
2020-10-19 19:21:39 +02:00
parent fc1d392211
commit 23dcb47ce5
18 changed files with 400 additions and 114 deletions

View File

@@ -381,12 +381,15 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
update_data
) do
with actor <- Utils.get_actor(update_data),
{:ok, %Actor{url: actor_url, suspended: false}} <-
{:ok, %Actor{url: actor_url, suspended: false} = actor} <-
ActivityPub.get_or_fetch_actor_by_url(actor),
{:ok, %Event{} = old_event} <-
object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
object_data <- Converter.Event.as_to_model_data(object),
{:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)},
{:origin_check, true} <-
{:origin_check,
Utils.origin_check?(actor_url, update_data) ||
Utils.can_update_group_object?(actor, old_event)},
{:ok, %Activity{} = activity, %Event{} = new_event} <-
ActivityPub.update(old_event, object_data, false) do
{:ok, activity, new_event}
@@ -431,11 +434,15 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:origin_check, true} <-
{:origin_check,
Utils.origin_check?(actor_url, update_data["object"]) ||
Utils.activity_actor_is_group_member?(actor, old_post)},
Utils.can_update_group_object?(actor, old_post)},
{:ok, %Activity{} = activity, %Post{} = new_post} <-
ActivityPub.update(old_post, object_data, false) do
{:ok, activity, new_post}
else
{:origin_check, _} ->
Logger.warn("Actor tried to update a post but doesn't has the required role")
:error
_e ->
:error
end
@@ -452,7 +459,10 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
{:ok, %Resource{} = old_resource} <-
object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
object_data <- Converter.Resource.as_to_model_data(object),
{:origin_check, true} <- {:origin_check, Utils.origin_check?(actor_url, update_data)},
{:origin_check, true} <-
{:origin_check,
Utils.origin_check?(actor_url, update_data) ||
Utils.can_update_group_object?(actor, old_resource)},
{:ok, %Activity{} = activity, %Resource{} = new_resource} <-
ActivityPub.update(old_resource, object_data, false) do
{:ok, activity, new_resource}
@@ -549,11 +559,11 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
with actor_url <- Utils.get_actor(data),
{:ok, %Actor{} = actor} <- ActivityPub.get_or_fetch_actor_by_url(actor_url),
object_id <- Utils.get_url(object),
{:ok, object} <- can_delete_group_object(object_id),
{:ok, object} <- is_group_object_gone(object_id),
{:origin_check, true} <-
{:origin_check,
Utils.origin_check_from_id?(actor_url, object_id) ||
Utils.activity_actor_is_group_member?(actor, object)},
Utils.can_delete_group_object?(actor, object)},
{:ok, activity, object} <- ActivityPub.delete(object, actor, false) do
{:ok, activity, object}
else
@@ -567,6 +577,29 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
end
def handle_incoming(
%{"type" => "Move", "object" => %{"type" => type} = object, "actor" => _actor} = data
)
when type in ["ResourceCollection", "Document"] do
with actor <- Utils.get_actor(data),
{:ok, %Actor{url: actor_url, suspended: false} = actor} <-
ActivityPub.get_or_fetch_actor_by_url(actor),
{:ok, %Resource{} = old_resource} <-
object |> Utils.get_url() |> ActivityPub.fetch_object_from_url(),
object_data <- Converter.Resource.as_to_model_data(object),
{:origin_check, true} <-
{:origin_check,
Utils.origin_check?(actor_url, data) ||
Utils.can_update_group_object?(actor, old_resource)},
{:ok, activity, new_resource} <- ActivityPub.move(:resource, old_resource, object_data) do
{:ok, activity, new_resource}
else
e ->
Logger.error(inspect(e))
:error
end
end
def handle_incoming(
%{
"type" => "Join",
@@ -1020,7 +1053,7 @@ defmodule Mobilizon.Federation.ActivityPub.Transmogrifier do
end
end
defp can_delete_group_object(object_id) do
defp is_group_object_gone(object_id) do
case ActivityPub.fetch_object_from_url(object_id, force: true) do
{:error, error_message, object} when error_message in ["Gone", "Not found"] ->
{:ok, object}

View File

@@ -88,6 +88,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Actors do
def group_actor(%Actor{} = actor), do: actor
def role_needed_to_update(%Actor{} = _group), do: :administrator
def role_needed_to_delete(%Actor{} = _group), do: :administrator
defp prepare_args_for_actor(args) do
args
|> maybe_sanitize_username()

View File

@@ -98,6 +98,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Comments do
def group_actor(_), do: nil
def role_needed_to_update(%Comment{attributed_to: %Actor{} = _group}), do: :administrator
def role_needed_to_delete(%Comment{attributed_to_id: _attributed_to_id}), do: :administrator
# Prepare and sanitize arguments for comments
defp prepare_args_for_comment(args) do
with in_reply_to_comment <-

View File

@@ -89,6 +89,9 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Discussions do
def group_actor(%Discussion{actor_id: actor_id}), do: Actors.get_actor(actor_id)
def role_needed_to_update(%Discussion{}), do: :moderator
def role_needed_to_delete(%Discussion{}), do: :moderator
@spec maybe_publish_graphql_subscription(Discussion.t()) :: :ok
defp maybe_publish_graphql_subscription(%Discussion{} = discussion) do
Absinthe.Subscription.publish(Endpoint, discussion,

View File

@@ -57,6 +57,8 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Managable do
end
defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do
@type group_role :: :member | :moderator | :administrator | nil
@spec group_actor(Entity.t()) :: Actor.t() | nil
@doc "Returns an eventual group for the entity"
def group_actor(entity)
@@ -64,6 +66,12 @@ defprotocol Mobilizon.Federation.ActivityPub.Types.Ownable do
@spec actor(Entity.t()) :: Actor.t() | nil
@doc "Returns the actor for the entity"
def actor(entity)
@spec role_needed_to_update(Entity.t()) :: group_role()
def role_needed_to_update(entity)
@spec role_needed_to_delete(Entity.t()) :: group_role()
def role_needed_to_delete(entity)
end
defimpl Managable, for: Event do
@@ -74,6 +82,8 @@ end
defimpl Ownable, for: Event do
defdelegate group_actor(entity), to: Events
defdelegate actor(entity), to: Events
defdelegate role_needed_to_update(entity), to: Events
defdelegate role_needed_to_delete(entity), to: Events
end
defimpl Managable, for: Comment do
@@ -84,6 +94,8 @@ end
defimpl Ownable, for: Comment do
defdelegate group_actor(entity), to: Comments
defdelegate actor(entity), to: Comments
defdelegate role_needed_to_update(entity), to: Comments
defdelegate role_needed_to_delete(entity), to: Comments
end
defimpl Managable, for: Post do
@@ -94,6 +106,8 @@ end
defimpl Ownable, for: Post do
defdelegate group_actor(entity), to: Posts
defdelegate actor(entity), to: Posts
defdelegate role_needed_to_update(entity), to: Posts
defdelegate role_needed_to_delete(entity), to: Posts
end
defimpl Managable, for: Actor do
@@ -104,6 +118,8 @@ end
defimpl Ownable, for: Actor do
defdelegate group_actor(entity), to: Actors
defdelegate actor(entity), to: Actors
defdelegate role_needed_to_update(entity), to: Actors
defdelegate role_needed_to_delete(entity), to: Actors
end
defimpl Managable, for: TodoList do
@@ -114,6 +130,8 @@ end
defimpl Ownable, for: TodoList do
defdelegate group_actor(entity), to: TodoLists
defdelegate actor(entity), to: TodoLists
defdelegate role_needed_to_update(entity), to: TodoLists
defdelegate role_needed_to_delete(entity), to: TodoLists
end
defimpl Managable, for: Todo do
@@ -124,6 +142,8 @@ end
defimpl Ownable, for: Todo do
defdelegate group_actor(entity), to: Todos
defdelegate actor(entity), to: Todos
defdelegate role_needed_to_update(entity), to: Todos
defdelegate role_needed_to_delete(entity), to: Todos
end
defimpl Managable, for: Resource do
@@ -134,6 +154,8 @@ end
defimpl Ownable, for: Resource do
defdelegate group_actor(entity), to: Resources
defdelegate actor(entity), to: Resources
defdelegate role_needed_to_update(entity), to: Resources
defdelegate role_needed_to_delete(entity), to: Resources
end
defimpl Managable, for: Discussion do
@@ -144,11 +166,15 @@ end
defimpl Ownable, for: Discussion do
defdelegate group_actor(entity), to: Discussions
defdelegate actor(entity), to: Discussions
defdelegate role_needed_to_update(entity), to: Discussions
defdelegate role_needed_to_delete(entity), to: Discussions
end
defimpl Ownable, for: Tombstone do
defdelegate group_actor(entity), to: Tombstones
defdelegate actor(entity), to: Tombstones
defdelegate role_needed_to_update(entity), to: Tombstones
defdelegate role_needed_to_delete(entity), to: Tombstones
end
defimpl Managable, for: Member do

View File

@@ -88,6 +88,10 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Events do
def group_actor(_), do: nil
def role_needed_to_update(%Event{attributed_to: %Actor{} = _group}), do: :moderator
def role_needed_to_delete(%Event{attributed_to_id: _attributed_to_id}), do: :moderator
def role_needed_to_delete(_), do: nil
def join(%Event{} = event, %Actor{} = actor, _local, additional) do
with {:maximum_attendee_capacity, true} <-
{:maximum_attendee_capacity, check_attendee_capacity(event)},

View File

@@ -44,7 +44,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
{:ok, %Post{attributed_to_id: group_id, author_id: creator_id} = post} <-
Posts.update_post(post, args),
{:ok, true} <- Cachex.del(:activity_pub, "post_#{post.slug}"),
{:ok, %Actor{} = group} <- Actors.get_group_by_actor_id(group_id),
{:ok, %Actor{url: group_url} = group} <- Actors.get_group_by_actor_id(group_id),
%Actor{url: creator_url} = creator <- Actors.get_actor(creator_id),
post_as_data <-
Convertible.model_to_as(%{post | attributed_to: group, author: creator}),
@@ -52,7 +52,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
"to" => [group.members_url],
"cc" => [],
"actor" => creator_url,
"attributedTo" => [creator_url]
"attributedTo" => [group_url]
} do
update_data = make_update_data(post_as_data, Map.merge(audience, additional))
@@ -95,4 +95,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Posts do
def group_actor(%Post{attributed_to_id: attributed_to_id}),
do: Actors.get_actor(attributed_to_id)
def role_needed_to_update(%Post{}), do: :moderator
def role_needed_to_delete(%Post{}), do: :moderator
end

View File

@@ -155,4 +155,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Resources do
do: Actors.get_actor(creator_id)
def group_actor(%Resource{actor_id: actor_id}), do: Actors.get_actor(actor_id)
def role_needed_to_update(%Resource{}), do: :member
def role_needed_to_delete(%Resource{}), do: :member
end

View File

@@ -67,4 +67,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.TodoLists do
def actor(%TodoList{}), do: nil
def group_actor(%TodoList{actor_id: actor_id}), do: Actors.get_actor(actor_id)
def role_needed_to_update(%TodoList{}), do: :member
def role_needed_to_delete(%TodoList{}), do: :member
end

View File

@@ -79,4 +79,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Todos do
nil
end
end
def role_needed_to_update(%Todo{}), do: :member
def role_needed_to_delete(%Todo{}), do: :member
end

View File

@@ -11,4 +11,7 @@ defmodule Mobilizon.Federation.ActivityPub.Types.Tombstones do
def actor(_), do: nil
def group_actor(_), do: nil
def role_needed_to_update(%Actor{}), do: nil
def role_needed_to_delete(%Actor{}), do: nil
end

View File

@@ -287,15 +287,41 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
def origin_check_from_id?(id, %{"id" => other_id} = _params) when is_binary(other_id),
do: origin_check_from_id?(id, other_id)
def activity_actor_is_group_member?(%Actor{id: actor_id, url: actor_url}, object) do
Logger.debug(
"Checking if activity actor #{actor_url} is a member from group from #{object.url}"
)
def activity_actor_is_group_member?(
%Actor{id: actor_id, url: actor_url},
object,
role \\ :member
) do
case Ownable.group_actor(object) do
%Actor{type: :Group, id: group_id} ->
Logger.debug("Group object ID is #{group_id}")
Actors.is_member?(actor_id, group_id)
%Actor{type: :Group, id: group_id, url: group_url} ->
Logger.debug("Group object url is #{group_url}")
case role do
:moderator ->
Logger.debug(
"Checking if activity actor #{actor_url} is a moderator from group from #{
object.url
}"
)
Actors.is_moderator?(actor_id, group_id)
:administrator ->
Logger.debug(
"Checking if activity actor #{actor_url} is an administrator from group from #{
object.url
}"
)
Actors.is_administrator?(actor_id, group_id)
_ ->
Logger.debug(
"Checking if activity actor #{actor_url} is a member from group from #{object.url}"
)
Actors.is_member?(actor_id, group_id)
end
_ ->
false
@@ -633,4 +659,39 @@ defmodule Mobilizon.Federation.ActivityPub.Utils do
:ok
end
def can_update_group_object?(%Actor{} = actor, object) do
can_manage_group_object?(:role_needed_to_update, actor, object)
end
def can_delete_group_object?(%Actor{} = actor, object) do
can_manage_group_object?(:role_needed_to_delete, actor, object)
end
@spec can_manage_group_object?(
:role_needed_to_update | :role_needed_to_delete,
Actor.t(),
any()
) :: boolean()
defp can_manage_group_object?(action_function, %Actor{url: actor_url} = actor, object) do
if Ownable.group_actor(object) != nil do
case apply(Ownable, action_function, [object]) do
role when role in [:member, :moderator, :administrator] ->
activity_actor_is_group_member?(actor, object, role)
_ ->
case action_function do
:role_needed_to_update ->
Logger.warn("Actor #{actor_url} can't update #{object.url}")
:role_needed_to_delete ->
Logger.warn("Actor #{actor_url} can't delete #{object.url}")
end
false
end
else
true
end
end
end

View File

@@ -88,6 +88,7 @@ defmodule Mobilizon.GraphQL.Error do
defp metadata(:post_not_found), do: {404, dgettext("errors", "Post not found")}
defp metadata(:event_not_found), do: {404, dgettext("errors", "Event not found")}
defp metadata(:group_not_found), do: {404, dgettext("errors", "Group not found")}
defp metadata(:resource_not_found), do: {404, dgettext("errors", "Resource not found")}
defp metadata(:unknown), do: {500, dgettext("errors", "Something went wrong")}
defp metadata(code) do

View File

@@ -149,7 +149,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
} = _resolution
) do
with {:uuid, {:ok, _uuid}} <- {:uuid, Ecto.UUID.cast(id)},
%Actor{id: actor_id} <- Users.get_actor_for_user(user),
%Actor{id: actor_id, url: actor_url} <- Users.get_actor_for_user(user),
{:post, %Post{attributed_to: %Actor{id: group_id} = group} = post} <-
{:post, Posts.get_post_with_preloads(id)},
args <-
@@ -158,7 +158,7 @@ defmodule Mobilizon.GraphQL.Resolvers.Post do
end),
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Post{} = post} <-
ActivityPub.update(post, args, true, %{}) do
ActivityPub.update(post, args, true, %{"actor" => actor_url}) do
{:ok, post}
else
{:uuid, :error} ->

View File

@@ -83,8 +83,9 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
{:resource, Resources.get_resource_by_group_and_path_with_preloads(group_id, path)} do
{:ok, resource}
else
{:group, _} -> {:error, :group_not_found}
{:member, false} -> {:error, dgettext("errors", "Profile is not member of group")}
{:resource, _} -> {:error, dgettext("errors", "No such resource")}
{:resource, _} -> {:error, :resource_not_found}
end
end
@@ -137,12 +138,12 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
}
} = _resolution
) do
with %Actor{id: actor_id} <- Users.get_actor_for_user(user),
with %Actor{id: actor_id, url: actor_url} <- Users.get_actor_for_user(user),
{:resource, %Resource{actor_id: group_id} = resource} <-
{:resource, Resources.get_resource_with_preloads(resource_id)},
{:member, true} <- {:member, Actors.is_member?(actor_id, group_id)},
{:ok, _, %Resource{} = resource} <-
ActivityPub.update(resource, args, true, %{}) do
ActivityPub.update(resource, args, true, %{"actor" => actor_url}) do
{:ok, resource}
else
{:resource, _} ->
@@ -195,8 +196,13 @@ defmodule Mobilizon.GraphQL.Resolvers.Resource do
}
} = _resolution
) do
with {:ok, data} when is_map(data) <- Parser.parse(resource_url) do
{:ok, struct(Metadata, data)}
case Parser.parse(resource_url) do
{:ok, data} when is_map(data) ->
{:ok, struct(Metadata, data)}
{:error, _err} ->
Logger.warn("Error while fetching preview from #{inspect(resource_url)}")
{:error, :unknown_resource}
end
end