Policy for explicitly setting relationship on create

I have a number of resources that are is_a other resources. That is:
defmacro is_a(attr, type, opts \\ []) do
quote do
relationships do
belongs_to unquote(attr), unquote(type) do
source_attribute unquote(opts[:source_attribute] || :id)
primary_key? true
allow_nil? false
end
end

# this part is not in yet
changes do
change MaybeCreateParent, on: [:create]
# TODO
end
end
end
defmacro is_a(attr, type, opts \\ []) do
quote do
relationships do
belongs_to unquote(attr), unquote(type) do
source_attribute unquote(opts[:source_attribute] || :id)
primary_key? true
allow_nil? false
end
end

# this part is not in yet
changes do
change MaybeCreateParent, on: [:create]
# TODO
end
end
end
When these resources are created, they optionally accept an:
actions do
create :create do
argument :parent, App.Parent
# ...
end
end
actions do
create :create do
argument :parent, App.Parent
# ...
end
end
I'd like to do two things: 1. Be able to roll the automatic creation of the parent into something like above. Can I pass change an MFA?
defmodule MaybeCreateParent do
def change(cs, _, _) do
parent = Changeset.get_argument(cs, :parent) || %{}

cs
|> Changeset.delete_argument(:parent)
|> Changeset.manage_relationship(:parent, parent, type: :create)
end
end
defmodule MaybeCreateParent do
def change(cs, _, _) do
parent = Changeset.get_argument(cs, :parent) || %{}

cs
|> Changeset.delete_argument(:parent)
|> Changeset.manage_relationship(:parent, parent, type: :create)
end
end
2. Be able to create policies that determine when one is allowed to explicitly set the parent while creating. For example, to specify only users with certain roles should be allowed to explicitly set the parent. Something like:
policies do
policy [action_type(:create), changing_relationship(:parent)] do
authorize_if IsSomeRole
authorize_if IsAnotherRole
deny_unless never()
end
end
policies do
policy [action_type(:create), changing_relationship(:parent)] do
authorize_if IsSomeRole
authorize_if IsAnotherRole
deny_unless never()
end
end
17 Replies
\ ឵឵឵
\ ឵឵឵OP3y ago
Note that this second policy needs to not conflict with the fact that the parent will be changed either way by the changes hook, so maybe something like has_argument would be a simpler way to represent this.
ZachDaniel
ZachDaniel3y ago
You can pass change a module that is an Ash.Resource.Change with opts, i.e {Module, opts}
defmodule YourApp.CreateParent do
use Ash.Resource.Change

def change(changeset, opts, _) do
Ash.Changeset.before_action(changeset, fn changeset ->
# use `opts` in here
end)
end
end

change {Module, opts}
defmodule YourApp.CreateParent do
use Ash.Resource.Change

def change(changeset, opts, _) do
Ash.Changeset.before_action(changeset, fn changeset ->
# use `opts` in here
end)
end
end

change {Module, opts}
And I think your policy might work basically as you've stated it. although deny_unless never() is unnecessary if a policy isn't explicitly authorized, then it is forbidden
\ ឵឵឵
\ ឵឵឵OP3y ago
The deny_unless never() is to prevent interference by following policies, just a question of ordering. So doing it as an after_action hook is the way to keep it from being picked up by the changing_relationship policy? I was thinking it might be a before_action otherwise it might choke on the allow_nil? or the foreign key constraint.
ZachDaniel
ZachDaniel3y ago
thats not how policies work FYI all policies that apply to a request must pass, regardless of ordering I actually meant to do it in a before_action sorry 😆
\ ឵឵឵
\ ឵឵឵OP3y ago
But the fact that it's in a hook means it should escape the notice of the changing_relationship policy? Right on.
ZachDaniel
ZachDaniel3y ago
Oh, yeah thats true
def change(changeset, opts, _) do
rel = opts[:rel] || :parent
parent = Changeset.get_argument(cs, rel) || %{}

Changeset.manage_relationship(cs, rel, parent, type: :create)
end
def change(changeset, opts, _) do
rel = opts[:rel] || :parent
parent = Changeset.get_argument(cs, rel) || %{}

Changeset.manage_relationship(cs, rel, parent, type: :create)
end
\ ឵឵឵
\ ឵឵឵OP3y ago
Yeah that's roughly where I'm at 🙂
changes do
change (fn cs, _ ->
Ash.Changeset.before_action(cs, fn cs ->
parent = Changeset.get_argument(cs, unquote(attr)) || %{}

cs
|> Ash.Changeset.delete_argument(unquote(attr))
|> Ash.Changeset.manage_relationship(unquote(attr), parent, type: :create)
end)
end), on: [:create]
end
changes do
change (fn cs, _ ->
Ash.Changeset.before_action(cs, fn cs ->
parent = Changeset.get_argument(cs, unquote(attr)) || %{}

cs
|> Ash.Changeset.delete_argument(unquote(attr))
|> Ash.Changeset.manage_relationship(unquote(attr), parent, type: :create)
end)
end), on: [:create]
end
ZachDaniel
ZachDaniel3y ago
If you write a generic change like the one listed above you can really simplify that change {ManageParent, rel: :parent}
\ ឵឵឵
\ ឵឵឵OP3y ago
True, but it is pretty much just for this macro 😂
ZachDaniel
ZachDaniel3y ago
And your point was correct actually you don't want to do it in a before_action hook your choice at the end of the day ¯\_(ツ)_/¯ Because then the policy will never know. Policies are about the changeset before hooks have executed.
\ ឵឵឵
\ ឵឵឵OP3y ago
Right, if it wouldn't be picked up in either, probably makes sense to do it in the before_action hook to avoid violating constraints. The parent can exist before the child but not the other way around.
ZachDaniel
ZachDaniel3y ago
it will be picked up with the example I gave Because changing_relationship looks for the structure added by manage_relationship
\ ឵឵឵
\ ឵឵឵OP3y ago
Ah...ok.
ZachDaniel
ZachDaniel3y ago
and manage_relationship does the bulk of the work in hooks So its "the best of both worlds" You can use policies about it, but the work is dispatched for later
\ ឵឵឵
\ ឵឵឵OP3y ago
Which one was going to not be noticed by the policy then, though?
ZachDaniel
ZachDaniel3y ago
using an explicit before_action hook and doing the work in that hook either manually or with manage_relationship
defmodule Change do
# this works
def change(changeset, opts, _) do
rel = opts[:rel] || :parent
parent = Changeset.get_argument(cs, rel) || %{}

Changeset.manage_relationship(cs, rel, parent, type: :create)
end

# this won't
def change(changeset, opts, _) do
Ash.Changeset.before_action(changeset, fn changeset ->
rel = opts[:rel] || :parent
parent = Changeset.get_argument(cs, rel) || %{}

Changeset.manage_relationship(cs, rel, parent, type: :create)
end)
end

end
defmodule Change do
# this works
def change(changeset, opts, _) do
rel = opts[:rel] || :parent
parent = Changeset.get_argument(cs, rel) || %{}

Changeset.manage_relationship(cs, rel, parent, type: :create)
end

# this won't
def change(changeset, opts, _) do
Ash.Changeset.before_action(changeset, fn changeset ->
rel = opts[:rel] || :parent
parent = Changeset.get_argument(cs, rel) || %{}

Changeset.manage_relationship(cs, rel, parent, type: :create)
end)
end

end
\ ឵឵឵
\ ឵឵឵OP3y ago
Ah, referring to this one, which doesn't use a hook at all, so it would be picked up. Righto, fantastic. Thanks a bunch!

Did you find this page helpful?