Skip to content

Commit e622334

Browse files
authored
Merge branch 'main' into support_unencrypted_data-true
2 parents 2f07858 + 32d9623 commit e622334

File tree

30 files changed

+500
-83
lines changed

30 files changed

+500
-83
lines changed

activerecord/CHANGELOG.md

Lines changed: 84 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,6 +21,90 @@
2121

2222
*Alex Ghiculescu*
2323

24+
* Model generator no longer needs a database connection to validate column types.
25+
26+
*Mike Dalessio*
27+
28+
* Allow signed ID verifiers to be configurable via `Rails.application.message_verifiers`
29+
30+
Prior to this change, the primary way to configure signed ID verifiers was
31+
to set `signed_id_verifier` on each model class:
32+
33+
```ruby
34+
Post.signed_id_verifier = ActiveSupport::MessageVerifier.new(...)
35+
Comment.signed_id_verifier = ActiveSupport::MessageVerifier.new(...)
36+
```
37+
38+
And if the developer did not set `signed_id_verifier`, a verifier would be
39+
instantiated with a secret derived from `secret_key_base` and the following
40+
options:
41+
42+
```ruby
43+
{ digest: "SHA256", serializer: JSON, url_safe: true }
44+
```
45+
46+
Thus it was cumbersome to rotate configuration for all verifiers.
47+
48+
This change defines a new Rails config: [`config.active_record.use_legacy_signed_id_verifier`][].
49+
The default value is `:generate_and_verify`, which preserves the previous
50+
behavior. However, when set to `:verify`, signed ID verifiers will use
51+
configuration from `Rails.application.message_verifiers` (specifically,
52+
`Rails.application.message_verifiers["active_record/signed_id"]`) to
53+
generate and verify signed IDs, but will also verify signed IDs using the
54+
older configuration.
55+
56+
To avoid complication, the new behavior only applies when `signed_id_verifier_secret`
57+
is not set on a model class or any of its ancestors. Additionally,
58+
`signed_id_verifier_secret` is now deprecated. If you are currently setting
59+
`signed_id_verifier_secret` on a model class, you can set `signed_id_verifier`
60+
instead:
61+
62+
```ruby
63+
# BEFORE
64+
Post.signed_id_verifier_secret = "my secret"
65+
66+
# AFTER
67+
Post.signed_id_verifier = ActiveSupport::MessageVerifier.new("my secret", digest: "SHA256", serializer: JSON, url_safe: true)
68+
```
69+
70+
To ease migration, `signed_id_verifier` has also been changed to behave as a
71+
`class_attribute` (i.e. inheritable), but _only when `signed_id_verifier_secret`
72+
is not set_:
73+
74+
```ruby
75+
# BEFORE
76+
ActiveRecord::Base.signed_id_verifier = ActiveSupport::MessageVerifier.new(...)
77+
Post.signed_id_verifier == ActiveRecord::Base.signed_id_verifier # => false
78+
79+
# AFTER
80+
ActiveRecord::Base.signed_id_verifier = ActiveSupport::MessageVerifier.new(...)
81+
Post.signed_id_verifier == ActiveRecord::Base.signed_id_verifier # => true
82+
83+
Post.signed_id_verifier_secret = "my secret" # => deprecation warning
84+
Post.signed_id_verifier == ActiveRecord::Base.signed_id_verifier # => false
85+
```
86+
87+
Note, however, that it is recommended to eventually migrate from
88+
model-specific verifiers to a unified configuration managed by
89+
`Rails.application.message_verifiers`. `ActiveSupport::MessageVerifier#rotate`
90+
can facilitate that transition. For example:
91+
92+
```ruby
93+
# BEFORE
94+
# Generate and verify signed Post IDs using Post-specific configuration
95+
Post.signed_id_verifier = ActiveSupport::MessageVerifier.new("post secret", ...)
96+
97+
# AFTER
98+
# Generate and verify signed Post IDs using the unified configuration
99+
Post.signed_id_verifier = Post.signed_id_verifier.dup
100+
# Fall back to Post-specific configuration when verifying signed IDs
101+
Post.signed_id_verifier.rotate("post secret", ...)
102+
```
103+
104+
[`config.active_record.use_legacy_signed_id_verifier`]: https://guides.rubyonrails.org/v8.1/configuring.html#config-active-record-use-legacy-signed-id-verifier
105+
106+
*Ali Sepehri*, *Jonathan Hefner*
107+
24108
* Prepend `extra_flags` in postgres' `structure_load`
25109
26110
When specifying `structure_load_flags` with a postgres adapter, the flags

activerecord/lib/active_record.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -506,6 +506,13 @@ def self.marshalling_format_version=(value)
506506
}
507507
)
508508

509+
##
510+
# :singleton-method: message_verifiers
511+
#
512+
# ActiveSupport::MessageVerifiers instance for Active Record. If you are using
513+
# Rails, this will be set to +Rails.application.message_verifiers+.
514+
singleton_class.attr_accessor :message_verifiers
515+
509516
def self.eager_load!
510517
super
511518
ActiveRecord::Locking.eager_load!

activerecord/lib/active_record/connection_adapters/abstract_adapter.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -260,7 +260,11 @@ def to_s
260260
end
261261

262262
def valid_type?(type) # :nodoc:
263-
!native_database_types[type].nil?
263+
self.class.valid_type?(type)
264+
end
265+
266+
def native_database_types # :nodoc:
267+
self.class.native_database_types
264268
end
265269

266270
# this method must only be called while holding connection pool's mutex
@@ -886,6 +890,10 @@ def extended_type_map(default_timezone:) # :nodoc:
886890
end
887891
end
888892

893+
def valid_type?(type) # :nodoc:
894+
!native_database_types[type].nil?
895+
end
896+
889897
private
890898
def initialize_type_map(m)
891899
register_class_with_limit m, %r(boolean)i, Type::Boolean

activerecord/lib/active_record/connection_adapters/abstract_mysql_adapter.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -81,6 +81,10 @@ def dbconsole(config, options = {})
8181

8282
find_cmd_and_exec(ActiveRecord.database_cli[:mysql], *args)
8383
end
84+
85+
def native_database_types # :nodoc:
86+
NATIVE_DATABASE_TYPES
87+
end
8488
end
8589

8690
def get_database_version # :nodoc:
@@ -196,10 +200,6 @@ def release_advisory_lock(lock_name) # :nodoc:
196200
query_value("SELECT RELEASE_LOCK(#{quote(lock_name.to_s)})") == 1
197201
end
198202

199-
def native_database_types
200-
NATIVE_DATABASE_TYPES
201-
end
202-
203203
def index_algorithms
204204
{
205205
default: "ALGORITHM = DEFAULT",

activerecord/lib/active_record/connection_adapters/postgresql_adapter.rb

Lines changed: 0 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -397,10 +397,6 @@ def discard! # :nodoc:
397397
@raw_connection = nil
398398
end
399399

400-
def native_database_types # :nodoc:
401-
self.class.native_database_types
402-
end
403-
404400
def self.native_database_types # :nodoc:
405401
@native_database_types ||= begin
406402
types = NATIVE_DATABASE_TYPES.dup

activerecord/lib/active_record/connection_adapters/sqlite3_adapter.rb

Lines changed: 4 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -66,6 +66,10 @@ def dbconsole(config, options = {})
6666

6767
find_cmd_and_exec(ActiveRecord.database_cli[:sqlite], *args)
6868
end
69+
70+
def native_database_types # :nodoc:
71+
NATIVE_DATABASE_TYPES
72+
end
6973
end
7074

7175
include SQLite3::Quoting
@@ -258,10 +262,6 @@ def supports_index_sort_order?
258262
true
259263
end
260264

261-
def native_database_types # :nodoc:
262-
NATIVE_DATABASE_TYPES
263-
end
264-
265265
# Returns the current database encoding format as a string, e.g. 'UTF-8'
266266
def encoding
267267
any_raw_connection.encoding.to_s

activerecord/lib/active_record/railtie.rb

Lines changed: 14 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,7 @@ class Railtie < Rails::Railtie # :nodoc:
3838
config.active_record.raise_on_assign_to_attr_readonly = false
3939
config.active_record.belongs_to_required_validates_foreign_key = true
4040
config.active_record.generate_secure_token_on = :create
41+
config.active_record.use_legacy_signed_id_verifier = :generate_and_verify
4142

4243
config.active_record.queues = ActiveSupport::InheritableOptions.new
4344

@@ -233,6 +234,7 @@ class Railtie < Rails::Railtie # :nodoc:
233234
:check_schema_cache_dump_version,
234235
:use_schema_cache_dump,
235236
:postgresql_adapter_decode_dates,
237+
:use_legacy_signed_id_verifier,
236238
)
237239

238240
configs_used_in_other_initializers.each do |k, v|
@@ -319,9 +321,18 @@ class Railtie < Rails::Railtie # :nodoc:
319321
end
320322
end
321323

322-
initializer "active_record.set_signed_id_verifier_secret" do
323-
ActiveSupport.on_load(:active_record) do
324-
self.signed_id_verifier_secret ||= -> { Rails.application.key_generator.generate_key("active_record/signed_id") }
324+
initializer "active_record.configure_message_verifiers" do |app|
325+
ActiveRecord.message_verifiers = app.message_verifiers
326+
327+
use_legacy_signed_id_verifier = app.config.active_record.use_legacy_signed_id_verifier
328+
legacy_options = { digest: "SHA256", serializer: JSON, url_safe: true }
329+
330+
if use_legacy_signed_id_verifier == :generate_and_verify
331+
app.message_verifiers.prepend { |salt| legacy_options if salt == "active_record/signed_id" }
332+
elsif use_legacy_signed_id_verifier == :verify
333+
app.message_verifiers.rotate { |salt| legacy_options if salt == "active_record/signed_id" }
334+
elsif use_legacy_signed_id_verifier
335+
raise ArgumentError, "Unrecognized value for config.active_record.use_legacy_signed_id_verifier: #{use_legacy_signed_id_verifier.inspect}"
325336
end
326337
end
327338

activerecord/lib/active_record/relation.rb

Lines changed: 12 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -272,7 +272,12 @@ def find_or_create_by!(attributes, &block)
272272
# such situation.
273273
def create_or_find_by(attributes, &block)
274274
with_connection do |connection|
275-
transaction(requires_new: true) { create(attributes, &block) }
275+
record = nil
276+
transaction(requires_new: true) do
277+
record = new(attributes, &block)
278+
record.save || raise(ActiveRecord::Rollback)
279+
end
280+
record
276281
rescue ActiveRecord::RecordNotUnique
277282
if connection.transaction_open?
278283
where(attributes).lock.find_by!(attributes)
@@ -287,7 +292,12 @@ def create_or_find_by(attributes, &block)
287292
# is raised if the created record is invalid.
288293
def create_or_find_by!(attributes, &block)
289294
with_connection do |connection|
290-
transaction(requires_new: true) { create!(attributes, &block) }
295+
record = nil
296+
transaction(requires_new: true) do
297+
record = new(attributes, &block)
298+
record.save! || raise(ActiveRecord::Rollback)
299+
end
300+
record
291301
rescue ActiveRecord::RecordNotUnique
292302
if connection.transaction_open?
293303
where(attributes).lock.find_by!(attributes)

activerecord/lib/active_record/signed_id.rb

Lines changed: 37 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -6,11 +6,27 @@ module SignedId
66
extend ActiveSupport::Concern
77

88
included do
9+
class_attribute :_signed_id_verifier, instance_accessor: false, instance_predicate: false
10+
911
##
1012
# :singleton-method:
1113
# Set the secret used for the signed id verifier instance when using Active Record outside of \Rails.
1214
# Within \Rails, this is automatically set using the \Rails application key generator.
1315
class_attribute :signed_id_verifier_secret, instance_writer: false
16+
module DeprecateSignedIdVerifierSecret
17+
def signed_id_verifier_secret=(secret)
18+
ActiveRecord.deprecator.warn(<<~MSG)
19+
ActiveRecord::Base.signed_id_verifier_secret is deprecated and will be removed in the future.
20+
21+
If the secret is model-specific, set Model.signed_id_verifier instead.
22+
23+
Otherwise, configure Rails.application.message_verifiers (or ActiveRecord.message_verifiers) with the secret.
24+
MSG
25+
26+
super
27+
end
28+
end
29+
singleton_class.prepend DeprecateSignedIdVerifierSecret
1430
end
1531

1632
module RelationMethods # :nodoc:
@@ -77,28 +93,38 @@ def find_signed!(signed_id, purpose: nil, on_rotation: nil)
7793
end
7894
end
7995

80-
# The verifier instance that all signed ids are generated and verified from. By default, it'll be initialized
81-
# with the class-level +signed_id_verifier_secret+, which within Rails comes from
82-
# {Rails.application.key_generator}[rdoc-ref:Rails::Application#key_generator].
83-
# By default, it's SHA256 for the digest and JSON for the serialization.
8496
def signed_id_verifier
85-
@signed_id_verifier ||= begin
86-
secret = signed_id_verifier_secret
87-
secret = secret.call if secret.respond_to?(:call)
97+
if signed_id_verifier_secret
98+
@signed_id_verifier ||= begin
99+
secret = signed_id_verifier_secret
100+
secret = secret.call if secret.respond_to?(:call)
101+
102+
if secret.nil?
103+
raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed IDs"
104+
end
88105

89-
if secret.nil?
90-
raise ArgumentError, "You must set ActiveRecord::Base.signed_id_verifier_secret to use signed ids"
91-
else
92106
ActiveSupport::MessageVerifier.new secret, digest: "SHA256", serializer: JSON, url_safe: true
93107
end
108+
else
109+
return _signed_id_verifier if _signed_id_verifier
110+
111+
if ActiveRecord.message_verifiers.nil?
112+
raise "You must set ActiveRecord.message_verifiers to use signed IDs"
113+
end
114+
115+
ActiveRecord.message_verifiers["active_record/signed_id"]
94116
end
95117
end
96118

97119
# Allows you to pass in a custom verifier used for the signed ids. This also allows you to use different
98120
# verifiers for different classes. This is also helpful if you need to rotate keys, as you can prepare
99121
# your custom verifier for that in advance. See ActiveSupport::MessageVerifier for details.
100122
def signed_id_verifier=(verifier)
101-
@signed_id_verifier = verifier
123+
if signed_id_verifier_secret
124+
@signed_id_verifier = verifier
125+
else
126+
self._signed_id_verifier = verifier
127+
end
102128
end
103129

104130
# :nodoc:

activerecord/test/cases/adapter_test.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -48,11 +48,13 @@ def test_create_record_with_pk_as_zero
4848
def test_valid_column
4949
@connection.native_database_types.each_key do |type|
5050
assert @connection.valid_type?(type)
51+
assert @connection.class.valid_type?(type)
5152
end
5253
end
5354

5455
def test_invalid_column
5556
assert_not @connection.valid_type?(:foobar)
57+
assert_not @connection.class.valid_type?(:foobar)
5658
end
5759

5860
def test_tables

0 commit comments

Comments
 (0)