activerecord 6.1.7.10 → 7.0.0.alpha1
Sign up to get free protection for your applications and to get access to all the features.
Potentially problematic release.
This version of activerecord might be problematic. Click here for more details.
- checksums.yaml +4 -4
- data/CHANGELOG.md +726 -1404
- data/MIT-LICENSE +1 -1
- data/README.rdoc +1 -1
- data/lib/active_record/aggregations.rb +1 -1
- data/lib/active_record/association_relation.rb +0 -10
- data/lib/active_record/associations/association.rb +31 -9
- data/lib/active_record/associations/association_scope.rb +1 -3
- data/lib/active_record/associations/belongs_to_association.rb +15 -4
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +10 -2
- data/lib/active_record/associations/builder/association.rb +8 -2
- data/lib/active_record/associations/builder/belongs_to.rb +19 -6
- data/lib/active_record/associations/builder/collection_association.rb +1 -1
- data/lib/active_record/associations/builder/has_many.rb +3 -2
- data/lib/active_record/associations/builder/has_one.rb +2 -1
- data/lib/active_record/associations/builder/singular_association.rb +2 -2
- data/lib/active_record/associations/collection_association.rb +14 -23
- data/lib/active_record/associations/collection_proxy.rb +8 -3
- data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
- data/lib/active_record/associations/has_many_association.rb +1 -1
- data/lib/active_record/associations/has_many_through_association.rb +2 -1
- data/lib/active_record/associations/has_one_association.rb +10 -7
- data/lib/active_record/associations/has_one_through_association.rb +1 -1
- data/lib/active_record/associations/preloader/association.rb +161 -47
- data/lib/active_record/associations/preloader/batch.rb +51 -0
- data/lib/active_record/associations/preloader/branch.rb +147 -0
- data/lib/active_record/associations/preloader/through_association.rb +37 -11
- data/lib/active_record/associations/preloader.rb +46 -110
- data/lib/active_record/associations/singular_association.rb +8 -2
- data/lib/active_record/associations/through_association.rb +1 -1
- data/lib/active_record/associations.rb +76 -81
- data/lib/active_record/asynchronous_queries_tracker.rb +57 -0
- data/lib/active_record/attribute_assignment.rb +1 -1
- data/lib/active_record/attribute_methods/before_type_cast.rb +7 -2
- data/lib/active_record/attribute_methods/dirty.rb +41 -16
- data/lib/active_record/attribute_methods/primary_key.rb +2 -2
- data/lib/active_record/attribute_methods/query.rb +2 -2
- data/lib/active_record/attribute_methods/read.rb +7 -5
- data/lib/active_record/attribute_methods/serialization.rb +66 -12
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -3
- data/lib/active_record/attribute_methods/write.rb +7 -10
- data/lib/active_record/attribute_methods.rb +6 -9
- data/lib/active_record/attributes.rb +24 -35
- data/lib/active_record/autosave_association.rb +3 -18
- data/lib/active_record/base.rb +19 -1
- data/lib/active_record/callbacks.rb +2 -2
- data/lib/active_record/coders/yaml_column.rb +2 -14
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +312 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +209 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +76 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +31 -558
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +45 -21
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -12
- data/lib/active_record/connection_adapters/abstract/quoting.rb +12 -14
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -17
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +30 -13
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +60 -16
- data/lib/active_record/connection_adapters/abstract/transaction.rb +3 -3
- data/lib/active_record/connection_adapters/abstract_adapter.rb +112 -66
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +96 -81
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +33 -23
- data/lib/active_record/connection_adapters/mysql/quoting.rb +16 -1
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +1 -1
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -6
- data/lib/active_record/connection_adapters/pool_config.rb +1 -3
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +19 -14
- data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
- data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
- data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
- data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
- data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +28 -0
- data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
- data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
- data/lib/active_record/connection_adapters/postgresql/quoting.rb +6 -32
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +32 -0
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +5 -1
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +12 -12
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +159 -102
- data/lib/active_record/connection_adapters/schema_cache.rb +36 -37
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +23 -19
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -2
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -30
- data/lib/active_record/connection_adapters.rb +6 -5
- data/lib/active_record/connection_handling.rb +20 -38
- data/lib/active_record/core.rb +111 -125
- data/lib/active_record/database_configurations/connection_url_resolver.rb +0 -1
- data/lib/active_record/database_configurations/database_config.rb +12 -0
- data/lib/active_record/database_configurations/hash_config.rb +27 -1
- data/lib/active_record/database_configurations/url_config.rb +2 -2
- data/lib/active_record/database_configurations.rb +17 -9
- data/lib/active_record/delegated_type.rb +33 -11
- data/lib/active_record/destroy_association_async_job.rb +1 -1
- data/lib/active_record/disable_joins_association_relation.rb +39 -0
- data/lib/active_record/dynamic_matchers.rb +1 -1
- data/lib/active_record/encryption/cipher/aes256_gcm.rb +98 -0
- data/lib/active_record/encryption/cipher.rb +53 -0
- data/lib/active_record/encryption/config.rb +44 -0
- data/lib/active_record/encryption/configurable.rb +61 -0
- data/lib/active_record/encryption/context.rb +35 -0
- data/lib/active_record/encryption/contexts.rb +72 -0
- data/lib/active_record/encryption/derived_secret_key_provider.rb +12 -0
- data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
- data/lib/active_record/encryption/encryptable_record.rb +208 -0
- data/lib/active_record/encryption/encrypted_attribute_type.rb +140 -0
- data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
- data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
- data/lib/active_record/encryption/encryptor.rb +155 -0
- data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
- data/lib/active_record/encryption/errors.rb +15 -0
- data/lib/active_record/encryption/extended_deterministic_queries.rb +160 -0
- data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +29 -0
- data/lib/active_record/encryption/key.rb +28 -0
- data/lib/active_record/encryption/key_generator.rb +42 -0
- data/lib/active_record/encryption/key_provider.rb +46 -0
- data/lib/active_record/encryption/message.rb +33 -0
- data/lib/active_record/encryption/message_serializer.rb +80 -0
- data/lib/active_record/encryption/null_encryptor.rb +21 -0
- data/lib/active_record/encryption/properties.rb +76 -0
- data/lib/active_record/encryption/read_only_null_encryptor.rb +24 -0
- data/lib/active_record/encryption/scheme.rb +99 -0
- data/lib/active_record/encryption.rb +55 -0
- data/lib/active_record/enum.rb +41 -41
- data/lib/active_record/errors.rb +66 -3
- data/lib/active_record/fixture_set/file.rb +15 -1
- data/lib/active_record/fixture_set/table_row.rb +40 -5
- data/lib/active_record/fixture_set/table_rows.rb +4 -4
- data/lib/active_record/fixtures.rb +16 -11
- data/lib/active_record/future_result.rb +139 -0
- data/lib/active_record/gem_version.rb +4 -4
- data/lib/active_record/inheritance.rb +55 -17
- data/lib/active_record/insert_all.rb +34 -5
- data/lib/active_record/integration.rb +1 -1
- data/lib/active_record/internal_metadata.rb +1 -5
- data/lib/active_record/locking/optimistic.rb +10 -9
- data/lib/active_record/log_subscriber.rb +6 -2
- data/lib/active_record/middleware/database_selector/resolver.rb +6 -10
- data/lib/active_record/middleware/database_selector.rb +8 -3
- data/lib/active_record/migration/command_recorder.rb +4 -4
- data/lib/active_record/migration/compatibility.rb +89 -10
- data/lib/active_record/migration/join_table.rb +1 -1
- data/lib/active_record/migration.rb +109 -79
- data/lib/active_record/model_schema.rb +45 -31
- data/lib/active_record/nested_attributes.rb +3 -3
- data/lib/active_record/no_touching.rb +2 -2
- data/lib/active_record/null_relation.rb +2 -6
- data/lib/active_record/persistence.rb +134 -45
- data/lib/active_record/query_cache.rb +2 -2
- data/lib/active_record/query_logs.rb +203 -0
- data/lib/active_record/querying.rb +15 -5
- data/lib/active_record/railtie.rb +117 -17
- data/lib/active_record/railties/controller_runtime.rb +1 -1
- data/lib/active_record/railties/databases.rake +72 -48
- data/lib/active_record/readonly_attributes.rb +11 -0
- data/lib/active_record/reflection.rb +45 -44
- data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
- data/lib/active_record/relation/batches.rb +3 -3
- data/lib/active_record/relation/calculations.rb +39 -26
- data/lib/active_record/relation/delegation.rb +6 -6
- data/lib/active_record/relation/finder_methods.rb +31 -22
- data/lib/active_record/relation/merger.rb +20 -13
- data/lib/active_record/relation/predicate_builder.rb +1 -6
- data/lib/active_record/relation/query_attribute.rb +5 -11
- data/lib/active_record/relation/query_methods.rb +230 -49
- data/lib/active_record/relation/record_fetch_warning.rb +2 -2
- data/lib/active_record/relation/spawn_methods.rb +2 -2
- data/lib/active_record/relation/where_clause.rb +8 -4
- data/lib/active_record/relation.rb +166 -77
- data/lib/active_record/result.rb +17 -2
- data/lib/active_record/runtime_registry.rb +2 -4
- data/lib/active_record/sanitization.rb +11 -7
- data/lib/active_record/schema_dumper.rb +3 -3
- data/lib/active_record/schema_migration.rb +0 -4
- data/lib/active_record/scoping/default.rb +61 -12
- data/lib/active_record/scoping/named.rb +3 -11
- data/lib/active_record/scoping.rb +40 -22
- data/lib/active_record/serialization.rb +1 -1
- data/lib/active_record/signed_id.rb +1 -1
- data/lib/active_record/store.rb +1 -6
- data/lib/active_record/tasks/database_tasks.rb +106 -22
- data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
- data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -11
- data/lib/active_record/test_databases.rb +1 -1
- data/lib/active_record/test_fixtures.rb +9 -13
- data/lib/active_record/timestamp.rb +3 -4
- data/lib/active_record/transactions.rb +9 -14
- data/lib/active_record/translation.rb +2 -2
- data/lib/active_record/type/adapter_specific_registry.rb +32 -7
- data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
- data/lib/active_record/type/internal/timezone.rb +2 -2
- data/lib/active_record/type/serialized.rb +1 -1
- data/lib/active_record/type/type_map.rb +17 -20
- data/lib/active_record/type.rb +1 -2
- data/lib/active_record/validations/associated.rb +1 -1
- data/lib/active_record.rb +170 -2
- data/lib/arel/attributes/attribute.rb +0 -8
- data/lib/arel/crud.rb +18 -22
- data/lib/arel/delete_manager.rb +2 -4
- data/lib/arel/insert_manager.rb +2 -3
- data/lib/arel/nodes/casted.rb +1 -1
- data/lib/arel/nodes/delete_statement.rb +8 -13
- data/lib/arel/nodes/insert_statement.rb +2 -2
- data/lib/arel/nodes/select_core.rb +2 -2
- data/lib/arel/nodes/select_statement.rb +2 -2
- data/lib/arel/nodes/update_statement.rb +3 -2
- data/lib/arel/predications.rb +1 -1
- data/lib/arel/select_manager.rb +10 -4
- data/lib/arel/table.rb +0 -1
- data/lib/arel/tree_manager.rb +0 -12
- data/lib/arel/update_manager.rb +2 -4
- data/lib/arel/visitors/dot.rb +80 -90
- data/lib/arel/visitors/mysql.rb +6 -1
- data/lib/arel/visitors/postgresql.rb +0 -10
- data/lib/arel/visitors/to_sql.rb +43 -2
- data/lib/arel.rb +1 -1
- data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
- data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
- data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
- data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
- metadata +52 -14
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
class DisableJoinsAssociationRelation < Relation # :nodoc:
|
5
|
+
attr_reader :ids, :key
|
6
|
+
|
7
|
+
def initialize(klass, key, ids)
|
8
|
+
@ids = ids.uniq
|
9
|
+
@key = key
|
10
|
+
super(klass)
|
11
|
+
end
|
12
|
+
|
13
|
+
def limit(value)
|
14
|
+
records.take(value)
|
15
|
+
end
|
16
|
+
|
17
|
+
def first(limit = nil)
|
18
|
+
if limit
|
19
|
+
records.limit(limit).first
|
20
|
+
else
|
21
|
+
records.first
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
def load
|
26
|
+
super
|
27
|
+
records = @records
|
28
|
+
|
29
|
+
records_by_id = records.group_by do |record|
|
30
|
+
record[key]
|
31
|
+
end
|
32
|
+
|
33
|
+
records = ids.flat_map { |id| records_by_id[id.to_i] }
|
34
|
+
records.compact!
|
35
|
+
|
36
|
+
@records = records
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,98 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "openssl"
|
4
|
+
require "base64"
|
5
|
+
|
6
|
+
module ActiveRecord
|
7
|
+
module Encryption
|
8
|
+
class Cipher
|
9
|
+
# A 256-GCM cipher.
|
10
|
+
#
|
11
|
+
# By default it will use random initialization vectors. For deterministic encryption, it will use a SHA-256 hash of
|
12
|
+
# the text to encrypt and the secret.
|
13
|
+
#
|
14
|
+
# See +Encryptor+
|
15
|
+
class Aes256Gcm
|
16
|
+
CIPHER_TYPE = "aes-256-gcm"
|
17
|
+
|
18
|
+
class << self
|
19
|
+
def key_length
|
20
|
+
OpenSSL::Cipher.new(CIPHER_TYPE).key_len
|
21
|
+
end
|
22
|
+
|
23
|
+
def iv_length
|
24
|
+
OpenSSL::Cipher.new(CIPHER_TYPE).iv_len
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
# When iv not provided, it will generate a random iv on each encryption operation (default and
|
29
|
+
# recommended operation)
|
30
|
+
def initialize(secret, deterministic: false)
|
31
|
+
@secret = secret
|
32
|
+
@deterministic = deterministic
|
33
|
+
end
|
34
|
+
|
35
|
+
def encrypt(clear_text)
|
36
|
+
# This code is extracted from +ActiveSupport::MessageEncryptor+. Not using it directly because we want to control
|
37
|
+
# the message format and only serialize things once at the +ActiveRecord::Encryption::Message+ level. Also, this
|
38
|
+
# cipher is prepared to deal with deterministic/non deterministic encryption modes.
|
39
|
+
|
40
|
+
cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
|
41
|
+
cipher.encrypt
|
42
|
+
cipher.key = @secret
|
43
|
+
|
44
|
+
iv = generate_iv(cipher, clear_text)
|
45
|
+
cipher.iv = iv
|
46
|
+
|
47
|
+
encrypted_data = clear_text.empty? ? clear_text.dup : cipher.update(clear_text)
|
48
|
+
encrypted_data << cipher.final
|
49
|
+
|
50
|
+
ActiveRecord::Encryption::Message.new(payload: encrypted_data).tap do |message|
|
51
|
+
message.headers.iv = iv
|
52
|
+
message.headers.auth_tag = cipher.auth_tag
|
53
|
+
end
|
54
|
+
end
|
55
|
+
|
56
|
+
def decrypt(encrypted_message)
|
57
|
+
encrypted_data = encrypted_message.payload
|
58
|
+
iv = encrypted_message.headers.iv
|
59
|
+
auth_tag = encrypted_message.headers.auth_tag
|
60
|
+
|
61
|
+
# Currently the OpenSSL bindings do not raise an error if auth_tag is
|
62
|
+
# truncated, which would allow an attacker to easily forge it. See
|
63
|
+
# https://github.com/ruby/openssl/issues/63
|
64
|
+
raise ActiveRecord::Encryption::Errors::EncryptedContentIntegrity if auth_tag.nil? || auth_tag.bytes.length != 16
|
65
|
+
|
66
|
+
cipher = OpenSSL::Cipher.new(CIPHER_TYPE)
|
67
|
+
|
68
|
+
cipher.decrypt
|
69
|
+
cipher.key = @secret
|
70
|
+
cipher.iv = iv
|
71
|
+
|
72
|
+
cipher.auth_tag = auth_tag
|
73
|
+
cipher.auth_data = ""
|
74
|
+
|
75
|
+
decrypted_data = encrypted_data.empty? ? encrypted_data : cipher.update(encrypted_data)
|
76
|
+
decrypted_data << cipher.final
|
77
|
+
|
78
|
+
decrypted_data
|
79
|
+
rescue OpenSSL::Cipher::CipherError, TypeError, ArgumentError
|
80
|
+
raise ActiveRecord::Encryption::Errors::Decryption
|
81
|
+
end
|
82
|
+
|
83
|
+
private
|
84
|
+
def generate_iv(cipher, clear_text)
|
85
|
+
if @deterministic
|
86
|
+
generate_deterministic_iv(clear_text)
|
87
|
+
else
|
88
|
+
cipher.random_iv
|
89
|
+
end
|
90
|
+
end
|
91
|
+
|
92
|
+
def generate_deterministic_iv(clear_text)
|
93
|
+
OpenSSL::HMAC.digest(OpenSSL::Digest::SHA256.new, @secret, clear_text)[0, ActiveRecord::Encryption.cipher.iv_length]
|
94
|
+
end
|
95
|
+
end
|
96
|
+
end
|
97
|
+
end
|
98
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# The algorithm used for encrypting and decrypting +Message+ objects.
|
6
|
+
#
|
7
|
+
# It uses AES-256-GCM. It will generate a random IV for non deterministic encryption (default)
|
8
|
+
# or derive an initialization vector from the encrypted content for deterministic encryption.
|
9
|
+
#
|
10
|
+
# See +Cipher::Aes256Gcm+.
|
11
|
+
class Cipher
|
12
|
+
DEFAULT_ENCODING = Encoding::UTF_8
|
13
|
+
|
14
|
+
# Encrypts the provided text and return an encrypted +Message+.
|
15
|
+
def encrypt(clean_text, key:, deterministic: false)
|
16
|
+
cipher_for(key, deterministic: deterministic).encrypt(clean_text).tap do |message|
|
17
|
+
message.headers.encoding = clean_text.encoding.name unless clean_text.encoding == DEFAULT_ENCODING
|
18
|
+
end
|
19
|
+
end
|
20
|
+
|
21
|
+
# Decrypt the provided +Message+.
|
22
|
+
#
|
23
|
+
# When +key+ is an Array, it will try all the keys raising a
|
24
|
+
# +ActiveRecord::Encryption::Errors::Decryption+ if none works.
|
25
|
+
def decrypt(encrypted_message, key:)
|
26
|
+
try_to_decrypt_with_each(encrypted_message, keys: Array(key)).tap do |decrypted_text|
|
27
|
+
decrypted_text.force_encoding(encrypted_message.headers.encoding || DEFAULT_ENCODING)
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
def key_length
|
32
|
+
Aes256Gcm.key_length
|
33
|
+
end
|
34
|
+
|
35
|
+
def iv_length
|
36
|
+
Aes256Gcm.iv_length
|
37
|
+
end
|
38
|
+
|
39
|
+
private
|
40
|
+
def try_to_decrypt_with_each(encrypted_text, keys:)
|
41
|
+
keys.each.with_index do |key, index|
|
42
|
+
return cipher_for(key).decrypt(encrypted_text)
|
43
|
+
rescue ActiveRecord::Encryption::Errors::Decryption
|
44
|
+
raise if index == keys.length - 1
|
45
|
+
end
|
46
|
+
end
|
47
|
+
|
48
|
+
def cipher_for(secret, deterministic: false)
|
49
|
+
Aes256Gcm.new(secret, deterministic: deterministic)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,44 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# Container of configuration options
|
6
|
+
class Config
|
7
|
+
attr_accessor :primary_key, :deterministic_key, :store_key_references, :key_derivation_salt,
|
8
|
+
:support_unencrypted_data, :encrypt_fixtures, :validate_column_size, :add_to_filter_parameters,
|
9
|
+
:excluded_from_filter_parameters, :extend_queries, :previous_schemes, :forced_encoding_for_deterministic_encryption
|
10
|
+
|
11
|
+
def initialize
|
12
|
+
set_defaults
|
13
|
+
end
|
14
|
+
|
15
|
+
# Configure previous encryption schemes.
|
16
|
+
#
|
17
|
+
# config.active_record.encryption.previous = [ { key_provider: MyOldKeyProvider.new } ]
|
18
|
+
def previous=(previous_schemes_properties)
|
19
|
+
previous_schemes_properties.each do |properties|
|
20
|
+
add_previous_scheme(**properties)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
private
|
25
|
+
def set_defaults
|
26
|
+
self.store_key_references = false
|
27
|
+
self.support_unencrypted_data = false
|
28
|
+
self.encrypt_fixtures = false
|
29
|
+
self.validate_column_size = true
|
30
|
+
self.add_to_filter_parameters = true
|
31
|
+
self.excluded_from_filter_parameters = []
|
32
|
+
self.previous_schemes = []
|
33
|
+
self.forced_encoding_for_deterministic_encryption = Encoding::UTF_8
|
34
|
+
|
35
|
+
# TODO: Setting to false for now as the implementation is a bit experimental
|
36
|
+
self.extend_queries = false
|
37
|
+
end
|
38
|
+
|
39
|
+
def add_previous_scheme(**properties)
|
40
|
+
previous_schemes << ActiveRecord::Encryption::Scheme.new(**properties)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
@@ -0,0 +1,61 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# Configuration API for +ActiveRecord::Encryption+
|
6
|
+
module Configurable
|
7
|
+
extend ActiveSupport::Concern
|
8
|
+
|
9
|
+
included do
|
10
|
+
mattr_reader :config, default: Config.new
|
11
|
+
mattr_accessor :encrypted_attribute_declaration_listeners
|
12
|
+
end
|
13
|
+
|
14
|
+
class_methods do
|
15
|
+
# Expose getters for context properties
|
16
|
+
Context::PROPERTIES.each do |name|
|
17
|
+
delegate name, to: :context
|
18
|
+
end
|
19
|
+
|
20
|
+
def configure(primary_key:, deterministic_key:, key_derivation_salt:, **properties) # :nodoc:
|
21
|
+
config.primary_key = primary_key
|
22
|
+
config.deterministic_key = deterministic_key
|
23
|
+
config.key_derivation_salt = key_derivation_salt
|
24
|
+
|
25
|
+
context.key_provider = ActiveRecord::Encryption::DerivedSecretKeyProvider.new(primary_key)
|
26
|
+
|
27
|
+
properties.each do |name, value|
|
28
|
+
[:context, :config].each do |configurable_object_name|
|
29
|
+
configurable_object = ActiveRecord::Encryption.send(configurable_object_name)
|
30
|
+
configurable_object.send "#{name}=", value if configurable_object.respond_to?("#{name}=")
|
31
|
+
end
|
32
|
+
end
|
33
|
+
end
|
34
|
+
|
35
|
+
# Register callback to be invoked when an encrypted attribute is declared.
|
36
|
+
#
|
37
|
+
# === Example:
|
38
|
+
#
|
39
|
+
# ActiveRecord::Encryption.on_encrypted_attribute_declared do |klass, attribute_name|
|
40
|
+
# ...
|
41
|
+
# end
|
42
|
+
def on_encrypted_attribute_declared(&block)
|
43
|
+
self.encrypted_attribute_declaration_listeners ||= Concurrent::Array.new
|
44
|
+
self.encrypted_attribute_declaration_listeners << block
|
45
|
+
end
|
46
|
+
|
47
|
+
def encrypted_attribute_was_declared(klass, name) # :nodoc:
|
48
|
+
self.encrypted_attribute_declaration_listeners&.each do |block|
|
49
|
+
block.call(klass, name)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
|
53
|
+
def install_auto_filtered_parameters(application) # :nodoc:
|
54
|
+
ActiveRecord::Encryption.on_encrypted_attribute_declared do |klass, encrypted_attribute_name|
|
55
|
+
application.config.filter_parameters << encrypted_attribute_name unless ActiveRecord::Encryption.config.excluded_from_filter_parameters.include?(name)
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
61
|
+
end
|
@@ -0,0 +1,35 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# An encryption context configures the different entities used to perform encryption:
|
6
|
+
#
|
7
|
+
# * A key provider
|
8
|
+
# * A key generator
|
9
|
+
# * An encryptor, the facade to encrypt data
|
10
|
+
# * A cipher, the encryption algorithm
|
11
|
+
# * A message serializer
|
12
|
+
class Context
|
13
|
+
PROPERTIES = %i[ key_provider key_generator cipher message_serializer encryptor frozen_encryption ]
|
14
|
+
|
15
|
+
PROPERTIES.each do |name|
|
16
|
+
attr_accessor name
|
17
|
+
end
|
18
|
+
|
19
|
+
def initialize
|
20
|
+
set_defaults
|
21
|
+
end
|
22
|
+
|
23
|
+
alias frozen_encryption? frozen_encryption
|
24
|
+
|
25
|
+
private
|
26
|
+
def set_defaults
|
27
|
+
self.frozen_encryption = false
|
28
|
+
self.key_generator = ActiveRecord::Encryption::KeyGenerator.new
|
29
|
+
self.cipher = ActiveRecord::Encryption::Cipher.new
|
30
|
+
self.encryptor = ActiveRecord::Encryption::Encryptor.new
|
31
|
+
self.message_serializer = ActiveRecord::Encryption::MessageSerializer.new
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,72 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# +ActiveRecord::Encryption+ uses encryption contexts to configure the different entities used to
|
6
|
+
# encrypt/decrypt at a given moment in time.
|
7
|
+
#
|
8
|
+
# By default, the library uses a default encryption context. This is the +Context+ that gets configured
|
9
|
+
# initially via +config.active_record.encryption+ options. Library users can define nested encryption contexts
|
10
|
+
# when running blocks of code.
|
11
|
+
#
|
12
|
+
# See +Context+.
|
13
|
+
module Contexts
|
14
|
+
extend ActiveSupport::Concern
|
15
|
+
|
16
|
+
included do
|
17
|
+
mattr_reader :default_context, default: Context.new
|
18
|
+
thread_mattr_accessor :custom_contexts
|
19
|
+
end
|
20
|
+
|
21
|
+
class_methods do
|
22
|
+
# Configures a custom encryption context to use when running the provided block of code.
|
23
|
+
#
|
24
|
+
# It supports overriding all the properties defined in +Context+.
|
25
|
+
#
|
26
|
+
# Example:
|
27
|
+
#
|
28
|
+
# ActiveRecord::Encryption.with_encryption_context(encryptor: ActiveRecord::Encryption::NullEncryptor.new) do
|
29
|
+
# ...
|
30
|
+
# end
|
31
|
+
#
|
32
|
+
# Encryption contexts can be nested.
|
33
|
+
def with_encryption_context(properties)
|
34
|
+
self.custom_contexts ||= []
|
35
|
+
self.custom_contexts << default_context.dup
|
36
|
+
properties.each do |key, value|
|
37
|
+
self.current_custom_context.send("#{key}=", value)
|
38
|
+
end
|
39
|
+
|
40
|
+
yield
|
41
|
+
ensure
|
42
|
+
self.custom_contexts.pop
|
43
|
+
end
|
44
|
+
|
45
|
+
# Runs the provided block in an encryption context where encryption is disabled:
|
46
|
+
#
|
47
|
+
# * Reading encrypted content will return its ciphertexts.
|
48
|
+
# * Writing encrypted content will write its clear text.
|
49
|
+
def without_encryption(&block)
|
50
|
+
with_encryption_context encryptor: ActiveRecord::Encryption::NullEncryptor.new, &block
|
51
|
+
end
|
52
|
+
|
53
|
+
# Runs the provided block in an encryption context where:
|
54
|
+
#
|
55
|
+
# * Reading encrypted content will return its ciphertext.
|
56
|
+
# * Writing encrypted content will fail.
|
57
|
+
def protecting_encrypted_data(&block)
|
58
|
+
with_encryption_context encryptor: ActiveRecord::Encryption::EncryptingOnlyEncryptor.new, frozen_encryption: true, &block
|
59
|
+
end
|
60
|
+
|
61
|
+
# Returns the current context. By default it will return the current context.
|
62
|
+
def context
|
63
|
+
self.current_custom_context || self.default_context
|
64
|
+
end
|
65
|
+
|
66
|
+
def current_custom_context
|
67
|
+
self.custom_contexts&.last
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
end
|
72
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# A +KeyProvider+ that derives keys from passwords.
|
6
|
+
class DerivedSecretKeyProvider < KeyProvider
|
7
|
+
def initialize(passwords)
|
8
|
+
super(Array(passwords).collect { |password| Key.derive_from(password) })
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|
@@ -0,0 +1,14 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# A +KeyProvider+ that derives keys from passwords.
|
6
|
+
class DeterministicKeyProvider < DerivedSecretKeyProvider
|
7
|
+
def initialize(password)
|
8
|
+
passwords = Array(password)
|
9
|
+
raise ActiveRecord::Encryption::Errors::Configuration, "Deterministic encryption keys can't be rotated" if passwords.length > 1
|
10
|
+
super(passwords)
|
11
|
+
end
|
12
|
+
end
|
13
|
+
end
|
14
|
+
end
|
@@ -0,0 +1,208 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# This is the concern mixed in Active Record models to make them encryptable. It adds the +encrypts+
|
6
|
+
# attribute declaration, as well as the API to encrypt and decrypt records.
|
7
|
+
module EncryptableRecord
|
8
|
+
extend ActiveSupport::Concern
|
9
|
+
|
10
|
+
included do
|
11
|
+
class_attribute :encrypted_attributes
|
12
|
+
|
13
|
+
validate :cant_modify_encrypted_attributes_when_frozen, if: -> { has_encrypted_attributes? && ActiveRecord::Encryption.context.frozen_encryption? }
|
14
|
+
end
|
15
|
+
|
16
|
+
class_methods do
|
17
|
+
# Encrypts the +name+ attribute.
|
18
|
+
#
|
19
|
+
# === Options
|
20
|
+
#
|
21
|
+
# * <tt>:key_provider</tt> - Configure a +KeyProvider+ for serving the keys to encrypt and
|
22
|
+
# decrypt this attribute. If not provided, it will default to +ActiveRecord::Encryption.key_provider+.
|
23
|
+
# * <tt>:key</tt> - A password to derive the key from. It's a shorthand for a +:key_provider+ that
|
24
|
+
# serves derivated keys. Both options can't be used at the same time.
|
25
|
+
# * <tt>:key_provider</tt> - Set a +:key_provider+ to provide encryption and decryption keys. If not
|
26
|
+
# provided, it will default to the key provider set with `config.key_provider`.
|
27
|
+
# * <tt>:deterministic</tt> - By default, encryption is not deterministic. It will use a random
|
28
|
+
# initialization vector for each encryption operation. This means that encrypting the same content
|
29
|
+
# with the same key twice will generate different ciphertexts. When set to +true+, it will generate the
|
30
|
+
# initialization vector based on the encrypted content. This means that the same content will generate
|
31
|
+
# the same ciphertexts. This enables querying encrypted text with Active Record. Deterministic encryption
|
32
|
+
# will use the oldest encryption scheme to encrypt new data by default. You can change this by setting
|
33
|
+
# +deterministic: { fixed: false }+. That will make it use the newest encryption scheme for encrypting new
|
34
|
+
# data.
|
35
|
+
# * <tt>:downcase</tt> - When true, it converts the encrypted content to downcase automatically. This allows to
|
36
|
+
# effectively ignore case when querying data. Notice that the case is lost. Use +:ignore_case+ if you are interested
|
37
|
+
# in preserving it.
|
38
|
+
# * <tt>:ignore_case</tt> - When true, it behaves like +:downcase+ but, it also preserves the original case in a specially
|
39
|
+
# designated column +original_<name>+. When reading the encrypted content, the version with the original case is
|
40
|
+
# server. But you can still execute queries that will ignore the case. This option can only be used when +:deterministic+
|
41
|
+
# is true.
|
42
|
+
# * <tt>:context_properties</tt> - Additional properties that will override +Context+ settings when this attribute is
|
43
|
+
# encrypted and decrypted. E.g: +encryptor:+, +cipher:+, +message_serializer:+, etc.
|
44
|
+
# * <tt>:previous</tt> - List of previous encryption schemes. When provided, they will be used in order when trying to read
|
45
|
+
# the attribute. Each entry of the list can contain the properties supported by #encrypts. Also, when deterministic
|
46
|
+
# encryption is used, they will be used to generate additional ciphertexts to check in the queries.
|
47
|
+
def encrypts(*names, key_provider: nil, key: nil, deterministic: false, downcase: false, ignore_case: false, previous: [], **context_properties)
|
48
|
+
self.encrypted_attributes ||= Set.new # not using :default because the instance would be shared across classes
|
49
|
+
scheme = scheme_for key_provider: key_provider, key: key, deterministic: deterministic, downcase: downcase, \
|
50
|
+
ignore_case: ignore_case, previous: previous, **context_properties
|
51
|
+
|
52
|
+
names.each do |name|
|
53
|
+
encrypt_attribute name, scheme
|
54
|
+
end
|
55
|
+
end
|
56
|
+
|
57
|
+
# Returns the list of deterministic encryptable attributes in the model class.
|
58
|
+
def deterministic_encrypted_attributes
|
59
|
+
@deterministic_encrypted_attributes ||= encrypted_attributes&.find_all do |attribute_name|
|
60
|
+
type_for_attribute(attribute_name).deterministic?
|
61
|
+
end
|
62
|
+
end
|
63
|
+
|
64
|
+
# Given a attribute name, it returns the name of the source attribute when it's a preserved one.
|
65
|
+
def source_attribute_from_preserved_attribute(attribute_name)
|
66
|
+
attribute_name.to_s.sub(ORIGINAL_ATTRIBUTE_PREFIX, "") if /^#{ORIGINAL_ATTRIBUTE_PREFIX}/.match?(attribute_name)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def scheme_for(key_provider: nil, key: nil, deterministic: false, downcase: false, ignore_case: false, previous: [], **context_properties)
|
71
|
+
ActiveRecord::Encryption::Scheme.new(key_provider: key_provider, key: key, deterministic: deterministic,
|
72
|
+
downcase: downcase, ignore_case: ignore_case, **context_properties).tap do |scheme|
|
73
|
+
scheme.previous_schemes = global_previous_schemes_for(scheme) +
|
74
|
+
Array.wrap(previous).collect { |scheme_config| ActiveRecord::Encryption::Scheme.new(**scheme_config) }
|
75
|
+
end
|
76
|
+
end
|
77
|
+
|
78
|
+
def global_previous_schemes_for(scheme)
|
79
|
+
ActiveRecord::Encryption.config.previous_schemes.collect do |previous_scheme|
|
80
|
+
scheme.merge(previous_scheme)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def encrypt_attribute(name, attribute_scheme)
|
85
|
+
encrypted_attributes << name.to_sym
|
86
|
+
|
87
|
+
attribute name do |cast_type|
|
88
|
+
ActiveRecord::Encryption::EncryptedAttributeType.new scheme: attribute_scheme, cast_type: cast_type
|
89
|
+
end
|
90
|
+
|
91
|
+
preserve_original_encrypted(name) if attribute_scheme.ignore_case?
|
92
|
+
ActiveRecord::Encryption.encrypted_attribute_was_declared(self, name)
|
93
|
+
end
|
94
|
+
|
95
|
+
def preserve_original_encrypted(name)
|
96
|
+
original_attribute_name = "#{ORIGINAL_ATTRIBUTE_PREFIX}#{name}".to_sym
|
97
|
+
|
98
|
+
if !ActiveRecord::Encryption.config.support_unencrypted_data && !column_names.include?(original_attribute_name.to_s)
|
99
|
+
raise Errors::Configuration, "To use :ignore_case for '#{name}' you must create an additional column named '#{original_attribute_name}'"
|
100
|
+
end
|
101
|
+
|
102
|
+
encrypts original_attribute_name
|
103
|
+
override_accessors_to_preserve_original name, original_attribute_name
|
104
|
+
end
|
105
|
+
|
106
|
+
def override_accessors_to_preserve_original(name, original_attribute_name)
|
107
|
+
include(Module.new do
|
108
|
+
define_method name do
|
109
|
+
if ((value = super()) && encrypted_attribute?(name)) || !ActiveRecord::Encryption.config.support_unencrypted_data
|
110
|
+
send(original_attribute_name)
|
111
|
+
else
|
112
|
+
value
|
113
|
+
end
|
114
|
+
end
|
115
|
+
|
116
|
+
define_method "#{name}=" do |value|
|
117
|
+
self.send "#{original_attribute_name}=", value
|
118
|
+
super(value)
|
119
|
+
end
|
120
|
+
end)
|
121
|
+
end
|
122
|
+
|
123
|
+
def load_schema!
|
124
|
+
super
|
125
|
+
|
126
|
+
add_length_validation_for_encrypted_columns if ActiveRecord::Encryption.config.validate_column_size
|
127
|
+
end
|
128
|
+
|
129
|
+
def add_length_validation_for_encrypted_columns
|
130
|
+
encrypted_attributes&.each do |attribute_name|
|
131
|
+
validate_column_size attribute_name
|
132
|
+
end
|
133
|
+
end
|
134
|
+
|
135
|
+
def validate_column_size(attribute_name)
|
136
|
+
if limit = columns_hash[attribute_name.to_s]&.limit
|
137
|
+
validates_length_of attribute_name, maximum: limit
|
138
|
+
end
|
139
|
+
end
|
140
|
+
end
|
141
|
+
|
142
|
+
# Returns whether a given attribute is encrypted or not.
|
143
|
+
def encrypted_attribute?(attribute_name)
|
144
|
+
ActiveRecord::Encryption.encryptor.encrypted? ciphertext_for(attribute_name)
|
145
|
+
end
|
146
|
+
|
147
|
+
# Returns the ciphertext for +attribute_name+.
|
148
|
+
def ciphertext_for(attribute_name)
|
149
|
+
read_attribute_before_type_cast(attribute_name)
|
150
|
+
end
|
151
|
+
|
152
|
+
# Encrypts all the encryptable attributes and saves the changes.
|
153
|
+
def encrypt
|
154
|
+
encrypt_attributes if has_encrypted_attributes?
|
155
|
+
end
|
156
|
+
|
157
|
+
# Decrypts all the encryptable attributes and saves the changes.
|
158
|
+
def decrypt
|
159
|
+
decrypt_attributes if has_encrypted_attributes?
|
160
|
+
end
|
161
|
+
|
162
|
+
private
|
163
|
+
ORIGINAL_ATTRIBUTE_PREFIX = "original_"
|
164
|
+
|
165
|
+
def encrypt_attributes
|
166
|
+
validate_encryption_allowed
|
167
|
+
|
168
|
+
update_columns build_encrypt_attribute_assignments
|
169
|
+
end
|
170
|
+
|
171
|
+
def decrypt_attributes
|
172
|
+
validate_encryption_allowed
|
173
|
+
|
174
|
+
decrypt_attribute_assignments = build_decrypt_attribute_assignments
|
175
|
+
ActiveRecord::Encryption.without_encryption { update_columns decrypt_attribute_assignments }
|
176
|
+
end
|
177
|
+
|
178
|
+
def validate_encryption_allowed
|
179
|
+
raise ActiveRecord::Encryption::Errors::Configuration, "can't be modified because it is encrypted" if ActiveRecord::Encryption.context.frozen_encryption?
|
180
|
+
end
|
181
|
+
|
182
|
+
def has_encrypted_attributes?
|
183
|
+
self.class.encrypted_attributes.present?
|
184
|
+
end
|
185
|
+
|
186
|
+
def build_encrypt_attribute_assignments
|
187
|
+
Array(self.class.encrypted_attributes).index_with do |attribute_name|
|
188
|
+
self[attribute_name]
|
189
|
+
end
|
190
|
+
end
|
191
|
+
|
192
|
+
def build_decrypt_attribute_assignments
|
193
|
+
Array(self.class.encrypted_attributes).collect do |attribute_name|
|
194
|
+
type = type_for_attribute(attribute_name)
|
195
|
+
encrypted_value = ciphertext_for(attribute_name)
|
196
|
+
new_value = type.deserialize(encrypted_value)
|
197
|
+
[attribute_name, new_value]
|
198
|
+
end.to_h
|
199
|
+
end
|
200
|
+
|
201
|
+
def cant_modify_encrypted_attributes_when_frozen
|
202
|
+
self.class&.encrypted_attributes.each do |attribute|
|
203
|
+
errors.add(attribute.to_sym, "can't be modified because it is encrypted") if changed_attributes.include?(attribute)
|
204
|
+
end
|
205
|
+
end
|
206
|
+
end
|
207
|
+
end
|
208
|
+
end
|