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,46 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # A +KeyProvider+ serves keys:
6
+ #
7
+ # * An encryption key
8
+ # * A list of potential decryption keys. Serving multiple decryption keys supports rotation-schemes
9
+ # where new keys are added but old keys need to continue working
10
+ class KeyProvider
11
+ def initialize(keys)
12
+ @keys = Array(keys)
13
+ end
14
+
15
+ # Returns the first key in the list as the active key to perform encryptions
16
+ #
17
+ # When +ActiveRecord::Encryption.config.store_key_references+ is true, the key will include
18
+ # a public tag referencing the key itself. That key will be stored in the public
19
+ # headers of the encrypted message
20
+ def encryption_key
21
+ @encryption_key ||= @keys.last.tap do |key|
22
+ key.public_tags.encrypted_data_key_id = key.id if ActiveRecord::Encryption.config.store_key_references
23
+ end
24
+
25
+ @encryption_key
26
+ end
27
+
28
+ # Returns the list of decryption keys
29
+ #
30
+ # When the message holds a reference to its encryption key, it will return an array
31
+ # with that key. If not, it will return the list of keys.
32
+ def decryption_keys(encrypted_message)
33
+ if encrypted_message.headers.encrypted_data_key_id
34
+ keys_grouped_by_id[encrypted_message.headers.encrypted_data_key_id]
35
+ else
36
+ @keys
37
+ end
38
+ end
39
+
40
+ private
41
+ def keys_grouped_by_id
42
+ @keys_grouped_by_id ||= @keys.group_by(&:id)
43
+ end
44
+ end
45
+ end
46
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # A message defines the structure of the data we store in encrypted attributes. It contains:
6
+ #
7
+ # * An encrypted payload
8
+ # * A list of unencrypted headers
9
+ #
10
+ # See +Encryptor#encrypt+
11
+ class Message
12
+ attr_accessor :payload, :headers
13
+
14
+ def initialize(payload: nil, headers: {})
15
+ validate_payload_type(payload)
16
+
17
+ @payload = payload
18
+ @headers = Properties.new(headers)
19
+ end
20
+
21
+ def ==(other_message)
22
+ payload == other_message.payload && headers == other_message.headers
23
+ end
24
+
25
+ private
26
+ def validate_payload_type(payload)
27
+ unless payload.is_a?(String) || payload.nil?
28
+ raise ActiveRecord::Encryption::Errors::ForbiddenClass, "Only string payloads allowed"
29
+ end
30
+ end
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # A message serializer that serializes +Messages+ with JSON.
6
+ #
7
+ # The generated structure is pretty simple:
8
+ #
9
+ # {
10
+ # p: <payload>,
11
+ # h: {
12
+ # header1: value1,
13
+ # header2: value2,
14
+ # ...
15
+ # }
16
+ # }
17
+ #
18
+ # Both the payload and the header values are encoded with Base64
19
+ # to prevent JSON parsing errors and encoding issues when
20
+ # storing the resulting serialized data.
21
+ class MessageSerializer
22
+ def load(serialized_content)
23
+ data = JSON.parse(serialized_content)
24
+ parse_message(data, 1)
25
+ rescue JSON::ParserError
26
+ raise ActiveRecord::Encryption::Errors::Encoding
27
+ end
28
+
29
+ def dump(message)
30
+ raise ActiveRecord::Encryption::Errors::ForbiddenClass unless message.is_a?(ActiveRecord::Encryption::Message)
31
+ JSON.dump message_to_json(message)
32
+ end
33
+
34
+ private
35
+ def parse_message(data, level)
36
+ raise ActiveRecord::Encryption::Errors::Decryption, "More than one level of hash nesting in headers is not supported" if level > 2
37
+ ActiveRecord::Encryption::Message.new(payload: decode_if_needed(data["p"]), headers: parse_properties(data["h"], level))
38
+ end
39
+
40
+ def parse_properties(headers, level)
41
+ ActiveRecord::Encryption::Properties.new.tap do |properties|
42
+ headers&.each do |key, value|
43
+ properties[key] = value.is_a?(Hash) ? parse_message(value, level + 1) : decode_if_needed(value)
44
+ end
45
+ end
46
+ end
47
+
48
+ def message_to_json(message)
49
+ {
50
+ p: encode_if_needed(message.payload),
51
+ h: headers_to_json(message.headers)
52
+ }
53
+ end
54
+
55
+ def headers_to_json(headers)
56
+ headers.transform_values do |value|
57
+ value.is_a?(ActiveRecord::Encryption::Message) ? message_to_json(value) : encode_if_needed(value)
58
+ end
59
+ end
60
+
61
+ def encode_if_needed(value)
62
+ if value.is_a?(String)
63
+ ::Base64.strict_encode64 value
64
+ else
65
+ value
66
+ end
67
+ end
68
+
69
+ def decode_if_needed(value)
70
+ if value.is_a?(String)
71
+ ::Base64.strict_decode64(value)
72
+ else
73
+ value
74
+ end
75
+ rescue ArgumentError, TypeError
76
+ raise Errors::Encoding
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # An encryptor that won't decrypt or encrypt. It will just return the passed
6
+ # values
7
+ class NullEncryptor
8
+ def encrypt(clean_text, key_provider: nil, cipher_options: {})
9
+ clean_text
10
+ end
11
+
12
+ def decrypt(encrypted_text, key_provider: nil, cipher_options: {})
13
+ encrypted_text
14
+ end
15
+
16
+ def encrypted?(text)
17
+ false
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,76 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # This is a wrapper for a hash of encryption properties. It is used by
6
+ # +Key+ (public tags) and +Message+ (headers).
7
+ #
8
+ # Since properties are serialized in messages, it is important for storage
9
+ # efficiency to keep their keys as short as possible. It defines accessors
10
+ # for common properties that will keep these keys very short while exposing
11
+ # a readable name.
12
+ #
13
+ # message.headers.encrypted_data_key # instead of message.headers[:k]
14
+ #
15
+ # See +Properties#DEFAULT_PROPERTIES+, +Key+, +Message+
16
+ class Properties
17
+ ALLOWED_VALUE_CLASSES = [String, ActiveRecord::Encryption::Message, Numeric, TrueClass, FalseClass, Symbol, NilClass]
18
+
19
+ delegate_missing_to :data
20
+ delegate :==, to: :data
21
+
22
+ # For each entry it generates an accessor exposing the full name
23
+ DEFAULT_PROPERTIES = {
24
+ encrypted_data_key: "k",
25
+ encrypted_data_key_id: "i",
26
+ compressed: "c",
27
+ iv: "iv",
28
+ auth_tag: "at",
29
+ encoding: "e"
30
+ }
31
+
32
+ DEFAULT_PROPERTIES.each do |name, key|
33
+ define_method name do
34
+ self[key.to_sym]
35
+ end
36
+
37
+ define_method "#{name}=" do |value|
38
+ self[key.to_sym] = value
39
+ end
40
+ end
41
+
42
+ def initialize(initial_properties = {})
43
+ @data = {}
44
+ add(initial_properties)
45
+ end
46
+
47
+ # Set a value for a given key
48
+ #
49
+ # It will raise an +EncryptedContentIntegrity+ if the value exists
50
+ def []=(key, value)
51
+ raise Errors::EncryptedContentIntegrity, "Properties can't be overridden: #{key}" if key?(key)
52
+ validate_value_type(value)
53
+ data[key] = value
54
+ end
55
+
56
+ def validate_value_type(value)
57
+ unless ALLOWED_VALUE_CLASSES.find { |klass| value.is_a?(klass) }
58
+ raise ActiveRecord::Encryption::Errors::ForbiddenClass, "Can't store a #{value.class}, only properties of type #{ALLOWED_VALUE_CLASSES.inspect} are allowed"
59
+ end
60
+ end
61
+
62
+ def add(other_properties)
63
+ other_properties.each do |key, value|
64
+ self[key.to_sym] = value
65
+ end
66
+ end
67
+
68
+ def to_h
69
+ data
70
+ end
71
+
72
+ private
73
+ attr_reader :data
74
+ end
75
+ end
76
+ end
@@ -0,0 +1,24 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # A +NullEncryptor+ that will raise an error when trying to encrypt data
6
+ #
7
+ # This is useful when you want to reveal ciphertexts for debugging purposes
8
+ # and you want to make sure you won't overwrite any encryptable attribute with
9
+ # the wrong content.
10
+ class ReadOnlyNullEncryptor
11
+ def encrypt(clean_text, key_provider: nil, cipher_options: {})
12
+ raise Errors::Encryption, "This encryptor is read-only"
13
+ end
14
+
15
+ def decrypt(encrypted_text, key_provider: nil, cipher_options: {})
16
+ encrypted_text
17
+ end
18
+
19
+ def encrypted?(text)
20
+ false
21
+ end
22
+ end
23
+ end
24
+ end
@@ -0,0 +1,99 @@
1
+ # frozen_string_literal: true
2
+
3
+ module ActiveRecord
4
+ module Encryption
5
+ # A container of attribute encryption options.
6
+ #
7
+ # It validates and serves attribute encryption options.
8
+ #
9
+ # See +EncryptedAttributeType+, +Context+
10
+ class Scheme
11
+ attr_accessor :previous_schemes
12
+
13
+ def initialize(key_provider: nil, key: nil, deterministic: nil, downcase: nil, ignore_case: nil,
14
+ previous_schemes: nil, **context_properties)
15
+ # Initializing all attributes to +nil+ as we want to allow a "not set" semantics so that we
16
+ # can merge schemes without overriding values with defaults. See +#merge+
17
+
18
+ @key_provider_param = key_provider
19
+ @key = key
20
+ @deterministic = deterministic
21
+ @downcase = downcase || ignore_case
22
+ @ignore_case = ignore_case
23
+ @previous_schemes_param = previous_schemes
24
+ @previous_schemes = Array.wrap(previous_schemes)
25
+ @context_properties = context_properties
26
+
27
+ validate_config!
28
+ end
29
+
30
+ def ignore_case?
31
+ @ignore_case
32
+ end
33
+
34
+ def downcase?
35
+ @downcase
36
+ end
37
+
38
+ def deterministic?
39
+ @deterministic
40
+ end
41
+
42
+ def fixed?
43
+ # by default deterministic encryption is fixed
44
+ @fixed ||= @deterministic && (!@deterministic.is_a?(Hash) || @deterministic[:fixed])
45
+ end
46
+
47
+ def key_provider
48
+ @key_provider ||= begin
49
+ validate_keys!
50
+ @key_provider_param || build_key_provider
51
+ end
52
+ end
53
+
54
+ def merge(other_scheme)
55
+ self.class.new(**to_h.merge(other_scheme.to_h))
56
+ end
57
+
58
+ def to_h
59
+ { key_provider: @key_provider_param, key: @key, deterministic: @deterministic, downcase: @downcase, ignore_case: @ignore_case,
60
+ previous_schemes: @previous_schemes_param, **@context_properties }.compact
61
+ end
62
+
63
+ def with_context(&block)
64
+ if @context_properties.present?
65
+ ActiveRecord::Encryption.with_encryption_context(**@context_properties, &block)
66
+ else
67
+ block.call
68
+ end
69
+ end
70
+
71
+ private
72
+ def validate_config!
73
+ raise Errors::Configuration, "ignore_case: can only be used with deterministic encryption" if @ignore_case && !@deterministic
74
+ raise Errors::Configuration, "key_provider: and key: can't be used simultaneously" if @key_provider_param && @key
75
+ end
76
+
77
+ def validate_keys!
78
+ validate_credential :key_derivation_salt
79
+ validate_credential :primary_key, "needs to be configured to use non-deterministic encryption" unless @deterministic
80
+ validate_credential :deterministic_key, "needs to be configured to use deterministic encryption" if @deterministic
81
+ end
82
+
83
+ def validate_credential(key, error_message = "is not configured")
84
+ unless ActiveRecord::Encryption.config.public_send(key).present?
85
+ raise Errors::Configuration, "#{key} #{error_message}. Please configure it via credential"\
86
+ "active_record_encryption.#{key} or by setting config.active_record.encryption.#{key}"
87
+ end
88
+ end
89
+
90
+ def build_key_provider
91
+ return DerivedSecretKeyProvider.new(@key) if @key.present?
92
+
93
+ if @deterministic && (deterministic_key = ActiveRecord::Encryption.config.deterministic_key)
94
+ DeterministicKeyProvider.new(deterministic_key)
95
+ end
96
+ end
97
+ end
98
+ end
99
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "active_support/core_ext/module"
4
+ require "active_support/core_ext/array"
5
+
6
+ module ActiveRecord
7
+ module Encryption
8
+ extend ActiveSupport::Autoload
9
+
10
+ eager_autoload do
11
+ autoload :Cipher
12
+ autoload :Config
13
+ autoload :Configurable
14
+ autoload :Context
15
+ autoload :Contexts
16
+ autoload :DerivedSecretKeyProvider
17
+ autoload :EncryptableRecord
18
+ autoload :EncryptedAttributeType
19
+ autoload :EncryptedFixtures
20
+ autoload :EncryptingOnlyEncryptor
21
+ autoload :DeterministicKeyProvider
22
+ autoload :Encryptor
23
+ autoload :EnvelopeEncryptionKeyProvider
24
+ autoload :Errors
25
+ autoload :ExtendedDeterministicQueries
26
+ autoload :ExtendedDeterministicUniquenessValidator
27
+ autoload :Key
28
+ autoload :KeyGenerator
29
+ autoload :KeyProvider
30
+ autoload :Message
31
+ autoload :MessageSerializer
32
+ autoload :NullEncryptor
33
+ autoload :Properties
34
+ autoload :ReadOnlyNullEncryptor
35
+ autoload :Scheme
36
+ end
37
+
38
+ class Cipher
39
+ extend ActiveSupport::Autoload
40
+
41
+ eager_autoload do
42
+ autoload :Aes256Gcm
43
+ end
44
+ end
45
+
46
+ include Configurable
47
+ include Contexts
48
+
49
+ def self.eager_load!
50
+ super
51
+
52
+ Cipher.eager_load!
53
+ end
54
+ end
55
+ end
@@ -1,5 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "active_support/core_ext/hash/slice"
3
4
  require "active_support/core_ext/object/deep_dup"
4
5
 
5
6
  module ActiveRecord
@@ -7,7 +8,7 @@ module ActiveRecord
7
8
  # but can be queried by name. Example:
8
9
  #
9
10
  # class Conversation < ActiveRecord::Base
10
- # enum status: [ :active, :archived ]
11
+ # enum :status, [ :active, :archived ]
11
12
  # end
12
13
  #
13
14
  # # conversation.update! status: 0
@@ -41,16 +42,16 @@ module ActiveRecord
41
42
  # Conversation.where(status: [:active, :archived])
42
43
  # Conversation.where.not(status: :active)
43
44
  #
44
- # Defining scopes can be disabled by setting +:_scopes+ to +false+.
45
+ # Defining scopes can be disabled by setting +:scopes+ to +false+.
45
46
  #
46
47
  # class Conversation < ActiveRecord::Base
47
- # enum status: [ :active, :archived ], _scopes: false
48
+ # enum :status, [ :active, :archived ], scopes: false
48
49
  # end
49
50
  #
50
- # You can set the default enum value by setting +:_default+, like:
51
+ # You can set the default enum value by setting +:default+, like:
51
52
  #
52
53
  # class Conversation < ActiveRecord::Base
53
- # enum status: [ :active, :archived ], _default: "active"
54
+ # enum :status, [ :active, :archived ], default: :active
54
55
  # end
55
56
  #
56
57
  # conversation = Conversation.new
@@ -60,7 +61,7 @@ module ActiveRecord
60
61
  # database integer with a hash:
61
62
  #
62
63
  # class Conversation < ActiveRecord::Base
63
- # enum status: { active: 0, archived: 1 }
64
+ # enum :status, active: 0, archived: 1
64
65
  # end
65
66
  #
66
67
  # Note that when an array is used, the implicit mapping from the values to database
@@ -85,14 +86,14 @@ module ActiveRecord
85
86
  #
86
87
  # Conversation.where("status <> ?", Conversation.statuses[:archived])
87
88
  #
88
- # You can use the +:_prefix+ or +:_suffix+ options when you need to define
89
+ # You can use the +:prefix+ or +:suffix+ options when you need to define
89
90
  # multiple enums with same values. If the passed value is +true+, the methods
90
91
  # are prefixed/suffixed with the name of the enum. It is also possible to
91
92
  # supply a custom value:
92
93
  #
93
94
  # class Conversation < ActiveRecord::Base
94
- # enum status: [:active, :archived], _suffix: true
95
- # enum comments_status: [:active, :inactive], _prefix: :comments
95
+ # enum :status, [ :active, :archived ], suffix: true
96
+ # enum :comments_status, [ :active, :inactive ], prefix: :comments
96
97
  # end
97
98
  #
98
99
  # With the above example, the bang and predicate methods along with the
@@ -103,7 +104,6 @@ module ActiveRecord
103
104
  #
104
105
  # conversation.comments_inactive!
105
106
  # conversation.comments_active? # => false
106
-
107
107
  module Enum
108
108
  def self.extended(base) # :nodoc:
109
109
  base.class_attribute(:defined_enums, instance_writer: false, default: {})
@@ -128,10 +128,8 @@ module ActiveRecord
128
128
  value.to_s
129
129
  elsif mapping.has_value?(value)
130
130
  mapping.key(value)
131
- elsif value.blank?
132
- nil
133
131
  else
134
- assert_valid_value(value)
132
+ value.presence
135
133
  end
136
134
  end
137
135
 
@@ -140,7 +138,11 @@ module ActiveRecord
140
138
  end
141
139
 
142
140
  def serialize(value)
143
- mapping.fetch(value, value)
141
+ subtype.serialize(mapping.fetch(value, value))
142
+ end
143
+
144
+ def serializable?(value, &block)
145
+ subtype.serializable?(mapping.fetch(value, value), &block)
144
146
  end
145
147
 
146
148
  def assert_valid_value(value)
@@ -155,15 +157,20 @@ module ActiveRecord
155
157
  attr_reader :name, :mapping
156
158
  end
157
159
 
158
- def enum(definitions)
159
- enum_prefix = definitions.delete(:_prefix)
160
- enum_suffix = definitions.delete(:_suffix)
161
- enum_scopes = definitions.delete(:_scopes)
160
+ def enum(name = nil, values = nil, **options)
161
+ if name
162
+ values, options = options, {} unless values
163
+ return _enum(name, values, **options)
164
+ end
165
+
166
+ definitions = options.slice!(:_prefix, :_suffix, :_scopes, :_default)
167
+ options.transform_keys! { |key| :"#{key[1..-1]}" }
162
168
 
163
- default = {}
164
- default[:default] = definitions.delete(:_default) if definitions.key?(:_default)
169
+ definitions.each { |name, values| _enum(name, values, **options) }
170
+ end
165
171
 
166
- definitions.each do |name, values|
172
+ private
173
+ def _enum(name, values, prefix: nil, suffix: nil, scopes: true, **options)
167
174
  assert_valid_enum_definition_values(values)
168
175
  # statuses = { }
169
176
  enum_values = ActiveSupport::HashWithIndifferentAccess.new
@@ -177,24 +184,19 @@ module ActiveRecord
177
184
  detect_enum_conflict!(name, name)
178
185
  detect_enum_conflict!(name, "#{name}=")
179
186
 
180
- attr = attribute_alias?(name) ? attribute_alias(name) : name
181
-
182
- decorate_attribute_type(attr, **default) do |subtype|
183
- EnumType.new(attr, enum_values, subtype)
187
+ attribute(name, **options) do |subtype|
188
+ subtype = subtype.subtype if EnumType === subtype
189
+ EnumType.new(name, enum_values, subtype)
184
190
  end
185
191
 
186
192
  value_method_names = []
187
193
  _enum_methods_module.module_eval do
188
- prefix = if enum_prefix == true
189
- "#{name}_"
190
- elsif enum_prefix
191
- "#{enum_prefix}_"
194
+ prefix = if prefix
195
+ prefix == true ? "#{name}_" : "#{prefix}_"
192
196
  end
193
197
 
194
- suffix = if enum_suffix == true
195
- "_#{name}"
196
- elsif enum_suffix
197
- "_#{enum_suffix}"
198
+ suffix = if suffix
199
+ suffix == true ? "_#{name}" : "_#{suffix}"
198
200
  end
199
201
 
200
202
  pairs = values.respond_to?(:each_pair) ? values.each_pair : values.each_with_index
@@ -204,23 +206,21 @@ module ActiveRecord
204
206
 
205
207
  value_method_name = "#{prefix}#{label}#{suffix}"
206
208
  value_method_names << value_method_name
207
- define_enum_methods(name, value_method_name, value, enum_scopes)
209
+ define_enum_methods(name, value_method_name, value, scopes)
208
210
 
209
211
  method_friendly_label = label.gsub(/[\W&&[:ascii:]]+/, "_")
210
212
  value_method_alias = "#{prefix}#{method_friendly_label}#{suffix}"
211
213
 
212
214
  if value_method_alias != value_method_name && !value_method_names.include?(value_method_alias)
213
215
  value_method_names << value_method_alias
214
- define_enum_methods(name, value_method_alias, value, enum_scopes)
216
+ define_enum_methods(name, value_method_alias, value, scopes)
215
217
  end
216
218
  end
217
219
  end
218
- detect_negative_enum_conditions!(value_method_names) if enum_scopes != false
220
+ detect_negative_enum_conditions!(value_method_names) if scopes
219
221
  enum_values.freeze
220
222
  end
221
- end
222
223
 
223
- private
224
224
  class EnumMethods < Module # :nodoc:
225
225
  def initialize(klass)
226
226
  @klass = klass
@@ -229,7 +229,7 @@ module ActiveRecord
229
229
  private
230
230
  attr_reader :klass
231
231
 
232
- def define_enum_methods(name, value_method_name, value, enum_scopes)
232
+ def define_enum_methods(name, value_method_name, value, scopes)
233
233
  # def active?() status_for_database == 0 end
234
234
  klass.send(:detect_enum_conflict!, name, "#{value_method_name}?")
235
235
  define_method("#{value_method_name}?") { public_send(:"#{name}_for_database") == value }
@@ -240,7 +240,7 @@ module ActiveRecord
240
240
 
241
241
  # scope :active, -> { where(status: 0) }
242
242
  # scope :not_active, -> { where.not(status: 0) }
243
- if enum_scopes != false
243
+ if scopes
244
244
  klass.send(:detect_enum_conflict!, name, value_method_name, true)
245
245
  klass.scope value_method_name, -> { where(name => value) }
246
246
 
@@ -260,7 +260,7 @@ module ActiveRecord
260
260
  end
261
261
 
262
262
  def assert_valid_enum_definition_values(values)
263
- unless values.is_a?(Hash) || values.all? { |v| v.is_a?(Symbol) } || values.all? { |v| v.is_a?(String) }
263
+ unless values.is_a?(Hash) || values.all?(Symbol) || values.all?(String)
264
264
  error_message = <<~MSG
265
265
  Enum values #{values} must be either a hash, an array of symbols, or an array of strings.
266
266
  MSG