Cast_assoc is null?

I’m not suggesting using |> cast_assoc(:user). What I am suggesting is putting the user_id inside the magnet struct before sending it into the changeset function.

Thanks did not see your example, so I think I finally understand how this should work. I updated everything which makes sense but I’m getting the following error when I got the add new magnet view

function Magnify.Magnets.change_magnet/1 is undefined (module Magnify.Magnets is not available)

I have this is my controller

alias Magnify.Magnets
alias Magnify.Magnets.Magnet
alias Magnify.Repo

Never I think I got it working, :slight_smile:

I’m getting

  • (CompileError) lib/magnify/magnets/magnets.ex:54: User.struct/0 is undefined, cannot expand struct User

Do I need to alias the user?

Ok so I did this

def create_magnet(attrs, for: %Magnify.Coherence.User{id: user_id}) do
    %Magnet{user_id: user_id}
    |> Magnet.changeset(attrs)
    |> Repo.insert()
  end

which is fine now, when create the object or post I get

Postgrex expected a binary of 16 bytes, got 1. Please make sure the value you are passing matches the definition in your table or in your query or convert the value accordingly.

I think most of elixir community vulnerable. Really thanks i see always cast_assocs or id in changesets. This is the first time i see your approach. But on the other hand we already modify user_id from controller so somebody cant put another user_id right ?

Thanks everyone, got it working, have to the community around Elixir is just amazing. Now to the next challenge / task

In your example above, yes. Since you use Map.put(params, "user_id", ...) it replaces the "user_id" provided in the params.

But what if you or someone else who starts working on your codebase forgets to use Map.put/3 or to delete the "user_id" key from params, or uses Map.put_new/3 in some other controller using the same changeset function (which casts user_id), or replaces :user_id with :owner_id in the schema but forgets to update Map.put(params, "user_id", ...) with Map.put(params, "owner_id", ...) in the controller?

Then the "user_id" from params leaks into the changeset function and you have a vulnerability. Any user can send whatever "user_id" they want thus completely bypassing any authentication logic you have.

The only way to avoid it, as I see it, is to not cast user_id (or any other foreign key) inside the changeset functions but put it into the schema structs explicitly when needed.

1 Like

@idi527 would you do the same thing for many_to_many

I have

schema "fluxes" do
    field :description, :string
    field :title, :string
    many_to_many :magnets, Magnify.Magnets.Magnet, join_through: "magnets_fluxes"

    timestamps()
  end

  @doc false
  def changeset(flux, attrs) do
    flux
    |> cast(attrs, [:title, :description])
    |> validate_required([:title, :description])
  end 

And not sure the correct to add magnets use puts_assoc but not sure?

I posted about this topic a few days ago:

The scenario might be similar but keep in mind that using cast_assoc requires you to handle the parent and child at the same time.

Hope this helps.

Best regards,

I’d probably use an ecto multi to insert the flux and create relationships between it and some magnets inside a single database transaction.

Adapted from Is there are way to bulk insert join records of a many-to-many relationship with Ecto?

# in the module containing your business logic

# here I suppose that the magnets already exist
# and we only need to associate them with the 
# created flux
@spec create_flux(%{(String.t | atom) => term}, [magnet_id :: pos_integer]) :: {:ok, %Flux{}} | {:error, Ecto.Changeset.t}
def create_flux(flux_attrs, magnet_ids) do
  alias Ecto.Multi

  flux_changeset = Flux.changeset(%Flux{}, flux_attrs)

  Multi.new()
  |> Multi.insert(:flux, flux_changeset)
  |> Multi.run(:magnet_relationships, fn %{flux: %Flux{id: flux_id}} ->
    # prepare relationships to be inserted into the many-to-many table
    relationships = Enum.map(magnet_ids, fn magnet_id ->
      %{magnet_id: magnet_id, flux_id: flux_id}
    end)
    
    # https://hexdocs.pm/ecto/Ecto.Repo.html#c:insert_all/3
    {:ok, Repo.insert_all("magnets_fluxes", relationships)}
  end)
  |> Repo.transaction()
  |> case do
    {:ok, %{flux: flux}} -> {:ok, flux}
    {:error, :flux, changeset, _changes} -> {:error, changeset}
  end
end

Used like this

flux_attrs = %{title: "a flux", description: "a flux"}
magnet_ids = [1, 2, 3] # I suppose these already exist in the database

create_flux(flux_attrs, magnet_ids)

@idi527 this is great, thanks appreciate it

I’m getting - function Magnify.Magnets.create_flux/1 is undefined or private. Did you mean one of:

Called with 1 arguments
%{"description" => "fff", "magnet_id" => ["2", "3"], "title" => "aff"}

I have the following in my templates

<%= multiple_select f, :magnet_id, @magnets, class: "form-control" %>

and the controller
magnets = Magnets.list_magnets(params) |> Enum.map(&{&1.description, &1.id})

I’m thinking that is were the issue is ?

Ok I know what the issue, is it’s the calling the create which only has case Magnets.create_flux(flux_params) do

just need to figure how to get magent_ids as that is inside flux_params

"Parameters: %{"_csrf_token" => "VxwHMmkfLwMhPhxuHRVcfn0DGTgfEAAA1lDzBRjRLGIVyqsL67MTOA==", "_utf8" => "✓", "flux" => %{"description" => "aaa", "magnet_id" => ["2"], "title" => "aaaa"}}
"

Maybe

magnet_ids = params["magnet_id"] || []
create_flux(params, magnet_ids)

But then you might need to check that the user actually has access to the magnet ids in params.

Well the create is like so

 def create(conn, %{"flux" => flux_params}) do
    IO.inspect flux_params
    case Magnets.create_flux(flux_params) do
      {:ok, flux} ->
        conn
        |> put_flash(:info, "Flux created successfully.")
        |> redirect(to: flux_path(conn, :show, flux))
      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

and flux_params had the keys

"Parameters: %{"_csrf_token" => "VxwHMmkfLwMhPhxuHRVcfn0DGTgfEAAA1lDzBRjRLGIVyqsL67MTOA==", "_utf8" => "✓", "flux" => %{"description" => "aaa", "magnet_id" => ["2"], "title" => "aaaa"}}

  def create(conn, %{"flux" => flux_params}) do
    magnet_ids = flux_params["magnet_id"] || []
    case Magnets.create_flux(flux_params, magnet_ids) do
      {:ok, flux} ->
        conn
        |> put_flash(:info, "Flux created successfully.")
        |> redirect(to: flux_path(conn, :show, flux))
      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

I did that got

Postgrex expected an integer in -9223372036854775808…9223372036854775807, got “1”. Please make sure the value you are passing matches the definition in your table or in your query or convert the value accordingly.

Which is working but the many to many is the wrong format

what I have
many_to_many :magnets, Magnify.Magnets.Magnet, join_through: “magnets_fluxes”

  def create(conn, %{"flux" => flux_params}) do
    magnet_ids = Enum.map(flux_params["magnet_id"] || [], fn magnet_id -> 
      String.to_integer(magnet_id)
    end)
    
    case Magnets.create_flux(flux_params, magnet_ids) do
      {:ok, flux} ->
        conn
        |> put_flash(:info, "Flux created successfully.")
        |> redirect(to: flux_path(conn, :show, flux))
      {:error, %Ecto.Changeset{} = changeset} ->
        render(conn, "new.html", changeset: changeset)
    end
  end

Well It’s getting there I now got

schema Magnify.Magnets.Flux does not have association :magents

Which I have no idea what is going on I added the many to many ?

What’s the file / line where this error is raised? And what’s on that line?