Skip to content

Commit

Permalink
Nest encrypted attribute types within serialized types
Browse files Browse the repository at this point in the history
Make the order in which `serializes :foo` and `encrypts :foo` are called
irrelevant by always nesting the encrypted attribute type inside the
serialized type.

This required switching from `DelegateClass` to `SimpleDelegator` so
that object we are delegating to can be replaced.

To ensure that the serialized type survives YAML serialization (there's
a test for this), we need to implement init_with to call __setobj__.
  • Loading branch information
djmb committed Aug 20, 2024
1 parent 1120184 commit ac4c171
Show file tree
Hide file tree
Showing 5 changed files with 36 additions and 10 deletions.
10 changes: 9 additions & 1 deletion activerecord/lib/active_record/encryption/encryptable_record.rb
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,15 @@ def encrypt_attribute(name, key_provider: nil, key: nil, deterministic: false, s
scheme = scheme_for key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, \
downcase: downcase, ignore_case: ignore_case, previous: previous, compress: compress, compressor: compressor, **context_properties

ActiveRecord::Encryption::EncryptedAttributeType.new(scheme: scheme, cast_type: cast_type, default: columns_hash[name.to_s]&.default)
if cast_type.serialized?
cast_type.tap do |serialized_type|
serialized_type.replace_serialized_subtype do |current_subtype|
ActiveRecord::Encryption::EncryptedAttributeType.new(scheme: scheme, cast_type: current_subtype, default: columns_hash[name.to_s]&.default)
end
end
else
ActiveRecord::Encryption::EncryptedAttributeType.new(scheme: scheme, cast_type: cast_type, default: columns_hash[name.to_s]&.default)
end
end

preserve_original_encrypted(name) if ignore_case
Expand Down
16 changes: 14 additions & 2 deletions activerecord/lib/active_record/type/serialized.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,7 @@

module ActiveRecord
module Type
class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
class Serialized < SimpleDelegator # :nodoc:
undef to_yaml if method_defined?(:to_yaml)

include ActiveModel::Type::Helpers::Mutable
Expand All @@ -12,7 +12,14 @@ class Serialized < DelegateClass(ActiveModel::Type::Value) # :nodoc:
def initialize(subtype, coder)
@subtype = subtype
@coder = coder
super(subtype)
__setobj__(subtype)
end

def init_with(coder) # :nodoc:
# Ensures YAML deserialization calls __setobj__
@subtype = coder["subtype"]
@coder = coder["coder"]
__setobj__(subtype)
end

def deserialize(value)
Expand Down Expand Up @@ -57,6 +64,11 @@ def serialized? # :nodoc:
true
end

def replace_serialized_subtype(&block) # :nodoc:
@subtype = block.call(subtype)
__setobj__(@subtype)
end

private
def default_value?(value)
value == coder.load(nil)
Expand Down
6 changes: 6 additions & 0 deletions activerecord/test/cases/encryption/encryptable_record_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -92,6 +92,12 @@ class ActiveRecord::Encryption::EncryptableRecordTest < ActiveRecord::Encryption
assert_encrypted_attribute(traffic_light, :state, states)
end

test "encrypts serialized attributes where encrypts is declared first" do
states = ["green", "red"]
traffic_light = EncryptedFirstTrafficLight.create!(state: states, long_state: states)
assert_encrypted_attribute(traffic_light, :state, states)
end

test "encrypts store attributes with accessors" do
traffic_light = EncryptedTrafficLightWithStoreState.create!(color: "red", long_state: ["green", "red"])
assert_equal "red", traffic_light.color
Expand Down
2 changes: 1 addition & 1 deletion activerecord/test/models/book_encrypted.rb
Original file line number Diff line number Diff line change
Expand Up @@ -53,8 +53,8 @@ class EncryptedBookWithBinary < ActiveRecord::Base
class EncryptedBookWithSerializedBinary < ActiveRecord::Base
self.table_name = "encrypted_books"

encrypts :logo
serialize :logo, coder: JSON
encrypts :logo
end

class EncryptedBookWithCustomCompressor < ActiveRecord::Base
Expand Down
12 changes: 6 additions & 6 deletions activerecord/test/models/traffic_light_encrypted.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,19 +3,19 @@

require "models/traffic_light"

class EncryptedTrafficLight < ActiveRecord::Base
self.table_name = "traffic_lights"

class EncryptedTrafficLight < TrafficLight
encrypts :state
serialize :state, type: Array
serialize :long_state, type: Array
end

class EncryptedTrafficLightWithStoreState < ActiveRecord::Base
class EncryptedFirstTrafficLight < ActiveRecord::Base
self.table_name = "traffic_lights"

encrypts :state
serialize :state, type: Array
serialize :long_state, type: Array
end

class EncryptedTrafficLightWithStoreState < TrafficLight
store :state, accessors: %i[ color ], coder: ActiveRecord::Coders::JSON
encrypts :state
end

0 comments on commit ac4c171

Please sign in to comment.