Commit 5725dbb2 authored by Alex Castaño's avatar Alex Castaño

Policy to execute actions in MoodleNet

parent 24b0749e
......@@ -2,6 +2,8 @@ defmodule MoodleNet do
import ActivityPub.Guards
alias ActivityPub.SQL.Query
alias MoodleNet.Policy
def list_communities(opts \\ %{}) do
Query.new()
|> Query.with_type("MoodleNet:Community")
......@@ -61,11 +63,13 @@ defmodule MoodleNet do
|> Query.all()
end
def create_community(attrs) do
def create_community(actor, attrs) do
attrs = Map.put(attrs, "type", "MoodleNet:Community")
with {:ok, entity} <- ActivityPub.new(attrs) do
ActivityPub.insert(entity)
with {:ok, entity} <- ActivityPub.new(attrs),
{:ok, entity} <- ActivityPub.insert(entity),
{:ok, true} <- MoodleNet.join_community(actor, entity) do
{:ok, entity}
end
end
......@@ -121,14 +125,18 @@ defmodule MoodleNet do
:ok
end
def create_collection(community, attrs) when has_type(community, "MoodleNet:Community") do
def create_collection(actor, community, attrs)
when has_type(community, "MoodleNet:Community") do
attrs =
attrs
|> Map.put(:type, "MoodleNet:Collection")
|> Map.put(:attributed_to, [community])
with {:ok, entity} <- ActivityPub.new(attrs) do
ActivityPub.insert(entity)
with :ok <- Policy.create_collection?(actor, community, attrs),
{:ok, entity} <- ActivityPub.new(attrs),
{:ok, entity} <- ActivityPub.insert(entity),
{:ok, true} <- follow_collection(actor, entity) do
{:ok, entity}
end
end
......@@ -158,14 +166,17 @@ defmodule MoodleNet do
:ok
end
def create_resource(_actor, collection, attrs)
def create_resource(actor, collection, attrs)
when has_type(collection, "MoodleNet:Collection") do
attrs =
attrs
|> Map.put(:type, "MoodleNet:EducationalResource")
|> Map.put(:attributed_to, [collection])
with {:ok, entity} <- ActivityPub.new(attrs) do
collection = Query.preload_assoc(collection, :attributed_to)
with :ok <- Policy.create_resource?(actor, collection, attrs),
{:ok, entity} <- ActivityPub.new(attrs) do
ActivityPub.insert(entity)
end
end
......@@ -186,9 +197,10 @@ defmodule MoodleNet do
end
def copy_resource(actor, resource, collection) do
resource = resource
|> Query.preload_aspect(:resource)
|> Query.preload_assoc([:icon])
resource =
resource
|> Query.preload_aspect(:resource)
|> Query.preload_assoc([:icon])
attrs =
Map.take(resource, [
......@@ -210,6 +222,7 @@ defmodule MoodleNet do
:time_required,
:typical_age_range
])
url = get_in(resource, [:icon, Access.at(0), :url])
attrs = Map.put(attrs, :icon, %{type: "Image", url: url})
create_resource(actor, collection, attrs)
......@@ -218,6 +231,8 @@ defmodule MoodleNet do
def create_thread(author, context, attrs)
when has_type(author, "Person") and has_type(context, "MoodleNet:Community")
when has_type(author, "Person") and has_type(context, "MoodleNet:Collection") do
context = preload_community(context)
attrs
|> Map.put(:context, [context])
|> Map.put(:attributed_to, [author])
......@@ -226,7 +241,11 @@ defmodule MoodleNet do
def create_reply(author, in_reply_to, attrs)
when has_type(author, "Person") and has_type(in_reply_to, "Note") do
context = Query.new() |> Query.belongs_to(:context, in_reply_to) |> Query.one()
context =
Query.new()
|> Query.belongs_to(:context, in_reply_to)
|> Query.one()
|> preload_community()
attrs
|> Map.put(:context, [context])
......@@ -237,8 +256,11 @@ defmodule MoodleNet do
defp create_comment(attrs) do
attrs = attrs |> Map.put("type", "Note")
[context] = attrs[:context]
[actor] = attrs[:attributed_to]
with {:ok, entity} <- ActivityPub.new(attrs) do
with :ok <- Policy.create_comment?(actor, context, attrs),
{:ok, entity} <- ActivityPub.new(attrs) do
ActivityPub.insert(entity)
end
end
......@@ -251,8 +273,9 @@ defmodule MoodleNet do
end
end
def follow(follower, following) do
params = %{type: "Follow", actor: follower, object: following}
def join_community(actor, community)
when has_type(actor, "Person") and has_type(community, "MoodleNet:Community") do
params = %{type: "Follow", actor: actor, object: community}
with {:ok, activity} = ActivityPub.new(params),
{:ok, _activity} <- ActivityPub.apply(activity) do
......@@ -260,6 +283,40 @@ defmodule MoodleNet do
end
end
def follow_collection(actor, collection)
when has_type(actor, "Person") and has_type(collection, "MoodleNet:Collection") do
params = %{type: "Follow", actor: actor, object: collection}
with {:ok, activity} = ActivityPub.new(params),
{:ok, _activity} <- ActivityPub.apply(activity) do
{:ok, true}
end
end
def like_comment(actor, comment)
when has_type(actor, "Person") and has_type(comment, "Note") do
comment = preload_community(comment)
attrs = %{type: "Like", actor: actor, object: comment}
with :ok <- Policy.like_comment?(actor, comment, attrs),
{:ok, activity} = ActivityPub.new(attrs),
{:ok, _activity} <- ActivityPub.apply(activity) do
{:ok, true}
end
end
def like_resource(actor, resource)
when has_type(actor, "Person") and has_type(resource, "MoodleNet:EducationalResource") do
resource = preload_community(resource)
attrs = %{type: "Like", actor: actor, object: resource}
with :ok <- Policy.like_resource?(actor, resource, attrs),
{:ok, activity} = ActivityPub.new(attrs),
{:ok, _activity} <- ActivityPub.apply(activity) do
{:ok, true}
end
end
def like(liker, liked) do
params = %{type: "Like", actor: liker, object: liked}
......@@ -290,9 +347,13 @@ defmodule MoodleNet do
end
defp find_current_relation(subject, relation, object) do
if Query.has?(subject, relation, object),
do: :ok,
else: {:error, {:not_found, nil, "Activity"}}
if Query.has?(subject, relation, object) do
:ok
else
subject_id = ActivityPub.Entity.local_id(subject)
object_id = ActivityPub.Entity.local_id(object)
{:error, {:not_found, [subject_id, object_id], "Activity"}}
end
end
defp find_activity(type, actor, object) do
......@@ -303,11 +364,30 @@ defmodule MoodleNet do
|> Query.last()
|> case do
nil ->
{:error, {:not_found, nil, "Activity"}}
actor_id = ActivityPub.Entity.local_id(actor)
object_id = ActivityPub.Entity.local_id(object)
{:error, {:not_found, [actor_id, object_id], "Activity"}}
activity ->
activity = Query.preload_assoc(activity, actor: {[:actor], []}, object: {[:actor], []})
{:ok, activity}
end
end
defp preload_community(community) when has_type(community, "MoodleNet:Community"),
do: community
defp preload_community(collection) when has_type(collection, "MoodleNet:Collection"),
do: Query.preload_assoc(collection, :attributed_to)
defp preload_community(resource) when has_type(resource, "MoodleNet:EducationalResource") do
Query.preload_assoc(resource, attributed_to: :attributed_to)
end
defp preload_community(comment) when has_type(comment, "Note") do
comment = Query.preload_assoc(comment, :context)
[context] = comment.context
context = preload_community(context)
%{comment | context: [context]}
end
end
......@@ -44,7 +44,7 @@ defmodule MoodleNet.Factory do
"icon" => attributes(:image),
"preferred_username" => Faker.Internet.user_name(),
"summary" => Faker.Lorem.sentence(),
"primaryLanguage" => "es",
"primary_language" => "es",
}
end
......@@ -55,7 +55,7 @@ defmodule MoodleNet.Factory do
"url" => Faker.Internet.url(),
"summary" => Faker.Lorem.sentence(),
"icon" => attributes(:image),
"primaryLanguage" => "es",
"primary_language" => "es",
"same_as" => "https://hq.moodle.net/r/98765",
"in_language" => ["en-GB"],
"public_access" => true,
......@@ -131,21 +131,21 @@ defmodule MoodleNet.Factory do
app
end
def community(attrs \\ %{}) do
def community(actor, attrs \\ %{}) do
attrs = attributes(:community, attrs)
{:ok, c} = MoodleNet.create_community(attrs)
{:ok, c} = MoodleNet.create_community(actor, attrs)
c
end
def collection(community, attrs \\ %{}) do
def collection(actor, community, attrs \\ %{}) do
attrs = attributes(:collection, attrs)
{:ok, c} = MoodleNet.create_collection(community, attrs)
{:ok, c} = MoodleNet.create_collection(actor, community, attrs)
c
end
def resource(context, attrs \\ %{}) do
def resource(actor, context, attrs \\ %{}) do
attrs = attributes(:resource, attrs)
{:ok, c} = MoodleNet.create_resource(nil, context, attrs)
{:ok, c} = MoodleNet.create_resource(actor, context, attrs)
c
end
......
defmodule MoodleNet.Policy do
import ActivityPub.Guards
alias ActivityPub.SQL.Query
def create_collection?(actor, community, _attrs)
when has_type(community, "MoodleNet:Community") and has_type(actor, "Person") do
actor_follows!(actor, community)
end
def create_resource?(actor, collection, _attrs)
when has_type(collection, "MoodleNet:Collection") and has_type(actor, "Person") do
community = get_community(collection)
actor_follows!(actor, community)
end
def create_comment?(actor, community, _attrs)
when has_type(community, "MoodleNet:Community") and has_type(actor, "Person") do
actor_follows!(actor, community)
end
def create_comment?(actor, collection, _attrs)
when has_type(collection, "MoodleNet:Collection") and has_type(actor, "Person") do
community = get_community(collection)
actor_follows!(actor, community)
end
def like_comment?(actor, comment, _attrs)
when has_type(comment, "Note") and has_type(actor, "Person") do
community = get_community(comment)
actor_follows!(actor, community)
end
def like_resource?(actor, resource, _attrs)
when has_type(resource, "MoodleNet:EducationalResource") and has_type(actor, "Person") do
community = get_community(resource)
actor_follows!(actor, community)
end
defp actor_follows!(actor, object) do
if Query.has?(actor, :following, object), do: :ok, else: {:error, :forbidden}
end
defp get_community(comment) when has_type(comment, "Note") do
[context] = comment.context
get_community(context)
end
defp get_community(resource) when has_type(resource, "MoodleNet:EducationalResource") do
[collection] = resource.attributed_to
get_community(collection)
end
defp get_community(collection) when has_type(collection, "MoodleNet:Collection") do
[community] = collection.attributed_to
community
end
defp get_community(community) when has_type(community, "MoodleNet:Community"), do: community
end
......@@ -170,28 +170,52 @@ defmodule MoodleNetWeb.GraphQL.Schema do
resolve(&MoodleNetSchema.delete_user/2)
end
@desc "Follow an actor"
field :follow, type: :boolean do
arg(:actor_local_id, non_null(:integer))
resolve(&MoodleNetSchema.create_follow/2)
@desc "Join a community"
field :join_community, type: :boolean do
arg(:community_local_id, non_null(:integer))
resolve(&MoodleNetSchema.join_community/2)
end
@desc "Undo join a community"
field :undo_join_community, type: :boolean do
arg(:community_local_id, non_null(:integer))
resolve(&MoodleNetSchema.undo_join_community/2)
end
@desc "Follow a collection"
field :follow_collection, type: :boolean do
arg(:collection_local_id, non_null(:integer))
resolve(&MoodleNetSchema.follow_collection/2)
end
@desc "Unfollow an actor"
field :unfollow, type: :boolean do
arg(:actor_local_id, non_null(:integer))
resolve(&MoodleNetSchema.destroy_follow/2)
@desc "Undo follow a collection"
field :undo_follow_collection, type: :boolean do
arg(:collection_local_id, non_null(:integer))
resolve(&MoodleNetSchema.undo_follow_collection/2)
end
@desc "Like a comment"
field :like_comment, type: :boolean do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.like_comment/2)
end
@desc "Like a resource"
field :like_resource, type: :boolean do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.like_resource/2)
end
@desc "Like an object"
field :like, type: :boolean do
@desc "Undo a previous like to a comment"
field :undo_like_comment, type: :boolean do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.create_like/2)
resolve(&MoodleNetSchema.undo_like_comment/2)
end
@desc "Unlike an object"
field :unlike, type: :boolean do
@desc "Undo a previous like to a resource"
field :undo_like_resource, type: :boolean do
arg(:local_id, non_null(:integer))
resolve(&MoodleNetSchema.destroy_like/2)
resolve(&MoodleNetSchema.undo_like_resource/2)
end
@desc "Login"
......
......@@ -392,7 +392,8 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
def create_community(%{community: attrs}, info) do
attrs = set_icon(attrs)
with {:ok, community} = MoodleNet.create_community(attrs) do
with {:ok, actor} <- current_actor(info),
{:ok, community} <- MoodleNet.create_community(actor, attrs) do
fields = requested_fields(info)
{:ok, prepare(community, fields)}
end
......@@ -417,25 +418,34 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
|> Errors.handle_error()
end
def create_follow(%{actor_local_id: id}, info) do
with {:ok, follower} <- current_actor(info),
{:ok, following} <- fetch(id, "Actor") do
MoodleNet.follow(follower, following)
def join_community(%{community_local_id: id}, info) do
with {:ok, actor} <- current_actor(info),
{:ok, community} <- fetch(id, "MoodleNet:Community") do
MoodleNet.join_community(actor, community)
end
|> Errors.handle_error()
end
def destroy_follow(%{actor_local_id: id}, info) do
with {:ok, follower} <- current_actor(info) do
MoodleNet.undo_follow(follower, id)
def undo_join_community(%{community_local_id: id}, info) do
with {:ok, actor} <- current_actor(info),
{:ok, community} <- fetch(id, "MoodleNet:Community") do
MoodleNet.undo_follow(actor, community)
end
|> Errors.handle_error()
end
def create_like(%{local_id: id}, info) do
with {:ok, liker} <- current_actor(info),
{:ok, liked} <- fetch(id) do
MoodleNet.like(liker, liked)
def follow_collection(%{collection_local_id: id}, info) do
with {:ok, actor} <- current_actor(info),
{:ok, collection} <- fetch(id, "MoodleNet:Collection") do
MoodleNet.follow_collection(actor, collection)
end
|> Errors.handle_error()
end
def undo_follow_collection(%{collection_local_id: id}, info) do
with {:ok, actor} <- current_actor(info),
{:ok, collection} <- fetch(id, "MoodleNet:Collection") do
MoodleNet.undo_follow(actor, collection)
end
|> Errors.handle_error()
end
......@@ -447,10 +457,43 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
|> Errors.handle_error()
end
def like_comment(%{local_id: comment_id}, info) do
with {:ok, liker} <- current_actor(info),
{:ok, comment} <- fetch(comment_id, "Note") do
MoodleNet.like_comment(liker, comment)
end
|> Errors.handle_error()
end
def like_resource(%{local_id: resource_id}, info) do
with {:ok, liker} <- current_actor(info),
{:ok, resource} <- fetch(resource_id, "MoodleNet:EducationalResource") do
MoodleNet.like_resource(liker, resource)
end
|> Errors.handle_error()
end
def undo_like_comment(%{local_id: comment_id}, info) do
with {:ok, actor} <- current_actor(info),
{:ok, comment} <- fetch(comment_id, "Note") do
MoodleNet.undo_like(actor, comment)
end
|> Errors.handle_error()
end
def undo_like_resource(%{local_id: resource_id}, info) do
with {:ok, actor} <- current_actor(info),
{:ok, resource} <- fetch(resource_id, "MoodleNet:EducationalResource") do
MoodleNet.undo_like(actor, resource)
end
|> Errors.handle_error()
end
def create_collection(%{collection: attrs, community_local_id: comm_id}, info) do
with {:ok, community} <- fetch(comm_id, "MoodleNet:Community"),
with {:ok, actor} <- current_actor(info),
{:ok, community} <- fetch(comm_id, "MoodleNet:Community"),
attrs = set_icon(attrs),
{:ok, collection} = MoodleNet.create_collection(community, attrs) do
{:ok, collection} <- MoodleNet.create_collection(actor, community, attrs) do
fields = requested_fields(info)
{:ok, prepare(collection, fields)}
end
......@@ -554,16 +597,6 @@ defmodule MoodleNetWeb.GraphQL.MoodleNetSchema do
|> Query.one()
end
defp fetch(local_id, type \\ nil)
defp fetch(local_id, nil) do
ActivityPub.SQLEntity.get_by_local_id(local_id)
|> case do
nil -> Errors.not_found_error(local_id, nil)
obj -> {:ok, obj}
end
end
defp fetch(local_id, type) do
case get_by_id_and_type(local_id, type) do
nil -> Errors.not_found_error(local_id, type)
......
......@@ -13,12 +13,21 @@
alias MoodleNet.Factory
MoodleNet.Repo.transaction(fn ->
communities = for _ <- 1..3, do: Factory.community()
collections = for _ <- 1..5, do: Factory.collection(Enum.random(communities))
_resources = for _ <- 1..10, do: Factory.resource(Enum.random(collections))
actors = for _ <- 1..5, do: Factory.actor()
communities = for _ <- 1..3, do: Factory.community(Enum.random(actors))
for a <- actor, c <- communitities, do: MoodleNet.follow(a, c)
collections =
for _ <- 1..5,
do:
Factory.collection(
Enum.random(actors),
Enum.random(communities)
)
_resources = for _ <- 1..10, do: Factory.resource(Enum.random(actors), Enum.random(collections))
commentables = communities ++ collections
threads = for _ <- 1..10, do: Factory.comment(Enum.random(actors), Enum.random(commentables))
......
defmodule MoodleNetTest do
use MoodleNet.DataCase, async: true
describe "create_collection" do
test "works" do
owner = Factory.actor()
comm = Factory.community(owner)
attrs = Factory.attributes(:collection)
actor = Factory.actor()
assert {:error, :forbidden} = MoodleNet.create_collection(actor, comm, attrs)
MoodleNet.join_community(actor, comm)
assert {:ok, collection} = MoodleNet.create_collection(actor, comm, attrs)
assert collection.name == %{"und" => attrs["name"]}
assert collection.summary == %{"und" => attrs["summary"]}
assert collection.content == %{"und" => attrs["content"]}
assert collection.preferred_username == attrs["preferred_username"]
assert collection["primary_language"] == attrs["primary_language"]
url = get_in(collection, [:icon, Access.at(0), :url, Access.at(0)])
assert url == attrs["icon"]["url"]
end
end
describe "create_resource" do
test "works" do
owner = Factory.actor()
comm = Factory.community(owner)
coll = Factory.collection(owner, comm)
attrs = Factory.attributes(:resource)
actor = Factory.actor()
assert {:error, :forbidden} = MoodleNet.create_resource(actor, coll, attrs)
MoodleNet.join_community(actor, comm)
assert {:ok, resource} = MoodleNet.create_resource(actor, coll, attrs)
assert resource.name == %{"und" => attrs["name"]}
assert resource.summary == %{"und" => attrs["summary"]}
assert resource.content == %{"und" => attrs["content"]}
assert resource.primary_language == attrs["primary_language"]
url = get_in(resource, [:icon, Access.at(0), :url, Access.at(0)])
assert url == attrs["icon"]["url"]
assert resource.url == [attrs["url"]]
assert resource.same_as == attrs["same_as"]
assert resource.public_access == attrs["public_access"]
assert resource.is_accesible_for_free == attrs["is_accesible_for_free"]
assert resource.license == attrs["license"]
assert resource.learning_resource_type == attrs["learning_resource_type"]
assert resource.educational_use == attrs["educational_use"]
assert resource.time_required == attrs["time_required"]
assert resource.typical_age_range == attrs["typical_age_range"]
end
end
describe "create_thread" do
test "works" do
owner = Factory.actor()
comm = Factory.community(owner)
coll = Factory.collection(owner, comm)
attrs = Factory.attributes(:comment)
actor = Factory.actor()
assert {:error, :forbidden} = MoodleNet.create_thread(actor, comm, attrs)
assert {:error, :forbidden} = MoodleNet.create_thread(actor, coll, attrs)
MoodleNet.join_community(actor, comm)
assert {:ok, comment} = MoodleNet.create_thread(actor, comm, attrs)
assert comment["primary_language"] == attrs["primary_language"]
assert comment.content == %{"und" => attrs["content"]}
assert {:ok, comment} = MoodleNet.create_thread(actor, coll, attrs)
assert comment["primary_language"] == attrs["primary_language"]
assert comment.content == %{"und" => attrs["content"]}
end
end
describe "create_reply" do
test "works" do
owner = Factory.actor()
comm = Factory.community(owner)
coll = Factory.collection(owner, comm)
c1 = Factory.comment(owner, comm)
c2 = Factory.comment(owner, coll)
attrs = Factory.attributes(:comment)
actor = Factory.actor()
assert {:error, :forbidden} = MoodleNet.create_reply(actor, c1, attrs)
assert {:error, :forbidden} = MoodleNet.create_reply(actor, c2, attrs)
MoodleNet.join_community(actor, comm)
assert {:ok, comment} = MoodleNet.create_reply(actor, c1, attrs)
assert comment["primary_language"] == attrs["primary_language"]
assert comment.content == %{"und" => attrs["content"]}
assert {:ok, comment} = MoodleNet.create_reply(actor, c2, attrs)
assert comment["primary_language"] == attrs["primary_language"]
assert comment.content == %{"und" => attrs["content"]}
end
end
describe "like_comment & undo" do
test "works" do
owner = Factory.actor()
comm = Factory.community(owner)
coll = Factory.collection(owner, comm)
c1 = Factory.comment(owner, comm)
c2 = Factory.comment(owner, coll)
actor = Factory.actor()
assert {:error, :forbidden} = MoodleNet.like_comment(actor, c1)
assert {:error, :forbidden} = MoodleNet.like_comment(actor, c2)
MoodleNet.join_community(actor, comm)
assert {:ok, true} = MoodleNet.like_comment(actor, c1)
assert {:ok, true} = MoodleNet.like_comment(actor, c2)
assert {:ok, true} = MoodleNet.undo_like(actor, c1)
assert {:ok, true} = MoodleNet.undo_like(actor, c2)
assert {:error, {:not_found, _, "Activity"}} = MoodleNet.undo_like(actor, c1)
assert {:error, {:not_found, _, "Activity"}} = MoodleNet.undo_like(actor, c2)
end
end
describe "like_resource & undo" do
test "works" do
owner = Factory.actor()
comm = Factory.community(owner)
coll = Factory.collection(owner, comm)
resource = Factory.resource(owner, coll)
actor = Factory.actor()
assert {:error, :forbidden} = MoodleNet.like_resource(actor, resource)
MoodleNet.join_community(actor, comm)
assert {:ok, true} = MoodleNet.like_resource(actor, resource)
assert {:ok, true} = MoodleNet.undo_like(actor, resource)
assert {:error, {:not_found, _, "Activity"}} = MoodleNet.undo_like(actor, resource)
end
end
describe "join_community & undo" do
test "works" do
owner = Factory.actor()
comm = Factory.community(owner)
actor = Factory.actor()
assert {:error, {:not_found, _, "Activity"}} = MoodleNet.undo_follow(actor, comm)
assert {:ok, true}