# Page Permission – Route and Test Coverage This document lists all protected routes, which permission set may access them, and how they are covered by tests. ## Protected Routes (Router scope with CheckPagePermission in :browser) | Route | own_data | read_only | normal_user | admin | |-------|----------|-----------|-------------|-------| | `/` | ✗ | ✓ | ✓ | ✓ | | `/members` | ✗ | ✓ | ✓ | ✓ | | `/members/new` | ✗ | ✗ | ✓ | ✓ | | `/members/:id` | ✓ (linked only) | ✓ | ✓ | ✓ | | `/members/:id/edit` | ✓ (linked only) | ✗ | ✓ | ✓ | | `/members/:id/show/edit` | ✓ (linked only) | ✗ | ✓ | ✓ | | `/users` | ✗ | ✗ | ✗ | ✓ | | `/users/new` | ✗ | ✗ | ✗ | ✓ | | `/users/:id` | ✓ (own only) | ✓ (own only) | ✓ (own only) | ✓ | | `/users/:id/edit` | ✓ (own only) | ✓ (own only) | ✓ (own only) | ✓ | | `/users/:id/show/edit` | ✓ (own only) | ✓ (own only) | ✓ (own only) | ✓ | | `/settings` | ✗ | ✗ | ✗ | ✓ | | `/membership_fee_settings` | ✗ | ✗ | ✗ | ✓ | | `/membership_fee_types` | ✗ | ✗ | ✗ | ✓ | | `/membership_fee_types/new` | ✗ | ✗ | ✗ | ✓ | | `/membership_fee_types/:id/edit` | ✗ | ✗ | ✗ | ✓ | | `/groups` | ✗ | ✓ | ✓ | ✓ | | `/groups/new` | ✗ | ✗ | ✗ | ✓ | | `/groups/:slug` | ✗ | ✓ | ✓ | ✓ | | `/groups/:slug/edit` | ✗ | ✗ | ✗ | ✓ | | `/admin/roles` | ✗ | ✗ | ✗ | ✓ | | `/admin/roles/new` | ✗ | ✗ | ✗ | ✓ | | `/admin/roles/:id` | ✗ | ✗ | ✗ | ✓ | | `/admin/roles/:id/edit` | ✗ | ✗ | ✗ | ✓ | **Note:** Permission sets define `/custom_field_values` and related paths, but there are no such routes in the router; those entries are for future use. ## Public Paths (no permission check) - `/auth*`, `/register`, `/reset`, `/sign-in`, `/sign-out`, `/confirm*`, `/password-reset*`, `/set_locale` ## Test Coverage **File:** `test/mv_web/plugs/check_page_permission_test.exs` ### Unit tests (plug called directly with mock conn) - Static: own_data denied `/members`; read_only allowed `/members`; flash on denial. - Dynamic: read_only allowed `/members/123`; normal_user allowed `/members/456/edit`; read_only denied `/members/123/edit`. - read_only / normal_user: denied `/admin/roles`; read_only denied `/members/new`. - Wildcard: admin allowed `/admin/roles`, `/members/999/edit`. - Unauthenticated: nil user denied, redirect `/sign-in`. - Public: unauthenticated allowed `/auth/sign-in`, `/register`. - Error: no role, invalid permission_set_name → denied. ### Integration tests (full router, Mitglied = own_data) **Denied (Mitglied gets 302 → `/users/:id`):** - `/members`, `/members/new`, `/users`, `/users/new`, `/settings`, `/membership_fee_settings`, `/membership_fee_types`, `/membership_fee_types/new`, `/groups`, `/groups/new`, `/admin/roles`, `/admin/roles/new` - `/members/:id/edit`, `/members/:id/show/edit`, `/users/:id` (other user), `/users/:id/edit` (other), `/users/:id/show/edit` (other), `/membership_fee_types/:id/edit`, `/groups/:slug`, `/admin/roles/:id`, `/admin/roles/:id/edit` **Allowed (Mitglied gets 200):** - `/users/:id` (own profile), `/users/:id/edit`, `/users/:id/show/edit` - `/members/:id`, `/members/:id/edit`, `/members/:id/show/edit` for linked member (plug unit tests; full-router tests for linked member skipped: session/LiveView constraints) **Root:** `GET /` redirects Mitglied to profile (root not allowed for own_data). All protected routes above are either covered by integration “denied” tests for Mitglied or by unit tests for the relevant permission set. ### Integration tests (full router, read_only = Vorstand/Buchhaltung) **Allowed (200):** `/`, `/members`, `/members/:id`, `/users/:id` (own profile), `/users/:id/edit`, `/users/:id/show/edit`, `/groups`, `/groups/:slug`. **Denied (302 → `/users/:id`):** `/members/new`, `/members/:id/edit`, `/members/:id/show/edit`, `/users`, `/users/new`, `/users/:id` (other user), `/settings`, `/membership_fee_settings`, `/membership_fee_types`, `/groups/new`, `/groups/:slug/edit`, `/admin/roles`, `/admin/roles/:id`. ### Integration tests (full router, normal_user = Kassenwart) **Allowed (200):** `/`, `/members`, `/members/new`, `/members/:id`, `/members/:id/edit`, `/members/:id/show/edit`, `/users/:id` (own profile), `/users/:id/edit`, `/users/:id/show/edit`, `/groups`, `/groups/:slug`. **Denied (302 → `/users/:id`):** `/users`, `/users/new`, `/users/:id` (other user), `/settings`, `/membership_fee_settings`, `/membership_fee_types`, `/groups/new`, `/groups/:slug/edit`, `/admin/roles`, `/admin/roles/:id`. ### Integration tests (full router, admin) **Allowed (200):** All protected routes (sample covered: `/`, `/members`, `/users`, `/settings`, `/membership_fee_settings`, `/admin/roles`, `/members/:id`, `/admin/roles/:id`, `/groups/:slug`). ## Plug behaviour: reserved segments The plug treats `"new"` as a reserved path segment so that patterns like `/members/:id` and `/groups/:slug` do not match `/members/new` or `/groups/new`. Thus `/groups/new` is only allowed when the permission set explicitly lists `/groups/new` (currently only admin). ## Role and member_id loading The plug may reload the user's role (and optionally `member_id`) before checking page permission. Session/`load_from_session` can leave the role unloaded; the plug uses `Mv.Authorization.Actor.ensure_loaded/1` (and, when needed, loads `member_id`) so that permission checks always have the required data. No change to session loading is required; this is documented for clarity.