Policies: authorize if user is an admin of the requested resource's organization

I have a group resource with these relationships:
belongs_to :educator, User do
source_attribute :educator_id
destination_attribute :id
end

belongs_to :organization, Organization
belongs_to :educator, User do
source_attribute :educator_id
destination_attribute :id
end

belongs_to :organization, Organization
And an organization resource with these:
belongs_to :parent_organization, Organization

has_many :child_organizations, Organization do
source_attribute :id
destination_attribute :parent_organization_id
end
belongs_to :parent_organization, Organization

has_many :child_organizations, Organization do
source_attribute :id
destination_attribute :parent_organization_id
end
And a user resource with this:
many_to_many :organizations, Organization do
through OrganizationMember
source_attribute_on_join_resource :user_id
destination_attribute_on_join_resource :organization_id
end
many_to_many :organizations, Organization do
through OrganizationMember
source_attribute_on_join_resource :user_id
destination_attribute_on_join_resource :organization_id
end
And this read action:
read :groups_for_organization do
argument :organization_id, :uuid, allow_nil?: false

prepare fn query, _ ->
Ash.Query.before_action(query, fn query ->
organization_id = query.arguments.organization_id
descendants = Organization.get_descendant_organizations!([organization_id])
ids = [organization_id | Enum.map(descendants, & &1.id)]
Ash.Query.filter(query, organization_id in ^ids)
end)
end

pagination offset?: true, countable: true, default_limit: 1000
end
read :groups_for_organization do
argument :organization_id, :uuid, allow_nil?: false

prepare fn query, _ ->
Ash.Query.before_action(query, fn query ->
organization_id = query.arguments.organization_id
descendants = Organization.get_descendant_organizations!([organization_id])
ids = [organization_id | Enum.map(descendants, & &1.id)]
Ash.Query.filter(query, organization_id in ^ids)
end)
end

pagination offset?: true, countable: true, default_limit: 1000
end
I'm trying to write a policy that applies to read actions like the above that only shows groups that: a) the educator is the owner of b) the educator is an admin inside the group that the organization belongs to I got the first one working, but the second bit I don't know how to go about. Basically a policy that checks the group's organization_id and then checks the actor's organization_memberships, finds that org with that id and checks to see if the role attribute on it is admin.
policies do
policy action_type(:read) do
description "Can only view a dashboard group if owner/creator of the dashboard group or admin of organization"
authorize_if relates_to_actor_via(:educator)
authorize_if ???
end
end
policies do
policy action_type(:read) do
description "Can only view a dashboard group if owner/creator of the dashboard group or admin of organization"
authorize_if relates_to_actor_via(:educator)
authorize_if ???
end
end
19 Replies
ZachDaniel
ZachDaniel5mo ago
It depends a bit on your data model, but how about something like:
authorize_if expr(exists(organization.user_roles, user_id == ^actor(:id)))
authorize_if expr(exists(organization.user_roles, user_id == ^actor(:id)))
Ege
EgeOP5mo ago
I tried this:
authorize_if expr(exists(organization.organization_members, user_id == ^actor(:id) and role == :admin))
authorize_if expr(exists(organization.organization_members, user_id == ^actor(:id) and role == :admin))
But I get some weird error:
Unknown Error

* ** (ArgumentError) `nil` is not a Spark DSL module.
Unknown Error

* ** (ArgumentError) `nil` is not a Spark DSL module.
Do I need to "preload" something? Actually, looks like I was a bit off in my description. There's a current_organization that the actor is viewing the page as. The policy needs to check if the actor is an admin on that organization. So should I be passing that organization's ID into the read actions?
ZachDaniel
ZachDaniel5mo ago
Interesting. Are you sure thats how you want to do it? Think about it this way, if the current organization on the page somehow passes an id down for some other organizations thing, if you do it the way you described, they'd be allowed to read it So I could read anything just by passing down a current_organization_id that I'm an admin of What you'd want to do is use current_organization_id to filter the data for the user, and then use criteria independent of that to determine what they can see. So, the best thing to do is describe, for some given group, when a user should be allowed to see that group (not factoring in the other action inputs)
Unknown Error

* ** (ArgumentError) `nil` is not a Spark DSL module.
Unknown Error

* ** (ArgumentError) `nil` is not a Spark DSL module.
is probably from one of those relationships not existing i.e organization.organization_members means "group -> organization -> organization_members"
Ege
EgeOP5mo ago
So the actual requirement is: the user should be able to see the group, if the user is an admin on the organization the group belongs to, OR one of the organizations higher in the org hierarchy.
ZachDaniel
ZachDaniel5mo ago
How does your organization hierarchy work?
Ege
EgeOP5mo ago
In other words, I want to get a list of groups while looking at the page from the perspective of a current organization
ZachDaniel
ZachDaniel5mo ago
is it just like organizations have a parent_id type thing?
Ege
EgeOP5mo ago
Org hierarchy is that each org can have a parent and also multiple child organizations yeah
ZachDaniel
ZachDaniel5mo ago
Gotcha. So, that is typically quite difficult to model TBH. It can be done, but its not fun, especially when you consider rules like this one it technically requires recursively walking the tree all the way up There are other ways to model hierarchies, or assistive things that you can do that will make your life much easier for example, if you add an column of type ltree representing the position in the tree of each organization and you can manage that as organizations are created and/or updated but I imagine you aren't just going to refactor all that stuff right now 😆
Ege
EgeOP5mo ago
The modeling is outside my purview. We're translating existing code (which uses Ecto) to Ash
ZachDaniel
ZachDaniel5mo ago
Gotcha. So in that case, yes you can use an argument like current_organization_id argument to do this manually There are two ways
Ege
EgeOP5mo ago
Right, I have access to that in the liveview
ZachDaniel
ZachDaniel5mo ago
Just promise me you will test the case where the user passes in a current_organization_id that they are the admin of, and attempts to read data from a different organization you will have to ensure the action logic appropriately filters based on current_organization_id Actually, I think you can do it all w/ one thing So doing it with a custom filter check
defmodule MyApp.AdminOfOrganizationInHierarchy do
use Ash.Policy.FilterCheck

def describe(_opts), do: "organization is one of the users orgs that they are an admin of"

def filter(actor, authorizer, _opts) do
# requires current_organization_id argument to work
organization_id = authorizer.subject.arguments.current_organization_id
organization_ids = get_parents(...)
# this prevents the issue I was worried aboute above
organization_ids = filter_where_admin_of(organization_ids)

expr(organization_id in ^organization_ids)
end
end
defmodule MyApp.AdminOfOrganizationInHierarchy do
use Ash.Policy.FilterCheck

def describe(_opts), do: "organization is one of the users orgs that they are an admin of"

def filter(actor, authorizer, _opts) do
# requires current_organization_id argument to work
organization_id = authorizer.subject.arguments.current_organization_id
organization_ids = get_parents(...)
# this prevents the issue I was worried aboute above
organization_ids = filter_where_admin_of(organization_ids)

expr(organization_id in ^organization_ids)
end
end
Ege
EgeOP5mo ago
So get_parents is the recursive function that compiles the parent orgs. What is filter_where_admin_of?
ZachDaniel
ZachDaniel5mo ago
That does something to ensure that its only organizations that the user is an admin of whatever that looks like
Ege
EgeOP5mo ago
OK, I was just wondering if it's an Ash thing Cool I'll bang on this for a bit and see how far I can get So I just do authorize_if AdminOfOrganizationInHierarchy inside policies right?
ZachDaniel
ZachDaniel5mo ago
yep!
Ege
EgeOP5mo ago
OK. I might come back here with my tail tucked between my legs. We'll see Thanks Zach
ZachDaniel
ZachDaniel5mo ago
My pleasure 🙇‍♂️

Did you find this page helpful?