[FEATURE]: Custom Policy Check - HasPermission #343

Closed
opened 2026-01-08 16:11:03 +01:00 by moritz · 0 comments
Owner

Description:

Create the core custom Ash Policy Check that reads permissions from the PermissionSets module and applies them to Ash queries. This is the bridge between hardcoded permissions and Ash's authorization system.

Tasks:

  1. Create lib/mv/authorization/checks/has_permission.ex
  2. Implement use Ash.Policy.Check
  3. Implement describe/1 - returns human-readable description
  4. Implement match?/3 - the core authorization logic:
    • Extract actor.role.permission_set_name
    • Convert to atom via PermissionSets.permission_set_name_to_atom/1
    • Call PermissionSets.get_permissions/1
    • Find matching permission for current resource + action
    • Apply scope filter
  5. Implement apply_scope/3 helper:
    • :all:authorized (no filter)
    • :own{:filter, expr(id == ^actor.id)}
    • :linked → resource-specific logic:
      • Member: {:filter, expr(user_id == ^actor.id)}
      • CustomFieldValue: {:filter, expr(member.user_id == ^actor.id)} (traverse relationship!)
  6. Handle errors gracefully:
    • No actor → {:error, :no_actor}
    • No role → {:error, :no_role}
    • Invalid permission_set_name → {:error, :invalid_permission_set}
    • No matching permission → {:error, :no_permission}
  7. Add logging for authorization failures (debug level)
  8. Add comprehensive @doc with examples

Acceptance Criteria:

  • Check module implements Ash.Policy.Check behavior
  • match?/3 correctly evaluates permissions from PermissionSets
  • Scope filters work correctly (:all, :own, :linked)
  • :linked scope handles Member and CustomFieldValue differently
  • Errors are handled gracefully (no crashes)
  • Authorization failures are logged
  • Module is well-documented

Test Strategy (TDD):

Permission Lookup Tests:

  • Actor with :admin permission_set has permission for all resources/actions
  • Actor with :read_only permission_set has read permission for Member
  • Actor with :read_only permission_set does NOT have create permission for Member
  • Actor with :own_data permission_set has update permission for User with scope :own

Scope Application Tests - :all:

  • Actor with scope :all can access any record
  • Query returns all records in database

Scope Application Tests - :own:

  • Actor with scope :own can access record where record.id == actor.id
  • Actor with scope :own cannot access record where record.id != actor.id
  • Query filters to only actor's own record

Scope Application Tests - :linked:

  • Actor with scope :linked can access Member where member.user_id == actor.id
  • Actor with scope :linked can access CustomFieldValue where custom_field_value.member.user_id == actor.id (relationship traversal!)
  • Actor with scope :linked cannot access unlinked member
  • Query correctly filters based on user_id relationship

Error Handling Tests:

  • match? with nil actor returns {:error, :no_actor}
  • match? with actor missing role returns {:error, :no_role}
  • match? with invalid permission_set_name returns {:error, :invalid_permission_set}
  • match? with no matching permission returns {:error, :no_permission}
  • No crashes on edge cases

Logging Tests:

  • Authorization failure logs at debug level
  • Log includes actor ID, resource, action, reason

Test Files:

  • test/mv/authorization/checks/has_permission_test.exs
**Description:** Create the core custom Ash Policy Check that reads permissions from the `PermissionSets` module and applies them to Ash queries. This is the bridge between hardcoded permissions and Ash's authorization system. **Tasks:** 1. Create `lib/mv/authorization/checks/has_permission.ex` 2. Implement `use Ash.Policy.Check` 3. Implement `describe/1` - returns human-readable description 4. Implement `match?/3` - the core authorization logic: - Extract `actor.role.permission_set_name` - Convert to atom via `PermissionSets.permission_set_name_to_atom/1` - Call `PermissionSets.get_permissions/1` - Find matching permission for current resource + action - Apply scope filter 5. Implement `apply_scope/3` helper: - `:all` → `:authorized` (no filter) - `:own` → `{:filter, expr(id == ^actor.id)}` - `:linked` → resource-specific logic: - Member: `{:filter, expr(user_id == ^actor.id)}` - CustomFieldValue: `{:filter, expr(member.user_id == ^actor.id)}` (traverse relationship!) 6. Handle errors gracefully: - No actor → `{:error, :no_actor}` - No role → `{:error, :no_role}` - Invalid permission_set_name → `{:error, :invalid_permission_set}` - No matching permission → `{:error, :no_permission}` 7. Add logging for authorization failures (debug level) 8. Add comprehensive `@doc` with examples **Acceptance Criteria:** - [x] Check module implements `Ash.Policy.Check` behavior - [x] `match?/3` correctly evaluates permissions from PermissionSets - [x] Scope filters work correctly (:all, :own, :linked) - [x] `:linked` scope handles Member and CustomFieldValue differently - [x] Errors are handled gracefully (no crashes) - [x] Authorization failures are logged - [x] Module is well-documented **Test Strategy (TDD):** **Permission Lookup Tests:** - Actor with :admin permission_set has permission for all resources/actions - Actor with :read_only permission_set has read permission for Member - Actor with :read_only permission_set does NOT have create permission for Member - Actor with :own_data permission_set has update permission for User with scope :own **Scope Application Tests - :all:** - Actor with scope :all can access any record - Query returns all records in database **Scope Application Tests - :own:** - Actor with scope :own can access record where record.id == actor.id - Actor with scope :own cannot access record where record.id != actor.id - Query filters to only actor's own record **Scope Application Tests - :linked:** - Actor with scope :linked can access Member where member.user_id == actor.id - Actor with scope :linked can access CustomFieldValue where custom_field_value.member.user_id == actor.id (relationship traversal!) - Actor with scope :linked cannot access unlinked member - Query correctly filters based on user_id relationship **Error Handling Tests:** - `match?` with nil actor returns `{:error, :no_actor}` - `match?` with actor missing role returns `{:error, :no_role}` - `match?` with invalid permission_set_name returns `{:error, :invalid_permission_set}` - `match?` with no matching permission returns `{:error, :no_permission}` - No crashes on edge cases **Logging Tests:** - Authorization failure logs at debug level - Log includes actor ID, resource, action, reason **Test Files:** - `test/mv/authorization/checks/has_permission_test.exs`
moritz added this to the Accounts & Logins milestone 2026-01-08 16:11:03 +01:00
moritz added the
L
label 2026-01-08 16:11:03 +01:00
moritz self-assigned this 2026-01-08 16:11:03 +01:00
moritz added this to the Sprint 11: 08.01-29.01 project 2026-01-08 16:11:04 +01:00
Sign in to join this conversation.
No milestone
No assignees
1 participant
Notifications
Due date
The due date is invalid or out of range. Please use the format "yyyy-mm-dd".

No due date set.

Dependencies

No dependencies set.

Reference: local-it/mitgliederverwaltung#343
No description provided.