Skip to content

Conversation

@brandonc
Copy link

@brandonc brandonc commented Oct 24, 2025

Description

I'd like vault-rails to set the *_encrypted value attribute without calling save. This way, it can be used with gems like activerecord-import that create many records at once.

Implementation Notes

Splits __vault_persist_attribute!, which is normally called on each attribute after_save, into two methods. The new method, __vault_write_encrypted_attribute!, is also called on each attribute when vault_encrypt_attributes! is called on the model

p = Person.new(ssn: "123-45-6789").vault_encrypt_attributes!
p.ssn_encrypted
"vault:dev:flu/yp9oeYYFgjcZH2hVBA=="

@chrisarcand
Copy link
Member

chrisarcand commented Oct 28, 2025

I'm a little uneasy at changing where in the lifecycle the write happens - at least without an explicit opt-in for the change in behavior. Ideally, the writes can happen after save even in an import scenario.

I'd suggest looking into one of these paths:

  1. Import, but with callbacks. Either a custom solution here in this gem to allow for it, or: Have you seen this? 👉 https://github.com/instacart/activerecord-import_with_callbacks/blob/main/lib/active_record/import_with_callbacks.rb

Sure, it may be a little slower than your current solution, but I'd trade a little bit of speed for more correctness (allowing for bulk inserts, but callbacks are still called after the database transaction).

  1. Encrypt on set, but explicitly opt-in.

Flavor A: Add a flag to opt-in to this new behavior:

class Person < ActiveRecord::Base
  include Vault::EncryptedModel
  vault_attribute :ssn, encrypt_on_set: true  # opt-in for activerecord-import
end

Flavor B: Add an explicit encrypt method to this API

# Then, at the caller, you can explicitly write the encrypted values ahead of the import cycle:
people = 1000.times.map { |i| 
  Person.new(ssn: "123-45-#{i}").tap(&:vault_encrypt_attributes!)
}
Person.import(people)

# This is essentially just publicizing your new `__vault_write_encrypted_attribute!` interface

Any of these paths might yield leaving the default behavior as-is while still allowing for bulk import scenarios.

@brandonc brandonc force-pushed the brandonc/encrypt_on_init branch from 6599b9d to 0ef2caf Compare October 30, 2025 13:02
@brandonc brandonc changed the title Encrypt attribute values on model init / attribute set Encrypt attribute values manually without needing save callback Oct 30, 2025
@brandonc brandonc force-pushed the brandonc/encrypt_on_init branch from 0ef2caf to 934d21e Compare October 30, 2025 13:15
Splits __vault_persist_attribute!, which is normally called on each attribute after_save, into two methods. The new method, __vault_write_encrypted_attribute!, is also called on each vault attribute when `vault_encrypt_attributes!` is called.

> p = Person.new(ssn: "123-45-6789").vault_encrypt_attributes!
> p.ssn_encrypted
"vault:dev:flu/yp9oeYYFgjcZH2hVBA=="
@brandonc brandonc force-pushed the brandonc/encrypt_on_init branch from dd47dae to c06c8ae Compare October 30, 2025 13:26
@brandonc
Copy link
Author

brandonc commented Oct 30, 2025

@chrisarcand Thanks for the ideas to leave the existing behavior intact. I was hesitant to expand the public interface at first but I think it is the right move to add an explicit encrypt method the the model.

I felt the config options like encrypt_on_set weren't suitable because the import use case shouldn't be dependent on the model definition.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants