3

I have a before_save callback in my model which encrypts 2 fields before they're saved to the database.

class Account < ActiveRecord::Base
  before_save :encrypt_credentials, if: "!username.blank? && !password.blank?"

  def encrypt_credentials
    crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
    self.username = crypt.encrypt_and_sign(username)
    self.password = crypt.encrypt_and_sign(password)
  end

  def decrypted_username
    crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
    crypt.decrypt_and_verify(username)
  end

  def decrypted_password
    crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
    crypt.decrypt_and_verify(password)
  end
end

The situation is very similar to Devise models run before_save multiple times?. When I call Model.create!(...) - which includes the 2 fields that need to be encrypted, the before_save gets called twice, ending up in the fields being encrypted twice.

Account.create!(
{
  username: ENV['USERNAME'],
  password: ENV['PASSWORD']
})

Why is before_save called multiple times? I don't like the solution of the post linked above and I don't want to do new/build followed by save.

2 Answers 2

1

It was user error :( After calling account = Account.create!, I had other code which called save! back on the model: account.foo = bar; account.save!. This obviously called befor_save again and re-encrypted my fields. I ended up with something like this:

class Account < ActiveRecord::Base
  before_save :encrypt_username, if: :username_changed?
  before_save :encrypt_password, if: :password_changed?

  def encrypt_username
    crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
    self.username = crypt.encrypt_and_sign(username)
  end

  def encrypt_password
    crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
    self.password = crypt.encrypt_and_sign(password)
  end

  def decrypted_username
    crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
    crypt.decrypt_and_verify(username)
  end

  def decrypted_password
    crypt = ActiveSupport::MessageEncryptor.new(ENV['KEY'])
    crypt.decrypt_and_verify(password)
  end
end
Sign up to request clarification or add additional context in comments.

Comments

1

Option 1 (could be a mistake in usage of callbacks):

Short answer: use after_save instead of before_save

Long answer: How to organize complex callbacks in Rails?

When you use the:

account = Account.new

account.save

You are firing the before_save hook each time.

Option 2 (could be a bug):

Maybe you're actually touching the record several times.

For example in:

def create
  @account = Customer.find(params[:customer_id]).accounts.create(account_params)

  if @account.save
    redirect_to customer_account_path(@account.customer.id, @account.id)
  else
    render :new
  end
end

You are in fact touching it with create and save. In which case I suggest:

def create
  @account = Customer.find(params[:customer_id]).accounts.build(account_params)

  if @account.save
    redirect_to customer_account_path(@account.customer.id, @account.id)
  else
    render :new
  end
end

Build doesn't try to save the record so you shouldn't have any more problems. Hope this helps! Have a great day!

Comments

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service and acknowledge you have read our privacy policy.

Start asking to get answers

Find the answer to your question by asking.

Ask question

Explore related questions

See similar questions with these tags.