Used by Mv.Release to resolve Admin role when creating/updating admin user from ENV.
198 lines
5.6 KiB
Elixir
198 lines
5.6 KiB
Elixir
defmodule Mv.Authorization.Role do
|
|
@moduledoc """
|
|
Represents a user role that references a permission set.
|
|
|
|
Roles are stored in the database and link users to permission sets.
|
|
Each role has a `permission_set_name` that references one of the four
|
|
hardcoded permission sets defined in `Mv.Authorization.PermissionSets`.
|
|
|
|
## Fields
|
|
|
|
- `name` - Unique role name (e.g., "Vorstand", "Admin")
|
|
- `description` - Human-readable description of the role
|
|
- `permission_set_name` - Must be one of: "own_data", "read_only", "normal_user", "admin"
|
|
- `is_system_role` - If true, role cannot be deleted (protects critical roles like "Mitglied")
|
|
|
|
## Relationships
|
|
|
|
- `has_many :users` - Users assigned to this role
|
|
|
|
## Validations
|
|
|
|
- `permission_set_name` must be a valid permission set (checked against PermissionSets.all_permission_sets/0)
|
|
- `name` must be unique
|
|
- System roles cannot be deleted (enforced via validation)
|
|
|
|
## Examples
|
|
|
|
# Create a new role
|
|
{:ok, role} = Mv.Authorization.create_role(%{
|
|
name: "Vorstand",
|
|
description: "Board member with read access",
|
|
permission_set_name: "read_only"
|
|
})
|
|
|
|
# List all roles
|
|
{:ok, roles} = Mv.Authorization.list_roles()
|
|
"""
|
|
use Ash.Resource,
|
|
domain: Mv.Authorization,
|
|
data_layer: AshPostgres.DataLayer,
|
|
authorizers: [Ash.Policy.Authorizer]
|
|
|
|
postgres do
|
|
table "roles"
|
|
repo Mv.Repo
|
|
|
|
references do
|
|
# Prevent deletion of roles that are assigned to users
|
|
reference :users, on_delete: :restrict
|
|
end
|
|
end
|
|
|
|
code_interface do
|
|
define :create_role
|
|
define :list_roles, action: :read
|
|
define :update_role
|
|
define :destroy_role, action: :destroy
|
|
end
|
|
|
|
actions do
|
|
defaults [:read]
|
|
|
|
create :create_role do
|
|
primary? true
|
|
# is_system_role is intentionally excluded - should only be set via seeds/internal actions
|
|
accept [:name, :description, :permission_set_name]
|
|
# Note: In Ash 3.0, require_atomic? is not available for create actions
|
|
# Custom validations will still work
|
|
end
|
|
|
|
create :create_role_with_system_flag do
|
|
description "Internal action to create roles, allowing `is_system_role` to be set. Used by seeds and migrations."
|
|
accept [:name, :description, :permission_set_name, :is_system_role]
|
|
end
|
|
|
|
update :update_role do
|
|
primary? true
|
|
# is_system_role is intentionally excluded - should only be set via seeds/internal actions
|
|
accept [:name, :description, :permission_set_name]
|
|
# Required because custom validation functions cannot be executed atomically
|
|
require_atomic? false
|
|
end
|
|
|
|
destroy :destroy do
|
|
# Required because custom validation functions cannot be executed atomically
|
|
require_atomic? false
|
|
end
|
|
end
|
|
|
|
policies do
|
|
policy action_type([:read, :create, :update, :destroy]) do
|
|
description "Role access: read for all permission sets, create/update/destroy for admin only (PermissionSets)"
|
|
authorize_if Mv.Authorization.Checks.HasPermission
|
|
end
|
|
end
|
|
|
|
validations do
|
|
validate one_of(
|
|
:permission_set_name,
|
|
Mv.Authorization.PermissionSets.all_permission_sets()
|
|
|> Enum.map(&Atom.to_string/1)
|
|
),
|
|
message:
|
|
"must be one of: #{Mv.Authorization.PermissionSets.all_permission_sets() |> Enum.map_join(", ", &Atom.to_string/1)}"
|
|
|
|
validate fn changeset, _context ->
|
|
if changeset.data.is_system_role do
|
|
{:error,
|
|
field: :is_system_role,
|
|
message:
|
|
"Cannot delete system role. System roles are required for the application to function."}
|
|
else
|
|
:ok
|
|
end
|
|
end,
|
|
on: [:destroy]
|
|
end
|
|
|
|
attributes do
|
|
uuid_v7_primary_key :id
|
|
|
|
attribute :name, :string do
|
|
allow_nil? false
|
|
public? true
|
|
end
|
|
|
|
attribute :description, :string do
|
|
allow_nil? true
|
|
public? true
|
|
end
|
|
|
|
attribute :permission_set_name, :string do
|
|
allow_nil? false
|
|
public? true
|
|
end
|
|
|
|
attribute :is_system_role, :boolean do
|
|
allow_nil? false
|
|
default false
|
|
public? true
|
|
end
|
|
|
|
timestamps()
|
|
end
|
|
|
|
relationships do
|
|
has_many :users, Mv.Accounts.User do
|
|
destination_attribute :role_id
|
|
end
|
|
end
|
|
|
|
identities do
|
|
identity :unique_name, [:name]
|
|
end
|
|
|
|
@doc """
|
|
Loads the "Mitglied" role without authorization (for bootstrap operations).
|
|
|
|
This is a helper function to avoid code duplication when loading the default
|
|
role in changes, migrations, and test setup.
|
|
|
|
## Returns
|
|
|
|
- `{:ok, %Mv.Authorization.Role{}}` - The "Mitglied" role
|
|
- `{:ok, nil}` - Role doesn't exist
|
|
- `{:error, term()}` - Error during lookup
|
|
|
|
## Examples
|
|
|
|
{:ok, mitglied_role} = Mv.Authorization.Role.get_mitglied_role()
|
|
# => {:ok, %Mv.Authorization.Role{name: "Mitglied", ...}}
|
|
|
|
{:ok, nil} = Mv.Authorization.Role.get_mitglied_role()
|
|
# => Role doesn't exist (e.g., in test environment before seeds run)
|
|
"""
|
|
@spec get_mitglied_role() :: {:ok, t() | nil} | {:error, term()}
|
|
def get_mitglied_role do
|
|
require Ash.Query
|
|
|
|
__MODULE__
|
|
|> Ash.Query.filter(name == "Mitglied")
|
|
|> Ash.read_one(authorize?: false, domain: Mv.Authorization)
|
|
end
|
|
|
|
@doc """
|
|
Returns the Admin role if it exists.
|
|
|
|
Used by release tasks (e.g. seed_admin) and OIDC role sync to assign the admin role.
|
|
"""
|
|
@spec get_admin_role() :: {:ok, t() | nil} | {:error, term()}
|
|
def get_admin_role do
|
|
require Ash.Query
|
|
|
|
__MODULE__
|
|
|> Ash.Query.filter(name == "Admin")
|
|
|> Ash.read_one(authorize?: false, domain: Mv.Authorization)
|
|
end
|
|
end
|