Merge branch 'main' into feature/337_polish_import
Some checks failed
continuous-integration/drone/push Build is failing
Some checks failed
continuous-integration/drone/push Build is failing
This commit is contained in:
commit
3415faeb21
87 changed files with 4381 additions and 1171 deletions
|
|
@ -4,6 +4,7 @@
|
|||
2. **DB constraints** - Prevent duplicates within same table (users.email, members.email)
|
||||
3. **Custom validations** - Prevent cross-table conflicts only for linked entities
|
||||
4. **Sync is bidirectional**: User ↔ Member (but User always wins on link)
|
||||
5. **Linked member email change** - When a member is linked, only administrators or the linked user may change that member's email (Member resource validation `EmailChangePermission`). Because User.email wins on link and changes sync Member → User, allowing anyone to change a linked member's email would overwrite that user's account email; this rule keeps sync under control.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -4,7 +4,7 @@
|
|||
**Feature:** Groups Management
|
||||
**Version:** 1.0
|
||||
**Last Updated:** 2025-01-XX
|
||||
**Status:** Architecture Design - Ready for Implementation
|
||||
**Status:** ✅ Implemented (authorization: see [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md))
|
||||
|
||||
---
|
||||
|
||||
|
|
@ -412,15 +412,17 @@ lib/
|
|||
|
||||
## Authorization
|
||||
|
||||
**Status:** ✅ Implemented. Group and MemberGroup resource policies and PermissionSets are in place. See [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md) for the full permission matrix and policy patterns.
|
||||
|
||||
### Permission Model (MVP)
|
||||
|
||||
**Resource:** `groups`
|
||||
**Resource:** `Group` (and `MemberGroup`)
|
||||
|
||||
**Actions:**
|
||||
- `read` - View groups (all users with member read permission)
|
||||
- `create` - Create groups (admin only)
|
||||
- `update` - Edit groups (admin only)
|
||||
- `destroy` - Delete groups (admin only)
|
||||
- `read` - View groups (all permission sets)
|
||||
- `create` - Create groups (normal_user and admin)
|
||||
- `update` - Edit groups (normal_user and admin)
|
||||
- `destroy` - Delete groups (normal_user and admin)
|
||||
|
||||
**Scopes:**
|
||||
- `:all` - All groups (for all permission sets that have read access)
|
||||
|
|
@ -442,7 +444,7 @@ lib/
|
|||
**Own Data Permission Set:**
|
||||
- `read` action on `Group` resource with `:all` scope - granted
|
||||
|
||||
**Note:** All permission sets use `:all` scope for groups. Groups are considered public information that all users with member read permission can view. Only admins can manage (create/update/destroy) groups.
|
||||
**Note:** All permission sets use `:all` scope for groups. Groups are considered public information that all users with member read permission can view. normal_user and admin can manage (create/update/destroy) groups.
|
||||
|
||||
### Member-Group Association Permissions
|
||||
|
||||
|
|
|
|||
|
|
@ -334,20 +334,18 @@ lib/
|
|||
|
||||
### Permission System Integration
|
||||
|
||||
**See:** [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md)
|
||||
**Status:** ✅ Implemented. See [roles-and-permissions-architecture.md](./roles-and-permissions-architecture.md) for the full permission matrix and policy patterns.
|
||||
|
||||
**Required Permissions:**
|
||||
**PermissionSets (lib/mv/authorization/permission_sets.ex):**
|
||||
|
||||
- `MembershipFeeType.create/update/destroy` - Admin only
|
||||
- `MembershipFeeType.read` - Admin, Treasurer, Board
|
||||
- `MembershipFeeCycle.update` (status changes) - Admin, Treasurer
|
||||
- `MembershipFeeCycle.read` - Admin, Treasurer, Board, Own member
|
||||
- **MembershipFeeType:** All permission sets can read (:all); only admin has create/update/destroy (:all).
|
||||
- **MembershipFeeCycle:** All can read (:all); read_only has read only; normal_user and admin have read + create + update + destroy (:all).
|
||||
- **Manual "Regenerate Cycles" (UI + server):** The "Regenerate Cycles" button in the member detail view is shown to users who have MembershipFeeCycle create permission (normal_user and admin). UI access is gated by `can_create_cycle`. The LiveView handler also enforces `can?(:create, MembershipFeeCycle)` server-side before running regeneration (so e.g. a read_only user cannot trigger it via DevTools). Regeneration runs with system actor.
|
||||
|
||||
**Policy Patterns:**
|
||||
**Resource Policies:**
|
||||
|
||||
- Use existing HasPermission check
|
||||
- Leverage existing roles (Admin, Kassenwart)
|
||||
- Member can read own cycles (linked via member_id)
|
||||
- **MembershipFeeType** (`lib/membership_fees/membership_fee_type.ex`): `authorizers: [Ash.Policy.Authorizer]`, single policy with `HasPermission` for read/create/update/destroy.
|
||||
- **MembershipFeeCycle** (`lib/membership_fees/membership_fee_cycle.ex`): Same pattern; update includes mark_as_paid, mark_as_suspended, mark_as_unpaid.
|
||||
|
||||
### LiveView Integration
|
||||
|
||||
|
|
@ -357,7 +355,7 @@ lib/
|
|||
2. MembershipFeeCycle table component (member detail view)
|
||||
- Implemented as `MvWeb.MemberLive.Show.MembershipFeesComponent`
|
||||
- Displays all cycles in a table with status management
|
||||
- Allows changing cycle status, editing amounts, and regenerating cycles
|
||||
- Allows changing cycle status, editing amounts, and manually regenerating cycles (normal_user and admin)
|
||||
3. Settings form section (admin)
|
||||
4. Member list column (membership fee status)
|
||||
|
||||
|
|
|
|||
|
|
@ -97,6 +97,10 @@ Control CRUD operations on:
|
|||
- CustomFieldValue (custom field values)
|
||||
- CustomField (custom field definitions)
|
||||
- Role (role management)
|
||||
- Group (group definitions; read all, create/update/destroy normal_user and admin)
|
||||
- MemberGroup (member–group associations; own_data read :linked, read_only read :all, normal_user/admin create/destroy)
|
||||
- MembershipFeeType (fee type definitions; all read, admin-only create/update/destroy)
|
||||
- MembershipFeeCycle (fee cycles; own_data read :linked, read_only read :all, normal_user/admin read+create+update+destroy; manual "Regenerate Cycles" for normal_user and admin)
|
||||
|
||||
**4. Page-Level Permissions**
|
||||
|
||||
|
|
@ -105,6 +109,7 @@ Control access to LiveView pages:
|
|||
- Show pages (detail views)
|
||||
- Form pages (create/edit)
|
||||
- Admin pages
|
||||
- Settings pages: `/settings` and `/membership_fee_settings` are admin-only (explicit in PermissionSets)
|
||||
|
||||
**5. Granular Scopes**
|
||||
|
||||
|
|
@ -121,6 +126,8 @@ Three scope levels for permissions:
|
|||
- **Linked Member Email:** Only admins can edit email of member linked to user
|
||||
- **System Roles:** "Mitglied" role cannot be deleted (is_system_role flag)
|
||||
- **User-Member Linking:** Only admins can link/unlink users and members
|
||||
- **User Role Assignment:** Only admins can change a user's role (via `update_user` with `role_id`). Last-admin validation ensures at least one user keeps the Admin role.
|
||||
- **Settings Pages:** `/settings` and `/membership_fee_settings` are admin-only (explicit in PermissionSets pages).
|
||||
|
||||
**7. UI Consistency**
|
||||
|
||||
|
|
@ -684,6 +691,12 @@ Quick reference table showing what each permission set allows:
|
|||
| **CustomFieldValue** (all) | - | R | R, C, U, D | R, C, U, D |
|
||||
| **CustomField** (all) | R | R | R | R, C, U, D |
|
||||
| **Role** (all) | - | - | - | R, C, U, D |
|
||||
| **Group** (all) | R | R | R, C, U, D | R, C, U, D |
|
||||
| **MemberGroup** (linked) | R | - | - | - |
|
||||
| **MemberGroup** (all) | - | R | R, C, D | R, C, D |
|
||||
| **MembershipFeeType** (all) | R | R | R | R, C, U, D |
|
||||
| **MembershipFeeCycle** (linked) | R | - | - | - |
|
||||
| **MembershipFeeCycle** (all) | - | R | R, C, U, D | R, C, U, D |
|
||||
|
||||
**Legend:** R=Read, C=Create, U=Update, D=Destroy
|
||||
|
||||
|
|
@ -1012,16 +1025,21 @@ defmodule Mv.Membership.Member do
|
|||
authorize_if expr(id == ^actor(:member_id))
|
||||
end
|
||||
|
||||
# 2. GENERAL: Check permissions from role
|
||||
# - :own_data → can UPDATE linked member (scope :linked via HasPermission)
|
||||
# - :read_only → can READ all members (scope :all), no update permission
|
||||
# - :normal_user → can CRUD all members (scope :all)
|
||||
# - :admin → can CRUD all members (scope :all)
|
||||
policy action_type([:read, :create, :update, :destroy]) do
|
||||
# 2. READ/DESTROY: Check permissions only (no :user argument on these actions)
|
||||
policy action_type([:read, :destroy]) do
|
||||
description "Check permissions from user's role"
|
||||
authorize_if Mv.Authorization.Checks.HasPermission
|
||||
end
|
||||
|
||||
|
||||
# 3. CREATE/UPDATE: Forbid user link unless admin; then check permissions
|
||||
# ForbidMemberUserLinkUnlessAdmin: only admins may pass :user (link or unlink via nil/empty).
|
||||
# HasPermission: :own_data → update linked; :read_only → no update; :normal_user/admin → update all
|
||||
policy action_type([:create, :update]) do
|
||||
description "Forbid user link unless admin; then check permissions"
|
||||
forbid_if Mv.Authorization.Checks.ForbidMemberUserLinkUnlessAdmin
|
||||
authorize_if Mv.Authorization.Checks.HasPermission
|
||||
end
|
||||
|
||||
# 4. DEFAULT: Ash implicitly forbids if no policy authorizes (fail-closed)
|
||||
end
|
||||
|
||||
|
|
@ -1041,6 +1059,8 @@ end
|
|||
- **READ list queries**: No record at strict_check time → bypass with `expr(id == ^actor(:member_id))` needed for auto_filter ✅
|
||||
- **UPDATE operations**: Changeset contains record → HasPermission evaluates `scope :linked` correctly ✅
|
||||
|
||||
**User–member link:** Only admins may pass the `:user` argument on create_member or update_member (link or unlink via `user: nil`/`user: %{}`). The check uses **argument presence** (key in arguments), not value, to avoid bypass (see [User-Member Linking](#user-member-linking)).
|
||||
|
||||
**Permission Matrix:**
|
||||
|
||||
| Action | Mitglied | Vorstand | Kassenwart | Buchhaltung | Admin |
|
||||
|
|
@ -1135,23 +1155,20 @@ end
|
|||
|
||||
**Location:** `lib/mv/authorization/role.ex`
|
||||
|
||||
**Special Protection:** System roles cannot be deleted.
|
||||
**Defense-in-depth:** The Role resource uses `authorizers: [Ash.Policy.Authorizer]` and policies with `Mv.Authorization.Checks.HasPermission`. **Read** is allowed for all permission sets (own_data, read_only, normal_user, admin) via `perm("Role", :read, :all)` in PermissionSets; reading roles is not a security concern. **Create, update, and destroy** are allowed only for admin (admin has full Role CRUD in PermissionSets). Seeds and bootstrap use `authorize?: false` where necessary.
|
||||
|
||||
**Special Protection:** System roles cannot be deleted (validation on destroy).
|
||||
|
||||
```elixir
|
||||
defmodule Mv.Authorization.Role do
|
||||
use Ash.Resource, ...
|
||||
use Ash.Resource,
|
||||
authorizers: [Ash.Policy.Authorizer]
|
||||
|
||||
policies do
|
||||
# Only admin can manage roles
|
||||
policy action_type([:read, :create, :update, :destroy]) do
|
||||
description "Check permissions from user's role"
|
||||
description "Check permissions from user's role (read all, create/update/destroy admin only)"
|
||||
authorize_if Mv.Authorization.Checks.HasPermission
|
||||
end
|
||||
|
||||
# DEFAULT: Forbid
|
||||
policy action_type([:read, :create, :update, :destroy]) do
|
||||
forbid_if always()
|
||||
end
|
||||
end
|
||||
|
||||
# Prevent deletion of system roles
|
||||
|
|
@ -1188,13 +1205,43 @@ end
|
|||
|
||||
| Action | Mitglied | Vorstand | Kassenwart | Buchhaltung | Admin |
|
||||
|--------|----------|----------|------------|-------------|-------|
|
||||
| Read | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| Read | ✅ | ✅ | ✅ | ✅ | ✅ |
|
||||
| Create | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| Update | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
| Destroy* | ❌ | ❌ | ❌ | ❌ | ✅ |
|
||||
|
||||
*Cannot destroy if `is_system_role=true`
|
||||
|
||||
### User Role Assignment (Admin-Only)
|
||||
|
||||
**Location:** `lib/accounts/user.ex` (update_user action), `lib/mv_web/live/user_live/form.ex`
|
||||
|
||||
Only admins can change a user's role. The `update_user` action accepts `role_id`; the User form shows a role dropdown when `can?(actor, :update, Mv.Authorization.Role)`. **Last-admin validation:** If the only non-system admin tries to change their role, the change is rejected with "At least one user must keep the Admin role." (System user is excluded from the admin count.) See [User-Member Linking](#user-member-linking) for the same admin-only pattern.
|
||||
|
||||
### Group Resource Policies
|
||||
|
||||
**Location:** `lib/membership/group.ex`
|
||||
|
||||
Policies use `HasPermission` for read/create/update/destroy. All permission sets can read; normal_user and admin can create, update, destroy. No bypass (scope :all only in PermissionSets).
|
||||
|
||||
### MemberGroup Resource Policies
|
||||
|
||||
**Location:** `lib/membership/member_group.ex`
|
||||
|
||||
Bypass for read restricted to own_data (MemberGroupReadLinkedForOwnData check: own_data only, filter `member_id == actor.member_id`); HasPermission for read (read_only/normal_user/admin :all) and create/destroy (normal_user + admin only). Admin with member_id set still gets :all from HasPermission (bypass does not apply).
|
||||
|
||||
### MembershipFeeType Resource Policies
|
||||
|
||||
**Location:** `lib/membership_fees/membership_fee_type.ex`
|
||||
|
||||
Policies use `HasPermission` for read/create/update/destroy. All permission sets can read; only admin can create, update, destroy.
|
||||
|
||||
### MembershipFeeCycle Resource Policies
|
||||
|
||||
**Location:** `lib/membership_fees/membership_fee_cycle.ex`
|
||||
|
||||
Bypass for read restricted to own_data (MembershipFeeCycleReadLinkedForOwnData: own_data only, filter `member_id == actor.member_id`); HasPermission for read (read_only/normal_user/admin :all) and create/update/destroy. own_data can only read cycles of the linked member; read_only can read all; normal_user and admin can read, create, update, and destroy (including mark_as_paid and manual "Regenerate Cycles"; UI button when `can_create_cycle`). Regenerate-cycles handler enforces `can?(:create, MembershipFeeCycle)` server-side.
|
||||
|
||||
---
|
||||
|
||||
## Page Permission System
|
||||
|
|
@ -2002,7 +2049,10 @@ Users and Members are separate entities that can be linked. Special rules:
|
|||
- A user cannot link themselves to an existing member
|
||||
- A user CAN create a new member and be directly linked to it (self-service)
|
||||
|
||||
**Enforcement:** The User resource restricts the `update_user` action (which accepts the `member` argument for link/unlink) to admins only via `Mv.Authorization.Checks.ActorIsAdmin`. The UserLive.Form shows the Member-Linking UI and runs member link/unlink on save only when the current user is admin; non-admins use the `:update` action (email only) for profile edit.
|
||||
**Enforcement:**
|
||||
|
||||
- **User side:** The User resource restricts the `update_user` action (which accepts the `member` argument for link/unlink) to admins only via `Mv.Authorization.Checks.ActorIsAdmin`. The UserLive.Form shows the Member-Linking UI and runs member link/unlink on save only when the current user is admin; non-admins use the `:update` action (email only) for profile edit.
|
||||
- **Member side:** Only admins may set or change the user–member link on **Member** create or update. When creating or updating a member, the `:user` argument (which links the member to a user account) is forbidden for non-admins. This is enforced by `Mv.Authorization.Checks.ForbidMemberUserLinkUnlessAdmin` in the Member resource policies (`forbid_if` before `authorize_if HasPermission`). Non-admins can still create and update members as long as they do **not** pass the `:user` argument. The Member resource uses **`on_missing: :ignore`** for the `:user` relationship on update_member, so **omitting** `:user` from params does **not** change the link (no "unlink by omission"); unlink is only possible by explicitly passing `:user` (e.g. `user: nil`), which is admin-only.
|
||||
|
||||
### Approach: Separate Ash Actions
|
||||
|
||||
|
|
|
|||
|
|
@ -78,10 +78,11 @@ Stored in database `roles` table, each referencing a `permission_set_name`:
|
|||
- ✅ Hardcoded PermissionSets module with 4 permission sets
|
||||
- ✅ Role database table and CRUD interface
|
||||
- ✅ Custom Ash Policy Check (`HasPermission`) that reads from PermissionSets
|
||||
- ✅ Policies on all resources (Member, User, CustomFieldValue, CustomField, Role)
|
||||
- ✅ Page-level permissions via Phoenix Plug
|
||||
- ✅ Policies on all resources (Member, User, CustomFieldValue, CustomField, Role, Group, MemberGroup, MembershipFeeType, MembershipFeeCycle)
|
||||
- ✅ Page-level permissions via Phoenix Plug (including admin-only `/settings` and `/membership_fee_settings`)
|
||||
- ✅ UI authorization helpers for conditional rendering
|
||||
- ✅ Special case: Member email validation for linked users
|
||||
- ✅ User role assignment: admin-only `role_id` in update_user; Last-Admin validation; role dropdown in User form when `can?(actor, :update, Role)`
|
||||
- ✅ Seed data for 5 roles
|
||||
|
||||
**Benefits of Hardcoded Approach:**
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue