Security: Require actor parameter in CSV import

Remove fallback to system_actor in process_chunk to prevent
unauthorized access. Actor must now be explicitly provided.
This commit is contained in:
Moritz 2026-01-25 18:33:25 +01:00 committed by Simon
parent 33dc8307c8
commit 64952d4ff4
Signed by: simon
GPG key ID: 40E7A58C4AA1EDB2
2 changed files with 48 additions and 43 deletions

View file

@ -73,25 +73,33 @@ defmodule Mv.Membership.Import.MemberCSVTest do
end
describe "process_chunk/4" do
test "function exists and accepts chunk_rows_with_lines, column_map, custom_field_map, and opts" do
setup do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
%{actor: system_actor}
end
test "function exists and accepts chunk_rows_with_lines, column_map, custom_field_map, and opts",
%{
actor: actor
} do
chunk_rows_with_lines = [{2, %{member: %{email: "john@example.com"}, custom: %{}}}]
column_map = %{email: 0}
custom_field_map = %{}
opts = []
opts = [actor: actor]
# This will fail until the function is implemented
result = MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
assert match?({:ok, _}, result) or match?({:error, _}, result)
end
test "creates member successfully with valid data" do
test "creates member successfully with valid data", %{actor: actor} do
chunk_rows_with_lines = [
{2, %{member: %{email: "john@example.com", first_name: "John"}, custom: %{}}}
]
column_map = %{email: 0, first_name: 1}
custom_field_map = %{}
opts = []
opts = [actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -106,14 +114,14 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert Enum.any?(members, &(&1.email == "john@example.com"))
end
test "returns error for invalid email" do
test "returns error for invalid email", %{actor: actor} do
chunk_rows_with_lines = [
{2, %{member: %{email: "invalid-email"}, custom: %{}}}
]
column_map = %{email: 0}
custom_field_map = %{}
opts = []
opts = [actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -130,14 +138,14 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert error.message != ""
end
test "returns error for missing email" do
test "returns error for missing email", %{actor: actor} do
chunk_rows_with_lines = [
{2, %{member: %{}, custom: %{}}}
]
column_map = %{}
custom_field_map = %{}
opts = []
opts = [actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -152,14 +160,14 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert is_binary(error.message)
end
test "returns error for whitespace-only email" do
test "returns error for whitespace-only email", %{actor: actor} do
chunk_rows_with_lines = [
{3, %{member: %{email: " "}, custom: %{}}}
]
column_map = %{email: 0}
custom_field_map = %{}
opts = []
opts = [actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -173,13 +181,11 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert error.field == :email
end
test "returns error for duplicate email" do
test "returns error for duplicate email", %{actor: actor} do
# Create existing member first
system_actor = Mv.Helpers.SystemActor.get_system_actor()
{:ok, _existing} =
Mv.Membership.create_member(%{email: "duplicate@example.com", first_name: "Existing"},
actor: system_actor
actor: actor
)
chunk_rows_with_lines = [
@ -188,7 +194,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
column_map = %{email: 0, first_name: 1}
custom_field_map = %{}
opts = []
opts = [actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -203,9 +209,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert error.message =~ "email" or error.message =~ "duplicate" or error.message =~ "unique"
end
test "creates member with custom field values" do
system_actor = Mv.Helpers.SystemActor.get_system_actor()
test "creates member with custom field values", %{actor: actor} do
# Create custom field first
{:ok, custom_field} =
Mv.Membership.CustomField
@ -213,7 +217,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
name: "Phone",
value_type: :string
})
|> Ash.create(actor: system_actor)
|> Ash.create(actor: actor)
chunk_rows_with_lines = [
{2,
@ -230,7 +234,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
to_string(custom_field.id) => %{id: custom_field.id, value_type: custom_field.value_type}
}
opts = [custom_field_lookup: custom_field_lookup]
opts = [custom_field_lookup: custom_field_lookup, actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -239,8 +243,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert chunk_result.failed == 0
# Verify member and custom field value were created
system_actor = Mv.Helpers.SystemActor.get_system_actor()
members = Mv.Membership.list_members!(actor: system_actor)
members = Mv.Membership.list_members!(actor: actor)
member = Enum.find(members, &(&1.email == "withcustom@example.com"))
assert member != nil
@ -251,7 +254,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert cfv.value.value == "123-456-7890"
end
test "handles multiple rows with mixed success and failure" do
test "handles multiple rows with mixed success and failure", %{actor: actor} do
chunk_rows_with_lines = [
{2, %{member: %{email: "valid1@example.com"}, custom: %{}}},
{3, %{member: %{email: "invalid-email"}, custom: %{}}},
@ -260,7 +263,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
column_map = %{email: 0}
custom_field_map = %{}
opts = []
opts = [actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -276,7 +279,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert is_binary(error.message)
end
test "preserves CSV line numbers in errors" do
test "preserves CSV line numbers in errors", %{actor: actor} do
chunk_rows_with_lines = [
{5, %{member: %{email: "invalid"}, custom: %{}}},
{10, %{member: %{email: "also-invalid"}, custom: %{}}}
@ -284,7 +287,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
column_map = %{email: 0}
custom_field_map = %{}
opts = []
opts = [actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -297,11 +300,11 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert 10 in line_numbers
end
test "returns {:ok, chunk_result} on success" do
test "returns {:ok, chunk_result} on success", %{actor: actor} do
chunk_rows_with_lines = [{2, %{member: %{email: "test@example.com"}, custom: %{}}}]
column_map = %{email: 0}
custom_field_map = %{}
opts = []
opts = [actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -315,11 +318,11 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert is_list(chunk_result.errors)
end
test "returns {:ok, _} with zero counts for empty chunk" do
test "returns {:ok, _} with zero counts for empty chunk", %{actor: actor} do
chunk_rows_with_lines = []
column_map = %{}
custom_field_map = %{}
opts = []
opts = [actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -334,7 +337,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert function_exported?(MemberCSV, :process_chunk, 4)
end
test "error capping collects exactly 50 errors" do
test "error capping collects exactly 50 errors", %{actor: actor} do
# Create 50 rows with invalid emails
chunk_rows_with_lines =
1..50
@ -344,7 +347,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
column_map = %{email: 0}
custom_field_map = %{}
opts = [existing_error_count: 0, max_errors: 50]
opts = [existing_error_count: 0, max_errors: 50, actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -354,7 +357,9 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert length(chunk_result.errors) == 50
end
test "error capping collects only first 50 errors when more than 50 errors occur" do
test "error capping collects only first 50 errors when more than 50 errors occur", %{
actor: actor
} do
# Create 60 rows with invalid emails
chunk_rows_with_lines =
1..60
@ -364,7 +369,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
column_map = %{email: 0}
custom_field_map = %{}
opts = [existing_error_count: 0, max_errors: 50]
opts = [existing_error_count: 0, max_errors: 50, actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -374,7 +379,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert length(chunk_result.errors) == 50
end
test "error capping respects existing_error_count" do
test "error capping respects existing_error_count", %{actor: actor} do
# Create 30 rows with invalid emails
chunk_rows_with_lines =
1..30
@ -384,7 +389,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
column_map = %{email: 0}
custom_field_map = %{}
opts = [existing_error_count: 25, max_errors: 50]
opts = [existing_error_count: 25, max_errors: 50, actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -395,7 +400,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert length(chunk_result.errors) == 25
end
test "error capping collects no errors when limit already reached" do
test "error capping collects no errors when limit already reached", %{actor: actor} do
# Create 10 rows with invalid emails
chunk_rows_with_lines =
1..10
@ -405,7 +410,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
column_map = %{email: 0}
custom_field_map = %{}
opts = [existing_error_count: 50, max_errors: 50]
opts = [existing_error_count: 50, max_errors: 50, actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -415,7 +420,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert chunk_result.errors == []
end
test "error capping with mixed success and failure" do
test "error capping with mixed success and failure", %{actor: actor} do
# Create 100 rows: 30 valid, 70 invalid
valid_rows =
1..30
@ -433,7 +438,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
column_map = %{email: 0}
custom_field_map = %{}
opts = [existing_error_count: 0, max_errors: 50]
opts = [existing_error_count: 0, max_errors: 50, actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)
@ -444,7 +449,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
assert length(chunk_result.errors) == 50
end
test "error capping with custom max_errors" do
test "error capping with custom max_errors", %{actor: actor} do
# Create 20 rows with invalid emails
chunk_rows_with_lines =
1..20
@ -454,7 +459,7 @@ defmodule Mv.Membership.Import.MemberCSVTest do
column_map = %{email: 0}
custom_field_map = %{}
opts = [existing_error_count: 0, max_errors: 10]
opts = [existing_error_count: 0, max_errors: 10, actor: actor]
assert {:ok, chunk_result} =
MemberCSV.process_chunk(chunk_rows_with_lines, column_map, custom_field_map, opts)