Fix: HasPermission auto_filter and strict_check implementation

Fixes security issue where auto_filter returned nil instead of proper
filter expressions, which could lead to incorrect authorization behavior.
This commit is contained in:
Moritz 2026-01-08 22:54:47 +01:00
parent 4192922fd3
commit 70729bdd73
Signed by: moritz
GPG key ID: 1020A035E5DD0824
3 changed files with 83 additions and 38 deletions

View file

@ -72,14 +72,15 @@ defmodule Mv.Authorization.Checks.HasPermissionIntegrationTest do
end
describe "Filter Expression Structure - :all scope" do
test "Admin can read all members without filter" do
test "Admin can read all members without filter (returns expr(true))" do
actor = create_actor_with_role("admin")
authorizer = create_authorizer(Mv.Membership.Member, :read)
filter = HasPermission.auto_filter(actor, authorizer, [])
# :all scope should return nil (no filter needed)
assert is_nil(filter)
# :all scope should return [] (empty keyword list = no filter = allow all records)
# After auto_filter fix: no longer returns nil, returns [] instead
assert filter == []
end
end

View file

@ -132,6 +132,8 @@ defmodule Mv.Membership.MemberPoliciesTest do
end
test "can update linked member", %{user: user, linked_member: linked_member} do
# Update is allowed via HasPermission check with :linked scope (not via special case)
# The special case policy only applies to :read actions
{:ok, updated_member} =
linked_member
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})
@ -367,23 +369,14 @@ defmodule Mv.Membership.MemberPoliciesTest do
end
end
describe "special case: user can always access linked member" do
test "own_data user can read linked member even without explicit permission" do
user = create_user_with_permission_set("own_data")
linked_member = create_linked_member_for_user(user)
describe "special case: user can always READ linked member" do
# Note: The special case policy only applies to :read actions.
# Updates are handled by HasPermission with :linked scope (if permission exists).
# Reload user to get updated member_id
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts)
# Should succeed (special case policy takes precedence)
{:ok, member} =
Ash.get(Membership.Member, linked_member.id, actor: user, domain: Mv.Membership)
assert member.id == linked_member.id
end
test "read_only user can read linked member (via special case)" do
test "read_only user can read linked member (via special case bypass)" do
# read_only has Member.read scope :all, but the special case ensures
# users can ALWAYS read their linked member, even if they had no read permission.
# This test verifies the special case works independently of permission sets.
user = create_user_with_permission_set("read_only")
linked_member = create_linked_member_for_user(user)
@ -391,14 +384,16 @@ defmodule Mv.Membership.MemberPoliciesTest do
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts)
# Should succeed (special case policy takes precedence)
# Should succeed (special case bypass policy for :read takes precedence)
{:ok, member} =
Ash.get(Membership.Member, linked_member.id, actor: user, domain: Mv.Membership)
assert member.id == linked_member.id
end
test "own_data user can update linked member even without explicit permission" do
test "own_data user can read linked member (via special case bypass)" do
# own_data has Member.read scope :linked, but the special case ensures
# users can ALWAYS read their linked member regardless of permission set.
user = create_user_with_permission_set("own_data")
linked_member = create_linked_member_for_user(user)
@ -406,7 +401,24 @@ defmodule Mv.Membership.MemberPoliciesTest do
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts)
# Should succeed (special case policy takes precedence)
# Should succeed (special case bypass policy for :read takes precedence)
{:ok, member} =
Ash.get(Membership.Member, linked_member.id, actor: user, domain: Mv.Membership)
assert member.id == linked_member.id
end
test "own_data user can update linked member (via HasPermission :linked scope)" do
# Update is NOT handled by special case - it's handled by HasPermission
# with :linked scope. own_data has Member.update scope :linked.
user = create_user_with_permission_set("own_data")
linked_member = create_linked_member_for_user(user)
# Reload user to get updated member_id
{:ok, user} = Ash.get(Accounts.User, user.id, domain: Mv.Accounts, load: [:role])
{:ok, user} = Ash.load(user, :member, domain: Mv.Accounts)
# Should succeed via HasPermission check (not special case)
{:ok, updated_member} =
linked_member
|> Ash.Changeset.for_update(:update_member, %{first_name: "Updated"})