defmodule Mv.Authorization.PermissionSets do @moduledoc """ Defines the four hardcoded permission sets for the application. Each permission set specifies: - Resource permissions (what CRUD operations on which resources) - Page permissions (which LiveView pages can be accessed) - Scopes (own, linked, all) ## Permission Sets 1. **own_data** - Default for "Mitglied" role - Can only access own user data and linked member/custom field values - Cannot create new members or manage system 2. **read_only** - For "Vorstand" and "Buchhaltung" roles - Can read all member data - Cannot create, update, or delete 3. **normal_user** - For "Kassenwart" role - Create/Read/Update members (no delete for safety), full CRUD on custom field values - Cannot manage custom fields or users 4. **admin** - For "Admin" role - Unrestricted access to all resources - Can manage users, roles, custom fields ## Usage # Get permissions for a role's permission set permissions = PermissionSets.get_permissions(:admin) # Check if a permission set name is valid PermissionSets.valid_permission_set?("read_only") # => true # Convert string to atom safely {:ok, atom} = PermissionSets.permission_set_name_to_atom("own_data") ## Performance All functions are pure and intended to be constant-time. Permission lookups are very fast (typically < 1 microsecond in practice) as they are simple pattern matches and map lookups with no database queries or external calls. """ @type scope :: :own | :linked | :all @type action :: :read | :create | :update | :destroy @type resource_permission :: %{ resource: String.t(), action: action(), scope: scope(), granted: boolean() } @type permission_set :: %{ resources: [resource_permission()], pages: [String.t()] } @doc """ Returns the list of all valid permission set names. ## Examples iex> PermissionSets.all_permission_sets() [:own_data, :read_only, :normal_user, :admin] """ @spec all_permission_sets() :: [atom()] def all_permission_sets do [:own_data, :read_only, :normal_user, :admin] end @doc """ Returns permissions for the given permission set. ## Examples iex> permissions = PermissionSets.get_permissions(:admin) iex> Enum.any?(permissions.resources, fn p -> ...> p.resource == "User" and p.action == :destroy ...> end) true iex> PermissionSets.get_permissions(:invalid) ** (ArgumentError) invalid permission set: :invalid. Must be one of: [:own_data, :read_only, :normal_user, :admin] """ @spec get_permissions(atom()) :: permission_set() def get_permissions(set) when set not in [:own_data, :read_only, :normal_user, :admin] do raise ArgumentError, "invalid permission set: #{inspect(set)}. Must be one of: #{inspect(all_permission_sets())}" end def get_permissions(:own_data) do %{ resources: [ # User: Can always read/update own credentials %{resource: "User", action: :read, scope: :own, granted: true}, %{resource: "User", action: :update, scope: :own, granted: true}, # Member: Can read/update linked member %{resource: "Member", action: :read, scope: :linked, granted: true}, %{resource: "Member", action: :update, scope: :linked, granted: true}, # CustomFieldValue: Can read/update custom field values of linked member %{resource: "CustomFieldValue", action: :read, scope: :linked, granted: true}, %{resource: "CustomFieldValue", action: :update, scope: :linked, granted: true}, # CustomField: Can read all (needed for forms) %{resource: "CustomField", action: :read, scope: :all, granted: true} ], pages: [ # Home page "/", # Own profile "/profile", # Linked member detail (filtered by policy) "/members/:id" ] } end def get_permissions(:read_only) do %{ resources: [ # User: Can read/update own credentials only %{resource: "User", action: :read, scope: :own, granted: true}, %{resource: "User", action: :update, scope: :own, granted: true}, # Member: Can read all members, no modifications %{resource: "Member", action: :read, scope: :all, granted: true}, # CustomFieldValue: Can read all custom field values %{resource: "CustomFieldValue", action: :read, scope: :all, granted: true}, # CustomField: Can read all %{resource: "CustomField", action: :read, scope: :all, granted: true} ], pages: [ "/", # Own profile "/profile", # Member list "/members", # Member detail "/members/:id", # Custom field values overview "/custom_field_values", # Custom field value detail "/custom_field_values/:id" ] } end def get_permissions(:normal_user) do %{ resources: [ # User: Can read/update own credentials only %{resource: "User", action: :read, scope: :own, granted: true}, %{resource: "User", action: :update, scope: :own, granted: true}, # Member: Full CRUD except destroy (safety) %{resource: "Member", action: :read, scope: :all, granted: true}, %{resource: "Member", action: :create, scope: :all, granted: true}, %{resource: "Member", action: :update, scope: :all, granted: true}, # Note: destroy intentionally omitted for safety # CustomFieldValue: Full CRUD %{resource: "CustomFieldValue", action: :read, scope: :all, granted: true}, %{resource: "CustomFieldValue", action: :create, scope: :all, granted: true}, %{resource: "CustomFieldValue", action: :update, scope: :all, granted: true}, %{resource: "CustomFieldValue", action: :destroy, scope: :all, granted: true}, # CustomField: Read only (admin manages definitions) %{resource: "CustomField", action: :read, scope: :all, granted: true} ], pages: [ "/", # Own profile "/profile", "/members", # Create member "/members/new", "/members/:id", # Edit member "/members/:id/edit", "/custom_field_values", # Custom field value detail "/custom_field_values/:id", "/custom_field_values/new", "/custom_field_values/:id/edit" ] } end def get_permissions(:admin) do %{ resources: [ # User: Full management including other users %{resource: "User", action: :read, scope: :all, granted: true}, %{resource: "User", action: :create, scope: :all, granted: true}, %{resource: "User", action: :update, scope: :all, granted: true}, %{resource: "User", action: :destroy, scope: :all, granted: true}, # Member: Full CRUD %{resource: "Member", action: :read, scope: :all, granted: true}, %{resource: "Member", action: :create, scope: :all, granted: true}, %{resource: "Member", action: :update, scope: :all, granted: true}, %{resource: "Member", action: :destroy, scope: :all, granted: true}, # CustomFieldValue: Full CRUD %{resource: "CustomFieldValue", action: :read, scope: :all, granted: true}, %{resource: "CustomFieldValue", action: :create, scope: :all, granted: true}, %{resource: "CustomFieldValue", action: :update, scope: :all, granted: true}, %{resource: "CustomFieldValue", action: :destroy, scope: :all, granted: true}, # CustomField: Full CRUD (admin manages custom field definitions) %{resource: "CustomField", action: :read, scope: :all, granted: true}, %{resource: "CustomField", action: :create, scope: :all, granted: true}, %{resource: "CustomField", action: :update, scope: :all, granted: true}, %{resource: "CustomField", action: :destroy, scope: :all, granted: true}, # Role: Full CRUD (admin manages roles) %{resource: "Role", action: :read, scope: :all, granted: true}, %{resource: "Role", action: :create, scope: :all, granted: true}, %{resource: "Role", action: :update, scope: :all, granted: true}, %{resource: "Role", action: :destroy, scope: :all, granted: true} ], pages: [ # Wildcard: Admin can access all pages "*" ] } end def get_permissions(invalid) do raise ArgumentError, "invalid permission set: #{inspect(invalid)}. Must be one of: #{inspect(all_permission_sets())}" end @doc """ Checks if a permission set name (string or atom) is valid. ## Examples iex> PermissionSets.valid_permission_set?("admin") true iex> PermissionSets.valid_permission_set?(:read_only) true iex> PermissionSets.valid_permission_set?("invalid") false """ @spec valid_permission_set?(any()) :: boolean() def valid_permission_set?(name) when is_binary(name) do case permission_set_name_to_atom(name) do {:ok, _atom} -> true {:error, _} -> false end end def valid_permission_set?(name) when is_atom(name) do name in all_permission_sets() end def valid_permission_set?(_), do: false @doc """ Converts a permission set name string to atom safely. ## Examples iex> PermissionSets.permission_set_name_to_atom("admin") {:ok, :admin} iex> PermissionSets.permission_set_name_to_atom("invalid") {:error, :invalid_permission_set} """ @spec permission_set_name_to_atom(String.t()) :: {:ok, atom()} | {:error, :invalid_permission_set} def permission_set_name_to_atom(name) when is_binary(name) do atom = String.to_existing_atom(name) if valid_permission_set?(atom) do {:ok, atom} else {:error, :invalid_permission_set} end rescue ArgumentError -> {:error, :invalid_permission_set} end end