Created
October 6, 2024 09:37
-
-
Save kamaroly/986c40d1b950373f6577d973a414f01b to your computer and use it in GitHub Desktop.
How To Use Ash framework in Phoenix with user permission example
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyApp.Accounts.Checks.Can do | |
use Ash.Policy.SimpleCheck | |
def describe(_opts) do | |
"Check if a user/ actor has permission on a specific resource" | |
end | |
# @impl true | |
def match?(nil, _, _), do: false | |
def match?(actor, %{query: query, changeset: changeset}, _opts) do | |
# for read actions, the query will be populated | |
# for create/update/destroy actions, the changeset will be populated | |
subject = query || changeset | |
resource = | |
subject.resource | |
|> Phoenix.Naming.resource_name() | |
|> String.downcase() | |
# replace `.type` with `.name` for action name | |
action_type = convert_action_name(subject.action.type) | |
{:ok, actor |> can?(action_type, resource)} | |
end | |
@doc false | |
def can?(user, action_type, resource) do | |
# Gather all permissions from the user's groups | |
# Check if any user permission matches the action and resource | |
Enum.flat_map(user.groups || [], & &1.permissions) | |
|> Enum.map(&%{action: &1.name, resource: &1.resource}) | |
|> Enum.any?(&(&1.action == action_type and &1.resource == resource)) | |
end | |
@doc false | |
def cannot?(user, action_type, resource) do | |
can?(user, action_type, resource) == false | |
end | |
defp convert_action_name(action_name) when is_binary(action_name) do | |
String.downcase(action_name) | |
end | |
defp convert_action_name(action_type) when is_atom(action_type) do | |
Atom.to_string(action_type) | |
|> String.downcase() | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyApp.Organisations.Department do | |
use Ash.Resource, | |
domain: MyApp.Organisations, | |
authorizers: [Ash.Policy.Authorizer], | |
data_layer: AshPostgres.DataLayer | |
postgres do | |
table "departments" | |
repo MyApp.Repo | |
end | |
actions do | |
default_accept [:name, :description, :status, :type] | |
defaults [:read, :destroy, :create, :update] | |
create :seed do | |
description "Seed default departments" | |
accept [] | |
manual Hr.Organisations.Departments.Actions.SeedDepartment | |
end | |
read :root do | |
filter expr(is_nil(parent_id)) | |
end | |
read :by_parent do | |
argument :parent_id, :uuid | |
filter expr(parent_id == ^arg(:parent_id)) | |
end | |
read :get_divisions do | |
argument :department_id, :uuid | |
filter expr(parent_id == ^arg(:parent_id)) | |
end | |
end | |
policies do | |
policy action_type(:create), authorize_if: MyApp.Accounts.Checks.Can | |
policy action_type(:read), authorize_if: MyApp.Accounts.Checks.Can | |
policy action_type(:update), authorize_if: MyApp.Accounts.Checks.Can | |
policy action_type(:destroy), authorize_if: MyApp.Accounts.Checks.Can | |
end | |
attributes do | |
uuid_primary_key :id | |
attribute :name, :string, allow_nil?: false | |
attribute :description, :string, allow_nil?: true | |
attribute :type, :atom do | |
default :department | |
constraints one_of: [:department, :division, :others] | |
end | |
attribute :status, :atom do | |
default :active | |
constraints one_of: [:active, :inactive] | |
end | |
timestamps() | |
end | |
identities do | |
identity :unique_name, :name | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyApp.Accounts.Group do | |
require Ash.Resource.Change.Builtins | |
use Ash.Resource, | |
domain: MyApp.Accounts, | |
data_layer: AshPostgres.DataLayer | |
postgres do | |
table "groups" | |
repo Hr.Repo | |
references do | |
reference :permissions, on_delete: :nilify | |
reference :users, on_delete: :nilify | |
end | |
end | |
actions do | |
default_accept [:name, :description] | |
defaults [:read, :destroy] | |
create :seed do | |
accept [] | |
manual MyApp.Accounts.Actions.SeedGroup | |
end | |
update :update do | |
argument :permission_ids, {:array, :uuid}, allow_nil?: true | |
manual MyApp.Accounts.Actions.UpdateGroup | |
primary? true | |
end | |
create :create do | |
argument :permission_ids, {:array, :uuid}, allow_nil?: true | |
primary? true | |
change after_action(&MyApp.Accounts.Group.Permission.add_permissions/3) | |
end | |
update :add_permissions do | |
argument :permission_ids, {:array, :uuid} | |
require_atomic? false | |
change manage_relationship(:permission_ids, :permissions, | |
type: :append_and_remove, | |
on_no_match: :create | |
) | |
end | |
update :add_users do | |
require_atomic? false | |
argument :user_ids, {:array, :uuid} | |
change manage_relationship(:user_ids, :users, | |
type: :append_and_remove, | |
on_no_match: :create | |
) | |
end | |
read :search do | |
argument :keyword, :string, allow_nil?: false | |
filter expr( | |
ilike(name, "%#{^arg(:keyword)}%") or | |
ilike(description, "%#{^arg(:keyword)}%") | |
) | |
end | |
end | |
attributes do | |
uuid_primary_key :id | |
attribute :name, :string, allow_nil?: false | |
attribute :description, :string | |
timestamps() | |
end | |
relationships do | |
many_to_many :permissions, MyApp.Accounts.Permission do | |
through Hr.Accounts.GroupPermission | |
source_attribute_on_join_resource :group_id | |
destination_attribute_on_join_resource :permission_id | |
end | |
many_to_many :users, MyApp.Accounts.User do | |
through Hr.Accounts.UserGroup | |
source_attribute_on_join_resource :group_id | |
destination_attribute_on_join_resource :user_id | |
end | |
end | |
identities do | |
identity :unique_name, :name | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyApp.Accounts.GroupPermission do | |
use Ash.Resource, | |
domain: MyApp.Accounts, | |
data_layer: AshPostgres.DataLayer | |
postgres do | |
table "group_permissions" | |
repo Hr.Repo | |
end | |
actions do | |
defaults [:read, :destroy, :create, :update] | |
end | |
multitenancy do | |
strategy :context | |
end | |
attributes do | |
timestamps() | |
end | |
relationships do | |
belongs_to :group, MyApp.Accounts.Group do | |
writable? true | |
allow_nil? false | |
primary_key? true | |
end | |
belongs_to :permission, MyApp.Accounts.Permission do | |
writable? true | |
allow_nil? false | |
primary_key? true | |
end | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyApp.Accounts.Permission do | |
use Ash.Resource, | |
domain: MyApp.Accounts, | |
data_layer: AshPostgres.DataLayer | |
postgres do | |
table "permissions" | |
repo MyApp.Repo | |
end | |
actions do | |
default_accept [:name, :resource, :description] | |
defaults [:create, :read, :update, :destroy] | |
create :seed_permissions do | |
description "Seed all application permissions as configured" | |
accept [] | |
manual MyApp.Accounts.Actions.SeedPermission | |
end | |
end | |
attributes do | |
uuid_primary_key :id | |
attribute :name, :string, allow_nil?: false | |
attribute :resource, :string, allow_nil?: false | |
attribute :description, :string | |
timestamps() | |
end | |
relationships do | |
many_to_many :groups, MyApp.Accounts.Group do | |
through MyApp.Accounts.GroupPermission | |
source_attribute_on_join_resource :permission_id | |
destination_attribute_on_join_resource :group_id | |
end | |
end | |
identities do | |
identity :unique_action_resource, [:name, :resource] | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyApp.Accounts.User do | |
use Ash.Resource, | |
domain: Hr.Accounts, | |
data_layer: AshPostgres.DataLayer, | |
# If using policies, enable the policy authorizer: | |
# authorizers: [Ash.Policy.Authorizer], | |
extensions: [AshAuthentication] | |
postgres do | |
table "users" | |
repo MyApp.Repo | |
end | |
authentication do | |
strategies do | |
password :password do | |
identity_field :email | |
end | |
end | |
tokens do | |
enabled? true | |
token_resource MyApp.Accounts.Token | |
signing_secret MyApp.Accounts.Secrets | |
end | |
end | |
actions do | |
default_accept [:email, :hashed_password] | |
defaults [:create, :read, :destroy, update: :*] | |
update :add_groups do | |
description "Adds Groups to a user" | |
argument :group_ids, {:array, :uuid} | |
require_atomic? false | |
change manage_relationship(:group_ids, :groups, | |
type: :append_and_remove, | |
on_no_match: :create | |
) | |
end | |
end | |
attributes do | |
uuid_primary_key :id | |
attribute :email, :ci_string do | |
allow_nil? false | |
public? true | |
end | |
attribute :current_tenant, :string, allow_nil?: true | |
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true | |
timestamps() | |
end | |
relationships do | |
many_to_many :groups, MyApp.Accounts.Group do | |
through MyApp.Accounts.UserGroup | |
source_attribute_on_join_resource :user_id | |
destination_attribute_on_join_resource :group_id | |
end | |
end | |
identities do | |
identity :unique_email, [:email] | |
end | |
end |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
defmodule MyApp.Accounts.UserGroup do | |
use Ash.Resource, | |
domain: MyApp.Accounts, | |
data_layer: AshPostgres.DataLayer | |
postgres do | |
table "user_groups" | |
repo MyApp.Repo | |
end | |
actions do | |
defaults [:create, :read, :update, :destroy] | |
end | |
attributes do | |
timestamps() | |
end | |
relationships do | |
belongs_to :group, MyApp.Accounts.Group, allow_nil?: false, primary_key?: true | |
belongs_to :user, MyApp.Accounts.User, allow_nil?: false, primary_key?: true | |
end | |
end |
To be clear: there is nothing wrong with your original example, it's a good one! Just chiming in with some feedback to potentially make it even better 🙇
I like this. Thanks @zachdaniel.
-
I wonder if it would work for attribute on a relationship of the
actor
. If yes, how would one access the relationship from actor_attribute_equals. -
Also, would this work if one manually authorizes the action by
authorize?: true
- right now you can't access relationships via
actor_attribute_equals
. Anything that does that will have to be a custom check. authorize?: true
doesn't make the action authorized. It determines if authorization occurs. The default value istrue
. To bypass authorization, you'd need to doauthorize?: false
.
Thanks for the clarificatin, @zachdaniel.
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
The simplest example of a policy would likely be something that requires no custom checks.
So now if you were calling actions on this resource:
Then, for read policies, you can see how they are applied as filters: