From 707e6f146738bd4da4c6039466bfef38fde7e296 Mon Sep 17 00:00:00 2001 From: Dominic Cleal Date: Tue, 20 Feb 2018 10:42:12 +0000 Subject: [PATCH] Fix exception in attr reader without selecting encrypted column When using AR's .select to retrieve a subset of columns for a model, calling the encrypted attribute reader results in an exception: ActiveModel::MissingAttributeError: missing attribute: encrypted_street gems/activerecord-5.1.5/lib/active_record/attribute_methods/read.rb:71:in `block in _read_attribute' gems/activerecord-5.1.5/lib/active_record/attribute_set.rb:45:in `block in fetch_value' gems/activerecord-5.1.5/lib/active_record/attribute.rb:219:in `value' gems/activerecord-5.1.5/lib/active_record/attribute_set.rb:45:in `fetch_value' gems/activerecord-5.1.5/lib/active_record/attribute_methods/read.rb:71:in `_read_attribute' gems/activerecord-5.1.5/lib/active_record/attribute_methods/read.rb:36:in `__temp__56e636279707475646f5374727565647' lib/attr_encrypted.rb:161:in `block (2 levels) in attr_encrypted' The virtual attribute `email` is defined in the test, but the reader tries to access the encrypted column which wasn't selected. This is a problem when rendering a model `#to_json` with the default serialiser, as it tries to read all defined columns. Allow the reader to return nil in the case when the encrypted attribute column isn't available. --- lib/attr_encrypted.rb | 16 +++++++++++++--- lib/attr_encrypted/adapters/active_record.rb | 14 ++++++++++++++ test/active_record_test.rb | 5 +++++ 3 files changed, 32 insertions(+), 3 deletions(-) diff --git a/lib/attr_encrypted.rb b/lib/attr_encrypted.rb index 3895b721..f743000a 100644 --- a/lib/attr_encrypted.rb +++ b/lib/attr_encrypted.rb @@ -158,12 +158,11 @@ def attr_encrypted(*attributes) end define_method(attribute) do - instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name))) + read_encrypted_attribute(attribute) end define_method("#{attribute}=") do |value| - send("#{encrypted_attribute_name}=", encrypt(attribute, value)) - instance_variable_set("@#{attribute}", value) + write_encrypted_attribute(attribute, value) end define_method("#{attribute}?") do @@ -395,6 +394,17 @@ def evaluate_attr_encrypted_option(option) end end + def read_encrypted_attribute(attribute) + encrypted_attribute_name = encrypted_attributes[attribute.to_sym][:attribute] + instance_variable_get("@#{attribute}") || instance_variable_set("@#{attribute}", decrypt(attribute, send(encrypted_attribute_name))) + end + + def write_encrypted_attribute(attribute, value) + encrypted_attribute_name = encrypted_attributes[attribute.to_sym][:attribute] + send("#{encrypted_attribute_name}=", encrypt(attribute, value)) + instance_variable_set("@#{attribute}", value) + end + def load_iv_for_attribute(attribute, options) encrypted_attribute_name = options[:attribute] encode_iv = options[:encode_iv] diff --git a/lib/attr_encrypted/adapters/active_record.rb b/lib/attr_encrypted/adapters/active_record.rb index a22108e4..deae2981 100644 --- a/lib/attr_encrypted/adapters/active_record.rb +++ b/lib/attr_encrypted/adapters/active_record.rb @@ -4,6 +4,7 @@ module Adapters module ActiveRecord def self.extended(base) # :nodoc: base.class_eval do + include InstanceMethods # https://github.com/attr-encrypted/attr_encrypted/issues/68 alias_method :reload_without_attr_encrypted, :reload @@ -130,6 +131,19 @@ def method_missing_with_attr_encrypted(method, *args, &block) end method_missing_without_attr_encrypted(method, *args, &block) end + + module InstanceMethods + def read_encrypted_attribute(attribute) + encrypted_attribute_name = encrypted_attributes[attribute.to_sym][:attribute].to_s + + # If the class does have the encrypted attribute, but this record doesn't (partial SELECT), + # return early as fetching the encrypted attribute will fail + return if self.class.column_names.include?(encrypted_attribute_name) && + !attributes.include?(encrypted_attribute_name) + + super + end + end end end end diff --git a/test/active_record_test.rb b/test/active_record_test.rb index 89134548..df059cac 100644 --- a/test/active_record_test.rb +++ b/test/active_record_test.rb @@ -335,4 +335,9 @@ def test_should_evaluate_proc_based_mode refute_equal address.encrypted_zipcode, zipcode assert_equal address.zipcode, zipcode end + + def test_should_read_attribute_without_encrypted_column_present + address = Address.create!(street: '123 Elm') + assert_nil Address.where(id: address.id).select(:id).first.street + end end