Skip to content

Commit

Permalink
Add default option to vault_attribute
Browse files Browse the repository at this point in the history
This adds a `:default` option to `vault_attribute`, allowing a default value to be set if the underlying value is `nil`.

```rb
vault_attribute :metadata,
  serializer: :json,
  default: {}

vault_attribute :access_level,
  default: "readonly"
```

This enables a simple fix for applications affected by the breaking change introduced in #81 for `serialize: :json` empty values.
  • Loading branch information
justincampbell committed Jun 10, 2019
1 parent 35031d4 commit 5c4bc51
Show file tree
Hide file tree
Showing 7 changed files with 113 additions and 4 deletions.
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -144,7 +144,21 @@ vault_attribute :credit_card,

The proc must take a single argument for the record.

#### Specifying a default value

An attribute can specify a default value, which will be set on initialization (`.new`) or after loading the value from the database. The default will be set if the value is `nil`.

```ruby
vault_attribute :access_level,
default: "readonly"
vault_attribute :metadata,
serialize: :json,
default: {}
```

#### Specifying a different Vault path

By default, the path to the transit backend in Vault is `transit/`. This is customizable by setting the `:path` option when declaring the attribute:

```ruby
Expand Down
17 changes: 14 additions & 3 deletions lib/vault/encrypted_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ module ClassMethods
# @option options [String, Symbol, Proc] :context
# either a string context, or a symbol or proc used to generate a
# context for key generation
# @option options [Object] :default
# a default value for this attribute to be set to if the underlying
# value is nil
# @option options [Symbol, Class] :serializer
# the name of the serializer to use (or a class)
# @option options [Proc] :encode
Expand All @@ -43,6 +46,7 @@ def vault_attribute(attribute, options = {})
path = options[:path] || "transit"
key = options[:key] || "#{Vault::Rails.application}_#{table_name}_#{attribute}"
context = options[:context]
default = options[:default]

# Sanity check options!
_vault_validate_options!(options)
Expand Down Expand Up @@ -111,11 +115,12 @@ def vault_attribute(attribute, options = {})

# Make a note of this attribute so we can use it in the future (maybe).
__vault_attributes[attribute.to_sym] = {
context: context,
default: default,
encrypted_column: encrypted_column,
key: key,
path: path,
serializer: serializer,
encrypted_column: encrypted_column,
context: context,
serializer: serializer
}

self
Expand Down Expand Up @@ -206,6 +211,7 @@ def __vault_load_attribute!(attribute, options)
serializer = options[:serializer]
column = options[:encrypted_column]
context = options[:context]
default = options[:default]

# Load the ciphertext
ciphertext = read_attribute(column)
Expand All @@ -230,6 +236,11 @@ def __vault_load_attribute!(attribute, options)
plaintext = serializer.decode(plaintext)
end

# Set to default if needed
if default && plaintext == nil
plaintext = default
end

# Write the virtual attribute with the plaintext value
instance_variable_set("@#{attribute}", plaintext)
end
Expand Down
10 changes: 10 additions & 0 deletions spec/dummy/app/models/lazy_person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,16 @@ class LazyPerson < ActiveRecord::Base

vault_attribute :non_ascii

vault_attribute :default,
default: "abc123"

vault_attribute :default_with_serializer,
serialize: :json,
default: {}

vault_attribute :context_string,
context: "production"

vault_attribute :context_symbol,
context: :encryption_context

Expand Down
8 changes: 7 additions & 1 deletion spec/dummy/app/models/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,13 @@ class Person < ActiveRecord::Base

vault_attribute :non_ascii

vault_attribute :default,
default: "abc123"

vault_attribute :default_with_serializer,
serialize: :json,
default: {}

vault_attribute :context_string,
context: "production"

Expand All @@ -35,4 +42,3 @@ def encryption_context
"user_#{id}"
end
end

2 changes: 2 additions & 0 deletions spec/dummy/db/migrate/20150428220101_create_people.rb
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,8 @@ def change
t.string :business_card_encrypted
t.string :favorite_color_encrypted
t.string :non_ascii_encrypted
t.string :default_encrypted
t.string :default_with_serializer_encrypted
t.string :context_string_encrypted
t.string :context_symbol_encrypted
t.string :context_proc_encrypted
Expand Down
2 changes: 2 additions & 0 deletions spec/dummy/db/schema.rb
Original file line number Diff line number Diff line change
Expand Up @@ -20,6 +20,8 @@
t.string "business_card_encrypted"
t.string "favorite_color_encrypted"
t.string "non_ascii_encrypted"
t.string "default_encrypted"
t.string "default_with_serializer_encrypted"
t.string "context_string_encrypted"
t.string "context_symbol_encrypted"
t.string "context_proc_encrypted"
Expand Down
64 changes: 64 additions & 0 deletions spec/integration/rails_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -294,11 +294,75 @@
end
end

context "with a default" do
%i[new create].each do |creation_method|
context "on #{creation_method}" do
context "without an initial attribute" do
it "sets the default" do
person = Person.public_send(creation_method)
expect(person.default).to eq("abc123")
person.save!
person.reload
expect(person.default).to eq("abc123")
end
end

context "with an initial attribute" do
it "does not set the default" do
person = Person.public_send(creation_method, default: "another")
expect(person.default).to eq("another")
person.save!
person.reload
expect(person.default).to eq("another")
end
end
end
end
end

context "with a default and serializer" do
%i[new create].each do |creation_method|
context "on #{creation_method}" do
context "without an initial attribute" do
it "sets the default" do
person = Person.public_send(creation_method)
expect(person.default_with_serializer).to eq({})
person.save!
person.reload
expect(person.default_with_serializer).to eq({})
end
end

context "with an initial attribute" do
it "does not set the default" do
person = Person.public_send(
creation_method,
default_with_serializer: { "foo" => "bar" }
)

expect(person.default_with_serializer).to eq({ "foo" => "bar" })
person.save!
person.reload
expect(person.default_with_serializer).to eq({ "foo" => "bar" })
end
end
end
end
end

context "with the :json serializer" do
before(:all) do
Vault::Rails.logical.write("transit/keys/dummy_people_details")
end

it "does not default to a hash" do
person = Person.new
expect(person.details).to eq(nil)
person.save!
person.reload
expect(person.details).to eq(nil)
end

it "tracks dirty attributes" do
person = Person.create!(details: { "foo" => "bar" })

Expand Down

0 comments on commit 5c4bc51

Please sign in to comment.