Custom Policy Check - HasPermission closes #343 #344
1 changed files with 264 additions and 0 deletions
264
test/mv/authorization/checks/has_permission_test.exs
Normal file
264
test/mv/authorization/checks/has_permission_test.exs
Normal file
|
|
@ -0,0 +1,264 @@
|
|||
defmodule Mv.Authorization.Checks.HasPermissionTest do
|
||||
@moduledoc """
|
||||
Tests for the HasPermission Ash Policy Check.
|
||||
|
||||
This check evaluates permissions from the PermissionSets module and applies
|
||||
scope filters to Ash queries.
|
||||
"""
|
||||
use ExUnit.Case, async: true
|
||||
|
||||
alias Mv.Authorization.Checks.HasPermission
|
||||
|
||||
# Helper to create a mock authorizer for strict_check/3
|
||||
defp create_authorizer(resource, action) do
|
||||
%Ash.Policy.Authorizer{
|
||||
resource: resource,
|
||||
subject: %{action: %{name: action}}
|
||||
}
|
||||
end
|
||||
|
||||
# Helper to create actor with role
|
||||
defp create_actor(id, permission_set_name) do
|
||||
%{
|
||||
id: id,
|
||||
role: %{permission_set_name: permission_set_name}
|
||||
}
|
||||
end
|
||||
|
||||
describe "describe/1" do
|
||||
test "returns human-readable description" do
|
||||
description = HasPermission.describe([])
|
||||
assert is_binary(description)
|
||||
assert description =~ "permission"
|
||||
end
|
||||
end
|
||||
|
||||
describe "strict_check/3 - Permission Lookup" do
|
||||
test "admin has permission for all resources/actions" do
|
||||
admin = create_actor("admin-123", "admin")
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(admin, authorizer, [])
|
||||
|
||||
assert result == true or result == :unknown
|
||||
end
|
||||
|
||||
test "read_only has read permission for Member" do
|
||||
read_only_user = create_actor("read-only-123", "read_only")
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(read_only_user, authorizer, [])
|
||||
|
||||
assert result == true or result == :unknown
|
||||
end
|
||||
|
||||
test "read_only does NOT have create permission for Member" do
|
||||
read_only_user = create_actor("read-only-123", "read_only")
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :create)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(read_only_user, authorizer, [])
|
||||
|
||||
assert result == false
|
||||
end
|
||||
|
||||
test "own_data has update permission for User with scope :own" do
|
||||
own_data_user = create_actor("user-123", "own_data")
|
||||
authorizer = create_authorizer(Mv.Accounts.User, :update)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(own_data_user, authorizer, [])
|
||||
|
||||
# Should return :unknown for :own scope (needs filter)
|
||||
assert result == :unknown
|
||||
end
|
||||
end
|
||||
|
||||
describe "strict_check/3 - Scope :all" do
|
||||
test "actor with scope :all can access any record" do
|
||||
admin = create_actor("admin-123", "admin")
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(admin, authorizer, [])
|
||||
|
||||
# :all scope should return true (no filter needed)
|
||||
assert result == true
|
||||
end
|
||||
|
||||
test "admin can read all members without filter" do
|
||||
admin = create_actor("admin-123", "admin")
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(admin, authorizer, [])
|
||||
|
||||
# Should return true for :all scope
|
||||
assert result == true
|
||||
end
|
||||
end
|
||||
|
||||
describe "strict_check/3 - Scope :own" do
|
||||
test "actor with scope :own returns :unknown (needs filter)" do
|
||||
user = create_actor("user-123", "own_data")
|
||||
authorizer = create_authorizer(Mv.Accounts.User, :read)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(user, authorizer, [])
|
||||
|
||||
# Should return :unknown for :own scope (needs filter via auto_filter)
|
||||
assert result == :unknown
|
||||
end
|
||||
end
|
||||
|
||||
describe "auto_filter/3 - Scope :own" do
|
||||
test "scope :own returns filter expression" do
|
||||
user = create_actor("user-123", "own_data")
|
||||
authorizer = create_authorizer(Mv.Accounts.User, :update)
|
||||
|
||||
filter = HasPermission.auto_filter(user, authorizer, [])
|
||||
|
||||
# Should return a filter expression
|
||||
assert not is_nil(filter)
|
||||
end
|
||||
end
|
||||
|
||||
describe "auto_filter/3 - Scope :linked" do
|
||||
test "scope :linked for Member returns user_id filter" do
|
||||
user = create_actor("user-123", "own_data")
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
filter = HasPermission.auto_filter(user, authorizer, [])
|
||||
|
||||
# Should return a filter expression
|
||||
assert not is_nil(filter)
|
||||
end
|
||||
|
||||
test "scope :linked for CustomFieldValue returns member.user_id filter" do
|
||||
user = create_actor("user-123", "own_data")
|
||||
authorizer = create_authorizer(Mv.Membership.CustomFieldValue, :update)
|
||||
|
||||
filter = HasPermission.auto_filter(user, authorizer, [])
|
||||
|
||||
# Should return a filter expression that traverses member relationship
|
||||
assert not is_nil(filter)
|
||||
end
|
||||
end
|
||||
|
||||
describe "strict_check/3 - Error Handling" do
|
||||
test "returns {:ok, false} for nil actor" do
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(nil, authorizer, [])
|
||||
|
||||
assert result == false
|
||||
end
|
||||
|
||||
test "returns {:ok, false} for actor missing role" do
|
||||
actor_without_role = %{id: "user-123"}
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(actor_without_role, authorizer, [])
|
||||
|
||||
assert result == false
|
||||
end
|
||||
|
||||
test "returns {:ok, false} for actor with nil role" do
|
||||
actor_with_nil_role = %{id: "user-123", role: nil}
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(actor_with_nil_role, authorizer, [])
|
||||
|
||||
assert result == false
|
||||
end
|
||||
|
||||
test "returns {:ok, false} for invalid permission_set_name" do
|
||||
actor_with_invalid_permission = %{
|
||||
id: "user-123",
|
||||
role: %{permission_set_name: "invalid_set"}
|
||||
}
|
||||
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(actor_with_invalid_permission, authorizer, [])
|
||||
|
||||
assert result == false
|
||||
end
|
||||
|
||||
test "returns {:ok, false} for no matching permission" do
|
||||
read_only_user = create_actor("read-only-123", "read_only")
|
||||
authorizer = create_authorizer(Mv.Authorization.Role, :create)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(read_only_user, authorizer, [])
|
||||
|
||||
assert result == false
|
||||
end
|
||||
|
||||
test "handles role with nil permission_set_name gracefully" do
|
||||
actor_with_nil_permission_set = %{
|
||||
id: "user-123",
|
||||
role: %{permission_set_name: nil}
|
||||
}
|
||||
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(actor_with_nil_permission_set, authorizer, [])
|
||||
|
||||
assert result == false
|
||||
end
|
||||
end
|
||||
|
||||
describe "strict_check/3 - Logging" do
|
||||
import ExUnit.CaptureLog
|
||||
|
||||
test "logs authorization failure for nil actor" do
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
log =
|
||||
capture_log(fn ->
|
||||
HasPermission.strict_check(nil, authorizer, [])
|
||||
end)
|
||||
|
||||
assert log =~ "Authorization failed" or log == ""
|
||||
end
|
||||
|
||||
test "logs authorization failure for missing role" do
|
||||
actor_without_role = %{id: "user-123"}
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
log =
|
||||
capture_log(fn ->
|
||||
HasPermission.strict_check(actor_without_role, authorizer, [])
|
||||
end)
|
||||
|
||||
assert log =~ "Authorization failed" or log == ""
|
||||
end
|
||||
end
|
||||
|
||||
describe "strict_check/3 - Resource Name Extraction" do
|
||||
test "correctly extracts resource name from nested module" do
|
||||
admin = create_actor("admin-123", "admin")
|
||||
authorizer = create_authorizer(Mv.Membership.Member, :read)
|
||||
|
||||
{:ok, result} = HasPermission.strict_check(admin, authorizer, [])
|
||||
|
||||
# Should work correctly (not crash)
|
||||
assert result == true or result == :unknown or result == false
|
||||
end
|
||||
|
||||
test "works with different resource modules" do
|
||||
admin = create_actor("admin-123", "admin")
|
||||
|
||||
resources = [
|
||||
Mv.Accounts.User,
|
||||
Mv.Membership.Member,
|
||||
Mv.Membership.CustomFieldValue,
|
||||
Mv.Membership.CustomField,
|
||||
Mv.Authorization.Role
|
||||
]
|
||||
|
||||
for resource <- resources do
|
||||
authorizer = create_authorizer(resource, :read)
|
||||
{:ok, result} = HasPermission.strict_check(admin, authorizer, [])
|
||||
|
||||
# Should not crash and should return valid result
|
||||
assert result == true or result == :unknown or result == false
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
Loading…
Add table
Add a link
Reference in a new issue