Both permission sets allow User:update :own, so users should be able to access their profile page. This makes the implementation consistent with the documentation and the logical permission model.
278 lines
9.1 KiB
Elixir
278 lines
9.1 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 compile-time. Permission lookups are < 1 microsecond.
|
|
"""
|
|
|
|
@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)
|
|
** (FunctionClauseError) no function clause matching
|
|
"""
|
|
@spec get_permissions(atom()) :: permission_set()
|
|
|
|
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
|