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|