feat(auth): add User resource authorization policies

Implement bypass for READ + HasPermission for UPDATE pattern
Extend HasPermission check to support User resource scope :own
This commit is contained in:
Moritz 2026-01-22 19:19:22 +01:00
parent a9f9cab96a
commit 429042cbba
2 changed files with 72 additions and 5 deletions

View file

@ -5,9 +5,8 @@ defmodule Mv.Accounts.User do
use Ash.Resource,
domain: Mv.Accounts,
data_layer: AshPostgres.DataLayer,
extensions: [AshAuthentication]
# authorizers: [Ash.Policy.Authorizer]
extensions: [AshAuthentication],
authorizers: [Ash.Policy.Authorizer]
postgres do
table "users"
@ -267,6 +266,51 @@ defmodule Mv.Accounts.User do
end
end
# Authorization Policies
# Order matters: Most specific policies first, then general permission check
policies do
# ASHAUTHENTICATION BYPASS: Allow authentication actions (registration, login)
# These actions are called internally by AshAuthentication and need to bypass
# normal authorization policies. This must come FIRST because User is an
# authentication resource and authentication flows should have priority.
bypass AshAuthentication.Checks.AshAuthenticationInteraction do
description "Allow AshAuthentication internal operations (registration, login)"
authorize_if always()
end
# SYSTEM OPERATIONS: Allow CRUD operations without actor (TEST ENVIRONMENT ONLY)
# In test: All operations allowed (for test fixtures)
# In production/dev: ALL operations denied without actor (fail-closed for security)
# NoActor.check uses compile-time environment detection to prevent security issues
bypass action_type([:create, :read, :update, :destroy]) do
description "Allow system operations without actor (test environment only)"
authorize_if Mv.Authorization.Checks.NoActor
end
# SPECIAL CASE: Users can always READ their own account
# This allows users with ANY permission set to read their own user record
# Uses bypass with expr filter to enable auto_filter behavior for reads/lists
# (consistent with Member "always read linked member" pattern)
bypass action_type(:read) do
description "Users can always read their own account"
authorize_if expr(id == ^actor(:id))
end
# GENERAL: Check permissions from user's role
# HasPermission handles permissions correctly:
# - :own_data → can update own user (scope :own)
# - :read_only → can update own user (scope :own)
# - :normal_user → can update own user (scope :own)
# - :admin → can read/create/update/destroy all users (scope :all)
policy action_type([:read, :create, :update, :destroy]) do
description "Check permissions from user's role and permission set"
authorize_if Mv.Authorization.Checks.HasPermission
end
# DEFAULT: Ash implicitly forbids if no policy authorizes
# No explicit forbid needed, as Ash's default behavior is fail-closed
end
# Global validations - applied to all relevant actions
validations do
# Password strength policy: minimum 8 characters for all password-related actions