Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions .github/workflows/run-tests.yml
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,13 @@ jobs:
fail-fast: false
matrix:
# https://endoflife.date/ruby
ruby: ["2.7", "3.0", "3.1", "3.2"]
vault: ["1.11.9", "1.12.5", "1.13.1"]
ruby: ["2.7", "3.0", "3.1", "3.3"]
vault: ["1.11.9", "1.19.5", "1.20.4", "1.21.0"]
runs-on: ubuntu-latest
timeout-minutes: 5
steps:
- uses: actions/checkout@3df4ab11eba7bda6032a0b82a6bb43b11571feac # v4.0.0
- uses: ruby/setup-ruby@250fcd6a742febb1123a77a841497ccaa8b9e939 # v1.152.0
- uses: ruby/setup-ruby@ab177d40ee5483edb974554986f56b33477e21d0 # v1.265.0
with:
ruby-version: ${{ matrix.ruby }}
bundler-cache: true # runs 'bundle install' and caches installed gems automatically
Expand Down
6 changes: 6 additions & 0 deletions Gemfile
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,12 @@ source "https://rubygems.org"
RAILS_VERSION = ENV.fetch("RAILS_VERSION", "6.0.0")

gem "rails", "~> #{RAILS_VERSION}"

# Rails versions before 7.1 have a dependency on concurrent-ruby but
# we need to pin to 1.3.4 because later versions removed a dependency on Logger
# that we need to start tests.
gem "concurrent-ruby", "1.3.4"
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

What's this for?

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

All rails versions before 7.1 have a direct dependency on concurrent-ruby (We use an older version for testing), but concurrent-ruby versions 1.3.5+ removed the dependency on Logger. When using Rails 7.0- and concurrent-ruby 1.3.5+, you get uninitialized constant ActiveSupport::LoggerThreadSafeLevel::Logger (NameError) when starting tests-- so I just pinned the version to 1.3.4 instead of upgrading the dummy rails installation.

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Added a clarifying comment to the Gemfile. This is not a gem dependency, just a developer one

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nice, got it!


if RAILS_VERSION.start_with?("6")
gem "sqlite3", "~> 1.4"
end
Expand Down
16 changes: 15 additions & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -332,6 +332,21 @@ So for the example above, the key would be:

my_app_people_ssn

### Encrypting without Saving
Normally, vault-rails will wait until the after_save callback to encrypt changed
values before updating them. If you'd like to encrypt changed attributes without
saving, call `vault_encrypt_attributes!`

```ruby
p = Person.new(ssn: "123-45-6789")
p.ssn_encrypted
=> nil
p.vault_encrypt_attributes!
p.ssn_encrypted
=> "vault:dev:flu/yp9oeYYFgjcZH2hVBA=="
p.persisted?
=> false
```

### Searching Encrypted Attributes
Because each column is uniquely encrypted, it is not possible to search for a
Expand All @@ -345,7 +360,6 @@ Person.where(ssn: "123-45-6789")
This is because the database is unaware of the plain-text data (which is part of
the security model).


Development
-----------
↥ [back to top](#table-of-contents)
Expand Down
35 changes: 27 additions & 8 deletions lib/vault/encrypted_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -325,12 +325,7 @@ def __vault_persist_attributes!
# Encrypt a single attribute using Vault and persist back onto the
# encrypted attribute value.
def __vault_persist_attribute!(attribute, options)
key = options[:key]
path = options[:path]
serializer = options[:serializer]
column = options[:encrypted_column]
context = options[:context]
transform = options[:transform_secret]
column = options[:encrypted_column]

# Only persist changed attributes to minimize requests - this helps
# minimize the number of requests to Vault.
Expand All @@ -346,6 +341,19 @@ def __vault_persist_attribute!(attribute, options)

# Get the current value of the plaintext attribute
plaintext = attributes[attribute.to_s]
ciphertext = __vault_write_encrypted_attribute!(plaintext, options)

# Return the updated column so we can save
{ column => ciphertext }
end

def __vault_write_encrypted_attribute!(plaintext, options)
column = options[:encrypted_column]
key = options[:key]
path = options[:path]
serializer = options[:serializer]
context = options[:context]
transform = options[:transform_secret]

# Apply the serialize to the plaintext value, if one exists
if serializer
Expand All @@ -372,8 +380,7 @@ def __vault_persist_attribute!(attribute, options)
# to get the ciphertext
write_attribute(column, ciphertext)

# Return the updated column so we can save
{ column => ciphertext }
ciphertext
end

# Generates an Vault Transit encryption context for use on derived keys.
Expand Down Expand Up @@ -405,6 +412,18 @@ def reload(*)
self.__vault_initialize_attributes!
end
end

def vault_encrypt_attributes!
self.class.__vault_attributes.each do |attribute, options|
next if !attribute_changed?(attribute) && options[:default].nil?

# Get the current value of the plaintext attribute
plaintext = attributes[attribute.to_s]

__vault_write_encrypted_attribute!(plaintext, options)
end
self
end
end
end
end
37 changes: 37 additions & 0 deletions spec/integration/rails_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -746,4 +746,41 @@
expect(person.credit_card).to eq("1234567890111213")
end
end

context "manual encryption" do
describe "#vault_encrypt_attributes!" do
it "encrypts vault attributes without saving" do
person = Person.new(ssn: "123-45-6789", favorite_color: "green")
expect {
person.vault_encrypt_attributes!
}.to change {
person.ssn_encrypted
}.from(nil).to(be_a(String))

expect(person.favorite_color).to eq("green")
expect(person.favorite_color_encrypted).to be_present
end

it "returns self" do
person = Person.new
result = person.vault_encrypt_attributes!
expect(result).to be(person)
end

it "encrypts attributes with a default option" do
person = Person.new
expect(person.default).to eq("abc123")
expect(person.default_with_serializer).to eq({})

expect {
person.vault_encrypt_attributes!
}.to change {
person.default_encrypted
}.from(nil).to(be_a(String))
.and change {
person.default_with_serializer_encrypted
}.from(nil).to(be_a(String))
end
end
end
end
Loading