diff --git a/lib/membership/member.ex b/lib/membership/member.ex index cebcc5c..650cf43 100644 --- a/lib/membership/member.ex +++ b/lib/membership/member.ex @@ -407,8 +407,16 @@ defmodule Mv.Membership.Member do actor = Map.get(changeset.context || %{}, :actor) # Check the current state of the user in the database - # Pass actor to ensure proper authorization (User might have policies in future) - case Ash.get(Mv.Accounts.User, user_id, actor: actor) do + # Check if authorization is disabled in the parent operation's context + # Access private context where authorize? flag is stored + authorize? = + case get_in(changeset.context, [:private, :authorize?]) do + false -> false + _ -> true + end + + # Pass actor and authorize? to ensure proper authorization (User might have policies in future) + case Ash.get(Mv.Accounts.User, user_id, actor: actor, authorize?: authorize?) do # User is free to be linked {:ok, %{member_id: nil}} -> :ok diff --git a/priv/repo/seeds.exs b/priv/repo/seeds.exs index ccc90f5..91b6fa3 100644 --- a/priv/repo/seeds.exs +++ b/priv/repo/seeds.exs @@ -161,24 +161,30 @@ end # This handles both existing users (e.g., from OIDC) and newly created users case Accounts.User |> Ash.Query.filter(email == ^admin_email) - |> Ash.read_one(domain: Mv.Accounts) do + |> Ash.read_one(domain: Mv.Accounts, authorize?: false) do {:ok, existing_admin_user} when not is_nil(existing_admin_user) -> # User already exists (e.g., via OIDC) - assign admin role + # Use authorize?: false for bootstrap - this is initial setup existing_admin_user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) - |> Ash.update!() + |> Ash.update!(authorize?: false) {:ok, nil} -> # User doesn't exist - create admin user with password - Accounts.create_user!(%{email: admin_email}, upsert?: true, upsert_identity: :unique_email) + # Use authorize?: false for bootstrap - no admin user exists yet to use as actor + Accounts.create_user!(%{email: admin_email}, + upsert?: true, + upsert_identity: :unique_email, + authorize?: false + ) |> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"}) - |> Ash.update!() + |> Ash.update!(authorize?: false) |> then(fn user -> user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) - |> Ash.update!() + |> Ash.update!(authorize?: false) end) {:error, error} -> @@ -190,10 +196,10 @@ end admin_user_with_role = case Accounts.User |> Ash.Query.filter(email == ^admin_email) - |> Ash.read_one(domain: Mv.Accounts) do + |> Ash.read_one(domain: Mv.Accounts, authorize?: false) do {:ok, user} when not is_nil(user) -> user - |> Ash.load!(:role) + |> Ash.load!(:role, authorize?: false) {:ok, nil} -> raise "Admin user not found after creation/assignment" @@ -209,13 +215,14 @@ system_user_email = Mv.Helpers.SystemActor.system_user_email() case Accounts.User |> Ash.Query.filter(email == ^system_user_email) - |> Ash.read_one(domain: Mv.Accounts) do + |> Ash.read_one(domain: Mv.Accounts, authorize?: false) do {:ok, existing_system_user} when not is_nil(existing_system_user) -> # System user already exists - ensure it has admin role + # Use authorize?: false for bootstrap existing_system_user |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) - |> Ash.update!() + |> Ash.update!(authorize?: false) {:ok, nil} -> # System user doesn't exist - create it with admin role @@ -224,13 +231,15 @@ case Accounts.User # - No OIDC ID (oidc_id = nil) - prevents OIDC login # - This user is ONLY for internal system operations via SystemActor # If either hashed_password or oidc_id is set, the user could potentially log in + # Use authorize?: false for bootstrap - system user creation happens before system actor exists Accounts.create_user!(%{email: system_user_email}, upsert?: true, - upsert_identity: :unique_email + upsert_identity: :unique_email, + authorize?: false ) |> Ash.Changeset.for_update(:update, %{}) |> Ash.Changeset.manage_relationship(:role, admin_role, type: :append_and_remove) - |> Ash.update!() + |> Ash.update!(authorize?: false) {:error, error} -> # Log error but don't fail seeds - SystemActor will fall back to admin user @@ -397,9 +406,20 @@ additional_users = [ created_users = Enum.map(additional_users, fn user_attrs -> - Accounts.create_user!(user_attrs, upsert?: true, upsert_identity: :unique_email) - |> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"}) - |> Ash.update!() + # Use admin user as actor for additional user creation (not bootstrap) + user = + Accounts.create_user!(user_attrs, + upsert?: true, + upsert_identity: :unique_email, + actor: admin_user_with_role + ) + |> Ash.Changeset.for_update(:admin_set_password, %{password: "testpassword"}) + |> Ash.update!(actor: admin_user_with_role) + + # Reload user to ensure all fields (including member_id) are loaded + Accounts.User + |> Ash.Query.filter(id == ^user.id) + |> Ash.read_one!(domain: Mv.Accounts, actor: admin_user_with_role) end) # Create members with linked users to demonstrate the 1:1 relationship @@ -449,11 +469,13 @@ Enum.with_index(linked_members) member = if user.member_id == nil do # User is free, create member and link - use upsert to prevent duplicates + # Use authorize?: false for User lookup during relationship management (bootstrap phase) Membership.create_member!( Map.put(member_attrs_without_fee_type, :user, %{id: user.id}), upsert?: true, upsert_identity: :unique_email, - actor: admin_user_with_role + actor: admin_user_with_role, + authorize?: false ) else # User already has a member, just create the member without linking - use upsert to prevent duplicates