Error in action when policy is added
I have this action on the DashboardGroup resource:
Attributes:
Relationships:
Policy:
Struggling with tests. I have this:
The second to last line is giving an error:
But inside LV modules, trying to get a dashboard group by id when the actor does not have permission results in
{:ok, nil}
14 Replies
That is working as intended. Authorization policies for read actions are applied as filters. This is designed to prevent a whole class of security related bugs.
If you look at the SQL being run, you'll see a
WHERE educator_id = <user.id>
in the query
Its not clear enough in the policies guide, I'm adding something now.
Adding the following to the policy docs now
Read Actions and Filtering Behavior
An important characteristic of read actions is that, by default, they are filtered by policies rather than returning authorization errors. This means:
- When a user is not allowed to see certain records, those records are simply filtered out of the results
- Instead of receiving a Forbidden
error, users typically get a NotFound
error (for single record queries) or an empty/reduced result set (for multi-record queries)
- This filtering behavior applies to all read actions, including get
, read
, and any custom read actions you define
For example, if a policy restricts users to only see their own posts, a query for all posts will automatically filter to only return the current user's posts, rather than raising an authorization error. Similarly, attempting to fetch a specific post that belongs to another user will result in a NotFound
error rather than Forbidden
.
This design is a security feature that prevents enumeration attacks and information disclosure. By not distinguishing between "record doesn't exist" and "record exists but you can't access it", the system prevents attackers from probing to discover the existence of protected data, mapping out the system's data structure, or conducting reconnaissance attacks through systematic querying.
#### Bypassing this behavior
You can bypass this behavior on a case-by-case basis with the authorize_with
option, for data layers that support error expressions (all of the core ones except AshSqlite
).
For example, given a post that the user cannot see:
As for {:ok, nil}
vs a not-found error, it could be the difference between read_one
and get
?
get
is "get the thing or its an error"The LV code is this:
When the actor doesn't have permission, the code goes into the first case block.
But in tests, when the actor doesn't have permission, I get that error.
I'm asking about this discrepancy. It's the same code so shouldn't the behavior be the same?
Sorry, misunderstood the original question.
That is quite strange 🤔
I can't think of a reason that behavior would somehow change in a LV vs in a test...
If I comment out the policy, the error goes away
And your LV user and test user have the same properties? So the only discernible difference is that one is in a LV and one is in a test?
If I comment out the policy, the error goes awayWhen you say the error goes away, what exactly do you mean? So if you put a made-up id in there, do you get
{:ok, nil}
?The full test is like this:
With the policy, the
{:ok, group2} = DashboardGroup.get_by_id(group2.id, ctx.parent_org.id, actor: ctx.user)
line gives the error. If I comment out the policy, then the groups that actually exist are found and returned as one would expect. Only non-existent groups return a not-found error.
Interesting. I should be able to reproduce that pretty easily, one sec.
So the question is why this policy:
Would result in this code:
To return an
:error
tuple. When it should be returning {:ok, nil}
based on how it behaves in LV.The LV one is the one that is wrong.
get_by_id
should return a not found error
if the thing does not exist or if policies filtered it
it shouldn't even be able to tell which one of those things is true, which is why this is confusing
You're sure your LV case returns {:ok, nil}
?Pretty sure. I'll play with it some more. Need to step away now for a meeting
Will post here later
What happens if the resource is being loaded during a read on an associated resource, and the permission check fails? Does the resource get loaded as
nil
by default?Yes
OK, maybe that's why I came to believe that it would also return
{:ok, nil}
when fetching the resource itselfYeah, that makes sense.
There are options to change how loading related fields that you can't see should be treated
and
With that combination, loading a thing you can't see would give you
%Resource{..., relationship: %Ash.ForbiddenField{}}
Okay