From a61692cf4152e4dcc6850207a55b6d8bab4280dd Mon Sep 17 00:00:00 2001 From: Jorge Manrubia Date: Mon, 22 Mar 2021 18:13:59 +0100 Subject: [PATCH] Add support for uniqueness validations --- activerecord/lib/active_record/encryption.rb | 1 + ...nded_deterministic_uniqueness_validator.rb | 29 +++++++++++++ activerecord/lib/active_record/railtie.rb | 3 +- .../encryption/uniqueness_validations_test.rb | 43 +++++++++++++++++++ activerecord/test/cases/helper.rb | 1 + activerecord/test/models/author_encrypted.rb | 1 + activerecord/test/models/book_encrypted.rb | 1 + 7 files changed, 78 insertions(+), 1 deletion(-) create mode 100644 activerecord/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb create mode 100644 activerecord/test/cases/encryption/uniqueness_validations_test.rb diff --git a/activerecord/lib/active_record/encryption.rb b/activerecord/lib/active_record/encryption.rb index 66e243ba9e..a8404918fa 100644 --- a/activerecord/lib/active_record/encryption.rb +++ b/activerecord/lib/active_record/encryption.rb @@ -23,6 +23,7 @@ module Encryption autoload :EnvelopeEncryptionKeyProvider autoload :Errors autoload :ExtendedDeterministicQueries + autoload :ExtendedDeterministicUniquenessValidator autoload :Key autoload :KeyGenerator autoload :KeyProvider diff --git a/activerecord/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb b/activerecord/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb new file mode 100644 index 0000000000..ec832e0f57 --- /dev/null +++ b/activerecord/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true + +module ActiveRecord + module Encryption + module ExtendedDeterministicUniquenessValidator + def self.install_support + ActiveRecord::Validations::UniquenessValidator.prepend(EncryptedUniquenessValidator) + end + + module EncryptedUniquenessValidator + def validate_each(record, attribute, value) + super(record, attribute, value) + + klass = record.class + if klass.deterministic_encrypted_attributes&.each do |attribute_name| + encrypted_type = klass.type_for_attribute(attribute_name) + [ encrypted_type, *encrypted_type.previous_encrypted_types ].each do |type| + encrypted_value = type.serialize(value) + ActiveRecord::Encryption.without_encryption do + super(record, attribute, encrypted_value) + end + end + end + end + end + end + end + end +end diff --git a/activerecord/lib/active_record/railtie.rb b/activerecord/lib/active_record/railtie.rb index d305326317..23ce343805 100644 --- a/activerecord/lib/active_record/railtie.rb +++ b/activerecord/lib/active_record/railtie.rb @@ -291,9 +291,10 @@ class Railtie < Rails::Railtie # :nodoc: ActiveRecord::Fixture.prepend ActiveRecord::Encryption::EncryptedFixtures end - # Support extended queries for deterministic attributes + # Support extended queries for deterministic attributes and validations if ActiveRecord::Encryption.config.extend_queries ActiveRecord::Encryption::ExtendedDeterministicQueries.install_support + ActiveRecord::Encryption::ExtendedDeterministicUniquenessValidator.install_support end end diff --git a/activerecord/test/cases/encryption/uniqueness_validations_test.rb b/activerecord/test/cases/encryption/uniqueness_validations_test.rb new file mode 100644 index 0000000000..d3015df1af --- /dev/null +++ b/activerecord/test/cases/encryption/uniqueness_validations_test.rb @@ -0,0 +1,43 @@ +# frozen_string_literal: true + +require "cases/encryption/helper" +require "models/book_encrypted" +require "models/author_encrypted" + +class ActiveRecord::Encryption::UniquenessValidationsTest < ActiveRecord::EncryptionTestCase + fixtures :books + + test "uniqueness validations work" do + EncryptedBookWithDowncaseName.create!(name: "dune") + assert_raises ActiveRecord::RecordInvalid do + EncryptedBookWithDowncaseName.create!(name: "dune") + end + end + + test "uniqueness validations work when mixing encrypted an unencrypted data" do + ActiveRecord::Encryption.config.support_unencrypted_data = true + + ActiveRecord::Encryption.without_encryption { EncryptedBookWithDowncaseName.create! name: "dune" } + + assert_raises ActiveRecord::RecordInvalid do + EncryptedBookWithDowncaseName.create!(name: "dune") + end + end + + test "uniqueness validations work when using old encryption schemes" do + ActiveRecord::Encryption.config.previous = [ { downcase: true } ] + + OldEncryptionBook = Class.new(Book) do + self.table_name = "books" + + validates :name, uniqueness: true + encrypts :name, deterministic: true, downcase: false + end + + OldEncryptionBook.create! name: "dune" + + assert_raises ActiveRecord::RecordInvalid do + OldEncryptionBook.create! name: "DUNE" + end + end +end diff --git a/activerecord/test/cases/helper.rb b/activerecord/test/cases/helper.rb index 448889c4a0..4a02625fda 100644 --- a/activerecord/test/cases/helper.rb +++ b/activerecord/test/cases/helper.rb @@ -231,3 +231,4 @@ def in_time_zone(zone) key_derivation_salt: "testing key derivation salt" ActiveRecord::Encryption::ExtendedDeterministicQueries.install_support +ActiveRecord::Encryption::ExtendedDeterministicUniquenessValidator.install_support diff --git a/activerecord/test/models/author_encrypted.rb b/activerecord/test/models/author_encrypted.rb index a5396e3ddc..d158850495 100644 --- a/activerecord/test/models/author_encrypted.rb +++ b/activerecord/test/models/author_encrypted.rb @@ -5,6 +5,7 @@ class EncryptedAuthor < Author self.table_name = "authors" + validates :name, uniqueness: true encrypts :name, previous: { deterministic: true } end diff --git a/activerecord/test/models/book_encrypted.rb b/activerecord/test/models/book_encrypted.rb index 67bceef3e7..b5cb837e51 100644 --- a/activerecord/test/models/book_encrypted.rb +++ b/activerecord/test/models/book_encrypted.rb @@ -11,6 +11,7 @@ class EncryptedBook < ActiveRecord::Base class EncryptedBookWithDowncaseName < ActiveRecord::Base self.table_name = "books" + validates :name, uniqueness: true encrypts :name, deterministic: true, downcase: true end