Annotating manual actions and custom changes for policies

My current understanding is that policy builtins like changing and selecting won't see into manual actions at all, and that they will see into custom changes to the degree expressed by the returned changeset and not further. In the case that one requires a manual action, or to overcome any limitations for custom changes, is it possible to annotate the action, indicating to Ash that the manual is going to affect certain attributes or otherwise do things that a policy might care about?
18 Replies
ZachDaniel
ZachDaniel2y ago
So changing and selecting will see everything up until the manual action invocation which should typically be enough, since everything in the manual action is hand-written code that should only do what the given user can do, right? What you can potentially do is make sure to call a different non-manual action after calling the manual one and write your policies against that non-manual one
\ ឵឵឵
\ ឵឵឵OP2y ago
(Very roughly) something like:
actions do
read :read do
manual fn cs, _ ->
Res.simulate_read(Ash.Changeset.get_arguments(cs)) # get_arguments doesn't exist, but I'm sure it's somewhere in the struct
# ...
end
end

read :simulate_read do
selecting [...]
end
end
actions do
read :read do
manual fn cs, _ ->
Res.simulate_read(Ash.Changeset.get_arguments(cs)) # get_arguments doesn't exist, but I'm sure it's somewhere in the struct
# ...
end
end

read :simulate_read do
selecting [...]
end
end
? There are some cases—such as those involving recursion—where I could maybe turn reads into a modify query, but not more than that. Similarly for relationships, I can make them manuals that define the ash_postgres_* callbacks, but not turn them into Ash relationships at the moment. For sure pushing the policy logic into the manual works, which is mostly what I've been doing. Generally, I'm quite interested in ideas for blurring the lines between policies and actions, as do filter checks for example.
ZachDaniel
ZachDaniel2y ago
It might help to have a concrete example to talk about
\ ឵឵឵
\ ឵឵឵OP2y ago
Sure, for example I'd like to use a recursive query to return all documents that are children of a specified Documentnil representing the root document. I believe I need a manual action for the time being, but I still want to control which attributes of each Document the user is allowed to select based on roles etc. Definitely something that can be accomplished in the manual action itself, no question.
ZachDaniel
ZachDaniel2y ago
Well, if you have policies on the select statement from the parent query and then you pass down the parent query's select then you should be good right?
\ ឵឵឵
\ ឵឵឵OP2y ago
In this case, the parent is simply passed in by id (or nil), and all documents are retrieved using a single recursive query in a manual. Am I right in thinking you're talking about the parent being selected in an initial read action that would hit policy checks? 😄
ZachDaniel
ZachDaniel2y ago
Well, when I call the manual action, I provide a query So you'd pass down the select/query from the parent action, and write policies against the select I provided from the outset
\ ឵឵឵
\ ឵឵឵OP2y ago
Right on, that makes sense. So the selecting policy is introspecting the incoming query, not so much how the action decides to satisfy it? Rather maybe a combination of both. To what degree does that apply to writes? Seems like less can be assumed about a write from its arguments.
ZachDaniel
ZachDaniel2y ago
So creates/updates/destroys actually generally work the same way. All that we do is run changes/preparations and then authorize, we don't actually do authorization based on the result of running all of the before action hooks The idea is that if there is behavior that you're adding in a hook, its internal in some way, and you should be able to infer enough from the user's initial request to do your authorization.
\ ឵឵឵
\ ឵឵឵OP2y ago
Ok, that makes sense, definitely a good piece of info though 🙂 Then for the example I described, and most manual reads, policies using selecting should basically work, because the policy will check the query up front, even if it's manual. This assumes the manual respects the user's query, but that's not a big deal since it should anyway. For writes, it won't be able to infer anything from the incoming arguments in the case it's a manual.
ZachDaniel
ZachDaniel2y ago
correct, usually for manual actions its best to end up writing policies about the arguments themselves and/or delegating to a different action w/ policies
\ ឵឵឵
\ ឵឵឵OP2y ago
Is it possible to mix changes with manual for the purposes of annotation? There would then be a pretty straightforward path to creating a change that would "pretend" to set the attributes that the manual would affect.
ZachDaniel
ZachDaniel2y ago
yes 🙂 changes are all run, and the changeset given to a manual action will honor all of that and before/after action hooks are still run the manual action just takes over the final "do the thing" step
\ ឵឵឵
\ ឵឵឵OP2y ago
Cool, will make a macro for now. Looking forward to those extendable builtins 😄
ZachDaniel
ZachDaniel2y ago
What do you mean?
\ ឵឵឵
\ ឵឵឵OP2y ago
GitHub
Add Spark.Builtins · Issue #33 · ash-project/spark
One of the spark types we have is {:spark_function_behaviour, ... that allows specifying a "builtins module" which is a module from which to show autocompletions automatically. What we sh...
\ ឵឵឵
\ ឵឵឵OP2y ago
But maybe one can already add and this is just for autocomplete? What I was referring to was the ability to augment a schema defined in another DSL/extension, e.g. now I have this for the annotations:
defmodule AshUtil.Change.ChangesAttributes do
@moduledoc """
Set the specified attributes to their current value to simulate them being
changed.

Useful for combining manual write actions with the `changing_attributes`
policy builtin.
"""
@moduledoc since: "0.1.0"

use Ash.Resource.Change

@impl Ash.Resource.Change
@spec change(Ash.Changeset.t(), keyword, Ash.Resource.Change.context()) :: Ash.Changeset.t()
def change(cs, opts, _) do
Ash.Changeset.change_attributes(cs,
Enum.map(opts[:attributes], &{&1, Ash.Changeset.get_attribute(cs, &1)}))
end
end
defmodule AshUtil.Change.ChangesAttributes do
@moduledoc """
Set the specified attributes to their current value to simulate them being
changed.

Useful for combining manual write actions with the `changing_attributes`
policy builtin.
"""
@moduledoc since: "0.1.0"

use Ash.Resource.Change

@impl Ash.Resource.Change
@spec change(Ash.Changeset.t(), keyword, Ash.Resource.Change.context()) :: Ash.Changeset.t()
def change(cs, opts, _) do
Ash.Changeset.change_attributes(cs,
Enum.map(opts[:attributes], &{&1, Ash.Changeset.get_attribute(cs, &1)}))
end
end
@doc """
Simulate changing the specified attributes, primarily for use with policies.

See `AshUtil.Change.ChangesAttributes`.
"""
@doc since: "0.1.0"
defmacro changes_attributes(attributes) do
quote do
change {AshUtil.Change.ChangesAttributes, attributes: unquote(attributes)}
end
end
@doc """
Simulate changing the specified attributes, primarily for use with policies.

See `AshUtil.Change.ChangesAttributes`.
"""
@doc since: "0.1.0"
defmacro changes_attributes(attributes) do
quote do
change {AshUtil.Change.ChangesAttributes, attributes: unquote(attributes)}
end
end
(and maybe there is a nicer way to do that besides setting the attribute to its current value)
ZachDaniel
ZachDaniel2y ago
Honestly I'm confused why you'd rather use the changes_attributes/1 macro at all I'd personally just put this:
change {AshUtil.Change.ChangesAttributes, attributes: unquote(attributes)}
change {AshUtil.Change.ChangesAttributes, attributes: unquote(attributes)}
in my resource alternatively, I'd define simulate_change_attributes/1 that returns the change tuple and say change simulate_change_attributes([:foo, :bar])

Did you find this page helpful?