activerecord 6.1.7 → 7.1.5
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/CHANGELOG.md +2030 -1020
- data/MIT-LICENSE +1 -1
- data/README.rdoc +18 -18
- data/lib/active_record/aggregations.rb +17 -14
- data/lib/active_record/association_relation.rb +1 -11
- data/lib/active_record/associations/association.rb +51 -19
- data/lib/active_record/associations/association_scope.rb +17 -12
- data/lib/active_record/associations/belongs_to_association.rb +28 -9
- data/lib/active_record/associations/belongs_to_polymorphic_association.rb +10 -2
- data/lib/active_record/associations/builder/association.rb +11 -5
- data/lib/active_record/associations/builder/belongs_to.rb +40 -14
- data/lib/active_record/associations/builder/collection_association.rb +10 -3
- data/lib/active_record/associations/builder/has_and_belongs_to_many.rb +1 -5
- 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 +6 -2
- data/lib/active_record/associations/collection_association.rb +39 -35
- data/lib/active_record/associations/collection_proxy.rb +30 -15
- data/lib/active_record/associations/disable_joins_association_scope.rb +59 -0
- data/lib/active_record/associations/foreign_association.rb +10 -3
- data/lib/active_record/associations/has_many_association.rb +28 -18
- data/lib/active_record/associations/has_many_through_association.rb +12 -7
- data/lib/active_record/associations/has_one_association.rb +20 -10
- data/lib/active_record/associations/has_one_through_association.rb +1 -1
- data/lib/active_record/associations/join_dependency/join_association.rb +3 -2
- data/lib/active_record/associations/join_dependency.rb +28 -20
- data/lib/active_record/associations/preloader/association.rb +210 -52
- data/lib/active_record/associations/preloader/batch.rb +48 -0
- data/lib/active_record/associations/preloader/branch.rb +147 -0
- data/lib/active_record/associations/preloader/through_association.rb +50 -14
- data/lib/active_record/associations/preloader.rb +50 -121
- data/lib/active_record/associations/singular_association.rb +9 -3
- data/lib/active_record/associations/through_association.rb +25 -14
- data/lib/active_record/associations.rb +446 -306
- data/lib/active_record/asynchronous_queries_tracker.rb +60 -0
- data/lib/active_record/attribute_assignment.rb +1 -3
- data/lib/active_record/attribute_methods/before_type_cast.rb +24 -2
- data/lib/active_record/attribute_methods/dirty.rb +73 -22
- data/lib/active_record/attribute_methods/primary_key.rb +78 -26
- data/lib/active_record/attribute_methods/query.rb +31 -19
- data/lib/active_record/attribute_methods/read.rb +27 -12
- data/lib/active_record/attribute_methods/serialization.rb +194 -37
- data/lib/active_record/attribute_methods/time_zone_conversion.rb +8 -3
- data/lib/active_record/attribute_methods/write.rb +12 -15
- data/lib/active_record/attribute_methods.rb +161 -40
- data/lib/active_record/attributes.rb +27 -38
- data/lib/active_record/autosave_association.rb +65 -31
- data/lib/active_record/base.rb +25 -2
- data/lib/active_record/callbacks.rb +18 -34
- data/lib/active_record/coders/column_serializer.rb +61 -0
- data/lib/active_record/coders/json.rb +1 -1
- data/lib/active_record/coders/yaml_column.rb +70 -46
- data/lib/active_record/connection_adapters/abstract/connection_handler.rb +367 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool/queue.rb +211 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool/reaper.rb +78 -0
- data/lib/active_record/connection_adapters/abstract/connection_pool.rb +113 -597
- data/lib/active_record/connection_adapters/abstract/database_limits.rb +5 -17
- data/lib/active_record/connection_adapters/abstract/database_statements.rb +172 -50
- data/lib/active_record/connection_adapters/abstract/query_cache.rb +78 -27
- data/lib/active_record/connection_adapters/abstract/quoting.rb +87 -73
- data/lib/active_record/connection_adapters/abstract/savepoints.rb +4 -3
- data/lib/active_record/connection_adapters/abstract/schema_creation.rb +21 -20
- data/lib/active_record/connection_adapters/abstract/schema_definitions.rb +186 -31
- data/lib/active_record/connection_adapters/abstract/schema_dumper.rb +14 -1
- data/lib/active_record/connection_adapters/abstract/schema_statements.rb +367 -141
- data/lib/active_record/connection_adapters/abstract/transaction.rb +281 -59
- data/lib/active_record/connection_adapters/abstract_adapter.rb +631 -150
- data/lib/active_record/connection_adapters/abstract_mysql_adapter.rb +317 -164
- data/lib/active_record/connection_adapters/column.rb +13 -0
- data/lib/active_record/connection_adapters/mysql/column.rb +1 -0
- data/lib/active_record/connection_adapters/mysql/database_statements.rb +25 -134
- data/lib/active_record/connection_adapters/mysql/quoting.rb +56 -25
- data/lib/active_record/connection_adapters/mysql/schema_creation.rb +9 -0
- data/lib/active_record/connection_adapters/mysql/schema_definitions.rb +10 -1
- data/lib/active_record/connection_adapters/mysql/schema_dumper.rb +8 -2
- data/lib/active_record/connection_adapters/mysql/schema_statements.rb +39 -14
- data/lib/active_record/connection_adapters/mysql2/database_statements.rb +151 -0
- data/lib/active_record/connection_adapters/mysql2_adapter.rb +112 -55
- data/lib/active_record/connection_adapters/pool_config.rb +20 -11
- data/lib/active_record/connection_adapters/pool_manager.rb +19 -9
- data/lib/active_record/connection_adapters/postgresql/column.rb +30 -1
- data/lib/active_record/connection_adapters/postgresql/database_statements.rb +89 -52
- data/lib/active_record/connection_adapters/postgresql/oid/array.rb +1 -1
- data/lib/active_record/connection_adapters/postgresql/oid/cidr.rb +6 -0
- 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/money.rb +3 -2
- data/lib/active_record/connection_adapters/postgresql/oid/range.rb +12 -3
- 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 +30 -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 +89 -56
- data/lib/active_record/connection_adapters/postgresql/referential_integrity.rb +28 -0
- data/lib/active_record/connection_adapters/postgresql/schema_creation.rb +92 -2
- data/lib/active_record/connection_adapters/postgresql/schema_definitions.rb +153 -3
- data/lib/active_record/connection_adapters/postgresql/schema_dumper.rb +78 -0
- data/lib/active_record/connection_adapters/postgresql/schema_statements.rb +397 -75
- data/lib/active_record/connection_adapters/postgresql/utils.rb +9 -10
- data/lib/active_record/connection_adapters/postgresql_adapter.rb +508 -246
- data/lib/active_record/connection_adapters/schema_cache.rb +319 -90
- data/lib/active_record/connection_adapters/sqlite3/column.rb +49 -0
- data/lib/active_record/connection_adapters/sqlite3/database_statements.rb +72 -53
- data/lib/active_record/connection_adapters/sqlite3/quoting.rb +37 -21
- data/lib/active_record/connection_adapters/sqlite3/schema_definitions.rb +7 -0
- data/lib/active_record/connection_adapters/sqlite3/schema_statements.rb +43 -22
- data/lib/active_record/connection_adapters/sqlite3_adapter.rb +296 -104
- data/lib/active_record/connection_adapters/statement_pool.rb +7 -0
- data/lib/active_record/connection_adapters/trilogy/database_statements.rb +99 -0
- data/lib/active_record/connection_adapters/trilogy_adapter.rb +258 -0
- data/lib/active_record/connection_adapters.rb +9 -6
- data/lib/active_record/connection_handling.rb +108 -137
- data/lib/active_record/core.rb +242 -233
- data/lib/active_record/counter_cache.rb +52 -27
- data/lib/active_record/database_configurations/connection_url_resolver.rb +3 -2
- data/lib/active_record/database_configurations/database_config.rb +21 -12
- data/lib/active_record/database_configurations/hash_config.rb +88 -16
- data/lib/active_record/database_configurations/url_config.rb +18 -12
- data/lib/active_record/database_configurations.rb +95 -59
- data/lib/active_record/delegated_type.rb +66 -20
- data/lib/active_record/deprecator.rb +7 -0
- data/lib/active_record/destroy_association_async_job.rb +4 -2
- 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/auto_filtered_parameters.rb +66 -0
- data/lib/active_record/encryption/cipher/aes256_gcm.rb +101 -0
- data/lib/active_record/encryption/cipher.rb +53 -0
- data/lib/active_record/encryption/config.rb +68 -0
- data/lib/active_record/encryption/configurable.rb +60 -0
- data/lib/active_record/encryption/context.rb +42 -0
- data/lib/active_record/encryption/contexts.rb +76 -0
- data/lib/active_record/encryption/derived_secret_key_provider.rb +18 -0
- data/lib/active_record/encryption/deterministic_key_provider.rb +14 -0
- data/lib/active_record/encryption/encryptable_record.rb +230 -0
- data/lib/active_record/encryption/encrypted_attribute_type.rb +155 -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 +157 -0
- data/lib/active_record/encryption/extended_deterministic_uniqueness_validator.rb +28 -0
- data/lib/active_record/encryption/key.rb +28 -0
- data/lib/active_record/encryption/key_generator.rb +53 -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 +92 -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 +100 -0
- data/lib/active_record/encryption.rb +58 -0
- data/lib/active_record/enum.rb +154 -63
- data/lib/active_record/errors.rb +172 -15
- data/lib/active_record/explain.rb +23 -3
- data/lib/active_record/explain_registry.rb +11 -6
- data/lib/active_record/explain_subscriber.rb +1 -1
- data/lib/active_record/fixture_set/file.rb +15 -1
- data/lib/active_record/fixture_set/model_metadata.rb +14 -4
- data/lib/active_record/fixture_set/render_context.rb +2 -0
- data/lib/active_record/fixture_set/table_row.rb +70 -14
- data/lib/active_record/fixture_set/table_rows.rb +4 -4
- data/lib/active_record/fixtures.rb +147 -86
- data/lib/active_record/future_result.rb +174 -0
- data/lib/active_record/gem_version.rb +3 -3
- data/lib/active_record/inheritance.rb +81 -29
- data/lib/active_record/insert_all.rb +135 -22
- data/lib/active_record/integration.rb +11 -10
- data/lib/active_record/internal_metadata.rb +119 -33
- data/lib/active_record/legacy_yaml_adapter.rb +2 -39
- data/lib/active_record/locking/optimistic.rb +37 -22
- data/lib/active_record/locking/pessimistic.rb +15 -6
- data/lib/active_record/log_subscriber.rb +52 -19
- data/lib/active_record/marshalling.rb +59 -0
- data/lib/active_record/message_pack.rb +124 -0
- data/lib/active_record/middleware/database_selector/resolver.rb +10 -10
- data/lib/active_record/middleware/database_selector.rb +23 -13
- data/lib/active_record/middleware/shard_selector.rb +62 -0
- data/lib/active_record/migration/command_recorder.rb +112 -14
- data/lib/active_record/migration/compatibility.rb +233 -46
- data/lib/active_record/migration/default_strategy.rb +23 -0
- data/lib/active_record/migration/execution_strategy.rb +19 -0
- data/lib/active_record/migration/join_table.rb +1 -1
- data/lib/active_record/migration/pending_migration_connection.rb +21 -0
- data/lib/active_record/migration.rb +361 -173
- data/lib/active_record/model_schema.rb +125 -101
- data/lib/active_record/nested_attributes.rb +50 -20
- data/lib/active_record/no_touching.rb +3 -3
- data/lib/active_record/normalization.rb +167 -0
- data/lib/active_record/persistence.rb +409 -88
- data/lib/active_record/promise.rb +84 -0
- data/lib/active_record/query_cache.rb +4 -22
- data/lib/active_record/query_logs.rb +174 -0
- data/lib/active_record/query_logs_formatter.rb +41 -0
- data/lib/active_record/querying.rb +29 -6
- data/lib/active_record/railtie.rb +220 -44
- data/lib/active_record/railties/controller_runtime.rb +15 -10
- data/lib/active_record/railties/databases.rake +188 -252
- data/lib/active_record/railties/job_runtime.rb +23 -0
- data/lib/active_record/readonly_attributes.rb +41 -3
- data/lib/active_record/reflection.rb +248 -81
- data/lib/active_record/relation/batches/batch_enumerator.rb +23 -7
- data/lib/active_record/relation/batches.rb +192 -63
- data/lib/active_record/relation/calculations.rb +246 -90
- data/lib/active_record/relation/delegation.rb +28 -14
- data/lib/active_record/relation/finder_methods.rb +108 -51
- data/lib/active_record/relation/merger.rb +22 -13
- data/lib/active_record/relation/predicate_builder/association_query_value.rb +31 -3
- data/lib/active_record/relation/predicate_builder/polymorphic_array_value.rb +10 -7
- data/lib/active_record/relation/predicate_builder/relation_handler.rb +5 -1
- data/lib/active_record/relation/predicate_builder.rb +27 -20
- data/lib/active_record/relation/query_attribute.rb +30 -12
- data/lib/active_record/relation/query_methods.rb +670 -129
- data/lib/active_record/relation/record_fetch_warning.rb +7 -9
- data/lib/active_record/relation/spawn_methods.rb +20 -3
- data/lib/active_record/relation/where_clause.rb +10 -19
- data/lib/active_record/relation.rb +287 -120
- data/lib/active_record/result.rb +37 -11
- data/lib/active_record/runtime_registry.rb +32 -13
- data/lib/active_record/sanitization.rb +65 -20
- data/lib/active_record/schema.rb +36 -22
- data/lib/active_record/schema_dumper.rb +73 -24
- data/lib/active_record/schema_migration.rb +68 -33
- data/lib/active_record/scoping/default.rb +72 -15
- data/lib/active_record/scoping/named.rb +5 -13
- data/lib/active_record/scoping.rb +65 -34
- data/lib/active_record/secure_password.rb +60 -0
- data/lib/active_record/secure_token.rb +21 -3
- data/lib/active_record/serialization.rb +6 -1
- data/lib/active_record/signed_id.rb +10 -8
- data/lib/active_record/store.rb +10 -10
- data/lib/active_record/suppressor.rb +13 -15
- data/lib/active_record/table_metadata.rb +16 -3
- data/lib/active_record/tasks/database_tasks.rb +251 -140
- data/lib/active_record/tasks/mysql_database_tasks.rb +16 -7
- data/lib/active_record/tasks/postgresql_database_tasks.rb +35 -26
- data/lib/active_record/tasks/sqlite_database_tasks.rb +15 -7
- data/lib/active_record/test_databases.rb +1 -1
- data/lib/active_record/test_fixtures.rb +117 -96
- data/lib/active_record/timestamp.rb +32 -19
- data/lib/active_record/token_for.rb +113 -0
- data/lib/active_record/touch_later.rb +11 -6
- data/lib/active_record/transactions.rb +48 -27
- data/lib/active_record/translation.rb +3 -3
- data/lib/active_record/type/adapter_specific_registry.rb +32 -14
- data/lib/active_record/type/hash_lookup_type_map.rb +34 -1
- data/lib/active_record/type/internal/timezone.rb +7 -2
- data/lib/active_record/type/serialized.rb +9 -5
- data/lib/active_record/type/time.rb +4 -0
- data/lib/active_record/type/type_map.rb +17 -20
- data/lib/active_record/type.rb +1 -2
- data/lib/active_record/validations/absence.rb +1 -1
- data/lib/active_record/validations/associated.rb +4 -4
- data/lib/active_record/validations/numericality.rb +5 -4
- data/lib/active_record/validations/presence.rb +5 -28
- data/lib/active_record/validations/uniqueness.rb +51 -6
- data/lib/active_record/validations.rb +8 -4
- data/lib/active_record/version.rb +1 -1
- data/lib/active_record.rb +335 -32
- data/lib/arel/attributes/attribute.rb +0 -8
- data/lib/arel/crud.rb +28 -22
- data/lib/arel/delete_manager.rb +18 -4
- data/lib/arel/errors.rb +10 -0
- data/lib/arel/factory_methods.rb +4 -0
- data/lib/arel/filter_predications.rb +9 -0
- data/lib/arel/insert_manager.rb +2 -3
- data/lib/arel/nodes/and.rb +4 -0
- data/lib/arel/nodes/binary.rb +6 -1
- data/lib/arel/nodes/bound_sql_literal.rb +61 -0
- data/lib/arel/nodes/casted.rb +1 -1
- data/lib/arel/nodes/cte.rb +36 -0
- data/lib/arel/nodes/delete_statement.rb +12 -13
- data/lib/arel/nodes/filter.rb +10 -0
- data/lib/arel/nodes/fragments.rb +35 -0
- data/lib/arel/nodes/function.rb +1 -0
- data/lib/arel/nodes/homogeneous_in.rb +1 -9
- data/lib/arel/nodes/insert_statement.rb +2 -2
- data/lib/arel/nodes/leading_join.rb +8 -0
- data/lib/arel/nodes/node.rb +111 -2
- data/lib/arel/nodes/select_core.rb +2 -2
- data/lib/arel/nodes/select_statement.rb +2 -2
- data/lib/arel/nodes/sql_literal.rb +6 -0
- data/lib/arel/nodes/table_alias.rb +4 -0
- data/lib/arel/nodes/update_statement.rb +8 -3
- data/lib/arel/nodes.rb +5 -0
- data/lib/arel/predications.rb +13 -3
- data/lib/arel/select_manager.rb +10 -4
- data/lib/arel/table.rb +9 -6
- data/lib/arel/tree_manager.rb +5 -13
- data/lib/arel/update_manager.rb +18 -4
- data/lib/arel/visitors/dot.rb +80 -90
- data/lib/arel/visitors/mysql.rb +16 -3
- data/lib/arel/visitors/postgresql.rb +0 -10
- data/lib/arel/visitors/to_sql.rb +141 -20
- data/lib/arel/visitors/visitor.rb +2 -2
- data/lib/arel.rb +18 -3
- data/lib/rails/generators/active_record/application_record/USAGE +8 -0
- data/lib/rails/generators/active_record/application_record/templates/application_record.rb.tt +1 -1
- data/lib/rails/generators/active_record/migration.rb +3 -1
- data/lib/rails/generators/active_record/model/USAGE +113 -0
- data/lib/rails/generators/active_record/model/model_generator.rb +15 -6
- 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
- data/lib/rails/generators/active_record/multi_db/multi_db_generator.rb +16 -0
- data/lib/rails/generators/active_record/multi_db/templates/multi_db.rb.tt +44 -0
- metadata +96 -16
- data/lib/active_record/connection_adapters/legacy_pool_manager.rb +0 -35
- data/lib/active_record/null_relation.rb +0 -67
@@ -0,0 +1,76 @@
|
|
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_accessor :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
|
+
|
70
|
+
def reset_default_context
|
71
|
+
self.default_context = Context.new
|
72
|
+
end
|
73
|
+
end
|
74
|
+
end
|
75
|
+
end
|
76
|
+
end
|
@@ -0,0 +1,18 @@
|
|
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, key_generator: ActiveRecord::Encryption.key_generator)
|
8
|
+
super(Array(passwords).collect { |password| derive_key_from(password, using: key_generator) })
|
9
|
+
end
|
10
|
+
|
11
|
+
private
|
12
|
+
def derive_key_from(password, using: key_generator)
|
13
|
+
secret = using.derive_key_from(password)
|
14
|
+
ActiveRecord::Encryption::Key.new(secret)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
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,230 @@
|
|
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> - A key provider to provide encryption and decryption keys. Defaults to
|
22
|
+
# +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>:deterministic</tt> - By default, encryption is not deterministic. It will use a random
|
26
|
+
# initialization vector for each encryption operation. This means that encrypting the same content
|
27
|
+
# with the same key twice will generate different ciphertexts. When set to +true+, it will generate the
|
28
|
+
# initialization vector based on the encrypted content. This means that the same content will generate
|
29
|
+
# the same ciphertexts. This enables querying encrypted text with Active Record. Deterministic encryption
|
30
|
+
# will use the oldest encryption scheme to encrypt new data by default. You can change this by setting
|
31
|
+
# <tt>deterministic: { fixed: false }</tt>. That will make it use the newest encryption scheme for encrypting new
|
32
|
+
# data.
|
33
|
+
# * <tt>:support_unencrypted_data</tt> - If `config.active_record.encryption.support_unencrypted_data` is +true+,
|
34
|
+
# you can set this to +false+ to opt out of unencrypted data support for this attribute. This is useful for
|
35
|
+
# scenarios where you encrypt one column, and want to disable support for unencrypted data without having to tweak
|
36
|
+
# the global setting.
|
37
|
+
# * <tt>:downcase</tt> - When true, it converts the encrypted content to downcase automatically. This allows to
|
38
|
+
# effectively ignore case when querying data. Notice that the case is lost. Use +:ignore_case+ if you are interested
|
39
|
+
# in preserving it.
|
40
|
+
# * <tt>:ignore_case</tt> - When true, it behaves like +:downcase+ but, it also preserves the original case in a specially
|
41
|
+
# designated column +original_<name>+. When reading the encrypted content, the version with the original case is
|
42
|
+
# served. But you can still execute queries that will ignore the case. This option can only be used when +:deterministic+
|
43
|
+
# is true.
|
44
|
+
# * <tt>:context_properties</tt> - Additional properties that will override +Context+ settings when this attribute is
|
45
|
+
# encrypted and decrypted. E.g: +encryptor:+, +cipher:+, +message_serializer:+, etc.
|
46
|
+
# * <tt>:previous</tt> - List of previous encryption schemes. When provided, they will be used in order when trying to read
|
47
|
+
# the attribute. Each entry of the list can contain the properties supported by #encrypts. Also, when deterministic
|
48
|
+
# encryption is used, they will be used to generate additional ciphertexts to check in the queries.
|
49
|
+
def encrypts(*names, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], **context_properties)
|
50
|
+
self.encrypted_attributes ||= Set.new # not using :default because the instance would be shared across classes
|
51
|
+
|
52
|
+
names.each do |name|
|
53
|
+
encrypt_attribute name, key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, downcase: downcase, ignore_case: ignore_case, previous: previous, **context_properties
|
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 attribute_name.start_with?(ORIGINAL_ATTRIBUTE_PREFIX)
|
67
|
+
end
|
68
|
+
|
69
|
+
private
|
70
|
+
def scheme_for(key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], **context_properties)
|
71
|
+
ActiveRecord::Encryption::Scheme.new(key_provider: key_provider, key: key, deterministic: deterministic,
|
72
|
+
support_unencrypted_data: support_unencrypted_data, 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.filter_map do |previous_scheme|
|
80
|
+
scheme.merge(previous_scheme) if scheme.compatible_with?(previous_scheme)
|
81
|
+
end
|
82
|
+
end
|
83
|
+
|
84
|
+
def encrypt_attribute(name, key_provider: nil, key: nil, deterministic: false, support_unencrypted_data: nil, downcase: false, ignore_case: false, previous: [], **context_properties)
|
85
|
+
encrypted_attributes << name.to_sym
|
86
|
+
|
87
|
+
attribute name do |cast_type|
|
88
|
+
scheme = scheme_for key_provider: key_provider, key: key, deterministic: deterministic, support_unencrypted_data: support_unencrypted_data, \
|
89
|
+
downcase: downcase, ignore_case: ignore_case, previous: previous, **context_properties
|
90
|
+
|
91
|
+
ActiveRecord::Encryption::EncryptedAttributeType.new(scheme: scheme, cast_type: cast_type, default: columns_hash[name.to_s]&.default)
|
92
|
+
end
|
93
|
+
|
94
|
+
preserve_original_encrypted(name) if ignore_case
|
95
|
+
ActiveRecord::Encryption.encrypted_attribute_was_declared(self, name)
|
96
|
+
end
|
97
|
+
|
98
|
+
def preserve_original_encrypted(name)
|
99
|
+
original_attribute_name = "#{ORIGINAL_ATTRIBUTE_PREFIX}#{name}".to_sym
|
100
|
+
|
101
|
+
if !ActiveRecord::Encryption.config.support_unencrypted_data && !column_names.include?(original_attribute_name.to_s)
|
102
|
+
raise Errors::Configuration, "To use :ignore_case for '#{name}' you must create an additional column named '#{original_attribute_name}'"
|
103
|
+
end
|
104
|
+
|
105
|
+
encrypts original_attribute_name
|
106
|
+
override_accessors_to_preserve_original name, original_attribute_name
|
107
|
+
end
|
108
|
+
|
109
|
+
def override_accessors_to_preserve_original(name, original_attribute_name)
|
110
|
+
include(Module.new do
|
111
|
+
define_method name do
|
112
|
+
if ((value = super()) && encrypted_attribute?(name)) || !ActiveRecord::Encryption.config.support_unencrypted_data
|
113
|
+
send(original_attribute_name)
|
114
|
+
else
|
115
|
+
value
|
116
|
+
end
|
117
|
+
end
|
118
|
+
|
119
|
+
define_method "#{name}=" do |value|
|
120
|
+
self.send "#{original_attribute_name}=", value
|
121
|
+
super(value)
|
122
|
+
end
|
123
|
+
end)
|
124
|
+
end
|
125
|
+
|
126
|
+
def load_schema!
|
127
|
+
super
|
128
|
+
|
129
|
+
add_length_validation_for_encrypted_columns if ActiveRecord::Encryption.config.validate_column_size
|
130
|
+
end
|
131
|
+
|
132
|
+
def add_length_validation_for_encrypted_columns
|
133
|
+
encrypted_attributes&.each do |attribute_name|
|
134
|
+
validate_column_size attribute_name
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def validate_column_size(attribute_name)
|
139
|
+
if limit = columns_hash[attribute_name.to_s]&.limit
|
140
|
+
validates_length_of attribute_name, maximum: limit
|
141
|
+
end
|
142
|
+
end
|
143
|
+
end
|
144
|
+
|
145
|
+
# Returns whether a given attribute is encrypted or not.
|
146
|
+
def encrypted_attribute?(attribute_name)
|
147
|
+
name = attribute_name.to_s
|
148
|
+
name = self.class.attribute_aliases[name] || name
|
149
|
+
|
150
|
+
return false unless self.class.encrypted_attributes&.include? name.to_sym
|
151
|
+
|
152
|
+
type = type_for_attribute(name)
|
153
|
+
type.encrypted? read_attribute_before_type_cast(name)
|
154
|
+
end
|
155
|
+
|
156
|
+
# Returns the ciphertext for +attribute_name+.
|
157
|
+
def ciphertext_for(attribute_name)
|
158
|
+
if encrypted_attribute?(attribute_name)
|
159
|
+
read_attribute_before_type_cast(attribute_name)
|
160
|
+
else
|
161
|
+
read_attribute_for_database(attribute_name)
|
162
|
+
end
|
163
|
+
end
|
164
|
+
|
165
|
+
# Encrypts all the encryptable attributes and saves the changes.
|
166
|
+
def encrypt
|
167
|
+
encrypt_attributes if has_encrypted_attributes?
|
168
|
+
end
|
169
|
+
|
170
|
+
# Decrypts all the encryptable attributes and saves the changes.
|
171
|
+
def decrypt
|
172
|
+
decrypt_attributes if has_encrypted_attributes?
|
173
|
+
end
|
174
|
+
|
175
|
+
private
|
176
|
+
ORIGINAL_ATTRIBUTE_PREFIX = "original_"
|
177
|
+
|
178
|
+
def _create_record(attribute_names = self.attribute_names)
|
179
|
+
if has_encrypted_attributes?
|
180
|
+
# Always persist encrypted attributes, because an attribute might be
|
181
|
+
# encrypting a column default value.
|
182
|
+
attribute_names |= self.class.encrypted_attributes.map(&:to_s)
|
183
|
+
end
|
184
|
+
super
|
185
|
+
end
|
186
|
+
|
187
|
+
def encrypt_attributes
|
188
|
+
validate_encryption_allowed
|
189
|
+
|
190
|
+
update_columns build_encrypt_attribute_assignments
|
191
|
+
end
|
192
|
+
|
193
|
+
def decrypt_attributes
|
194
|
+
validate_encryption_allowed
|
195
|
+
|
196
|
+
decrypt_attribute_assignments = build_decrypt_attribute_assignments
|
197
|
+
ActiveRecord::Encryption.without_encryption { update_columns decrypt_attribute_assignments }
|
198
|
+
end
|
199
|
+
|
200
|
+
def validate_encryption_allowed
|
201
|
+
raise ActiveRecord::Encryption::Errors::Configuration, "can't be modified because it is encrypted" if ActiveRecord::Encryption.context.frozen_encryption?
|
202
|
+
end
|
203
|
+
|
204
|
+
def has_encrypted_attributes?
|
205
|
+
self.class.encrypted_attributes.present?
|
206
|
+
end
|
207
|
+
|
208
|
+
def build_encrypt_attribute_assignments
|
209
|
+
Array(self.class.encrypted_attributes).index_with do |attribute_name|
|
210
|
+
self[attribute_name]
|
211
|
+
end
|
212
|
+
end
|
213
|
+
|
214
|
+
def build_decrypt_attribute_assignments
|
215
|
+
Array(self.class.encrypted_attributes).to_h do |attribute_name|
|
216
|
+
type = type_for_attribute(attribute_name)
|
217
|
+
encrypted_value = ciphertext_for(attribute_name)
|
218
|
+
new_value = type.deserialize(encrypted_value)
|
219
|
+
[attribute_name, new_value]
|
220
|
+
end
|
221
|
+
end
|
222
|
+
|
223
|
+
def cant_modify_encrypted_attributes_when_frozen
|
224
|
+
self.class&.encrypted_attributes.each do |attribute|
|
225
|
+
errors.add(attribute.to_sym, "can't be modified because it is encrypted") if changed_attributes.include?(attribute)
|
226
|
+
end
|
227
|
+
end
|
228
|
+
end
|
229
|
+
end
|
230
|
+
end
|
@@ -0,0 +1,155 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# An ActiveModel::Type::Value that encrypts/decrypts strings of text.
|
6
|
+
#
|
7
|
+
# This is the central piece that connects the encryption system with +encrypts+ declarations in the
|
8
|
+
# model classes. Whenever you declare an attribute as encrypted, it configures an +EncryptedAttributeType+
|
9
|
+
# for that attribute.
|
10
|
+
class EncryptedAttributeType < ::ActiveRecord::Type::Text
|
11
|
+
include ActiveModel::Type::Helpers::Mutable
|
12
|
+
|
13
|
+
attr_reader :scheme, :cast_type
|
14
|
+
|
15
|
+
delegate :key_provider, :downcase?, :deterministic?, :previous_schemes, :with_context, :fixed?, to: :scheme
|
16
|
+
delegate :accessor, to: :cast_type
|
17
|
+
|
18
|
+
# === Options
|
19
|
+
#
|
20
|
+
# * <tt>:scheme</tt> - A +Scheme+ with the encryption properties for this attribute.
|
21
|
+
# * <tt>:cast_type</tt> - A type that will be used to serialize (before encrypting) and deserialize
|
22
|
+
# (after decrypting). ActiveModel::Type::String by default.
|
23
|
+
def initialize(scheme:, cast_type: ActiveModel::Type::String.new, previous_type: false, default: nil)
|
24
|
+
super()
|
25
|
+
@scheme = scheme
|
26
|
+
@cast_type = cast_type
|
27
|
+
@previous_type = previous_type
|
28
|
+
@default = default
|
29
|
+
end
|
30
|
+
|
31
|
+
def cast(value)
|
32
|
+
cast_type.cast(value)
|
33
|
+
end
|
34
|
+
|
35
|
+
def deserialize(value)
|
36
|
+
cast_type.deserialize decrypt(value)
|
37
|
+
end
|
38
|
+
|
39
|
+
def serialize(value)
|
40
|
+
if serialize_with_oldest?
|
41
|
+
serialize_with_oldest(value)
|
42
|
+
else
|
43
|
+
serialize_with_current(value)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
def encrypted?(value)
|
48
|
+
with_context { encryptor.encrypted? value }
|
49
|
+
end
|
50
|
+
|
51
|
+
def changed_in_place?(raw_old_value, new_value)
|
52
|
+
old_value = raw_old_value.nil? ? nil : deserialize(raw_old_value)
|
53
|
+
old_value != new_value
|
54
|
+
end
|
55
|
+
|
56
|
+
def previous_types # :nodoc:
|
57
|
+
@previous_types ||= {} # Memoizing on support_unencrypted_data so that we can tweak it during tests
|
58
|
+
@previous_types[support_unencrypted_data?] ||= build_previous_types_for(previous_schemes_including_clean_text)
|
59
|
+
end
|
60
|
+
|
61
|
+
def support_unencrypted_data?
|
62
|
+
ActiveRecord::Encryption.config.support_unencrypted_data && scheme.support_unencrypted_data? && !previous_type?
|
63
|
+
end
|
64
|
+
|
65
|
+
private
|
66
|
+
def previous_schemes_including_clean_text
|
67
|
+
previous_schemes.including((clean_text_scheme if support_unencrypted_data?)).compact
|
68
|
+
end
|
69
|
+
|
70
|
+
def previous_types_without_clean_text
|
71
|
+
@previous_types_without_clean_text ||= build_previous_types_for(previous_schemes)
|
72
|
+
end
|
73
|
+
|
74
|
+
def build_previous_types_for(schemes)
|
75
|
+
schemes.collect do |scheme|
|
76
|
+
EncryptedAttributeType.new(scheme: scheme, previous_type: true)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
def previous_type?
|
81
|
+
@previous_type
|
82
|
+
end
|
83
|
+
|
84
|
+
def decrypt(value)
|
85
|
+
with_context do
|
86
|
+
unless value.nil?
|
87
|
+
if @default && @default == value
|
88
|
+
value
|
89
|
+
else
|
90
|
+
encryptor.decrypt(value, **decryption_options)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
rescue ActiveRecord::Encryption::Errors::Base => error
|
95
|
+
if previous_types_without_clean_text.blank?
|
96
|
+
handle_deserialize_error(error, value)
|
97
|
+
else
|
98
|
+
try_to_deserialize_with_previous_encrypted_types(value)
|
99
|
+
end
|
100
|
+
end
|
101
|
+
|
102
|
+
def try_to_deserialize_with_previous_encrypted_types(value)
|
103
|
+
previous_types.each.with_index do |type, index|
|
104
|
+
break type.deserialize(value)
|
105
|
+
rescue ActiveRecord::Encryption::Errors::Base => error
|
106
|
+
handle_deserialize_error(error, value) if index == previous_types.length - 1
|
107
|
+
end
|
108
|
+
end
|
109
|
+
|
110
|
+
def handle_deserialize_error(error, value)
|
111
|
+
if error.is_a?(Errors::Decryption) && support_unencrypted_data?
|
112
|
+
value
|
113
|
+
else
|
114
|
+
raise error
|
115
|
+
end
|
116
|
+
end
|
117
|
+
|
118
|
+
def serialize_with_oldest?
|
119
|
+
@serialize_with_oldest ||= fixed? && previous_types_without_clean_text.present?
|
120
|
+
end
|
121
|
+
|
122
|
+
def serialize_with_oldest(value)
|
123
|
+
previous_types.first.serialize(value)
|
124
|
+
end
|
125
|
+
|
126
|
+
def serialize_with_current(value)
|
127
|
+
casted_value = cast_type.serialize(value)
|
128
|
+
casted_value = casted_value&.downcase if downcase?
|
129
|
+
encrypt(casted_value.to_s) unless casted_value.nil?
|
130
|
+
end
|
131
|
+
|
132
|
+
def encrypt(value)
|
133
|
+
with_context do
|
134
|
+
encryptor.encrypt(value, **encryption_options)
|
135
|
+
end
|
136
|
+
end
|
137
|
+
|
138
|
+
def encryptor
|
139
|
+
ActiveRecord::Encryption.encryptor
|
140
|
+
end
|
141
|
+
|
142
|
+
def encryption_options
|
143
|
+
{ key_provider: key_provider, cipher_options: { deterministic: deterministic? } }.compact
|
144
|
+
end
|
145
|
+
|
146
|
+
def decryption_options
|
147
|
+
{ key_provider: key_provider }.compact
|
148
|
+
end
|
149
|
+
|
150
|
+
def clean_text_scheme
|
151
|
+
@clean_text_scheme ||= ActiveRecord::Encryption::Scheme.new(downcase: downcase?, encryptor: ActiveRecord::Encryption::NullEncryptor.new)
|
152
|
+
end
|
153
|
+
end
|
154
|
+
end
|
155
|
+
end
|
@@ -0,0 +1,38 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
module EncryptedFixtures
|
6
|
+
def initialize(fixture, model_class)
|
7
|
+
@clean_values = {}
|
8
|
+
encrypt_fixture_data(fixture, model_class)
|
9
|
+
process_preserved_original_columns(fixture, model_class)
|
10
|
+
super
|
11
|
+
end
|
12
|
+
|
13
|
+
private
|
14
|
+
def encrypt_fixture_data(fixture, model_class)
|
15
|
+
model_class&.encrypted_attributes&.each do |attribute_name|
|
16
|
+
if clean_value = fixture[attribute_name.to_s]
|
17
|
+
@clean_values[attribute_name.to_s] = clean_value
|
18
|
+
|
19
|
+
type = model_class.type_for_attribute(attribute_name)
|
20
|
+
encrypted_value = type.serialize(clean_value)
|
21
|
+
fixture[attribute_name.to_s] = encrypted_value
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
def process_preserved_original_columns(fixture, model_class)
|
27
|
+
model_class&.encrypted_attributes&.each do |attribute_name|
|
28
|
+
if source_attribute_name = model_class.source_attribute_from_preserved_attribute(attribute_name)
|
29
|
+
clean_value = @clean_values[source_attribute_name.to_s]
|
30
|
+
type = model_class.type_for_attribute(attribute_name)
|
31
|
+
encrypted_value = type.serialize(clean_value)
|
32
|
+
fixture[attribute_name.to_s] = encrypted_value
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
@@ -0,0 +1,12 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module ActiveRecord
|
4
|
+
module Encryption
|
5
|
+
# An encryptor that can encrypt data but can't decrypt it.
|
6
|
+
class EncryptingOnlyEncryptor < Encryptor
|
7
|
+
def decrypt(encrypted_text, key_provider: nil, cipher_options: {})
|
8
|
+
encrypted_text
|
9
|
+
end
|
10
|
+
end
|
11
|
+
end
|
12
|
+
end
|