From 4535551b8d83e8e52e11ef9fbeda5aed3a2fb529 Mon Sep 17 00:00:00 2001 From: Moritz Date: Tue, 6 Jan 2026 17:18:32 +0100 Subject: [PATCH] feat: add Role resource with validations Create Role resource with name, description, permission_set_name, and is_system_role fields. Add validations for permission_set_name and system role deletion protection. --- lib/mv/authorization/role.ex | 152 +++++++++++++++++++++++++++++++++++ 1 file changed, 152 insertions(+) create mode 100644 lib/mv/authorization/role.ex diff --git a/lib/mv/authorization/role.ex b/lib/mv/authorization/role.ex new file mode 100644 index 0000000..e5b9795 --- /dev/null +++ b/lib/mv/authorization/role.ex @@ -0,0 +1,152 @@ +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 + + postgres do + table "roles" + repo Mv.Repo + 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 + accept [:name, :description, :permission_set_name, :is_system_role] + # Note: In Ash 3.0, require_atomic? is not available for create actions + # Custom validations will still work + end + + update :update_role do + primary? true + accept [:name, :description, :permission_set_name, :is_system_role] + # 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 + + validations do + validate fn changeset, _context -> + permission_set_name = Ash.Changeset.get_attribute(changeset, :permission_set_name) + + if permission_set_name do + valid_sets = + Mv.Authorization.PermissionSets.all_permission_sets() + |> Enum.map(&Atom.to_string/1) + + if permission_set_name in valid_sets do + :ok + else + valid_sets_string = Enum.join(valid_sets, ", ") + + {:error, + field: :permission_set_name, + message: "Invalid permission set name. Must be one of: #{valid_sets_string}"} + end + else + :ok + end + end + + validate fn changeset, _context -> + if changeset.action_type == :destroy do + if changeset.data.is_system_role do + {:error, + message: + "Cannot delete system role. System roles are required for the application to function."} + else + :ok + end + else + :ok + end + end, + on: [:destroy] + end + + attributes do + uuid_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 +end