AF
Ash Framework•2mo ago
Malian

How to write a policy to ensure the user belongs to the tenant through a membership relation?

I have read many examples on multitenancy implementation with Ash, but most of these examples assume that the user belongs to one tenant. In my case, a user can belong to one or more tenants and can switch from one to another using a select list. We therefore have three resources, Organization, User, and Membership, to make the connection. Our application mainly uses a JSON API where we pass an authentication token in each request, as well as a custom X-Org-Id header with the current tenant ID the user selected. I would like to create a policy that ensures that the user belongs to the organization through the Membership relationship. I thought about using the relates_to_actor_via function (https://hexdocs.pm/ash/Ash.Policy.Check.Builtins.html#relates_to_actor_via/2), but it doesn't work when creating a resource. I understand this restriction if it depends on the content of the resource, but that's not the case here. This use case seems standard to me, so I'm thinking I'm probably missing something. Any ideas on how I could do this? Edit: Whitout that check, it means that the User could create ressource under Organization that he does not belongs to, correct?
Solution:
You should be able to create a SimpleCheck that can confirm that the actor has a membership for the tenant though: https://hexdocs.pm/ash/Ash.Policy.SimpleCheck.html#content
Jump to solution
10 Replies
Geril
Geril•2mo ago
Based on my exp. it will require writing a custom FilterCheck . I was dealing with something similar here - https://discord.com/channels/711271361523351632/1418525578306257007 .
Malian
MalianOP•2mo ago
@Geril thanks to point me to your case ! I also tried something similar with exist :
policy action([:create]) do
authorize_if expr(
exists(MyApp.Accounts.Membership, user_id == ^actor(:id) and org_id == ^context(:tenant))
)
end
policy action([:create]) do
authorize_if expr(
exists(MyApp.Accounts.Membership, user_id == ^actor(:id) and org_id == ^context(:tenant))
)
end
Unfortunatly, it generates this error: Data layer does not support unrelated exists expressions Indeed, the Membership is not a direct relation of the Ressource. Another solution is to ensure this constraint at the controller/api level.
Torkan
Torkan•2mo ago
For create actions, inline checks referencing related data cannot be used, see the "Inline checks" subsection here for more info: https://hexdocs.pm/ash/policies.html#types-of-checks
Solution
Torkan
Torkan•2mo ago
You should be able to create a SimpleCheck that can confirm that the actor has a membership for the tenant though: https://hexdocs.pm/ash/Ash.Policy.SimpleCheck.html#content
Malian
MalianOP•2mo ago
Simple check is exactly what I was looking for. At the moment, with my use case, I believe I can stay with SimpleCheck even for update and other actions.
Malian
MalianOP•2mo ago
Here is the check:
defmodule MyApp.Policy.OrgMemberCheck do
use Ash.Policy.SimpleCheck

@impl true
def describe(_opts) do
"check that the user is a member of the organization"
end

@impl true
def match?(actor, %{subject: %Ash.Changeset{tenant: tenant}}, opts) do
tenant_id = Ash.ToTenant.to_tenant(tenant, nil)

allowed? =
Enum.any?(actor.memberships, fn
membership ->
membership.org_id == tenant_id
end)

{:ok, allowed?}
end
end
defmodule MyApp.Policy.OrgMemberCheck do
use Ash.Policy.SimpleCheck

@impl true
def describe(_opts) do
"check that the user is a member of the organization"
end

@impl true
def match?(actor, %{subject: %Ash.Changeset{tenant: tenant}}, opts) do
tenant_id = Ash.ToTenant.to_tenant(tenant, nil)

allowed? =
Enum.any?(actor.memberships, fn
membership ->
membership.org_id == tenant_id
end)

{:ok, allowed?}
end
end
The memberships relation has been preloaded before.
Malian
MalianOP•2mo ago
This article (unfrotunatly behind the paywall of medium :() explains in details how to do what I wanted: https://medium.com/@lambert.kamaro/part-16-understanding-authorization-in-ash-framework-7c12160535b8
ZachDaniel
ZachDaniel•2mo ago
Freeium **freedium
Malian
MalianOP•2mo ago
Whaaat?! Why am I only finding this out now?! 😱 So many thanks! I'm going to share this treasure with my colleagues right away.

Did you find this page helpful?