All checks were successful
continuous-integration/drone/push Build is passing
- Add explicit ArgumentError for invalid permission set names with helpful message - Soften performance claim in documentation (intended to be constant-time) - Add tests for error handling - Improve maintainability with guard clause for invalid inputs
285 lines
9.5 KiB
Elixir
285 lines
9.5 KiB
Elixir
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"
|
|
]
|
|
}
|
|
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_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
|
|
|
|
@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?(String.t() | atom()) :: 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
|