From 4733e04dfaaa39b22292eef168bc5c1d1638c9b2 Mon Sep 17 00:00:00 2001
From: Seb Jacobs <me@sebjacobs.com>
Date: Fri, 22 Mar 2019 08:20:36 +0000
Subject: Reintroduce support for overriding `has_secure_password` attributes

In Rails 5.2.x calling `has_secure_password` would define attribute
readers and writers on the superclass of the model, which meant that you
could override these attributes in a model and call the superclass for
example:

```
class Dog < ApplicationRecord
  has_secure_password

  def password=(new_password)
    @password_set = new_password.present?
    super
  end
end
```

However this behaviour was broken in Rails 6 when the ability to
customise the name of the attribute was introduced [1] since they are no
longer being defined on the superclass you will now see the following
error:

```
NoMethodError:
super: no superclass method `password=' for #<Dog:0x00007ffbbc7ce290>
Did you mean?  password
```

In order to resolve this issue and retain support for setting a custom
attribute name we can define these attribute readers/writers in a module
and then ensure that the module is included in the inheritance chain.

[1] https://www.github.com/rails/rails/commit/86a48b4da3
    https://www.github.com/rails/rails/commit/9b63bf1dfd
---
 activemodel/lib/active_model/secure_password.rb | 56 +++++++++++++------------
 1 file changed, 30 insertions(+), 26 deletions(-)

diff --git a/activemodel/lib/active_model/secure_password.rb b/activemodel/lib/active_model/secure_password.rb
index 51d54f34f3..cc1368d3a0 100644
--- a/activemodel/lib/active_model/secure_password.rb
+++ b/activemodel/lib/active_model/secure_password.rb
@@ -69,38 +69,42 @@ module ActiveModel
           raise
         end
 
-        attr_reader attribute
+        mod = Module.new do
+          attr_reader attribute
 
-        define_method("#{attribute}=") do |unencrypted_password|
-          if unencrypted_password.nil?
-            self.send("#{attribute}_digest=", nil)
-          elsif !unencrypted_password.empty?
-            instance_variable_set("@#{attribute}", unencrypted_password)
-            cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
-            self.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
+          define_method("#{attribute}=") do |unencrypted_password|
+            if unencrypted_password.nil?
+              self.send("#{attribute}_digest=", nil)
+            elsif !unencrypted_password.empty?
+              instance_variable_set("@#{attribute}", unencrypted_password)
+              cost = ActiveModel::SecurePassword.min_cost ? BCrypt::Engine::MIN_COST : BCrypt::Engine.cost
+              self.send("#{attribute}_digest=", BCrypt::Password.create(unencrypted_password, cost: cost))
+            end
           end
-        end
 
-        define_method("#{attribute}_confirmation=") do |unencrypted_password|
-          instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
-        end
+          define_method("#{attribute}_confirmation=") do |unencrypted_password|
+            instance_variable_set("@#{attribute}_confirmation", unencrypted_password)
+          end
+
+          # Returns +self+ if the password is correct, otherwise +false+.
+          #
+          #   class User < ActiveRecord::Base
+          #     has_secure_password validations: false
+          #   end
+          #
+          #   user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
+          #   user.save
+          #   user.authenticate_password('notright')      # => false
+          #   user.authenticate_password('mUc3m00RsqyRe') # => user
+          define_method("authenticate_#{attribute}") do |unencrypted_password|
+            attribute_digest = send("#{attribute}_digest")
+            BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
+          end
 
-        # Returns +self+ if the password is correct, otherwise +false+.
-        #
-        #   class User < ActiveRecord::Base
-        #     has_secure_password validations: false
-        #   end
-        #
-        #   user = User.new(name: 'david', password: 'mUc3m00RsqyRe')
-        #   user.save
-        #   user.authenticate_password('notright')      # => false
-        #   user.authenticate_password('mUc3m00RsqyRe') # => user
-        define_method("authenticate_#{attribute}") do |unencrypted_password|
-          attribute_digest = send("#{attribute}_digest")
-          BCrypt::Password.new(attribute_digest).is_password?(unencrypted_password) && self
+          alias_method :authenticate, :authenticate_password if attribute == :password
         end
 
-        alias_method :authenticate, :authenticate_password if attribute == :password
+        include mod
 
         if validations
           include ActiveModel::Validations
-- 
cgit v1.2.3