Merge remote-tracking branch 'origin/main' into feature/ui-for-adding-members-groups
Some checks failed
continuous-integration/drone/push Build is failing
continuous-integration/drone/promote/production Build is passing

This commit is contained in:
Simon 2026-02-12 15:16:35 +01:00
commit 2f8a6a2768
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
136 changed files with 9999 additions and 3601 deletions

View file

@ -0,0 +1,54 @@
# Admin Bootstrap and OIDC Role Sync
## Overview
- **Admin bootstrap:** In production, no seeds run. The first admin user is created/updated from environment variables in the Docker entrypoint (after migrate, before server). Password can be changed without redeploy via `bin/mv eval "Mv.Release.seed_admin()"`.
- **OIDC role sync:** Optional mapping from OIDC groups (e.g. from Authentik profile scope) to the Admin role. Users in the configured admin group get the Admin role on registration and on each sign-in.
## Admin Bootstrap (Part A)
### Environment Variables
- `ADMIN_EMAIL` Email of the admin user to create/update. If unset, seed_admin/0 does nothing.
- `ADMIN_PASSWORD` Password for the admin user. If unset (and no file), no new user is created; if a user with ADMIN_EMAIL already exists (e.g. OIDC-only), their role is set to Admin (no password change).
- `ADMIN_PASSWORD_FILE` Path to a file containing the password (e.g. Docker secret).
### Release Task
- `Mv.Release.seed_admin/0` Reads ADMIN_EMAIL and password from ADMIN_PASSWORD or ADMIN_PASSWORD_FILE. If both email and password are set: creates or updates the user with the Admin role. If only ADMIN_EMAIL is set: sets the Admin role on an existing user with that email (for OIDC-only admins); does not create a user. Idempotent.
### Entrypoint
- rel/overlays/bin/docker-entrypoint.sh After migrate, runs seed_admin(), then starts the server.
### Seeds (Dev/Test)
- priv/repo/seeds.exs Uses ADMIN_PASSWORD or ADMIN_PASSWORD_FILE when set; otherwise fallback "testpassword" only in dev/test.
## OIDC Role Sync (Part B)
### Configuration
- `OIDC_ADMIN_GROUP_NAME` OIDC group name that maps to the Admin role. If unset, no role sync.
- `OIDC_GROUPS_CLAIM` JWT claim name for group list (default "groups").
- Module: Mv.OidcRoleSyncConfig (oidc_admin_group_name/0, oidc_groups_claim/0).
### Sync Logic
- Mv.OidcRoleSync.apply_admin_role_from_user_info(user, user_info) If admin group configured, sets user role to Admin or Mitglied based on user_info groups.
### Where It Runs
1. Registration: register_with_rauthy after_action calls OidcRoleSync.
2. Sign-in: sign_in_with_rauthy prepare after_action calls OidcRoleSync for each user.
### Internal Action
- User.set_role_from_oidc_sync Internal update (role_id only). Used by OidcRoleSync; not exposed.
## See Also
- .env.example Admin and OIDC group env vars.
- lib/mv/release.ex seed_admin/0.
- lib/mv/oidc_role_sync.ex Sync implementation.
- docs/oidc-account-linking.md OIDC account linking.

View file

@ -696,11 +696,14 @@ lib/
│ └── membership/
│ └── import/
│ ├── member_csv.ex # prepare + process_chunk
│ ├── import_runner.ex # orchestration: file read, progress merge, chunk process, error format
│ ├── csv_parser.ex # delimiter detection + parsing + BOM handling
│ └── header_mapper.ex # normalization + header mapping
└── mv_web/
└── live/
└── global_settings_live.ex # add import section + LV message loop
├── import_export_live.ex # mount / handle_event / handle_info + glue only
└── import_export_live/
└── components.ex # UI: custom_fields_notice, template_links, import_form, import_progress, import_results
priv/
└── static/

View file

@ -15,10 +15,10 @@ This document provides a comprehensive overview of the Mila Membership Managemen
| Metric | Count |
|--------|-------|
| **Tables** | 9 |
| **Tables** | 11 |
| **Domains** | 4 (Accounts, Membership, MembershipFees, Authorization) |
| **Relationships** | 7 |
| **Indexes** | 20+ |
| **Relationships** | 9 |
| **Indexes** | 25+ |
| **Triggers** | 1 (Full-text search) |
## Tables Overview
@ -77,6 +77,23 @@ This document provides a comprehensive overview of the Mila Membership Managemen
- Membership fee default settings
- Environment variable support for club name
#### `groups`
- **Purpose:** Group definitions for organizing members
- **Rows (Estimated):** Low (typically 5-20 groups per club)
- **Key Features:**
- Unique group names (case-insensitive)
- URL-friendly slugs (auto-generated, immutable)
- Optional descriptions
- Many-to-many relationship with members
#### `member_groups`
- **Purpose:** Join table for many-to-many relationship between members and groups
- **Rows (Estimated):** Medium to High (multiple groups per member)
- **Key Features:**
- Unique constraint on (member_id, group_id)
- CASCADE delete on both sides
- Efficient indexes for queries
### Authorization Domain
#### `roles`
@ -100,6 +117,10 @@ Member (1) → (N) MembershipFeeCycles
MembershipFeeType (1)
Member (N) ←→ (N) Group
↓ ↓
MemberGroups (N) MemberGroups (N)
Settings (1) → MembershipFeeType (0..1)
```
@ -145,6 +166,12 @@ Settings (1) → MembershipFeeType (0..1)
- Settings can reference a default fee type
- `ON DELETE SET NULL` - if fee type is deleted, setting is cleared
9. **Member ↔ Group (N:N via MemberGroup)**
- Many-to-many relationship through `member_groups` join table
- `ON DELETE CASCADE` on both sides - removing member/group removes associations
- Unique constraint on (member_id, group_id) prevents duplicates
- Groups searchable via member search vector
## Important Business Rules
### Email Synchronization
@ -509,7 +536,7 @@ mix run priv/repo/seeds.exs
---
**Last Updated:** 2026-01-13
**Schema Version:** 1.4
**Last Updated:** 2026-01-27
**Schema Version:** 1.5
**Database:** PostgreSQL 17.6 (dev) / 16 (prod)

View file

@ -1752,8 +1752,151 @@ This project demonstrates a modern Phoenix application built with:
---
**Document Version:** 1.4
**Last Updated:** 2026-01-13
---
## Recent Updates (2026-01-13 to 2026-01-27)
### Groups Feature Implementation (2026-01-27)
**PR #378:** *Add groups resource* (closes #371)
- Created `Mv.Membership.Group` resource with name, slug, description
- Created `Mv.Membership.MemberGroup` join table for many-to-many relationship
- Automatic slug generation from name (immutable after creation)
- Case-insensitive name uniqueness via LOWER(name) index
- Database migration: `20260127141620_add_groups_and_member_groups.exs`
**PR #382:** *Groups Admin UI* (closes #372)
- Groups management LiveViews (`/groups`)
- Create, edit, delete groups with confirmation
- Member count display per group
- Add/remove members from groups
- Groups displayed in member overview and detail views
- Filter and sort by groups in member list
**Key Features:**
- Many-to-many relationship: Members can belong to multiple groups
- Groups searchable via member search vector (full-text search)
- CASCADE delete: Removing member/group removes associations
- Unique constraint prevents duplicate member-group associations
### CSV Import Feature Implementation (2026-01-27)
**PR #359:** *Implements CSV Import UI* (closes #335)
- Import/Export LiveView (`/import_export`)
- CSV file upload with auto-upload
- Real-time import progress tracking
- Error and warning reporting
- Chunked processing (200 rows per chunk)
**PR #394:** *Adds config for import limits* (closes #336)
- Configurable maximum file size (default: 10 MB)
- Configurable maximum rows (default: 1000)
- Configuration via `config :mv, csv_import: [max_file_size_mb: ..., max_rows: ...]`
- UI displays limits to users
**PR #395:** *Implements custom field CSV import* (closes #338)
- Support for importing custom field values via CSV
- Custom field mapping by slug or name
- Validation of custom field value types
- Error reporting with line numbers and field names
- CSV templates (German and English) available for download
**Key Features:**
- Member field import (email, first_name, last_name, etc.)
- Custom field value import (all types: string, integer, boolean, date, email)
- Error capping (max 50 errors per import to prevent memory issues)
- Async chunk processing with progress updates
- Admin-only access (requires `:create` permission on Member resource)
### Page Permission Router Plug (2026-01-27)
**PR #390:** *Page Permission Router Plug* (closes #388)
- `MvWeb.Plugs.CheckPagePermission` plug for page-level authorization
- Route-based permission checking
- Automatic redirects for unauthorized access
- Integration with permission sets (own_data, read_only, normal_user, admin)
- Documentation: `docs/page-permission-route-coverage.md`
**Key Features:**
- Page-level access control before LiveView mount
- Permission set-based route matrix
- Redirect targets for different permission levels
- Public paths (login, OIDC callbacks) excluded from checks
### Resource Policies Implementation (2026-01-27)
**PR #387:** *CustomField Resource Policies* (closes #386)
- CustomField resource policies with actor-based authorization
- Admin-only create/update/destroy operations
- Read access for authenticated users
- No system-actor fallback (explicit actor required)
**PR #377:** *CustomFieldValue Resource Policies* (closes #369)
- CustomFieldValue resource policies
- own_data permission set: can create/update own linked member's custom field values
- Admin and normal_user: full access
- Bypass read rule for CustomFieldValue pattern (documented)
**PR #364:** *User Resource Policies* (closes #363)
- User resource policies with scope filtering
- own_data: can read/update own user record
- Admin: full access
- Email change validation for linked members
### System Actor Improvements (2026-01-27)
**PR #379:** *Fix System missing system actor in prod and prevent deletion*
- System actor user creation in migrations
- Block update/destroy on system-actor user
- System user handling in UserLive forms
- Normalize system actor email
**PR #361:** *System Actor Mode for Systemic Flows* (closes #348)
- System actor pattern for systemic operations
- Email synchronization uses system actor
- Cycle generation uses system actor
- Documentation: `docs/roles-and-permissions-architecture.md` (Authorization Bootstrap Patterns)
**PR #367:** *Remove NoActor bypass*
- Removed NoActor bypass to prevent masking authorization bugs
- All tests now require explicit actor
- Exception: AshAuthentication bypass tests (conscious exception)
### Email Sync Fixes (2026-01-27)
**PR #380:** *Fix email sync (user->member) when changing password and email*
- Email sync when admin sets password via `admin_set_password`
- Bidirectional email synchronization improvements
- Validation fixes for linked user-member pairs
### UI/UX Improvements (2026-01-27)
**PR #389:** *Change Logo* (closes #385)
- Updated application logo
- Logo display in sidebar and navigation
**PR #362:** *Add boolean custom field filters to member overview* (closes #309)
- Boolean custom field filtering in member list
- Filter by true/false values
- Integration with existing filter system
### Test Performance Optimization (2026-01-27)
**PR #384:** *Minor test refactoring to improve on performance* (closes #383)
- Moved slow tests to nightly test suite
- Optimized policy tests
- Reduced test complexity in seeds tests
- Documentation: `docs/test-performance-optimization.md`
**Key Changes:**
- Fast tests (standard CI): Business logic, validations, data persistence
- Slow tests (nightly): Performance tests, large datasets, query optimization
- UI tests: Basic HTML rendering, navigation, translations
---
**Document Version:** 1.5
**Last Updated:** 2026-01-27
**Maintainer:** Development Team
**Status:** Living Document (update as project evolves)

View file

@ -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.
---

View file

@ -1,7 +1,7 @@
# Feature Roadmap & Implementation Plan
**Project:** Mila - Membership Management System
**Last Updated:** 2026-01-13
**Last Updated:** 2026-01-27
**Status:** Active Development
---
@ -29,6 +29,10 @@
- ✅ **OIDC account linking with password verification** (PR #192, closes #171)
- ✅ **Secure OIDC email collision handling** (PR #192)
- ✅ **Automatic linking for passwordless users** (PR #192)
- ✅ **Page Permission Router Plug** - Page-level authorization (PR #390, closes #388, 2026-01-27)
- Route-based permission checking
- Automatic redirects for unauthorized access
- Integration with permission sets
**Closed Issues:**
- ✅ [#171](https://git.local-it.org/local-it/mitgliederverwaltung/issues/171) - OIDC handling and linking (closed 2025-11-13)
@ -55,6 +59,10 @@
- ✅ [#191](https://git.local-it.org/local-it/mitgliederverwaltung/issues/191) - Implement Roles in Ash (M) - Completed
- ✅ [#190](https://git.local-it.org/local-it/mitgliederverwaltung/issues/190) - Implement Permissions in Ash (M) - Completed
- ✅ [#151](https://git.local-it.org/local-it/mitgliederverwaltung/issues/151) - Define implementation plan for roles and permissions (M) - Completed
- ✅ [#388](https://git.local-it.org/local-it/mitgliederverwaltung/issues/388) - Page Permission Router Plug (closed 2026-01-27)
- ✅ [#386](https://git.local-it.org/local-it/mitgliederverwaltung/issues/386) - CustomField Resource Policies (closed 2026-01-27)
- ✅ [#369](https://git.local-it.org/local-it/mitgliederverwaltung/issues/369) - CustomFieldValue Resource Policies (closed 2026-01-27)
- ✅ [#363](https://git.local-it.org/local-it/mitgliederverwaltung/issues/363) - User Resource Policies (closed 2026-01-27)
---
@ -73,9 +81,24 @@
- ✅ User-Member linking (optional 1:1)
- ✅ Email synchronization between User and Member
- ✅ **Bulk email copy** - Copy selected members' email addresses to clipboard (Issue #230)
- ✅ **Groups** - Organize members into groups (PR #378, #382, closes #371, #372, 2026-01-27)
- Many-to-many relationship with groups
- Groups management UI (`/groups`)
- Filter and sort by groups in member list
- Groups displayed in member overview and detail views
- ✅ **CSV Import** - Import members from CSV files (PR #359, #394, #395, closes #335, #336, #338, 2026-01-27)
- Member field import
- Custom field value import
- Real-time progress tracking
- Error reporting
**Closed Issues:**
- ✅ [#162](https://git.local-it.org/local-it/mitgliederverwaltung/issues/162) - Fuzzy and substring search (closed 2025-11-12)
- ✅ [#371](https://git.local-it.org/local-it/mitgliederverwaltung/issues/371) - Add groups resource (closed 2026-01-27)
- ✅ [#372](https://git.local-it.org/local-it/mitgliederverwaltung/issues/372) - Groups Admin UI (closed 2026-01-27)
- ✅ [#335](https://git.local-it.org/local-it/mitgliederverwaltung/issues/335) - CSV Import UI (closed 2026-01-27)
- ✅ [#336](https://git.local-it.org/local-it/mitgliederverwaltung/issues/336) - Config for import limits (closed 2026-01-27)
- ✅ [#338](https://git.local-it.org/local-it/mitgliederverwaltung/issues/338) - Custom field CSV import (closed 2026-01-27)
**Open Issues:**
- [#169](https://git.local-it.org/local-it/mitgliederverwaltung/issues/169) - Allow combined creation of Users/Members (M, Low priority)
@ -88,7 +111,7 @@
- ❌ Advanced filters (date ranges, multiple criteria)
- ❌ Pagination (currently all members loaded)
- ❌ Bulk operations (bulk delete, bulk update)
- ❌ Member import/export (CSV, Excel)
- ❌ Excel import for members
- ❌ Member profile photos/avatars
- ❌ Member history/audit log
- ❌ Duplicate detection
@ -288,12 +311,24 @@
- ✅ **CSV Import Templates** - German and English templates (#329, 2026-01-13)
- Template files in `priv/static/templates/member_import_de.csv` and `member_import_en.csv`
- CSV specification documented in `docs/csv-member-import-v1.md`
- ✅ **CSV Import Implementation** - Full CSV import feature (#335, #336, #338, 2026-01-27)
- Import/Export LiveView (`/import_export`)
- Member field import (email, first_name, last_name, etc.)
- Custom field value import (all types: string, integer, boolean, date, email)
- Real-time progress tracking
- Error and warning reporting with line numbers
- Configurable limits (max file size, max rows)
- Chunked processing (200 rows per chunk)
- Admin-only access
**Closed Issues:**
- ✅ [#335](https://git.local-it.org/local-it/mitgliederverwaltung/issues/335) - CSV Import UI (closed 2026-01-27)
- ✅ [#336](https://git.local-it.org/local-it/mitgliederverwaltung/issues/336) - Config for import limits (closed 2026-01-27)
- ✅ [#338](https://git.local-it.org/local-it/mitgliederverwaltung/issues/338) - Custom field CSV import (closed 2026-01-27)
**Missing Features:**
- ❌ CSV import implementation (templates ready, import logic pending)
- ❌ Excel import for members
- ❌ Import validation and preview
- ❌ Import error handling
- ❌ Import validation preview (before import)
- ❌ Bulk data export
- ❌ Backup export
- ❌ Data migration tools

View file

@ -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))
---
@ -427,15 +427,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)
@ -457,7 +459,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

View file

@ -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)

View file

@ -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 (membergroup 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 ✅
**Usermember 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 usermember 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

View file

@ -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:**