From 2fd1d9c68a4e780feb500c345de8f09a45b33f73 Mon Sep 17 00:00:00 2001 From: Dan Schultzer <1254724+danschultzer@users.noreply.github.com> Date: Mon, 25 Sep 2023 20:14:40 -0700 Subject: [PATCH] Accept MFAs along with anonymous functions --- CHANGELOG.md | 5 +++ lib/pow/ecto/schema/changeset.ex | 38 +++++++++-------- test/pow/ecto/schema/changeset_test.exs | 54 +++++++++++++++++++++++-- 3 files changed, 77 insertions(+), 20 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e1044e5..e2b936bd 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -2,6 +2,11 @@ ## v1.0.35 (TBA) +### Enhancements + +* [`Pow.Ecto.Schema.Changeset`] Now handles MFA for `:password_hash_verify` +* [`Pow.Ecto.Schema.Changeset`] Now handles MFA for `:email_validator` + ### Deprecations * [`Pow.Ecto.Schema.Changeset`] Deprecated `:password_hash_methods` in favor of `:password_hash_verify` diff --git a/lib/pow/ecto/schema/changeset.ex b/lib/pow/ecto/schema/changeset.ex index b4578ef8..2f1574a1 100644 --- a/lib/pow/ecto/schema/changeset.ex +++ b/lib/pow/ecto/schema/changeset.ex @@ -12,12 +12,15 @@ defmodule Pow.Ecto.Schema.Changeset do * `:password_min_length` - minimum password length, defaults to 8 * `:password_max_length` - maximum password length, defaults to 4096 - * `:password_hash_verify` - the password hash and verify functions to use, - defaults to: + * `:password_hash_verify` - the password hash and verify anonymous + functions or MFAs, defaults to: {&Pow.Ecto.Schema.Password.pbkdf2_hash/1, &Pow.Ecto.Schema.Password.pbkdf2_verify/2} - * `:email_validator` - the email validation function, defaults to: + + It may be anonymous functions of MFAs. + * `:email_validator` - the email validation anonymous function or + MFA, defaults to: &Pow.Ecto.Schema.Changeset.validate_email/1 @@ -169,7 +172,7 @@ defmodule Pow.Ecto.Schema.Changeset do validator = get_email_validator(config) Changeset.validate_change(changeset, :email, {:email_format, validator}, fn :email, email -> - case validator.(email) do + case apply_function_or_mfa(validator, [email]) do :ok -> [] :error -> [email: {"has invalid format", validation: :email_format}] {:error, reason} -> [email: {"has invalid format", validation: :email_format, reason: reason}] @@ -215,16 +218,12 @@ defmodule Pow.Ecto.Schema.Changeset do """ @spec verify_password(Ecto.Schema.t(), binary(), Config.t()) :: boolean() def verify_password(%{password_hash: nil}, _password, config) do - config - |> get_password_hash_function() - |> apply([""]) + apply_password_hash_function(config, [""]) false end def verify_password(%{password_hash: password_hash}, password, config) do - config - |> get_password_verify_function() - |> apply([password, password_hash]) + apply_password_verify_function(config, [password, password_hash]) end defp maybe_require_password(%{data: %{password_hash: nil}} = changeset) do @@ -259,21 +258,26 @@ defmodule Pow.Ecto.Schema.Changeset do defp maybe_validate_password_hash(changeset), do: changeset defp hash_password(password, config) do - config - |> get_password_hash_function() - |> apply([password]) + apply_password_hash_function(config, [password]) end - defp get_password_hash_function(config) do + defp apply_password_hash_function(config, args) do {password_hash_function, _} = get_password_hash_functions(config) - password_hash_function + apply_function_or_mfa(password_hash_function, args) + end + + defp apply_function_or_mfa(fun_or_mfa, apply_args) do + case fun_or_mfa do + fun when is_function(fun) -> apply(fun, apply_args) + {mod, fun, args} when is_list(args) -> apply(mod, fun, args ++ apply_args) + end end - defp get_password_verify_function(config) do + defp apply_password_verify_function(config, args) do {_, password_verify_function} = get_password_hash_functions(config) - password_verify_function + apply_function_or_mfa(password_verify_function, args) end defp get_password_hash_functions(config) do diff --git a/test/pow/ecto/schema/changeset_test.exs b/test/pow/ecto/schema/changeset_test.exs index 909bce0d..c05567f3 100644 --- a/test/pow/ecto/schema/changeset_test.exs +++ b/test/pow/ecto/schema/changeset_test.exs @@ -48,7 +48,7 @@ defmodule Pow.Ecto.Schema.ChangesetTest do assert changeset.valid? end - test "can validate with custom e-mail validator" do + test "can validate with custom anonymous function e-mail validator" do config = [email_validator: &{:error, "custom message #{&1}"}] changeset = Changeset.user_id_field_changeset(%User{}, @valid_params, config) @@ -62,6 +62,31 @@ defmodule Pow.Ecto.Schema.ChangesetTest do assert changeset.valid? end + defmodule CustomEmailValidator do + def email_validate("ok@example.com") do + :ok + end + + def email_validate(email) do + {:error, "custom message #{email}"} + end + end + + test "can validate with custom MFA function e-mail validator" do + config = [email_validator: {CustomEmailValidator, :email_validate, []}] + changeset = Changeset.user_id_field_changeset(%User{}, @valid_params, config) + + refute changeset.valid? + assert changeset.errors[:email] == {"has invalid format", [validation: :email_format, reason: "custom message john.doe@example.com"]} + assert changeset.validations[:email] == {:email_format, config[:email_validator]} + + config = [email_validator: fn _email -> :ok end] + params = Map.put(@valid_params, "email", "ok@example.com") + changeset = Changeset.user_id_field_changeset(%User{}, params, config) + + assert changeset.valid? + end + test "uses case insensitive value for user id" do changeset = User.changeset(%User{}, Map.put(@valid_params, "email", "Test@EXAMPLE.com")) assert changeset.valid? @@ -210,12 +235,35 @@ defmodule Pow.Ecto.Schema.ChangesetTest do end) =~ "passing `confirm_password` value to `Pow.Ecto.Schema.Changeset.confirm_password_changeset/3` has been deprecated, please use `password_confirmation` instead" end - test "can use custom password hash functions" do + test "can use custom anonymous password hash functions" do password_hash = &(&1 <> "123") password_verify = &(&1 == &2 <> "123") config = [password_hash_verify: {password_hash, password_verify}] + params = Map.put(@valid_params, "current_password", "secret") - changeset = Changeset.password_changeset(%User{}, @valid_params, config) + changeset = Changeset.password_changeset(%User{password: "secret123"}, params, config) + + assert changeset.valid? + assert changeset.changes[:password_hash] == "secret1234123" + end + + defmodule CustomPasswordHash do + def hash(password), do: password <> "123" + def verify(_password, hash), do: hash == "hashed" + end + + test "can use custom MFA password hash functions" do + config = + [ + password_hash_verify: { + {CustomPasswordHash, :hash, []}, + {CustomPasswordHash, :verify, []} + } + ] + + params = Map.put(@valid_params, "current_password", "secret") + + changeset = Changeset.password_changeset(%User{password_hash: "secret123"}, params, config) assert changeset.valid? assert changeset.changes[:password_hash] == "secret1234123"