Add CustomField resource policies and tests
- Add policies block with HasPermission for read/create/update/destroy - Add authorizers: [Ash.Policy.Authorizer] to CustomField resource - Add custom_field_policies_test.exs (read all roles, write admin only) - Fix CustomField path in roles-and-permissions doc (lib/membership)
This commit is contained in:
parent
ca88a230b9
commit
36b5d5880b
3 changed files with 203 additions and 12 deletions
|
|
@ -1101,28 +1101,23 @@ end
|
||||||
|
|
||||||
### CustomField Resource Policies
|
### CustomField Resource Policies
|
||||||
|
|
||||||
**Location:** `lib/mv/membership/custom_field.ex`
|
**Location:** `lib/membership/custom_field.ex`
|
||||||
|
|
||||||
**No Special Cases:** All users can read, only admin can write.
|
**No Special Cases:** All users can read, only admin can write.
|
||||||
|
|
||||||
```elixir
|
```elixir
|
||||||
defmodule Mv.Membership.CustomField do
|
defmodule Mv.Membership.CustomField do
|
||||||
use Ash.Resource, ...
|
use Ash.Resource,
|
||||||
|
domain: Mv.Membership,
|
||||||
|
data_layer: AshPostgres.DataLayer,
|
||||||
|
authorizers: [Ash.Policy.Authorizer]
|
||||||
|
|
||||||
policies do
|
policies do
|
||||||
# All authenticated users can read custom fields (needed for forms)
|
|
||||||
# Write operations are admin-only
|
|
||||||
policy action_type([:read, :create, :update, :destroy]) do
|
policy action_type([:read, :create, :update, :destroy]) do
|
||||||
description "Check permissions from user's role"
|
description "Check permissions from user's role"
|
||||||
authorize_if Mv.Authorization.Checks.HasPermission
|
authorize_if Mv.Authorization.Checks.HasPermission
|
||||||
end
|
end
|
||||||
|
|
||||||
# DEFAULT: Forbid
|
|
||||||
policy action_type([:read, :create, :update, :destroy]) do
|
|
||||||
forbid_if always()
|
|
||||||
end
|
|
||||||
end
|
end
|
||||||
|
|
||||||
# ...
|
# ...
|
||||||
end
|
end
|
||||||
```
|
```
|
||||||
|
|
|
||||||
|
|
@ -50,7 +50,8 @@ defmodule Mv.Membership.CustomField do
|
||||||
"""
|
"""
|
||||||
use Ash.Resource,
|
use Ash.Resource,
|
||||||
domain: Mv.Membership,
|
domain: Mv.Membership,
|
||||||
data_layer: AshPostgres.DataLayer
|
data_layer: AshPostgres.DataLayer,
|
||||||
|
authorizers: [Ash.Policy.Authorizer]
|
||||||
|
|
||||||
postgres do
|
postgres do
|
||||||
table "custom_fields"
|
table "custom_fields"
|
||||||
|
|
@ -79,6 +80,13 @@ defmodule Mv.Membership.CustomField do
|
||||||
end
|
end
|
||||||
end
|
end
|
||||||
|
|
||||||
|
policies do
|
||||||
|
policy action_type([:read, :create, :update, :destroy]) do
|
||||||
|
description "Check permissions from user's role"
|
||||||
|
authorize_if Mv.Authorization.Checks.HasPermission
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
attributes do
|
attributes do
|
||||||
uuid_primary_key :id
|
uuid_primary_key :id
|
||||||
|
|
||||||
|
|
|
||||||
188
test/mv/membership/custom_field_policies_test.exs
Normal file
188
test/mv/membership/custom_field_policies_test.exs
Normal file
|
|
@ -0,0 +1,188 @@
|
||||||
|
defmodule Mv.Membership.CustomFieldPoliciesTest do
|
||||||
|
@moduledoc """
|
||||||
|
Tests for CustomField resource authorization policies.
|
||||||
|
|
||||||
|
Verifies that all authenticated users with a valid role can read custom fields,
|
||||||
|
and only admin can create/update/destroy custom fields.
|
||||||
|
"""
|
||||||
|
use Mv.DataCase, async: false
|
||||||
|
|
||||||
|
alias Mv.Membership.CustomField
|
||||||
|
alias Mv.Accounts
|
||||||
|
alias Mv.Authorization
|
||||||
|
|
||||||
|
setup do
|
||||||
|
system_actor = Mv.Helpers.SystemActor.get_system_actor()
|
||||||
|
%{actor: system_actor}
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_role_with_permission_set(permission_set_name, actor) do
|
||||||
|
role_name = "Test Role #{permission_set_name} #{System.unique_integer([:positive])}"
|
||||||
|
|
||||||
|
case Authorization.create_role(
|
||||||
|
%{
|
||||||
|
name: role_name,
|
||||||
|
description: "Test role for #{permission_set_name}",
|
||||||
|
permission_set_name: permission_set_name
|
||||||
|
},
|
||||||
|
actor: actor
|
||||||
|
) do
|
||||||
|
{:ok, role} -> role
|
||||||
|
{:error, error} -> raise "Failed to create role: #{inspect(error)}"
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_user_with_permission_set(permission_set_name, actor) do
|
||||||
|
role = create_role_with_permission_set(permission_set_name, actor)
|
||||||
|
|
||||||
|
{:ok, user} =
|
||||||
|
Accounts.User
|
||||||
|
|> Ash.Changeset.for_create(:register_with_password, %{
|
||||||
|
email: "user#{System.unique_integer([:positive])}@example.com",
|
||||||
|
password: "testpassword123"
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: actor)
|
||||||
|
|
||||||
|
{:ok, user} =
|
||||||
|
user
|
||||||
|
|> Ash.Changeset.for_update(:update, %{})
|
||||||
|
|> Ash.Changeset.manage_relationship(:role, role, type: :append_and_remove)
|
||||||
|
|> Ash.update(actor: actor)
|
||||||
|
|
||||||
|
{:ok, user_with_role} = Ash.load(user, :role, domain: Mv.Accounts, actor: actor)
|
||||||
|
user_with_role
|
||||||
|
end
|
||||||
|
|
||||||
|
defp create_custom_field(actor) do
|
||||||
|
{:ok, field} =
|
||||||
|
CustomField
|
||||||
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|
name: "test_field_#{System.unique_integer([:positive])}",
|
||||||
|
value_type: :string
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: actor, domain: Mv.Membership)
|
||||||
|
|
||||||
|
field
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "read access (all roles)" do
|
||||||
|
test "user with own_data can read all custom fields", %{actor: actor} do
|
||||||
|
custom_field = create_custom_field(actor)
|
||||||
|
user = create_user_with_permission_set("own_data", actor)
|
||||||
|
|
||||||
|
{:ok, fields} = Ash.read(CustomField, actor: user, domain: Mv.Membership)
|
||||||
|
ids = Enum.map(fields, & &1.id)
|
||||||
|
assert custom_field.id in ids
|
||||||
|
|
||||||
|
{:ok, fetched} = Ash.get(CustomField, custom_field.id, actor: user, domain: Mv.Membership)
|
||||||
|
assert fetched.id == custom_field.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "user with read_only can read all custom fields", %{actor: actor} do
|
||||||
|
custom_field = create_custom_field(actor)
|
||||||
|
user = create_user_with_permission_set("read_only", actor)
|
||||||
|
|
||||||
|
{:ok, fields} = Ash.read(CustomField, actor: user, domain: Mv.Membership)
|
||||||
|
ids = Enum.map(fields, & &1.id)
|
||||||
|
assert custom_field.id in ids
|
||||||
|
|
||||||
|
{:ok, fetched} = Ash.get(CustomField, custom_field.id, actor: user, domain: Mv.Membership)
|
||||||
|
assert fetched.id == custom_field.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "user with normal_user can read all custom fields", %{actor: actor} do
|
||||||
|
custom_field = create_custom_field(actor)
|
||||||
|
user = create_user_with_permission_set("normal_user", actor)
|
||||||
|
|
||||||
|
{:ok, fields} = Ash.read(CustomField, actor: user, domain: Mv.Membership)
|
||||||
|
ids = Enum.map(fields, & &1.id)
|
||||||
|
assert custom_field.id in ids
|
||||||
|
|
||||||
|
{:ok, fetched} = Ash.get(CustomField, custom_field.id, actor: user, domain: Mv.Membership)
|
||||||
|
assert fetched.id == custom_field.id
|
||||||
|
end
|
||||||
|
|
||||||
|
test "user with admin can read all custom fields", %{actor: actor} do
|
||||||
|
custom_field = create_custom_field(actor)
|
||||||
|
user = create_user_with_permission_set("admin", actor)
|
||||||
|
|
||||||
|
{:ok, fields} = Ash.read(CustomField, actor: user, domain: Mv.Membership)
|
||||||
|
ids = Enum.map(fields, & &1.id)
|
||||||
|
assert custom_field.id in ids
|
||||||
|
|
||||||
|
{:ok, fetched} = Ash.get(CustomField, custom_field.id, actor: user, domain: Mv.Membership)
|
||||||
|
assert fetched.id == custom_field.id
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "write access - non-admin cannot create/update/destroy" do
|
||||||
|
setup %{actor: actor} do
|
||||||
|
user = create_user_with_permission_set("normal_user", actor)
|
||||||
|
custom_field = create_custom_field(actor)
|
||||||
|
%{user: user, custom_field: custom_field}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-admin cannot create custom field (forbidden)", %{user: user} do
|
||||||
|
assert {:error, %Ash.Error.Forbidden{}} =
|
||||||
|
CustomField
|
||||||
|
|> Ash.Changeset.for_create(:create, %{
|
||||||
|
name: "forbidden_field_#{System.unique_integer([:positive])}",
|
||||||
|
value_type: :string
|
||||||
|
})
|
||||||
|
|> Ash.create(actor: user, domain: Mv.Membership)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-admin cannot update custom field (forbidden)", %{
|
||||||
|
user: user,
|
||||||
|
custom_field: custom_field
|
||||||
|
} do
|
||||||
|
assert {:error, %Ash.Error.Forbidden{}} =
|
||||||
|
custom_field
|
||||||
|
|> Ash.Changeset.for_update(:update, %{description: "Updated"})
|
||||||
|
|> Ash.update(actor: user, domain: Mv.Membership)
|
||||||
|
end
|
||||||
|
|
||||||
|
test "non-admin cannot destroy custom field (forbidden)", %{
|
||||||
|
user: user,
|
||||||
|
custom_field: custom_field
|
||||||
|
} do
|
||||||
|
assert {:error, %Ash.Error.Forbidden{}} =
|
||||||
|
Ash.destroy(custom_field, actor: user, domain: Mv.Membership)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
|
||||||
|
describe "write access - admin can create/update/destroy" do
|
||||||
|
setup %{actor: actor} do
|
||||||
|
user = create_user_with_permission_set("admin", actor)
|
||||||
|
custom_field = create_custom_field(actor)
|
||||||
|
%{user: user, custom_field: custom_field}
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can create custom field", %{user: user} do
|
||||||
|
name = "admin_field_#{System.unique_integer([:positive])}"
|
||||||
|
|
||||||
|
assert {:ok, %CustomField{} = field} =
|
||||||
|
CustomField
|
||||||
|
|> Ash.Changeset.for_create(:create, %{name: name, value_type: :string})
|
||||||
|
|> Ash.create(actor: user, domain: Mv.Membership)
|
||||||
|
|
||||||
|
assert field.name == name
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can update custom field", %{user: user, custom_field: custom_field} do
|
||||||
|
assert {:ok, updated} =
|
||||||
|
custom_field
|
||||||
|
|> Ash.Changeset.for_update(:update, %{description: "Admin updated"})
|
||||||
|
|> Ash.update(actor: user, domain: Mv.Membership)
|
||||||
|
|
||||||
|
assert updated.description == "Admin updated"
|
||||||
|
end
|
||||||
|
|
||||||
|
test "admin can destroy custom field", %{user: user, custom_field: custom_field} do
|
||||||
|
assert :ok = Ash.destroy(custom_field, actor: user, domain: Mv.Membership)
|
||||||
|
|
||||||
|
assert {:error, _} =
|
||||||
|
Ash.get(CustomField, custom_field.id, domain: Mv.Membership, actor: user)
|
||||||
|
end
|
||||||
|
end
|
||||||
|
end
|
||||||
Loading…
Add table
Add a link
Reference in a new issue