From 5c4bc51498d68f2c8d5ca62c80964db256cd510b Mon Sep 17 00:00:00 2001 From: Justin Campbell Date: Mon, 10 Jun 2019 15:20:45 -0400 Subject: [PATCH] Add default option to vault_attribute 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. --- README.md | 14 ++++ lib/vault/encrypted_model.rb | 17 ++++- spec/dummy/app/models/lazy_person.rb | 10 +++ spec/dummy/app/models/person.rb | 8 ++- .../migrate/20150428220101_create_people.rb | 2 + spec/dummy/db/schema.rb | 2 + spec/integration/rails_spec.rb | 64 +++++++++++++++++++ 7 files changed, 113 insertions(+), 4 deletions(-) diff --git a/README.md b/README.md index 6ee80fa8..af6e43c6 100644 --- a/README.md +++ b/README.md @@ -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 diff --git a/lib/vault/encrypted_model.rb b/lib/vault/encrypted_model.rb index f57cc548..01c553a0 100644 --- a/lib/vault/encrypted_model.rb +++ b/lib/vault/encrypted_model.rb @@ -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 @@ -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) @@ -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 @@ -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) @@ -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 diff --git a/spec/dummy/app/models/lazy_person.rb b/spec/dummy/app/models/lazy_person.rb index d853dfb6..e543ad33 100644 --- a/spec/dummy/app/models/lazy_person.rb +++ b/spec/dummy/app/models/lazy_person.rb @@ -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 diff --git a/spec/dummy/app/models/person.rb b/spec/dummy/app/models/person.rb index 460f22a3..fc6514a7 100644 --- a/spec/dummy/app/models/person.rb +++ b/spec/dummy/app/models/person.rb @@ -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" @@ -35,4 +42,3 @@ def encryption_context "user_#{id}" end end - diff --git a/spec/dummy/db/migrate/20150428220101_create_people.rb b/spec/dummy/db/migrate/20150428220101_create_people.rb index 7b13e820..344c262f 100644 --- a/spec/dummy/db/migrate/20150428220101_create_people.rb +++ b/spec/dummy/db/migrate/20150428220101_create_people.rb @@ -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 diff --git a/spec/dummy/db/schema.rb b/spec/dummy/db/schema.rb index 98635e28..769df4b2 100644 --- a/spec/dummy/db/schema.rb +++ b/spec/dummy/db/schema.rb @@ -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" diff --git a/spec/integration/rails_spec.rb b/spec/integration/rails_spec.rb index d500f1db..54344995 100644 --- a/spec/integration/rails_spec.rb +++ b/spec/integration/rails_spec.rb @@ -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" })