Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix for ActiveRecord dirty attributes for rails 5.2 #76

Closed
Closed
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
4 changes: 4 additions & 0 deletions Appraisals
Original file line number Diff line number Diff line change
Expand Up @@ -9,3 +9,7 @@ end
appraise "rails-5.1" do
gem "rails", "~> 5.1.0"
end

appraise "rails-5.2" do
gem "rails", "~> 5.2.0"
end
85 changes: 53 additions & 32 deletions lib/vault/encrypted_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -41,10 +41,18 @@ module ClassMethods
# a proc to encode the value with
# @option options [Proc] :decode
# a proc to decode the value with
def vault_attribute(attribute, options = {})
encrypted_column = options[:encrypted_column] || "#{attribute}_encrypted"
def vault_attribute(vault_attr, options = {})
# https://github.com/rails/rails/issues/33753#issuecomment-417503496
# You cannot call attribute_will_change! on something not registered
# with the attributes API -- There must be attribute :user_ids if you
# want that to be managed by Active Record.
if defined? ActiveRecord::Type # for rails versions > 4.1
attribute(vault_attr, ActiveRecord::Type::Value.new)
end

encrypted_column = options[:encrypted_column] || "#{vault_attr}_encrypted"
path = options[:path] || "transit"
key = options[:key] || "#{Vault::Rails.application}_#{table_name}_#{attribute}"
key = options[:key] || "#{Vault::Rails.application}_#{table_name}_#{vault_attr}"
context = options[:context]
default = options[:default]

Expand All @@ -68,53 +76,56 @@ def vault_attribute(attribute, options = {})
end

# Getter
define_method("#{attribute}") do
define_method(vault_attr.to_s) do
self.__vault_load_attributes! unless @__vault_loaded
instance_variable_get("@#{attribute}")
instance_variable_get("@#{vault_attr}")
end

# Setter
define_method("#{attribute}=") do |value|
define_method("#{vault_attr}=") do |value|
self.__vault_load_attributes! unless @__vault_loaded

# We always set it as changed without comparing with the current value
# because we allow our held values to be mutated, so we need to assume
# that if you call attr=, you want it send back regardless.
attribute_will_change!(vault_attr.to_s)
instance_variable_set("@#{vault_attr}", value)

attribute_will_change!("#{attribute}")
instance_variable_set("@#{attribute}", value)

# Return the value to be consistent with other AR methods.
value
# Call super method or return value to be consistent with other AR methods.
if defined?(super)
super(value)
else
value
end
end

# Checker
define_method("#{attribute}?") do
define_method("#{vault_attr}?") do
self.__vault_load_attributes! unless @__vault_loaded
instance_variable_get("@#{attribute}").present?
instance_variable_get("@#{vault_attr}").present?
end

# Dirty method
define_method("#{attribute}_change") do
changes["#{attribute}"]
define_method("#{vault_attr}_change") do
changes["#{vault_attr}"]
end

# Dirty method
define_method("#{attribute}_changed?") do
changed.include?("#{attribute}")
define_method("#{vault_attr}_changed?") do
changed.include?("#{vault_attr}")
end

# Dirty method
define_method("#{attribute}_was") do
if changes["#{attribute}"]
changes["#{attribute}"][0]
define_method("#{vault_attr}_was") do
if changes["#{vault_attr}"]
changes["#{vault_attr}"][0]
else
public_send("#{attribute}")
public_send("#{vault_attr}")
end
end

# Make a note of this attribute so we can use it in the future (maybe).
__vault_attributes[attribute.to_sym] = {
__vault_attributes[vault_attr.to_sym] = {
context: context,
default: default,
encrypted_column: encrypted_column,
Expand Down Expand Up @@ -175,6 +186,9 @@ def vault_lazy_decrypt!
# Vault and decrypt any attributes unless vault_lazy_decrypt is set.
after_initialize :__vault_initialize_attributes!

# Before save we keep changed attributes to variable
before_save :__vault_keep_changed_attributes!

# After we save the record, persist all the values to Vault and reload
# them attributes from Vault to ensure we have the proper attributes set.
# The reason we use `after_save` here is because a `before_save` could
Expand Down Expand Up @@ -251,10 +265,13 @@ def __vault_load_attribute!(attribute, options)
def __vault_persist_attributes!
changes = {}

self.class.__vault_attributes.each do |attribute, options|
if c = self.__vault_persist_attribute!(attribute, options)
changes.merge!(c)
end
changed_attributes = instance_variable_get('@vault_changed_attributes')
# Only persist changed attributes to minimize requests - this helps
# minimize the number of requests to Vault.
changed_attributes.each do |attribute|
options = self.class.__vault_attributes[attribute]
persist_attribute = self.__vault_persist_attribute!(attribute, options)
changes.merge!(persist_attribute) if persist_attribute
end

# If there are any changes to the model, update them all at once,
Expand All @@ -276,12 +293,6 @@ def __vault_persist_attribute!(attribute, options)
column = options[:encrypted_column]
context = options[:context]

# Only persist changed attributes to minimize requests - this helps
# minimize the number of requests to Vault.
if !changed.include?("#{attribute}")
return
end

# Get the current value of the plaintext attribute
plaintext = instance_variable_get("@#{attribute}")

Expand Down Expand Up @@ -321,6 +332,16 @@ def __vault_generate_context(context)
end
end

# Keep changed attributes
def __vault_keep_changed_attributes!
instance_variable_set(
'@vault_changed_attributes',
self.class.__vault_attributes.keys & changed.map(&:to_sym)
)

true
end

# Override the reload method to reload the Vault attributes. This will
# ensure that we always have the most recent data from Vault when we
# reload a record from the database.
Expand Down
1 change: 1 addition & 0 deletions spec/dummy/app/models/person.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,3 +42,4 @@ def encryption_context
"user_#{id}"
end
end