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,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
|
data/lib/active_record/enum.rb
CHANGED
@@ -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
|
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 +:
|
45
|
+
# Defining scopes can be disabled by setting +:scopes+ to +false+.
|
45
46
|
#
|
46
47
|
# class Conversation < ActiveRecord::Base
|
47
|
-
# enum status
|
48
|
+
# enum :status, [ :active, :archived ], scopes: false
|
48
49
|
# end
|
49
50
|
#
|
50
|
-
# You can set the default enum value by setting +:
|
51
|
+
# You can set the default enum value by setting +:default+, like:
|
51
52
|
#
|
52
53
|
# class Conversation < ActiveRecord::Base
|
53
|
-
# enum status
|
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
|
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 +:
|
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
|
95
|
-
# enum comments_status
|
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
|
-
|
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(
|
159
|
-
|
160
|
-
|
161
|
-
|
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
|
-
|
164
|
-
|
169
|
+
definitions.each { |name, values| _enum(name, values, **options) }
|
170
|
+
end
|
165
171
|
|
166
|
-
|
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
|
-
|
181
|
-
|
182
|
-
|
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
|
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
|
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,
|
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,
|
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
|
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,
|
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
|
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?
|
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
|