3
votes

I am making an API only Phoenix app. I come from a Ruby on Rails background, so bear with me.

Say I have a User model with email, password, password_hash, and role fields.

I need to restrict the role and password_hash fields from user input, or whitelist the email and password fields. Right now anyone could POST this sign up as an admin:

{
    "user": {
        "email": "[email protected]",
        "password": "testpw",
        "password_hash": "shouldn't allow user input",
        "role": "admin"
    }
}

This is typically accomplished in Rails using strong params, which will strip out fields that are not explicitly specified.

How do I do restrict/whitelist params with Phoenix using best practices?

This is my create method in my user_controller:

  def create(conn, %{"user" => user_params}) do
    changeset = User.registration_changeset(%User{}, user_params)
    ...
    ...
  end

And here is my schema and changesets in the model, user.ex. I'm following this tutorial, it says "we pipe the new changeset through our original one"

  schema "users" do
    field :email, :string
    field :password, :string, virtual: true
    field :password_hash, :string
    field :role, :string

    timestamps()
  end

  def changeset(model, params \\ :empty) do
    model
    |> cast(params, ~w(email), [])
    |> downcase_email()
    |> unique_constraint(:email)
    |> validate_format(:email, ~r/@/)
  end

  def registration_changeset(model, params) do
    model
    |> changeset(params)
    |> cast(params, ~w(password), [])
    |> validate_length(:password, min: 6)
    |> put_password_hash()
  end

Phoenix's scrub_params is close, but it doesn't sound like what I need.

I think I can accomplish this by pattern matching but I'm not sure how.

2
It's odd that cast is not working, because it should do exactly what you're asking: only allowing the email parameter. Also, your controller is using User.registration_changeset/2, but you're showing us the code for User.changeset/2.tompave
What does your registration_changeset look like? Your changeset should already be ignoring the role field in params.Dogbert
I would also recommend to use cast/3 over cast/4, as the latter is deprecated.tompave
Thanks for the replies, @tompave , I've amended my question with the registration_changeset. It's definitely allowing at least the role field to go through and be saved. Also, I added the role field using a separate migration but that shouldn't make a difference?Kevin Y

2 Answers

1
votes

Actually the code behaves as expected and does not save the role field. (I was reading the request in the console instead of actually checking the database.)

0
votes

I know this is late here but here is this approach:

defmodule MyApp.Utils do
  def strong_params(params, allowed_fields) when is_map(params) do
    allowed_strings = Enum.map(allowed_fields, &Atom.to_string(&1))

    Enum.reduce(params, [], fn {k, v}, acc ->
      key = check_key(k, allowed_strings)
      acc ++ [{key, v}]
    end)
    |> Enum.reject(fn {k, _v} -> k == nil end)
    |> Map.new()
  end

  defp check_key(k, allowed_strings) when is_atom(k) do
    str_key = Atom.to_string(k)

    if str_key in allowed_strings do
      k
    end
  end
  defp check_key(k, allowed_strings) when is_binary(k) do
    if k in allowed_strings do
      String.to_existing_atom(k)
    end
  end
  defp check_key(_, _), do: nil
end

enter image description here

Reference: https://medium.com/@alves.lcs/phoenix-strong-params-9db4bd9f56d8