docs(auth): document User policies and bypass pattern
All checks were successful
continuous-integration/drone/push Build is passing

Add bypass vs HasPermission pattern documentation
Update architecture and implementation plan docs
This commit is contained in:
Moritz 2026-01-22 19:19:27 +01:00
parent 63d8c4668d
commit 5506b5b2dc
4 changed files with 757 additions and 65 deletions

View file

@ -524,61 +524,68 @@ Add authorization policies to the Member resource using the new `HasPermission`
**Size:** M (2 days)
**Dependencies:** #6 (HasPermission check)
**Can work in parallel:** Yes (parallel with #7, #9, #10)
**Assignable to:** Backend Developer
**Assignable to:** Backend Developer
**Status:** ✅ **COMPLETED**
**Description:**
Add authorization policies to the User resource. Special case: Users can always read/update their own credentials.
Add authorization policies to the User resource. Users can always read their own credentials (via bypass), and update their own credentials (via HasPermission with scope :own).
**Implementation Pattern:**
Following the same pattern as Member resource:
- **Bypass for READ** - Handles list queries (auto_filter)
- **HasPermission for UPDATE** - Handles updates with scope :own
**Tasks:**
1. Open `lib/mv/accounts/user.ex`
2. Add `policies` block
3. Add special policy: Allow user to always access their own account (before general policy)
1. ✅ Open `lib/mv/accounts/user.ex`
2. ✅ Add `policies` block
3. ✅ Add AshAuthentication bypass (registration/login without actor)
4. ✅ Add NoActor bypass (test environment only)
5. ✅ Add bypass for READ: Allow user to always read their own account
```elixir
policy action_type([:read, :update]) do
bypass action_type(:read) do
description "Users can always read their own account"
authorize_if expr(id == ^actor(:id))
end
```
4. Add general policy: Check HasPermission for all actions
5. Ensure :destroy is admin-only (via HasPermission)
6. Preload :role relationship for actor
6. ✅ Add general policy: Check HasPermission for all actions (including UPDATE with scope :own)
7. ✅ Ensure :destroy is admin-only (via HasPermission)
8. ✅ Preload :role relationship for actor in tests
**Policy Order:**
1. Allow user to read/update own account (id == actor.id)
2. Check HasPermission (for admin operations)
3. Default: Forbid
1. ✅ AshAuthentication bypass (registration/login)
2. ✅ NoActor bypass (test environment)
3. ✅ Bypass: User can READ own account (id == actor.id)
4. ✅ HasPermission: General permission check (UPDATE uses scope :own, admin uses scope :all)
5. ✅ Default: Ash implicitly forbids (fail-closed)
**Why Bypass for READ but not UPDATE?**
- **READ list queries**: No record at strict_check time → bypass with `expr()` needed for auto_filter ✅
- **UPDATE operations**: Changeset contains record → HasPermission evaluates `scope :own` correctly ✅
This ensures `scope :own` in PermissionSets is actually used (not redundant).
**Acceptance Criteria:**
- [ ] User can always read/update own credentials
- [ ] Only admin can read/update other users
- [ ] Only admin can destroy users
- [ ] Policy order is correct
- [ ] Actor preloads :role relationship
- ✅ User can always read own credentials (via bypass)
- ✅ User can always update own credentials (via HasPermission with scope :own)
- ✅ Only admin can read/update other users (scope :all)
- ✅ Only admin can destroy users (scope :all)
- ✅ Policy order is correct (AshAuth → NoActor → Bypass READ → HasPermission)
- ✅ Actor preloads :role relationship
- ✅ All tests pass (30/31 pass, 1 skipped)
**Test Strategy (TDD):**
**Own Data Tests (All Roles):**
- User with :own_data can read own user record
- User with :own_data can update own email/password
- User with :own_data cannot read other users
- User with :read_only can read own data
- User with :normal_user can read own data
- Verify special policy takes precedence
**Admin Tests:**
- Admin can read all users
- Admin can update any user's credentials
- Admin can destroy users
- Admin has unrestricted access
**Forbidden Tests:**
- Non-admin cannot read other users
- Non-admin cannot update other users
- Non-admin cannot destroy users
**Test Results:**
**Test File:** `test/mv/accounts/user_policies_test.exs`
- ✅ 31 tests total: 30 passing, 1 skipped (AshAuthentication edge case)
- ✅ Tests for all 4 permission sets: own_data, read_only, normal_user, admin
- ✅ Tests for AshAuthentication bypass (registration/login)
- ✅ Tests for NoActor bypass (test environment)
- ✅ Tests verify scope :own is used for UPDATE (not redundant)
---