Devise models run before_save multiple times? - ruby-on-rails

My client wants all user data encrypted, so I've created a before_save and after_find call back that will encrypt certain properties using Gibberish:
# user.rb
before_save UserEncryptor.new
after_find UserEncryptor.new
# user_encryptor.rb
class UserEncryptor
def initialize
#cipher = Gibberish::AES.new("password")
end
def before_save(user)
user.first_name = encrypt(user.first_name)
user.last_name = encrypt(user.last_name)
user.email = encrypt(user.email) unless not user.confirmed? or user.unconfirmed_email
end
def after_find(user)
user.first_name = decrypt(user.first_name)
user.last_name = decrypt(user.last_name)
user.email = decrypt(user.email) unless not user.confirmed? or user.unconfirmed_email
end
private
def encrypt(value)
#cipher.enc(value)
end
def decrypt(value)
#cipher.dec(value)
end
end
Well, when the user first signs up using Devise, the model looks about like it should. But then once the user confirms, if I inspect the user, the first_name and last_name properties look to have been encrypted multiple times. So I put a breakpoint in the before_save method and click the confirmation link, and I see that it's getting executed three times in a row. The result is that the encrypted value gets encrypted again, and then again, so next time we retrieve the record, and every time thereafter, we get a twice encrypted value.
Now, why the heck is this happening? It's not occurring for other non-devise models that are executing the same logic. Does Devise have the current_user cached in a few different places, and it saves the user in each location? How else could a before_save callback be called 3 times before the next before_find is executed?
And, more importantly, how can I successfully encrypt my user data when I'm using Devise? I've also had problems with attr_encrypted and devise_aes_encryptable so if I get a lot of those suggestions then I guess I have some more questions to post :-)

I solved my problem with the help of a coworker.
For encrypting the first and last name, it was sufficient to add a flag to the model indicating whether or not it's been encrypted. That way, if multiple saves occur, the model knows it's already encrypted and can skip that step:
def before_update(user)
unless user.encrypted
user.first_name = encrypt(user.first_name)
user.last_name = encrypt(user.last_name)
user.encrypted = true
end
end
def after_find(user)
if user.encrypted
user.first_name = decrypt(user.first_name)
user.last_name = decrypt(user.last_name)
user.encrypted = false
end
end
For the email address, this was not sufficient. Devise was doing some really weird stuff with resetting cached values, so the email address was still getting double encrypted. So instead of hooking into the callbacks to encrypt the email address, we overrode some methods on the user model:
def email_before_type_cast
super.present? ? AES.decrypt(super, KEY) : ""
end
def email
return "" unless self[:email]
#email ||= AES.decrypt(self[:email], KEY)
end
def email=(provided_email)
self[:email] = encrypted_email(provided_email)
#email = provided_email
end
def self.find_for_authentication(conditions={})
conditions[:email] = encrypted_email(conditions[:email])
super
end
def self.find_or_initialize_with_errors(required_attributes, attributes, error=:invalid)
attributes[:email] = encrypted_email(attributes[:email]) if attributes[:email]
super
end
def self.encrypted_email decrypted_email
AES.encrypt(decrypted_email, KEY, {:iv => IV})
end
This got us most of the way there. However, my Devise models are reconfirmable, so when I changed a user's email address and tried to save, the reconfirmable module encountered something funky, the record got saved like a hundred times or so, and then I got a stack overflow and a rollback. We found that we needed to override one more method on the user model to do the trick:
def email_was
super.present? ? AES.decrypt(super, KEY) : ""
end
Now all of our personally identifiable information is encrypted! Yay!

Related

Validation for few not saved objects

I have form, that creates some objects in batch. For my flow i have to either save them all at once, or not save at all. Problem is - when I do my validations, they are not failing, since each object, validates according to current records in db(i have uniqueness validation), but i also need to validate current object to each of my unsaved objects. Small example
class User
#field: email
end
In my form object i have an array of users. And in loop i do
#users.each do |user|
valid_users << user if user.valid? #and this is where i need to validate `user` within not just DB, but within #users aswell
end
How do i achieve this?
You first need to check if all the unsaved object pass the validation test or not, if yes, to do that you can do this instead for the unique field email
if #users.map { |user| user.email.downcase }.uniq.length == #users.size
#users.each do |user|
valid_users << user if user.valid?
end
else
# Error: Emails of the users must be unique
end
Hope this helps!
You can wrap them in a transaction which will roll back the entire batch if one fails.
begin
ActiveRecord::Base.transaction do
#users.map(&:save!)
end
rescue ActiveRecord::RecordInvalid
end
Why not just check the user.valid? that validates the DB records and then manually check the #users and only save if it's not a duplicate.
#users.each do |user|
#the following would check for identical objects in #users, but that may not be what you want
valid_users << user if user.valid? and #users.count(user)<2
#this would check only the required field on the array
valid_users << user if user.valid? and #users.map(&:email).count(user.email)<2
end
One way to do this, can be using all?:
if #users.all?{|u| u.valid? } # true, only when all users are validated
# save all users
else
# redirect/render with error message.
end

Prevent fetching password attribute value for an instance of User object

I have this in my :show action of users_controller.
def show
#user = User.find(params[:id])
end
But there are some columns in the users table that I wouldn't want accessed from the #user instance variable.
There is the encrypted_password column and the salt column. What can i do on the model or the controller to ensure that #user has no password or salt values.
I want when I do #user.password or #user.salt, it returns nil or something that can't compromise a user's security.
Limiting your Ruby code from fetching some data from the DB hardly enhances security - chances are a hacker will get to your DB not through your ruby code, but by hacking straight to you database...
If all that is saved in the database is an encrypted password and the salt (both of which you need to authenticate the user) - you should be fine, having both is not enough to know the user's password (at least not easily, assuming the encryption is strong enough)
If you want to be extra careful, you can save the salt in a separate repository than the encrypted password repository. This way a hacker will have to break into both repositories to even start brute forcing your users' passwords.
Don't store the password in the database in unencrypted format. Ever.
Hiding an attribute in an ActiveRecord model provides some basic information on how you could hide various attributes in the model class.
The best approach would be to serve a facade to your controller from some intermediate object, and the facace would basically only expose those fields that the controler needed to manipulate.
You can add an after_find callback in the model...
class User << ActiveRecord::Base
after_find :nil_secure_fields
def nil_secure_fields
password = nil
salt = nil
end
end
If the record is updated you'd want to ensure the password and salt attributes aren't included in the attributes to update.
For User model add
class << self
def find_secure(id)
user = self.find(id)
user.secure_attributes!
user
end
end
def secure_attributes!
#attributes_secure = true
end
def secure_attributes?
!!#attributes_secure
end
def encrypted_password
secure_attributes? ? nil : super
end
def salt
secure_attributes? ? nil : super
end
Now you can use find_secure method instead of find which will restrict access to secure attributes.
PS This is not obviate the need to store passwords encrypted.

Rails getting access to current user

I'm trying to save in Note which Employee was the last editor of a `Note'
In a View, I'm able to access the current employee like this:
<h4><%= current_user.employee.id%></h4>
But, in a Model, I can't use current_user.employee.id.
So, I'm trying this in the User model:
def self.current
Thread.current[:user]
end
def self.current=(user)
Thread.current[:user] = user
end
And this in the Note model:
before_create :record_update
before_update :record_update
protected
def record_update
self.lasteditor_id = User.current.employee.id unless User.current.employee.id.nil?
end
What I'm getting is the last User in the Users table.
Thanks for the help!
current_user gets the logged in user information from the session. You cannot access session variables from model. If you want to update the Note model with the Last employee who viewed it, do it in your controller(most likely show action of your note or any other action you think would be right)
def show
#note = Note.find(params[:id])
#note.update_atribute(:last_viewed_by, current_user.id)
end
You code might look different from above. But this is the idea

Updating password with BCrypt

When I login with a username and password by BCrypt checks no problem, everything is fine.
But when I go through the process of recovering password and try to login with the new password the BCrypt never returns true.
The code I have is as follows:
before_save :encrypt_password
before_update :encrypt_password
def authenticate
player = Player.find_by(mail: self.mail)
unless player.nil?
current_password = BCrypt::Password.new(player.password)
if current_password == self.password
player
else
nil
end
end
end
private
def encrypt_password
unless self.password.nil?
self.password = BCrypt::Password.create(self.password)
end
I'm using rails 4
You don't need the before_update callback.
When creating a new record (user in this case), only before_save is triggered. So you get the right behavior.
But when updating a record, both before_update and before_save are triggered, which means your password column is encrypted twice. That's why you get unexpected behavior.
Check this page for more information about callbacks.
What's more, I think it's a bad idea to make password a real column in database. All you need is a column called encrypted_password in database and making password a virtual attribute.
So you can write encrypt_password method like this:
def encrypt_password
unless self.password.nil?
self.encrypt_password = BCrypt::Password.create(self.password)
end
Which gave you no chance to make a mistake like you just made.

After Created in Ruby on Rails

In controller my code is
query = "insert into users (name, email,updated_at) values (#{name},#{email},now()) ON DUPLICATE KEY UPDATE updated_at=now()"
User.connection.execute(query)
and in model
after_create :change_updated_at
def change_updated_at
if !self.email.blank?
chk_user = User.find_by_id(self.email)
if !chk_user.blank? && chk_user.updated_at.blank?
chk_user.updated_at =Time.now
chk_user.save!
end
end
end
but it's not working please help
Your query should be replaced by something like this. This will provide the same functionality as using ON DUPLICATE KEY UPDATE
user = User.where("name = ? and email = ?", name, email)
if user.nil?
user = User.new
end
user.name = name
user.email = email
user.save
Since you want this function to run even if it is an update. You want to use the after_save callback http://apidock.com/rails/ActiveRecord/Callbacks/after_save
The after_create is called only on creation of brand new objects.
http://apidock.com/rails/ActiveRecord/Callbacks/after_create
List of all callbacks provided by active record are here
http://apidock.com/rails/ActiveRecord/Callbacks
Don't run your query directly, that's VERY un-rails like. Let rails generate the query for you.
By running it yourself, rails has no way of knowing that you've created the record, so the after_create filter isn't be called.
Change your code to something like :
User.create(name: name, email: email)
Then it'll run. Also, don't update the 'create_at' field yourself. If you use rails methods, it'll do this automatically for you as well :)
You need to get with params object input attributes.
So first of all you need new method, and then create method (if you use standart form helper for model):
def create
#user=User.new(params[:user])
#other code
end

Resources