Modeling roles for access control

Role enforcement will be done via policies. Is it better to add each role to a User as a property or should I create a Role resource and add a relationship between User and Role? Seems like having a separate Role relationship would make pick screens easier; otherwise I'd probably be hard-coding them.
1 Reply
frankdugan3
frankdugan32y ago
Personally, I use an Enum type:
defmodule Hsm.Authentication.UserRole do
@moduledoc false
@employee_roles [
:IT_Admin,
# ...
]

@all_roles [:Auditor | @employee_roles]

use Ash.Type.Enum, values: @all_roles

for role <- @all_roles do
def unquote(:"_#{role}")(), do: unquote(role)
end

def _employee_roles, do: @employee_roles

def graphql_type, do: :user_role
end
defmodule Hsm.Authentication.UserRole do
@moduledoc false
@employee_roles [
:IT_Admin,
# ...
]

@all_roles [:Auditor | @employee_roles]

use Ash.Type.Enum, values: @all_roles

for role <- @all_roles do
def unquote(:"_#{role}")(), do: unquote(role)
end

def _employee_roles, do: @employee_roles

def graphql_type, do: :user_role
end
I use it as an array and also added an active field to disable users while keeping their record (auditing for ERP):
attribute :roles, {:array, UserRole}, allow_nil?: false
attribute :active, :boolean, allow_nil?: false, default: true
attribute :roles, {:array, UserRole}, allow_nil?: false
attribute :active, :boolean, allow_nil?: false, default: true
I also made some custom checks to facilitate policies and roles:
forbid_unless actor_is_active()
authorize_if actor_roles_has_any([
UserRole._Executive(),
UserRole._Human_Resources(),
UserRole._IT_Admin()
])
forbid_unless actor_is_active()
authorize_if actor_roles_has_any([
UserRole._Executive(),
UserRole._Human_Resources(),
UserRole._IT_Admin()
])
There are lots of pros/cons with this setup, but it's worked well for my particular scenario and is quite performant since all the data is right on the user resource. Using the helpers to get the enum value is good for refactoring and avoiding typos in policies. Oh, and the array of enums works out of the box w/ a Phoenix or Surface multiple select, and the enum helpers provide the options as well. I use capitals and underscores as spaces in my enum names. This is compatible with GraphQL, and allows to easily humanize them:
defmodule Hsm.EnumHelpers do
@moduledoc false
def humanize_enum(enum) when is_binary(enum) do
String.replace(enum, "_", " ")
end

def humanize_enum(enum) when is_atom(enum) do
String.replace(Atom.to_string(enum), "_", " ")
end

def humanize_enum_list(list) do
Enum.map_join(list, ", ", fn e -> humanize_enum(e) end)
end

# Ecto Enums
def enum_to_form_options(schema, field, opts \\ [allow_nil?: false]) do
if Keyword.get(opts, :allow_nil?, false) do
Enum.map(["" | Ecto.Enum.values(schema, field)], fn e ->
{humanize_enum(e), e}
end)
else
Enum.map(Ecto.Enum.values(schema, field), fn e ->
{humanize_enum(e), e}
end)
end
end

# Ash Enums
def enum_to_form_options(module) do
Enum.map(apply(module, :values, []), fn e ->
{humanize_enum(e), Atom.to_string(e)}
end)
end
end
defmodule Hsm.EnumHelpers do
@moduledoc false
def humanize_enum(enum) when is_binary(enum) do
String.replace(enum, "_", " ")
end

def humanize_enum(enum) when is_atom(enum) do
String.replace(Atom.to_string(enum), "_", " ")
end

def humanize_enum_list(list) do
Enum.map_join(list, ", ", fn e -> humanize_enum(e) end)
end

# Ecto Enums
def enum_to_form_options(schema, field, opts \\ [allow_nil?: false]) do
if Keyword.get(opts, :allow_nil?, false) do
Enum.map(["" | Ecto.Enum.values(schema, field)], fn e ->
{humanize_enum(e), e}
end)
else
Enum.map(Ecto.Enum.values(schema, field), fn e ->
{humanize_enum(e), e}
end)
end
end

# Ash Enums
def enum_to_form_options(module) do
Enum.map(apply(module, :values, []), fn e ->
{humanize_enum(e), Atom.to_string(e)}
end)
end
end

Did you find this page helpful?