AlecStewart1#1125
AlecStewart1#1125
AEAsh Elixir
Created by AlecStewart1#1125 on 11/29/2023 in #support
Simple policy checks based with relationships
Hello, friends! So on the website documentation, we have an example of a simple policy check:
# we're inside of a module here
def match?(%MyApp.User{age: age} = _actor, %{resource: MyApp.Beer} = _context, _opts) do
age >= 21
end
# we're inside of a module here
def match?(%MyApp.User{age: age} = _actor, %{resource: MyApp.Beer} = _context, _opts) do
age >= 21
end
All well and good, but in a more real world example we'd probably have: - Roles for Users - Many permissions for those roles - Potentially many permissions for users So let's say we have something like the following: A User resource:
defmodule MyApp.Accounts.User do
@moduledoc """
Close to a real world example of a basic users, but
obviously there's a lot more stuff here.
Shortened for the sake of brevity.
"""
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication]

postgres do
table "app_users"
repo MyApp.Repo
end

attributes do
uuid_primary_key :id
attribute :first_name, :ci_string, allow_nil?: false
attribute :last_name, :ci_string, allow_nil?: false
attribute :email, :ci_string, allow_nil?: false, sensitive?: true
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true

create_timestamp(:created_at)
update_timestamp(:updated_at)
end

relationships do
has_one :role, MyApp.Accounts.Role
many_to_many :permissions, MyApp.Accounts.User do
through MyApp.Accounts.UserPermission
source_attribute_on_join_resource :user_id
destination_attribute_on_join_resource :permission_id
end
end
defmodule MyApp.Accounts.User do
@moduledoc """
Close to a real world example of a basic users, but
obviously there's a lot more stuff here.
Shortened for the sake of brevity.
"""
use Ash.Resource,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication]

postgres do
table "app_users"
repo MyApp.Repo
end

attributes do
uuid_primary_key :id
attribute :first_name, :ci_string, allow_nil?: false
attribute :last_name, :ci_string, allow_nil?: false
attribute :email, :ci_string, allow_nil?: false, sensitive?: true
attribute :hashed_password, :string, allow_nil?: false, sensitive?: true

create_timestamp(:created_at)
update_timestamp(:updated_at)
end

relationships do
has_one :role, MyApp.Accounts.Role
many_to_many :permissions, MyApp.Accounts.User do
through MyApp.Accounts.UserPermission
source_attribute_on_join_resource :user_id
destination_attribute_on_join_resource :permission_id
end
end
And you want to check both the role, the permissions a role has and user permissions. My question would be how do we go about doing the simple policy checks when we have a role-permission setup like this? Because the check itself isn't complex, it's more of a "how do we get there" question.
6 replies
AEAsh Elixir
Created by AlecStewart1#1125 on 6/6/2023 in #support
Aggregates with resource relationships
Hello! So maybe there's something I'm not quite understanding from the documentation, but say I have a State and City. A state can have many cities, so:
relationships do
has_many :cities, MyProject.City
end
relationships do
has_many :cities, MyProject.City
end
And a city belongs to a state:
relationships do
belongs_to :state, MyProject.State
end
relationships do
belongs_to :state, MyProject.State
end
Now for a state, say I want an aggregate for the number of cities in the state. Do I have an aggregates block like the following?
aggregates do
count :number_of_cities, :cities do
filterable? true
end
end
aggregates do
count :number_of_cities, :cities do
filterable? true
end
end
Also, could this be a oneliner as just count :number_of_cities, :cities? No do ... end? Thanks!
25 replies
AEAsh Elixir
Created by AlecStewart1#1125 on 2/27/2023 in #support
Ash Way of Doing Password Hashing and Storage for Phoenix.
So mix phx.gen.auth will give you the some of the following in your_app/lib/your_app/accounts/user.ex:
defp validate_email(changeset) do
changeset
|> validate_required([:email])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|> validate_length(:email, max: 160)
|> unsafe_validate_unique(:email, MyApp.Repo)
|> unique_constraint(:email)
end

defp validate_password(changeset, opts) do
changeset
|> validate_required([:password])
|> validate_length(:password, min: 12, max: 72)
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
# |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|> maybe_hash_password(opts)
end

defp maybe_hash_password(changeset, opts) do
hash_password? = Keyword.get(opts, :hash_password, true)
password = get_change(changeset, :password)

if hash_password? && password && changeset.valid? do
changeset
|> put_change(:hashed_password, Argon2.hash_pwd_salt(password))
|> delete_change(:password)
else
changeset
end
end
defp validate_email(changeset) do
changeset
|> validate_required([:email])
|> validate_format(:email, ~r/^[^\s]+@[^\s]+$/, message: "must have the @ sign and no spaces")
|> validate_length(:email, max: 160)
|> unsafe_validate_unique(:email, MyApp.Repo)
|> unique_constraint(:email)
end

defp validate_password(changeset, opts) do
changeset
|> validate_required([:password])
|> validate_length(:password, min: 12, max: 72)
# |> validate_format(:password, ~r/[a-z]/, message: "at least one lower case character")
# |> validate_format(:password, ~r/[A-Z]/, message: "at least one upper case character")
# |> validate_format(:password, ~r/[!?@#$%^&*_0-9]/, message: "at least one digit or punctuation character")
|> maybe_hash_password(opts)
end

defp maybe_hash_password(changeset, opts) do
hash_password? = Keyword.get(opts, :hash_password, true)
password = get_change(changeset, :password)

if hash_password? && password && changeset.valid? do
changeset
|> put_change(:hashed_password, Argon2.hash_pwd_salt(password))
|> delete_change(:password)
else
changeset
end
end
This seems like the most straightforward way when starting a Phoenix app that has users. Phoenix generates a lot of other private functions for user auth, so for Ash do you create a new module to reference and use similar functions for validate/change blocks?
22 replies
AEAsh Elixir
Created by AlecStewart1#1125 on 2/22/2023 in #support
Compile error: module Any is reserved and cannot be defined
Say I have the following resources: City:
# myapp/lib/myapp/data/resources/city.ex
defmodule MyApp.Data.City do
use Ash.Resource

actions do
defaults [:create, :update, :read, :destroy]
end

attributes do
uuid_primary_key :id
attribute :name, :string do
allow_nil? false
end
end

relationships do
has_one :state, MyApp.Data.State
end
end
# myapp/lib/myapp/data/resources/city.ex
defmodule MyApp.Data.City do
use Ash.Resource

actions do
defaults [:create, :update, :read, :destroy]
end

attributes do
uuid_primary_key :id
attribute :name, :string do
allow_nil? false
end
end

relationships do
has_one :state, MyApp.Data.State
end
end
State:
defmodule MyApp.Data.State do
@moduledoc false
use Ash.Resource

actions do
defaults [:create, :update, :read, :destroy]
end

attributes do
uuid_primary_key :state_id
attribute :name, :string do
allow_nil? false
end
attribute :url, :string do
allow_nil? true
end
end

relationships do
has_one :region, MyApp.Data.Region
end
end
defmodule MyApp.Data.State do
@moduledoc false
use Ash.Resource

actions do
defaults [:create, :update, :read, :destroy]
end

attributes do
uuid_primary_key :state_id
attribute :name, :string do
allow_nil? false
end
attribute :url, :string do
allow_nil? true
end
end

relationships do
has_one :region, MyApp.Data.Region
end
end
Region:
defmodule MyApp.Data.Region do
@moduledoc false
use Ash.Resource

actions do
defaults [:create, :update, :read, :destroy]
end

attributes do
uuid_primary_key :region_id
attribute :name, :string do
allow_nil? false
end
end
end
defmodule MyApp.Data.Region do
@moduledoc false
use Ash.Resource

actions do
defaults [:create, :update, :read, :destroy]
end

attributes do
uuid_primary_key :region_id
attribute :name, :string do
allow_nil? false
end
end
end
Credo seems to complain about state, saying:
** (CompileError) iex:1: module Any is reserved and cannot be defined
Elixir reserves the following module names: Elixir, Any, BitString, PID, and Reference.

(ArgumentError) schema does not have the field :id used by association :region, please set the :references option accordingly

Stacktrace:
│ (ecto 3.9.4) lib/ecto/association.ex:696: Ecto.Association.Has.struct/3
│ (ecto 3.9.4) lib/ecto/schema.ex:1879: Ecto.Schema.association/5
│ (ecto 3.9.4) lib/ecto/schema.ex:2006: Ecto.Schema.__has_one__/4
│ /Users/alec-s/Projects/myapp/lib/myapp/data/resources/state.ex:1: (file)
│ /Users/alec-s/Projects/myapp/lib/myapp/data/resources/state.ex:1: (file)
│ (stdlib 4.2) erl_eval.erl:748: :erl_eval.do_apply/7
│ (stdlib 4.2) erl_eval.erl:136: :erl_eval.exprs/6Elixir
** (CompileError) iex:1: module Any is reserved and cannot be defined
Elixir reserves the following module names: Elixir, Any, BitString, PID, and Reference.

(ArgumentError) schema does not have the field :id used by association :region, please set the :references option accordingly

Stacktrace:
│ (ecto 3.9.4) lib/ecto/association.ex:696: Ecto.Association.Has.struct/3
│ (ecto 3.9.4) lib/ecto/schema.ex:1879: Ecto.Schema.association/5
│ (ecto 3.9.4) lib/ecto/schema.ex:2006: Ecto.Schema.__has_one__/4
│ /Users/alec-s/Projects/myapp/lib/myapp/data/resources/state.ex:1: (file)
│ /Users/alec-s/Projects/myapp/lib/myapp/data/resources/state.ex:1: (file)
│ (stdlib 4.2) erl_eval.erl:748: :erl_eval.do_apply/7
│ (stdlib 4.2) erl_eval.erl:136: :erl_eval.exprs/6Elixir
Not really sure what to do about that.
20 replies