From 8804128ad4cfea228ce97005072b029d0379d80f Mon Sep 17 00:00:00 2001 From: Kevin Jacoby Date: Wed, 29 Jun 2022 04:02:26 +0100 Subject: [PATCH] Allow passing hash on secure password validations I was running into a case where I didn't want to just disabled the validations and add my own. In fact, I would very much like to keep the default validation but just de-activate it on some scenario: e.g. Inviting a user without having to set a password for them yet so they can add it themselves later when they receive an email invitation to finish setting up their account. My understanding of the validations flag originally intended was to just disabled them and if you needed something more custom, you could run your own validations instead. This would be an acceptable solution, but it would add more code to my controller. Instead validations can receive a `Hash` wich is then use to apply validations rules to `validate`. This is just a suggestion, I am not sure if there is a need, and I am aware this PR is probably far from perfect. Any feedback welcome. EDIT: implemented changes as per feedback. --- activemodel/CHANGELOG.md | 10 ++++++++++ activemodel/lib/active_model/secure_password.rb | 11 ++++++++--- activemodel/test/cases/secure_password_test.rb | 10 ++++++++++ 3 files changed, 28 insertions(+), 3 deletions(-) diff --git a/activemodel/CHANGELOG.md b/activemodel/CHANGELOG.md index 1422b04ce8..96e7bc204e 100644 --- a/activemodel/CHANGELOG.md +++ b/activemodel/CHANGELOG.md @@ -1,3 +1,13 @@ +* Support passing `Hash` with keys `:if / :unless / :on` to SecurePassword. + This provides more flexibility when you want to trigger or skip validations on + specific conditions + + ```ruby + secure_password validations: {if: :requires_password?}` + ``` + + *Kevin Jacoby* + * `has_secure_password` now supports password challenges via a `password_challenge` accessor and validation. diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb index 7cc207204e..7480c2a152 100644 --- a/activemodel/lib/active_model/secure_password.rb +++ b/activemodel/lib/active_model/secure_password.rb @@ -35,8 +35,11 @@ module ActiveModel # ActiveModel::Dirty; if dirty tracking methods are not defined, this # validation will fail. # - # All of the above validations can be omitted by passing - # validations: false as an argument. This allows complete + # The password presence validation can be conditionally enforced by + # passing an options hash to +:validations+ with the standard +:if+ / + # +:unless+ / +:on+ keys. (See ActiveModel::Validations::ClassMethods#validates + # for more information.) Alternatively, all of the above validations can + # be omitted by passing validations: false. This allows complete # customizability of validation behavior. # # To use +has_secure_password+, add bcrypt (~> 3.1.7) to your Gemfile: @@ -93,11 +96,13 @@ module ActiveModel if validations include ActiveModel::Validations + validation_options = validations.is_a?(Hash) ? validations : {} + # This ensures the model has a password by checking whether the password_digest # is present, so that this works with both new and existing records. However, # when there is an error, the message is added to the password attribute instead # so that the error message will make sense to the end-user. - validate do |record| + validate(validation_options) do |record| record.errors.add(attribute, :blank) unless record.public_send("#{attribute}_digest").present? end diff --git a/activemodel/test/cases/secure_password_test.rb b/activemodel/test/cases/secure_password_test.rb index c404561f33..bcae80292c 100644 --- a/activemodel/test/cases/secure_password_test.rb +++ b/activemodel/test/cases/secure_password_test.rb @@ -31,6 +31,16 @@ class SecurePasswordTest < ActiveModel::TestCase assert_not_respond_to @visitor, :valid? end + test "support conditional validation" do + user = Struct.new(:requires_password, :password_digest) do + include ActiveModel::SecurePassword + has_secure_password validations: { if: :requires_password } + end + + assert_predicate user.new(false), :valid? + assert_predicate user.new(true), :invalid? + end + test "create a new user with validations and valid password/confirmation" do @user.password = "password" @user.password_confirmation = "password"