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.

Community
  • 1
  • 1
Jenna S
  • 616
  • 1
  • 6
  • 24

2 Answers2

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
Jenna S
  • 616
  • 1
  • 6
  • 24
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!

Jose Paez
  • 747
  • 1
  • 11
  • 18