Merge remote-tracking branch 'origin/main' into feature/ui-for-adding-members-groups
This commit is contained in:
commit
2f8a6a2768
136 changed files with 9999 additions and 3601 deletions
54
docs/admin-bootstrap-and-oidc-role-sync.md
Normal file
54
docs/admin-bootstrap-and-oidc-role-sync.md
Normal 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.
|
||||
|
|
@ -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/
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
||||
|
|
|
|||
|
|
@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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