简体   繁体   中英

Hiding an attribute in an ActiveRecord model

I am somewhat new to Rails and working on designing a User model using ActiveRecord. In this model, I have a password attribute which is intended to keep a hash of the user's password.

I want to remove both reading and setting of this attribute directly. However, I can't seem to find a way to remove the accessors when using the Rails console. The only viable solution so far has been to explicitly override the accessor methods for password and I don't really want to override them, I want the accessors gone - or at least the reader.

Here is my model:

class User < ActiveRecord::Base

  // various associations

  def password_correct?(password)
    read_attribute(:password) == hash(password)
  end

  def password=(password)
    write_attribute(:password, hash(password))
  end

  def password
    "get your dirty fingers off this attribute"
  end

  private

  def hash(input)
    Digest::SHA2.new(512).update(input).hexdigest
  end

end

Any ideas how to achieve this or any shortcomings to this approach?

Based on the above answers, I have done some experimentation to obtain the desired result. I ended up making a "private" password_hash attribute and a virtual accessor called password.

I made some observations in this process:

  • It seems that ActiveRecord doesn't have any concept of private attributes. Making the accessor methods private using symbols such as private :password, :password= is not an option, because Rails throws an NameError: undefined method when instantiating a model, as the model itself does not have these two methods defined (they seem to be inherited from ActiveRecord::Base ).

  • Overriding the password_hash accessors with pure nothing is great for preventing the manipulation of the attribute, however it also means that ActiveRecord itself fails when updating the password_hash attribute as it is calling an empty implementation.

So making the accessors private fails because they're undefined in the actual model. Defining them also fails, because it breaks ActiveRecord. So what can you do?

I did both and a bit more. I made the accessors private, defined them and implemented both by calling super . This prevents the controller (and the rails console) from accessing them by throwing a NoMethodError , but doesn't reject ActiveRecord.

A side note: Validation issues

One problem I encountered with my approach was broken validation. Enforcing a minimum length on the password_hash was no good as any password (even nothing) results in a 128 character SHA512 hash. So validating the hash made little sense. Instead I added validation to the virtual password accessor and added a before_save :hash_password callback which checks to see if the virtual accessor attribute has been set and if so, hashes it and writes it to the password_hash attribute.

Final implementation

My implementation ended up this way:

class User < ActiveRecord::Base
  attr_accessible :first_name, :last_name, :email
  attr_accessor :password
  validates :password, :length => { :minimum => 8 }, :if => :password_changed?
  validates :first_name, :last_name, :email, presence: true
  # Various associations
  before_save :hash_password

  def password_correct?(p)
    if(password.present?)
      password == p
    else
      read_attribute(:password_hash) == hash_string(p)
    end
  end

  def role_symbols
    roles.collect do |r|
      r.name.to_sym
    end
  end

  private

  def hash_string(input)
    Digest::SHA2.new(512).update(input).hexdigest
  end

  def hash_password
    if(password.present?)
      write_attribute(:password_hash, hash_string(password))
      self.password = nil
    end
  end

  def password_changed?
    password.present? or new_record?
  end

  def password_hash
    super
  end

  def password_hash=(p)
    super
  end

end

You can easily make the accessor private with the private method, add the line below in your model :

private :password, :password=

Also I would suggest you to not override the password field accessor. Maybe you can name the field password_hash in the database and make the accessor of this column private. Then you write your methods password , password= as expected.

If it does not fit your needs, could you please explain me why ? And why what you want will be better solution ?

I stumbled upon gems like devise while looking for an authentication solution for an app, finally took this simpler approach exemplified in http://bcrypt-ruby.rubyforge.org/ .

require 'bcrypt'

class Account < ActiveRecord::Base
  attr_accessor :password, :password_confirmation, :role

  def password
    @password ||= BCrypt::Password.new(crypted_password)
  end

  def password=(new_password)
    @password = BCrypt::Password.create(new_password)
    self.crypted_password = @password
  end
end

EDIT: Demo, as being implemented in a Padrino app with ActivecRecord and Postgresql

[6] pry(main)> a = Account.new
=> #<Account id: nil, name: nil, email: nil, role: nil, uid: nil, provider: nil, created_at: nil, updated_at: nil, crypted_password: nil>
[7] pry(main)> a.password = "asdf"
=> "asdf"
[8] pry(main)> a.password
=> "$2a$10$9udQKttf5zFqCv7da9ZY0uMsWYlbeGK3apEkIY6x05KND1v3vOkh2"
[9] pry(main)> a.password == "asdf"
=> true

Let me tell a short story on why I had to go through similar path as you. I once had to "unsubscribe" lots of my model's attributes/columns.

The thing is that I wanted to reach the column values through read_attribute method. I was on a quest where these columns should be removed from DB some weeks later, so I wanted to make a graceful migration of the data in these "deprecated" columns first (and port them into new tables) but I still wanted to use the same old attribute accessor methods throughout the app, but they would use the new DB structure.

So instead of simple overwriting method by method (reader and writter) I simple wrote method_missing to catch all the old columns (through a Regex) and redirect their actions into a new logic. But I could only accomplish this if I could "undefine" the old accessor ("dirty") methods, otherwise they would never reach method_missing method.

So after a lot of struggling I got into the following solution (adapting to your case):

class Account < ActiveRecord::Base

  # ...

  unless attribute_methods_generated?
    define_attribute_methods
    undef_method "password"
    undef_method "password="
  end

  # ...

end

I just needed to remove the reader and writter methods, but you can remove all the other "dirty" methods if you want.

The attribute_methods_generated? class method returns true if the dirty methods were already generated. Then it forces to generate the dirty methods and only then it removes the desired methods. If you simple try to use undef_method method directly in the class scope it'll throw an exception telling you the methods you're trying to remove don't exist (yet). That's why you need to make it that way above.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM