activerecord 6.1.6 → 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.

Files changed (218) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +728 -1279
  3. data/MIT-LICENSE +1 -1
  4. data/README.rdoc +1 -1
  5. data/lib/active_record/aggregations.rb +1 -1
  6. data/lib/active_record/association_relation.rb +0 -10
  7. data/lib/active_record/associations/association.rb +31 -9
  8. data/lib/active_record/associations/association_scope.rb +1 -3
  9. data/lib/active_record/associations/belongs_to_association.rb +15 -4
  10. data/lib/active_record/associations/belongs_to_polymorphic_association.rb +10 -2
  11. data/lib/active_record/associations/builder/association.rb +8 -2
  12. data/lib/active_record/associations/builder/belongs_to.rb +19 -6
  13. data/lib/active_record/associations/builder/collection_association.rb +1 -1
  14. data/lib/active_record/associations/builder/has_many.rb +3 -2
  15. data/lib/active_record/associations/builder/has_one.rb +2 -1
  16. data/lib/active_record/associations/builder/singular_association.rb +2 -2
  17. data/lib/active_record/associations/collection_association.rb +14 -23
  18. data/lib/active_record/associations/collection_proxy.rb +8 -3
  19. data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
  20. data/lib/active_record/associations/has_many_association.rb +1 -1
  21. data/lib/active_record/associations/has_many_through_association.rb +2 -1
  22. data/lib/active_record/associations/has_one_association.rb +10 -7
  23. data/lib/active_record/associations/has_one_through_association.rb +1 -1
  24. data/lib/active_record/associations/preloader/association.rb +161 -47
  25. data/lib/active_record/associations/preloader/batch.rb +51 -0
  26. data/lib/active_record/associations/preloader/branch.rb +147 -0
  27. data/lib/active_record/associations/preloader/through_association.rb +37 -11
  28. data/lib/active_record/associations/preloader.rb +46 -110
  29. data/lib/active_record/associations/singular_association.rb +8 -2
  30. data/lib/active_record/associations/through_association.rb +1 -1
  31. data/lib/active_record/associations.rb +76 -81
  32. data/lib/active_record/asynchronous_queries_tracker.rb +57 -0
  33. data/lib/active_record/attribute_assignment.rb +1 -1
  34. data/lib/active_record/attribute_methods/before_type_cast.rb +7 -2
  35. data/lib/active_record/attribute_methods/dirty.rb +41 -16
  36. data/lib/active_record/attribute_methods/primary_key.rb +2 -2
  37. data/lib/active_record/attribute_methods/query.rb +2 -2
  38. data/lib/active_record/attribute_methods/read.rb +7 -5
  39. data/lib/active_record/attribute_methods/serialization.rb +66 -12
  40. data/lib/active_record/attribute_methods/time_zone_conversion.rb +4 -3
  41. data/lib/active_record/attribute_methods/write.rb +7 -10
  42. data/lib/active_record/attribute_methods.rb +6 -9
  43. data/lib/active_record/attributes.rb +24 -35
  44. data/lib/active_record/autosave_association.rb +3 -18
  45. data/lib/active_record/base.rb +19 -1
  46. data/lib/active_record/callbacks.rb +2 -2
  47. data/lib/active_record/connection_adapters/abstract/connection_handler.rb +312 -0
  48. data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +209 -0
  49. data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +76 -0
  50. data/lib/active_record/connection_adapters/abstract/connection_pool.rb +31 -558
  51. data/lib/active_record/connection_adapters/abstract/database_statements.rb +45 -21
  52. data/lib/active_record/connection_adapters/abstract/query_cache.rb +24 -12
  53. data/lib/active_record/connection_adapters/abstract/quoting.rb +11 -4
  54. data/lib/active_record/connection_adapters/abstract/schema_creation.rb +4 -17
  55. data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +30 -13
  56. data/lib/active_record/connection_adapters/abstract/schema_statements.rb +60 -16
  57. data/lib/active_record/connection_adapters/abstract/transaction.rb +3 -3
  58. data/lib/active_record/connection_adapters/abstract_adapter.rb +112 -66
  59. data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +96 -81
  60. data/lib/active_record/connection_adapters/mysql/database_statements.rb +33 -23
  61. data/lib/active_record/connection_adapters/mysql/quoting.rb +16 -1
  62. data/lib/active_record/connection_adapters/mysql/schema_statements.rb +1 -1
  63. data/lib/active_record/connection_adapters/mysql2_adapter.rb +12 -6
  64. data/lib/active_record/connection_adapters/pool_config.rb +1 -3
  65. data/lib/active_record/connection_adapters/postgresql/database_statements.rb +19 -14
  66. data/lib/active_record/connection_adapters/postgresql/oid/date.rb +8 -0
  67. data/lib/active_record/connection_adapters/postgresql/oid/date_time.rb +5 -0
  68. data/lib/active_record/connection_adapters/postgresql/oid/hstore.rb +53 -14
  69. data/lib/active_record/connection_adapters/postgresql/oid/range.rb +1 -1
  70. data/lib/active_record/connection_adapters/postgresql/oid/timestamp.rb +15 -0
  71. data/lib/active_record/connection_adapters/postgresql/oid/timestamp_with_time_zone.rb +28 -0
  72. data/lib/active_record/connection_adapters/postgresql/oid/type_map_initializer.rb +18 -6
  73. data/lib/active_record/connection_adapters/postgresql/oid.rb +2 -0
  74. data/lib/active_record/connection_adapters/postgresql/quoting.rb +6 -6
  75. data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +32 -0
  76. data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +5 -1
  77. data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +12 -12
  78. data/lib/active_record/connection_adapters/postgresql_adapter.rb +157 -100
  79. data/lib/active_record/connection_adapters/schema_cache.rb +36 -37
  80. data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +23 -19
  81. data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +4 -2
  82. data/lib/active_record/connection_adapters/sqlite3_adapter.rb +61 -30
  83. data/lib/active_record/connection_adapters.rb +6 -5
  84. data/lib/active_record/connection_handling.rb +20 -38
  85. data/lib/active_record/core.rb +111 -110
  86. data/lib/active_record/database_configurations/connection_url_resolver.rb +0 -1
  87. data/lib/active_record/database_configurations/database_config.rb +12 -0
  88. data/lib/active_record/database_configurations/hash_config.rb +27 -1
  89. data/lib/active_record/database_configurations/url_config.rb +2 -2
  90. data/lib/active_record/database_configurations.rb +17 -9
  91. data/lib/active_record/delegated_type.rb +33 -11
  92. data/lib/active_record/destroy_association_async_job.rb +1 -1
  93. data/lib/active_record/disable_joins_association_relation.rb +39 -0
  94. data/lib/active_record/dynamic_matchers.rb +1 -1
  95. data/lib/active_record/encryption/cipher/aes256_gcm.rb +98 -0
  96. data/lib/active_record/encryption/cipher.rb +53 -0
  97. data/lib/active_record/encryption/config.rb +44 -0
  98. data/lib/active_record/encryption/configurable.rb +61 -0
  99. data/lib/active_record/encryption/context.rb +35 -0
  100. data/lib/active_record/encryption/contexts.rb +72 -0
  101. data/lib/active_record/encryption/derived_secret_key_provider.rb +12 -0
  102. data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
  103. data/lib/active_record/encryption/encryptable_record.rb +208 -0
  104. data/lib/active_record/encryption/encrypted_attribute_type.rb +140 -0
  105. data/lib/active_record/encryption/encrypted_fixtures.rb +38 -0
  106. data/lib/active_record/encryption/encrypting_only_encryptor.rb +12 -0
  107. data/lib/active_record/encryption/encryptor.rb +155 -0
  108. data/lib/active_record/encryption/envelope_encryption_key_provider.rb +55 -0
  109. data/lib/active_record/encryption/errors.rb +15 -0
  110. data/lib/active_record/encryption/extended_deterministic_queries.rb +160 -0
  111. data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +29 -0
  112. data/lib/active_record/encryption/key.rb +28 -0
  113. data/lib/active_record/encryption/key_generator.rb +42 -0
  114. data/lib/active_record/encryption/key_provider.rb +46 -0
  115. data/lib/active_record/encryption/message.rb +33 -0
  116. data/lib/active_record/encryption/message_serializer.rb +80 -0
  117. data/lib/active_record/encryption/null_encryptor.rb +21 -0
  118. data/lib/active_record/encryption/properties.rb +76 -0
  119. data/lib/active_record/encryption/read_only_null_encryptor.rb +24 -0
  120. data/lib/active_record/encryption/scheme.rb +99 -0
  121. data/lib/active_record/encryption.rb +55 -0
  122. data/lib/active_record/enum.rb +41 -41
  123. data/lib/active_record/errors.rb +66 -3
  124. data/lib/active_record/fixture_set/file.rb +15 -1
  125. data/lib/active_record/fixture_set/table_row.rb +40 -5
  126. data/lib/active_record/fixture_set/table_rows.rb +4 -4
  127. data/lib/active_record/fixtures.rb +16 -11
  128. data/lib/active_record/future_result.rb +139 -0
  129. data/lib/active_record/gem_version.rb +4 -4
  130. data/lib/active_record/inheritance.rb +55 -17
  131. data/lib/active_record/insert_all.rb +34 -5
  132. data/lib/active_record/integration.rb +1 -1
  133. data/lib/active_record/internal_metadata.rb +1 -5
  134. data/lib/active_record/locking/optimistic.rb +10 -9
  135. data/lib/active_record/log_subscriber.rb +6 -2
  136. data/lib/active_record/middleware/database_selector/resolver.rb +6 -10
  137. data/lib/active_record/middleware/database_selector.rb +8 -3
  138. data/lib/active_record/migration/command_recorder.rb +4 -4
  139. data/lib/active_record/migration/compatibility.rb +89 -10
  140. data/lib/active_record/migration/join_table.rb +1 -1
  141. data/lib/active_record/migration.rb +109 -79
  142. data/lib/active_record/model_schema.rb +45 -31
  143. data/lib/active_record/nested_attributes.rb +3 -3
  144. data/lib/active_record/no_touching.rb +2 -2
  145. data/lib/active_record/null_relation.rb +2 -6
  146. data/lib/active_record/persistence.rb +134 -45
  147. data/lib/active_record/query_cache.rb +2 -2
  148. data/lib/active_record/query_logs.rb +203 -0
  149. data/lib/active_record/querying.rb +15 -5
  150. data/lib/active_record/railtie.rb +117 -17
  151. data/lib/active_record/railties/controller_runtime.rb +1 -1
  152. data/lib/active_record/railties/databases.rake +72 -48
  153. data/lib/active_record/readonly_attributes.rb +11 -0
  154. data/lib/active_record/reflection.rb +45 -44
  155. data/lib/active_record/relation/batches/batch_enumerator.rb +19 -5
  156. data/lib/active_record/relation/batches.rb +3 -3
  157. data/lib/active_record/relation/calculations.rb +39 -26
  158. data/lib/active_record/relation/delegation.rb +6 -6
  159. data/lib/active_record/relation/finder_methods.rb +31 -22
  160. data/lib/active_record/relation/merger.rb +20 -13
  161. data/lib/active_record/relation/predicate_builder.rb +1 -6
  162. data/lib/active_record/relation/query_attribute.rb +5 -11
  163. data/lib/active_record/relation/query_methods.rb +230 -47
  164. data/lib/active_record/relation/record_fetch_warning.rb +2 -2
  165. data/lib/active_record/relation/spawn_methods.rb +2 -2
  166. data/lib/active_record/relation/where_clause.rb +8 -4
  167. data/lib/active_record/relation.rb +166 -77
  168. data/lib/active_record/result.rb +17 -2
  169. data/lib/active_record/runtime_registry.rb +2 -4
  170. data/lib/active_record/sanitization.rb +11 -7
  171. data/lib/active_record/schema_dumper.rb +3 -3
  172. data/lib/active_record/schema_migration.rb +0 -4
  173. data/lib/active_record/scoping/default.rb +61 -12
  174. data/lib/active_record/scoping/named.rb +3 -11
  175. data/lib/active_record/scoping.rb +40 -22
  176. data/lib/active_record/serialization.rb +1 -1
  177. data/lib/active_record/signed_id.rb +1 -1
  178. data/lib/active_record/tasks/database_tasks.rb +106 -22
  179. data/lib/active_record/tasks/mysql_database_tasks.rb +1 -1
  180. data/lib/active_record/tasks/postgresql_database_tasks.rb +14 -11
  181. data/lib/active_record/test_databases.rb +1 -1
  182. data/lib/active_record/test_fixtures.rb +4 -4
  183. data/lib/active_record/timestamp.rb +3 -4
  184. data/lib/active_record/transactions.rb +9 -14
  185. data/lib/active_record/translation.rb +2 -2
  186. data/lib/active_record/type/adapter_specific_registry.rb +32 -7
  187. data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
  188. data/lib/active_record/type/internal/timezone.rb +2 -2
  189. data/lib/active_record/type/serialized.rb +1 -1
  190. data/lib/active_record/type/type_map.rb +17 -20
  191. data/lib/active_record/type.rb +1 -2
  192. data/lib/active_record/validations/associated.rb +1 -1
  193. data/lib/active_record.rb +170 -2
  194. data/lib/arel/attributes/attribute.rb +0 -8
  195. data/lib/arel/crud.rb +18 -22
  196. data/lib/arel/delete_manager.rb +2 -4
  197. data/lib/arel/insert_manager.rb +2 -3
  198. data/lib/arel/nodes/casted.rb +1 -1
  199. data/lib/arel/nodes/delete_statement.rb +8 -13
  200. data/lib/arel/nodes/insert_statement.rb +2 -2
  201. data/lib/arel/nodes/select_core.rb +2 -2
  202. data/lib/arel/nodes/select_statement.rb +2 -2
  203. data/lib/arel/nodes/update_statement.rb +3 -2
  204. data/lib/arel/predications.rb +1 -1
  205. data/lib/arel/select_manager.rb +10 -4
  206. data/lib/arel/table.rb +0 -1
  207. data/lib/arel/tree_manager.rb +0 -12
  208. data/lib/arel/update_manager.rb +2 -4
  209. data/lib/arel/visitors/dot.rb +80 -90
  210. data/lib/arel/visitors/mysql.rb +6 -1
  211. data/lib/arel/visitors/postgresql.rb +0 -10
  212. data/lib/arel/visitors/to_sql.rb +43 -2
  213. data/lib/arel.rb +1 -1
  214. data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
  215. data/lib/rails/generators/active_record/model/templates/abstract_base_class.rb.tt +1 -1
  216. data/lib/rails/generators/active_record/model/templates/model.rb.tt +1 -1
  217. data/lib/rails/generators/active_record/model/templates/module.rb.tt +2 -2
  218. metadata +55 -17
@@ -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
@@ -1,7 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module ActiveRecord
4
- module DynamicMatchers #:nodoc:
4
+ module DynamicMatchers # :nodoc:
5
5
  private
6
6
  def respond_to_missing?(name, _)
7
7
  if self == Base
@@ -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