fix: CustomField policies, no system-actor fallback, guidelines

- Tests and UI pass actor for CustomField create/read/destroy; seeds use actor
- Member required-custom-fields validation uses context.actor only (no fallback)
- CODE_GUIDELINES: add rule forbidding system-actor fallbacks
This commit is contained in:
Moritz 2026-01-29 13:53:55 +01:00
parent 250369d142
commit 21dbdbe366
10 changed files with 116 additions and 43 deletions

View file

@ -471,11 +471,12 @@ defmodule Mv.Membership.Member do
end
end
# Validate required custom fields
validate fn changeset, _ ->
# Validate required custom fields (actor from validation context only; no fallback)
validate fn changeset, context ->
provided_values = provided_custom_field_values(changeset)
actor = context.actor
case Mv.Membership.list_required_custom_fields() do
case Mv.Membership.list_required_custom_fields(actor: actor) do
{:ok, required_custom_fields} ->
missing_fields = missing_required_fields(required_custom_fields, provided_values)

View file

@ -155,23 +155,31 @@ defmodule Mv.Membership do
Lists only required custom fields.
This is an optimized version that filters at the database level instead of
loading all custom fields and filtering in memory.
loading all custom fields and filtering in memory. Requires an actor for
authorization (CustomField read policy).
## Options
- `:actor` - Required. The actor for authorization (e.g. current user).
All roles can read CustomField; actor must have a valid role.
## Returns
- `{:ok, required_custom_fields}` - List of required custom fields
- `{:error, error}` - Error reading custom fields
- `{:error, error}` - Error reading custom fields (e.g. Forbidden when no actor)
## Examples
iex> {:ok, required_fields} = Mv.Membership.list_required_custom_fields()
iex> {:ok, required_fields} = Mv.Membership.list_required_custom_fields(actor: actor)
iex> Enum.all?(required_fields, & &1.required)
true
"""
def list_required_custom_fields do
def list_required_custom_fields(opts \\ []) do
actor = Keyword.get(opts, :actor)
Mv.Membership.CustomField
|> Ash.Query.filter(expr(required == true))
|> Ash.read(domain: __MODULE__)
|> Ash.read(domain: __MODULE__, actor: actor)
end
@doc """