lib/lockbox/model.rb in lockbox-1.4.1 vs lib/lockbox/model.rb in lockbox-2.0.0

- old
+ new

@@ -58,11 +58,11 @@ decrypt_method_name = "decrypt_#{encrypted_attribute}" class_eval do # Lockbox uses custom inspect # but this could be useful for other gems - if activerecord && ActiveRecord::VERSION::MAJOR >= 6 + if activerecord # only add virtual attribute # need to use regexp since strings do partial matching # also, need to use += instead of << self.filter_attributes += [/\A#{Regexp.escape(options[:attribute])}\z/] end @@ -112,16 +112,10 @@ # check if ciphertext attribute nil to avoid loading attribute v = send(k).nil? ? "nil" : "[FILTERED]" k = lockbox_encrypted_attributes[k] elsif values.key?(k) v = respond_to?(:attribute_for_inspect) ? attribute_for_inspect(k) : values[k].inspect - - # fix for https://github.com/rails/rails/issues/40725 - # TODO only apply to Active Record 6.0 - if respond_to?(:inspection_filter, true) && v != "nil" - v = inspection_filter.filter_param(k, v) - end else next end inspection << "#{k}: #{v}" @@ -146,13 +140,46 @@ rescue ArgumentError => e raise e if e.message != "No decryption key set" end end end - super + + # remove attributes that do not have a ciphertext attribute + attributes = super + self.class.lockbox_attributes.each do |k, lockbox_attribute| + if !attributes.include?(lockbox_attribute[:encrypted_attribute].to_s) + attributes.delete(k.to_s) + attributes.delete(lockbox_attribute[:attribute]) + end + end + attributes end + # remove attribute names that do not have a ciphertext attribute + def attribute_names + # hash preserves key order + names_set = super.to_h { |v| [v, true] } + self.class.lockbox_attributes.each do |k, lockbox_attribute| + if !names_set.include?(lockbox_attribute[:encrypted_attribute].to_s) + names_set.delete(k.to_s) + names_set.delete(lockbox_attribute[:attribute]) + end + end + names_set.keys + end + + # check the ciphertext attribute for encrypted attributes + def has_attribute?(attr_name) + attr_name = attr_name.to_s + _, lockbox_attribute = self.class.lockbox_attributes.find { |_, la| la[:attribute] == attr_name } + if lockbox_attribute + super(lockbox_attribute[:encrypted_attribute]) + else + super + end + end + # needed for in-place modifications # assigned attributes are encrypted on assignment # and then again here def lockbox_sync_attributes self.class.lockbox_attributes.each do |_, lockbox_attribute| @@ -230,75 +257,73 @@ end result end - if ActiveRecord::VERSION::MAJOR >= 6 - if ActiveRecord::VERSION::STRING.to_f >= 7.2 - def self.insert(attributes, **options) - super(lockbox_map_record_attributes(attributes), **options) - end - - def self.insert!(attributes, **options) - super(lockbox_map_record_attributes(attributes), **options) - end - - def self.upsert(attributes, **options) - super(lockbox_map_record_attributes(attributes, check_readonly: true), **options) - end + if ActiveRecord::VERSION::STRING.to_f >= 7.2 + def self.insert(attributes, **options) + super(lockbox_map_record_attributes(attributes), **options) end - def self.insert_all(attributes, **options) - super(lockbox_map_attributes(attributes), **options) + def self.insert!(attributes, **options) + super(lockbox_map_record_attributes(attributes), **options) end - def self.insert_all!(attributes, **options) - super(lockbox_map_attributes(attributes), **options) + def self.upsert(attributes, **options) + super(lockbox_map_record_attributes(attributes, check_readonly: true), **options) end + end - def self.upsert_all(attributes, **options) - super(lockbox_map_attributes(attributes, check_readonly: true), **options) - end + def self.insert_all(attributes, **options) + super(lockbox_map_attributes(attributes), **options) + end - # private - # does not try to handle :returning option for simplicity - def self.lockbox_map_attributes(records, check_readonly: false) - return records unless records.is_a?(Array) + def self.insert_all!(attributes, **options) + super(lockbox_map_attributes(attributes), **options) + end - records.map do |attributes| - lockbox_map_record_attributes(attributes, check_readonly: false) - end - end + def self.upsert_all(attributes, **options) + super(lockbox_map_attributes(attributes, check_readonly: true), **options) + end - # private - def self.lockbox_map_record_attributes(attributes, check_readonly: false) - return attributes unless attributes.is_a?(Hash) + # private + # does not try to handle :returning option for simplicity + def self.lockbox_map_attributes(records, check_readonly: false) + return records unless records.is_a?(Array) - # transform keys like Active Record - attributes = attributes.transform_keys do |key| - n = key.to_s - attribute_aliases[n] || n - end + records.map do |attributes| + lockbox_map_record_attributes(attributes, check_readonly: false) + end + end - lockbox_attributes = self.lockbox_attributes.slice(*attributes.keys.map(&:to_sym)) - lockbox_attributes.each do |key, lockbox_attribute| - attribute = key.to_s - # check read only - # users should mark both plaintext and ciphertext columns - if check_readonly && readonly_attributes.include?(attribute) && !readonly_attributes.include?(lockbox_attribute[:encrypted_attribute].to_s) - warn "[lockbox] WARNING: Mark attribute as readonly: #{lockbox_attribute[:encrypted_attribute]}" - end + # private + def self.lockbox_map_record_attributes(attributes, check_readonly: false) + return attributes unless attributes.is_a?(Hash) - message = attributes[attribute] - attributes.delete(attribute) unless lockbox_attribute[:migrating] - encrypted_attribute = lockbox_attribute[:encrypted_attribute] - ciphertext = send("generate_#{encrypted_attribute}", message) - attributes[encrypted_attribute] = ciphertext + # transform keys like Active Record + attributes = attributes.transform_keys do |key| + n = key.to_s + attribute_aliases[n] || n + end + + lockbox_attributes = self.lockbox_attributes.slice(*attributes.keys.map(&:to_sym)) + lockbox_attributes.each do |key, lockbox_attribute| + attribute = key.to_s + # check read only + # users should mark both plaintext and ciphertext columns + if check_readonly && readonly_attributes.include?(attribute) && !readonly_attributes.include?(lockbox_attribute[:encrypted_attribute].to_s) + warn "[lockbox] WARNING: Mark attribute as readonly: #{lockbox_attribute[:encrypted_attribute]}" end - attributes + message = attributes[attribute] + attributes.delete(attribute) unless lockbox_attribute[:migrating] + encrypted_attribute = lockbox_attribute[:encrypted_attribute] + ciphertext = send("generate_#{encrypted_attribute}", message) + attributes[encrypted_attribute] = ciphertext end + + attributes end else def reload self.class.lockbox_attributes.each do |_, v| instance_variable_set("@#{v[:attribute]}", nil) @@ -325,17 +350,12 @@ warn "[lockbox] WARNING: attributes with `:default` option are not supported. Use `after_initialize` instead." end elsif attributes_to_define_after_schema_loads.key?(name.to_s) opt = attributes_to_define_after_schema_loads[name.to_s][1] - has_default = - if ActiveRecord::VERSION::MAJOR >= 7 - # not ideal, since NO_DEFAULT_PROVIDED is private - opt != ActiveRecord::Attributes::ClassMethods.const_get(:NO_DEFAULT_PROVIDED) - else - opt.is_a?(Hash) && opt.key?(:default) - end + # not ideal, since NO_DEFAULT_PROVIDED is private + has_default = opt != ActiveRecord::Attributes::ClassMethods.const_get(:NO_DEFAULT_PROVIDED) if has_default warn "[lockbox] WARNING: attributes with `:default` option are not supported. Use `after_initialize` instead." end end @@ -406,25 +426,18 @@ # so we can use a generic value here attribute name, ActiveRecord::Type::Value.new else attribute name, :string end - else + elsif attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc) # hack for Active Record 6.1+ to set string type after serialize # otherwise, type gets set to ActiveModel::Type::Value # which always returns false for changed_in_place? # earlier versions of Active Record take the previous code path - if ActiveRecord::VERSION::STRING.to_f >= 7.0 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc) - attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call(nil) - if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil? - attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder) - end - elsif ActiveRecord::VERSION::STRING.to_f >= 6.1 && attributes_to_define_after_schema_loads[name.to_s].first.is_a?(Proc) - attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call - if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil? - attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder) - end + attribute_type = attributes_to_define_after_schema_loads[name.to_s].first.call(nil) + if attribute_type.is_a?(ActiveRecord::Type::Serialized) && attribute_type.subtype.nil? + attribute name, ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, attribute_type.coder) end end define_method("#{name}_was") do send(name) # writes attribute when not already set @@ -579,19 +592,17 @@ unless message.nil? case options[:type] when :boolean message = ActiveRecord::Type::Boolean.new.serialize(message) - message = nil if message == "" # for Active Record < 5.2 message = message ? "t" : "f" unless message.nil? when :date message = ActiveRecord::Type::Date.new.serialize(message) # strftime should be more stable than to_s(:db) message = message.strftime("%Y-%m-%d") unless message.nil? when :datetime message = ActiveRecord::Type::DateTime.new.serialize(message) - message = nil unless message.respond_to?(:iso8601) # for Active Record < 5.2 message = message.iso8601(9) unless message.nil? when :time message = ActiveRecord::Type::Time.new.serialize(message) message = nil unless message.respond_to?(:strftime) message = message.strftime("%H:%M:%S.%N") unless message.nil? @@ -604,18 +615,11 @@ when :float message = ActiveRecord::Type::Float.new.serialize(message) # double precision, big endian message = [message].pack("G") unless message.nil? when :decimal - message = - if ActiveRecord::VERSION::MAJOR >= 6 - ActiveRecord::Type::Decimal.new.serialize(message) - else - # issue with serialize in Active Record < 6 - # https://github.com/rails/rails/commit/a741208f80dd33420a56486bd9ed2b0b9862234a - ActiveRecord::Type::Decimal.new.cast(message) - end + message = ActiveRecord::Type::Decimal.new.serialize(message) # Postgres stores 4 decimal digits in 2 bytes # plus 3 to 8 bytes of overhead # but use string for simplicity message = message.to_s("F") unless message.nil? when :inet @@ -712,15 +716,9 @@ end prepend m end end end - end - - def lockbox_encrypts(*attributes, **options) - deprecator = ActiveSupport::VERSION::STRING.to_f >= 7.2 ? ActiveSupport.deprecator : ActiveSupport::Deprecation - deprecator.warn("`#{__callee__}` is deprecated in favor of `has_encrypted`") - has_encrypted(*attributes, **options) end module Attached def encrypts_attached(*attributes, **options) attributes.each do |name|