Skip to content

Commit 324880c

Browse files
Merge pull request rails#46452 from jonathanhefner/parameter_filter-precompile_filters
Add `AS::ParameterFilter.precompile_filters`
2 parents cef42e6 + e5693c5 commit 324880c

File tree

8 files changed

+134
-2
lines changed

8 files changed

+134
-2
lines changed

activesupport/lib/active_support/parameter_filter.rb

Lines changed: 29 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
# frozen_string_literal: true
22

33
require "active_support/core_ext/object/duplicable"
4+
require "active_support/core_ext/array/extract"
45

56
module ActiveSupport
67
# +ParameterFilter+ allows you to specify keys for sensitive data from
@@ -32,6 +33,34 @@ module ActiveSupport
3233
class ParameterFilter
3334
FILTERED = "[FILTERED]" # :nodoc:
3435

36+
# Precompiles an array of filters that otherwise would be passed directly to
37+
# #initialize. Depending on the quantity and types of filters,
38+
# precompilation can improve filtering performance, especially in the case
39+
# where the ParameterFilter instance itself cannot be retained (but the
40+
# precompiled filters can be retained).
41+
#
42+
# filters = [/foo/, :bar, "nested.baz", /nested\.qux/]
43+
#
44+
# precompiled = ActiveSupport::ParameterFilter.precompile_filters(filters)
45+
# # => [/(?-mix:foo)|(?i:bar)/, /(?i:nested\.baz)|(?-mix:nested\.qux)/]
46+
#
47+
# ActiveSupport::ParameterFilter.new(precompiled)
48+
#
49+
def self.precompile_filters(filters)
50+
filters, patterns = filters.partition { |filter| filter.is_a?(Proc) }
51+
52+
patterns.map! do |pattern|
53+
pattern.is_a?(Regexp) ? pattern : "(?i:#{Regexp.escape pattern.to_s})"
54+
end
55+
56+
deep_patterns = patterns.extract! { |pattern| pattern.to_s.include?("\\.") }
57+
58+
filters << Regexp.new(patterns.join("|")) if patterns.any?
59+
filters << Regexp.new(deep_patterns.join("|")) if deep_patterns.any?
60+
61+
filters
62+
end
63+
3564
# Create instance with given filters. Supported type of filters are +String+, +Regexp+, and +Proc+.
3665
# Other types of filters are treated as +String+ using +to_s+.
3766
# For +Proc+ filters, key, value, and optional original hash is passed to block arguments.

activesupport/test/parameter_filter_test.rb

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -121,4 +121,28 @@ class ParameterFilterTest < ActiveSupport::TestCase
121121
assert_equal after_filter, parameter_filter.filter(before_filter)
122122
end
123123
end
124+
125+
test "precompile_filters" do
126+
patterns = [/A.a/, /b.B/i, "ccC", :ddD]
127+
keys = ["Aaa", "Bbb", "Ccc", "Ddd"]
128+
deep_patterns = [/A\.a/, /b\.B/i, "c.C", :"d.D"]
129+
deep_keys = ["A.a", "B.b", "C.c", "D.d"]
130+
procs = [proc { }, proc { }]
131+
132+
precompiled = ActiveSupport::ParameterFilter.precompile_filters([*patterns, *deep_patterns, *procs])
133+
134+
assert_equal 2, precompiled.grep(Regexp).length
135+
assert_equal 2 + procs.length, precompiled.length
136+
137+
regexp = precompiled.find { |filter| filter.to_s.include?(patterns.first.to_s) }
138+
keys.each { |key| assert_match regexp, key }
139+
assert_no_match regexp, keys.first.swapcase
140+
141+
deep_regexp = precompiled.find { |filter| filter.to_s.include?(deep_patterns.first.to_s) }
142+
deep_keys.each { |deep_key| assert_match deep_regexp, deep_key }
143+
assert_no_match deep_regexp, deep_keys.first.swapcase
144+
145+
assert_not_equal regexp, deep_regexp
146+
assert_equal procs, precompiled & procs
147+
end
124148
end

guides/source/configuring.md

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ Below are the default values associated with each target version. In cases of co
7474
- [`config.active_support.raise_on_invalid_cache_expiration_time`](#config-active-support-raise-on-invalid-cache-expiration-time): `true`
7575
- [`config.add_autoload_paths_to_load_path`](#config-add-autoload-paths-to-load-path): `false`
7676
- [`config.log_file_size`](#config-log-file-size): `100 * 1024 * 1024`
77+
- [`config.precompile_filter_parameters`](#config-precompile-filter-parameters): `true`
7778

7879
#### Default Values for Target Version 7.0
7980

@@ -387,6 +388,20 @@ config.logger = ActiveSupport::TaggedLogging.new(mylogger)
387388

388389
Allows you to configure the application's middleware. This is covered in depth in the [Configuring Middleware](#configuring-middleware) section below.
389390

391+
#### `config.precompile_filter_parameters`
392+
393+
When `true`, will precompile [`config.filter_parameters`](#config-filter-parameters)
394+
using [`ActiveSupport::ParameterFilter.precompile_filters`][].
395+
396+
The default value depends on the `config.load_defaults` target version:
397+
398+
| Starting with version | The default value is |
399+
| --------------------- | -------------------- |
400+
| (original) | `false` |
401+
| 7.1 | `true` |
402+
403+
[`ActiveSupport::ParameterFilter.precompile_filters`]: https://api.rubyonrails.org/classes/ActiveSupport/ParameterFilter.html#method-c-precompile_filters
404+
390405
#### `config.public_file_server.enabled`
391406

392407
Configures Rails to serve static files from the public directory. This option defaults to `true`, but in the production environment it is set to `false` because the server software (e.g. NGINX or Apache) used to run the application should serve static files instead. If you are running or testing your app in production using WEBrick (it is not recommended to use WEBrick in production), set the option to `true`. Otherwise, you won't be able to use page caching and request for files that exist under the public directory.

railties/CHANGELOG.md

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,3 +1,13 @@
1+
* Add `config.precompile_filter_parameters`, which enables precompilation of
2+
`config.filter_parameters` using `ActiveSupport::ParameterFilter.precompile_filters`.
3+
Precompilation can improve filtering performance, depending on the quantity
4+
and types of filters.
5+
6+
`config.precompile_filter_parameters` defaults to `true` for
7+
`config.load_defaults 7.1` and above.
8+
9+
*Jonathan Hefner*
10+
111
* Add `after_routes_loaded` hook to `Rails::Railtie::Configuration` for
212
engines to add a hook to be called after application routes have been
313
loaded.

railties/lib/rails/application.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -301,7 +301,7 @@ def config_for(name, env: Rails.env)
301301
# will be used by middlewares and engines to configure themselves.
302302
def env_config
303303
@app_env_config ||= super.merge(
304-
"action_dispatch.parameter_filter" => config.filter_parameters,
304+
"action_dispatch.parameter_filter" => filter_parameters,
305305
"action_dispatch.redirect_filter" => config.filter_redirect,
306306
"action_dispatch.secret_key_base" => secret_key_base,
307307
"action_dispatch.show_exceptions" => config.action_dispatch.show_exceptions,
@@ -675,5 +675,13 @@ def build_middleware
675675
def coerce_same_site_protection(protection)
676676
protection.respond_to?(:call) ? protection : proc { protection }
677677
end
678+
679+
def filter_parameters
680+
if config.precompile_filter_parameters
681+
ActiveSupport::ParameterFilter.precompile_filters(config.filter_parameters)
682+
else
683+
config.filter_parameters
684+
end
685+
end
678686
end
679687
end

railties/lib/rails/application/configuration.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ class Application
1212
class Configuration < ::Rails::Engine::Configuration
1313
attr_accessor :allow_concurrency, :asset_host, :autoflush_log,
1414
:cache_classes, :cache_store, :consider_all_requests_local, :console,
15-
:eager_load, :exceptions_app, :file_watcher, :filter_parameters,
15+
:eager_load, :exceptions_app, :file_watcher, :filter_parameters, :precompile_filter_parameters,
1616
:force_ssl, :helpers_paths, :hosts, :host_authorization, :logger, :log_formatter,
1717
:log_tags, :railties_order, :relative_url_root, :secret_key_base,
1818
:ssl_options, :public_file_server,
@@ -278,6 +278,7 @@ def load_defaults(target_version)
278278
load_defaults "7.0"
279279

280280
self.add_autoload_paths_to_load_path = false
281+
self.precompile_filter_parameters = true
281282

282283
if Rails.env.development? || Rails.env.test?
283284
self.log_file_size = 100 * 1024 * 1024

railties/lib/rails/generators/rails/app/templates/config/initializers/new_framework_defaults_7_1.rb.tt

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -117,3 +117,7 @@
117117
# The previous behavior was to validate the presence of the parent record, which performed an extra query
118118
# to get the parent every time the child record was updated, even when parent has not changed.
119119
# Rails.application.config.active_record.belongs_to_required_validates_foreign_key = false
120+
121+
# Enable precompilation of `config.filter_parameters`. Precompilation can
122+
# improve filtering performance, depending on the quantity and types of filters.
123+
# Rails.application.config.precompile_filter_parameters = true

railties/test/application/configuration_test.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -521,6 +521,7 @@ class Comment < ActiveRecord::Base
521521
end
522522

523523
test "filter_parameters should be able to set via config.filter_parameters in an initializer" do
524+
remove_from_config '.*config\.load_defaults.*\n'
524525
app_file "config/initializers/filter_parameters_logging.rb", <<-RUBY
525526
Rails.application.config.filter_parameters += [ :password, :foo, 'bar' ]
526527
RUBY
@@ -530,6 +531,46 @@ class Comment < ActiveRecord::Base
530531
assert_equal [:password, :foo, "bar"], Rails.application.env_config["action_dispatch.parameter_filter"]
531532
end
532533

534+
test "filter_parameters is precompiled when config.precompile_filter_parameters is true" do
535+
filters = [/foo/, :bar, "baz.qux"]
536+
537+
add_to_config <<~RUBY
538+
config.filter_parameters += #{filters.inspect}
539+
config.precompile_filter_parameters = true
540+
RUBY
541+
542+
app "development"
543+
544+
assert_equal ActiveSupport::ParameterFilter.precompile_filters(filters), Rails.application.env_config["action_dispatch.parameter_filter"]
545+
end
546+
547+
test "filter_parameters is not precompiled when config.precompile_filter_parameters is false" do
548+
filters = [/foo/, :bar, "baz.qux"]
549+
550+
add_to_config <<~RUBY
551+
config.filter_parameters += #{filters.inspect}
552+
config.precompile_filter_parameters = false
553+
RUBY
554+
555+
app "development"
556+
557+
assert_equal filters, Rails.application.env_config["action_dispatch.parameter_filter"]
558+
end
559+
560+
test "config.precompile_filter_parameters is true by default for new apps" do
561+
app "development"
562+
563+
assert Rails.application.config.precompile_filter_parameters
564+
end
565+
566+
test "config.precompile_filter_parameters is false by default for upgraded apps" do
567+
remove_from_config '.*config\.load_defaults.*\n'
568+
add_to_config 'config.load_defaults "7.0"'
569+
app "development"
570+
571+
assert_not Rails.application.config.precompile_filter_parameters
572+
end
573+
533574
test "config.to_prepare is forwarded to ActionDispatch" do
534575
$prepared = false
535576

0 commit comments

Comments
 (0)