AE
Ash Elixir•2y ago
Jason

Filter check is returning an error tuple, rather than filtering rows.

I'm trying to use policies to filter out rows whose visibility attribute is not 1. The table contains some rows where visibility == 2, so I expected the query will filter those out.
policy action_type(:read) do
forbid_if Invisible
end
policy action_type(:read) do
forbid_if Invisible
end
But Task |> Ash.Query.for_read(:read) |> Tasks.read() returns an error tuple like this. What am I missing?
{:error,
%Ash.Error.Forbidden{
errors: [
%Ash.Error.Forbidden.Policy{
scenarios: [],
facts: %{
false => false,
true => true,
{Ash.Policy.Check.ActionType, [type: :read, access_type: :filter]} => true
},
filter: nil,
policy_breakdown?: false,
{:error,
%Ash.Error.Forbidden{
errors: [
%Ash.Error.Forbidden.Policy{
scenarios: [],
facts: %{
false => false,
true => true,
{Ash.Policy.Check.ActionType, [type: :read, access_type: :filter]} => true
},
filter: nil,
policy_breakdown?: false,
7 Replies
Jason
JasonOP•2y ago
defmodule TaskApp.Checks.Invisible do

use Ash.Policy.FilterCheck

require Ash.Query
import Ash.Filter.TemplateHelpers, only: [actor: 1]

def filter(_options) do

Ash.Query.expr(visibility != 1)
end
end
defmodule TaskApp.Checks.Invisible do

use Ash.Policy.FilterCheck

require Ash.Query
import Ash.Filter.TemplateHelpers, only: [actor: 1]

def filter(_options) do

Ash.Query.expr(visibility != 1)
end
end
ZachDaniel
ZachDaniel•2y ago
ah so in that case, its because of the way the policy is worded its not possible for that policy to pass Policies have to have a check inside of them that explicitly authorizes the policy, otherwise we assume it to fail
policy action_type(:read) do
forbid_if Invisible
authorize_if always()
end
policy action_type(:read) do
forbid_if Invisible
authorize_if always()
end
If you were to do that, your policy would filter the way that you want it to Any policy that only contains (one or more) forbid_if statements will always fail with a forbidden error.
Jason
JasonOP•2y ago
Ahhh.... makes perfect sense 🙂 Thank you. I have been doing these filtering through making calculations (such as calculate :invisible, calculate: :is_author, etc) and then mixing and matching them in read actions. It appears that using policies can replace them in many cases, but it feels that having calculations will provide more flexibility because it can be used elsewhere (e.g., to show visibility of a task, rather than filtering invisible ones out altogether). Is there a useful rule of thumb in terms of where to define filters, for instance, policies vs. calculation+action?
ZachDaniel
ZachDaniel•2y ago
There isn't really :/ It typically depends on what you're doing BTW, you can do expressions inline if you want
forbid_if expr(visibility != 1)
forbid_if expr(visibility != 1)
But I can see why you might want to put them in a module to centralize the logic/make it clear
Jason
JasonOP•2y ago
Yeah, I read it from the doc. Invisible was a simplified example. The real one is more complex like visible if admin, or author, or autorized by author, etc. It becomes pretty complex pretty quickly. Then that is also used for routing as well. For instance, if an unauthorized user tries to access a url that requires authorization, the condition defined as calculations is used to determine it is an unauthorized access and route them to the appropriate page. router
ash_authentication_live_session :authenticated_tasks_view,
# live_user_optional
on_mount: {TaskApp.LiveUserAuth, :task_visible_to_live_user} do
ash_authentication_live_session :authenticated_tasks_view,
# live_user_optional
on_mount: {TaskApp.LiveUserAuth, :task_visible_to_live_user} do
<live_user_auth.ex>
def on_mount(
:task_visible_to_live_user,
params,
_session,
socket
) do
task =
TaskApp.Tasks.Task.get!(task_id,
load: [
editable_by: %{user_id: current_user.id},
visible_to: %{user_id: current_user.id}
]
)
cond do
not task.visible_to ->
{:half, redirect(socket, to: ~p"/unauthorized/")}

def on_mount(
:task_visible_to_live_user,
params,
_session,
socket
) do
task =
TaskApp.Tasks.Task.get!(task_id,
load: [
editable_by: %{user_id: current_user.id},
visible_to: %{user_id: current_user.id}
]
)
cond do
not task.visible_to ->
{:half, redirect(socket, to: ~p"/unauthorized/")}

Let me know if there is an easier, more recommended Ash way to do this.
ZachDaniel
ZachDaniel•2y ago
You can try using YourApi.can? If there aren't good examples in the docs we should add them, but there are likely examples in the forums here
Jason
JasonOP•2y ago
Will do. Thanks!

Did you find this page helpful?