273 lines
8.4 KiB
Elixir
273 lines
8.4 KiB
Elixir
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: %{type: action},
|
|
data: nil
|
|
}
|
|
}
|
|
end
|
|
|
|
# Helper to create actor with role
|
|
defp create_actor(id, permission_set_name, opts \\ []) do
|
|
actor = %{
|
|
id: id,
|
|
role: %{permission_set_name: permission_set_name}
|
|
}
|
|
|
|
# Add member_id if provided (needed for :linked scope tests)
|
|
case Keyword.get(opts, :member_id) do
|
|
nil -> actor
|
|
member_id -> Map.put(actor, :member_id, member_id)
|
|
end
|
|
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", member_id: "member-456")
|
|
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", member_id: "member-456")
|
|
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
|