lib/lockbox/model.rb in lockbox-1.3.3 vs lib/lockbox/model.rb in lockbox-1.4.0
- old
+ new
@@ -135,17 +135,20 @@
def attributes
# load attributes
# essentially a no-op if already loaded
# an exception is thrown if decryption fails
self.class.lockbox_attributes.each do |_, lockbox_attribute|
- # don't try to decrypt if no decryption key given
- next if lockbox_attribute[:algorithm] == "hybrid" && lockbox_attribute[:decryption_key].nil?
-
# it is possible that the encrypted attribute is not loaded, eg.
# if the record was fetched partially (`User.select(:id).first`).
# accessing a not loaded attribute raises an `ActiveModel::MissingAttributeError`.
- send(lockbox_attribute[:attribute]) if has_attribute?(lockbox_attribute[:encrypted_attribute])
+ if has_attribute?(lockbox_attribute[:encrypted_attribute])
+ begin
+ send(lockbox_attribute[:attribute])
+ rescue ArgumentError => e
+ raise e if e.message != "No decryption key set"
+ end
+ end
end
super
end
# needed for in-place modifications
@@ -228,10 +231,24 @@
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
+ end
+
def self.insert_all(attributes, **options)
super(lockbox_map_attributes(attributes), **options)
end
def self.insert_all!(attributes, **options)
@@ -246,34 +263,41 @@
# 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)
records.map do |attributes|
- # transform keys like Active Record
- attributes = attributes.transform_keys do |key|
- n = key.to_s
- attribute_aliases[n] || n
- end
+ 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
end
else
def reload
self.class.lockbox_attributes.each do |_, v|
@@ -293,11 +317,16 @@
if stored_attributes.any? { |k, v| v.include?(name) }
warn "[lockbox] WARNING: encrypting store accessors is not supported. Encrypt the column instead."
end
# warn on default attributes
- if attributes_to_define_after_schema_loads.key?(name.to_s)
+ if ActiveRecord::VERSION::STRING.to_f >= 7.2
+ # TODO improve
+ if pending_attribute_modifications.any? { |v| v.is_a?(ActiveModel::AttributeRegistration::ClassMethods::PendingDefault) && v.name == name.to_s }
+ 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
@@ -345,10 +374,30 @@
serialize name, Hash
when :array
serialize name, Array
end
end
+ elsif ActiveRecord::VERSION::STRING.to_f >= 7.2
+ decorate_attributes([name]) do |attr_name, cast_type|
+ if cast_type.instance_of?(ActiveRecord::Type::Value)
+ original_type = pending_attribute_modifications.find { |v| v.is_a?(ActiveModel::AttributeRegistration::ClassMethods::PendingType) && v.name == original_name.to_s && !v.type.nil? }&.type
+ if original_type
+ original_type
+ elsif options[:migrating]
+ cast_type
+ else
+ ActiveRecord::Type::String.new
+ end
+ elsif cast_type.is_a?(ActiveRecord::Type::Serialized) && cast_type.subtype.instance_of?(ActiveModel::Type::Value)
+ # hack to set string type after serialize
+ # otherwise, type gets set to ActiveModel::Type::Value
+ # which always returns false for changed_in_place?
+ ActiveRecord::Type::Serialized.new(ActiveRecord::Type::String.new, cast_type.coder)
+ else
+ cast_type
+ end
+ end
elsif !attributes_to_define_after_schema_loads.key?(name.to_s)
# when migrating it's best to specify the type directly
# however, we can try to use the original type if its already defined
if attributes_to_define_after_schema_loads.key?(original_name.to_s)
attribute name, attributes_to_define_after_schema_loads[original_name.to_s].first
@@ -358,12 +407,11 @@
attribute name, ActiveRecord::Type::Value.new
else
attribute name, :string
end
else
- # hack for Active Record 6.1
- # to set string type after serialize
+ # 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)
@@ -445,16 +493,16 @@
define_method("#{name}=") do |message|
# decrypt first for dirty tracking
# don't raise error if can't decrypt previous
# don't try to decrypt if no decryption key given
- unless options[:algorithm] == "hybrid" && options[:decryption_key].nil?
- begin
- send(name)
- rescue Lockbox::DecryptionError
- warn "[lockbox] Decrypting previous value failed"
- end
+ begin
+ send(name)
+ rescue Lockbox::DecryptionError
+ warn "[lockbox] Decrypting previous value failed"
+ rescue ArgumentError => e
+ raise e if e.message != "No decryption key set"
end
send("lockbox_direct_#{name}=", message)
# warn every time, as this should be addressed
@@ -667,10 +715,11 @@
end
end
end
def lockbox_encrypts(*attributes, **options)
- ActiveSupport::Deprecation.warn("`#{__callee__}` is deprecated in favor of `has_encrypted`")
+ 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)