Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
7 changes: 6 additions & 1 deletion app/models/solid_cache/entry.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,7 +42,12 @@ def read_multi(keys)
query = Arel.sql(select_sql(keys_batch), *key_hashes_for(keys_batch))

with_connection do |connection|
results.merge!(connection.select_all(query, "SolidCache::Entry Load").cast_values(attribute_types).to_h)
key_type, value_type = attribute_types.values_at("key", "value")
connection.select_all(query, "SolidCache::Entry Load").rows.each do |key, value|
results[key_type.deserialize(key)] = value_type.deserialize(value)
rescue ActiveRecord::Encryption::Errors::Decryption => error
ActiveSupport.error_reporter&.report(error, handled: true, severity: :warning)
end
end
end
end
Expand Down
79 changes: 79 additions & 0 deletions test/unit/encryption_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -38,7 +38,86 @@ class SolidCache::EncryptionTest < ActiveSupport::TestCase
assert_not_nil message["h"]
end

[ :rotated_key, :plaintext ].each do |scenario|
test "read treats unreadable entries as misses (#{scenario})" do
skip unless SolidCache.configuration.encrypt?

@cache.write(@shard_key, "value")
make_unreadable(@shard_key, scenario)

report = assert_error_reported(ActiveRecord::Encryption::Errors::Decryption) do
assert_nil @cache.read(@shard_key)
end
assert_predicate report, :handled?
assert_equal :warning, report.severity
end

test "read multi preserves readable entries (#{scenario})" do
skip unless SolidCache.configuration.encrypt?

other_key = shard_keys(@cache, first_shard_key).second
@cache.write(@shard_key, "unreadable")
@cache.write(other_key, "readable")
make_unreadable(@shard_key, scenario)

assert_error_reported(ActiveRecord::Encryption::Errors::Decryption) do
assert_equal({ other_key => "readable" }, @cache.read_multi(@shard_key, other_key))
end
end

test "fetch replaces unreadable entries (#{scenario})" do
skip unless SolidCache.configuration.encrypt?

@cache.write(@shard_key, "value")
make_unreadable(@shard_key, scenario)

assert_error_reported(ActiveRecord::Encryption::Errors::Decryption) do
assert_equal "replacement", @cache.fetch(@shard_key) { "replacement" }
end
assert_no_error_reported { assert_equal "replacement", @cache.read(@shard_key) }
end

test "expiry rebuilds unreadable size estimates and expires old entries (#{scenario})" do
skip unless SolidCache.configuration.encrypt?

estimates_key = SolidCache::Entry::Size::MovingAverageEstimate::ESTIMATES_KEY
SolidCache::Entry.write(@shard_key, "value")
SolidCache::Entry.write(estimates_key, "1000000000")
make_unreadable(@shard_key, scenario)
make_unreadable(estimates_key, scenario)
SolidCache::Entry.where(key: @shard_key).update_all(created_at: 3.weeks.ago)
expected_size = SolidCache::Entry.sum(:byte_size)

assert_error_reported(ActiveRecord::Encryption::Errors::Decryption) do
SolidCache::Entry.expire(10, max_age: 2.weeks, max_entries: nil, max_size: 1.megabyte)
end
assert_not SolidCache::Entry.exists?(key: @shard_key)
assert_no_error_reported do
assert_equal expected_size.to_s, SolidCache::Entry.read(estimates_key)
end
end
end

private
def make_unreadable(key, scenario)
value = SolidCache::Entry.find_by!(key: key).value
if scenario == :rotated_key
key_provider = ActiveRecord::Encryption::EnvelopeEncryptionKeyProvider.new
key_provider.stubs(:active_primary_key).returns(
ActiveRecord::Encryption::DerivedSecretKeyProvider.new("old cache key").encryption_key
)
context = SolidCache.configuration.encryption_context_properties.merge(
key_provider: key_provider
)
value = ActiveRecord::Encryption.with_encryption_context(context) do
ActiveRecord::Encryption.encryptor.encrypt(value)
end
end

quoted_value = SolidCache::Entry.connection.quote(ActiveRecord::Type::Binary::Data.new(value))
SolidCache::Entry.where(key: key).update_all(value: Arel.sql(quoted_value))
end

def raw_first_value
raw = SolidCache::Entry.connection.select_all("select * from solid_cache_entries order by id desc limit 1").first["value"]

Expand Down
Loading