e5693c56c6
`ActiveSupport::ParameterFilter.precompile_filters` precompiles filters that otherwise would be passed directly to `ParameterFilter.new`. Depending on the quantity and types of filters, precompilation can improve filtering performance, especially in the case where the `ParameterFilter` instance cannot be retained, such as with per-request instances in `ActionDispatch::Http::FilterParameters`. **Benchmark script** ```ruby # frozen_string_literal: true require "benchmark/ips" require "benchmark/memory" require "active_support" require "active_support/parameter_filter" ootb = [:passw, :secret, :token, :_key, :crypt, :salt, :certificate, :otp, :ssn] mixed = [:passw, "secret", /token/, :crypt, "salt", /certificate/, "user.otp", /user\.ssn/, proc {}] precompiled_ootb = ActiveSupport::ParameterFilter.precompile_filters(ootb) precompiled_mixed = ActiveSupport::ParameterFilter.precompile_filters(mixed) params = { "user" => { "name" => :name, "email" => :email, "password" => :password, "ssn" => :ssn, "locations" => [ { "city" => :city, "country" => :country }, { "city" => :city, "country" => :country }, ], } } Benchmark.ips do |x| x.report("ootb") do ActiveSupport::ParameterFilter.new(ootb).filter(params) end x.report("precompiled ootb") do ActiveSupport::ParameterFilter.new(precompiled_ootb).filter(params) end x.compare! end Benchmark.ips do |x| x.report("mixed") do ActiveSupport::ParameterFilter.new(mixed).filter(params) end x.report("precompiled mixed") do ActiveSupport::ParameterFilter.new(precompiled_mixed).filter(params) end x.compare! end Benchmark.memory do |x| x.report("ootb") do ActiveSupport::ParameterFilter.new(ootb).filter(params) end x.report("precompiled ootb") do ActiveSupport::ParameterFilter.new(precompiled_ootb).filter(params) end end Benchmark.memory do |x| x.report("mixed") do ActiveSupport::ParameterFilter.new(mixed).filter(params) end x.report("precompiled mixed") do ActiveSupport::ParameterFilter.new(precompiled_mixed).filter(params) end end ``` **Results** ``` Warming up -------------------------------------- ootb 2.151k i/100ms precompiled ootb 4.251k i/100ms Calculating ------------------------------------- ootb 21.567k (± 1.1%) i/s - 109.701k in 5.086983s precompiled ootb 42.840k (± 0.8%) i/s - 216.801k in 5.061022s Comparison: precompiled ootb: 42840.4 i/s ootb: 21567.5 i/s - 1.99x (± 0.00) slower ``` ``` Warming up -------------------------------------- mixed 1.622k i/100ms precompiled mixed 2.455k i/100ms Calculating ------------------------------------- mixed 16.085k (± 1.3%) i/s - 81.100k in 5.042764s precompiled mixed 24.640k (± 1.0%) i/s - 125.205k in 5.081988s Comparison: precompiled mixed: 24639.6 i/s mixed: 16085.0 i/s - 1.53x (± 0.00) slower ``` ``` Calculating ------------------------------------- ootb 2.684k memsize ( 0.000 retained) 30.000 objects ( 0.000 retained) 10.000 strings ( 0.000 retained) precompiled ootb 1.104k memsize ( 0.000 retained) 9.000 objects ( 0.000 retained) 1.000 strings ( 0.000 retained) ``` ``` Calculating ------------------------------------- mixed 3.541k memsize ( 0.000 retained) 46.000 objects ( 0.000 retained) 20.000 strings ( 0.000 retained) precompiled mixed 1.856k memsize ( 0.000 retained) 29.000 objects ( 0.000 retained) 13.000 strings ( 0.000 retained) ``` This commit also adds `config.precompile_filter_parameters`, which enables precompilation of `config.filter_parameters`. It defaults to `true` for `config.load_defaults 7.1` and above.
149 lines
7.1 KiB
Ruby
149 lines
7.1 KiB
Ruby
# frozen_string_literal: true
|
|
|
|
require_relative "abstract_unit"
|
|
require "active_support/core_ext/hash"
|
|
require "active_support/parameter_filter"
|
|
|
|
class ParameterFilterTest < ActiveSupport::TestCase
|
|
test "process parameter filter" do
|
|
test_hashes = [
|
|
[{ "foo" => "bar" }, { "foo" => "bar" }, %w'food'],
|
|
[{ "foo" => "bar" }, { "foo" => "[FILTERED]" }, %w'foo'],
|
|
[{ "foo" => "bar", "bar" => "foo" }, { "foo" => "[FILTERED]", "bar" => "foo" }, %w'foo baz'],
|
|
[{ "foo" => "bar", "baz" => "foo" }, { "foo" => "[FILTERED]", "baz" => "[FILTERED]" }, %w'foo baz'],
|
|
[{ "bar" => { "foo" => "bar", "bar" => "foo" } }, { "bar" => { "foo" => "[FILTERED]", "bar" => "foo" } }, %w'fo'],
|
|
[{ "foo" => { "foo" => "bar", "bar" => "foo" } }, { "foo" => "[FILTERED]" }, %w'f banana'],
|
|
[{ "deep" => { "cc" => { "code" => "bar", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, { "deep" => { "cc" => { "code" => "[FILTERED]", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, %w'deep.cc.code'],
|
|
[{ "baz" => [{ "foo" => "baz" }, "1"] }, { "baz" => [{ "foo" => "[FILTERED]" }, "1"] }, [/foo/]]]
|
|
|
|
test_hashes.each do |before_filter, after_filter, filter_words|
|
|
parameter_filter = ActiveSupport::ParameterFilter.new(filter_words)
|
|
assert_equal after_filter, parameter_filter.filter(before_filter)
|
|
|
|
filter_words << "blah"
|
|
filter_words << lambda { |key, value|
|
|
value.reverse! if /bargain/.match?(key)
|
|
}
|
|
filter_words << lambda { |key, value, original_params|
|
|
value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello"
|
|
}
|
|
|
|
filter_words << lambda { |key, value|
|
|
value.upcase! if key == "array_elements"
|
|
}
|
|
|
|
parameter_filter = ActiveSupport::ParameterFilter.new(filter_words)
|
|
before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } }
|
|
after_filter["barg"] = { :bargain => "niag", "blah" => "[FILTERED]", "bar" => { "bargain" => { "blah" => "[FILTERED]", "hello" => "world!" } } }
|
|
|
|
before_filter["array_elements"] = %w(element1 element2)
|
|
after_filter["array_elements"] = %w(ELEMENT1 ELEMENT2)
|
|
|
|
assert_equal after_filter, parameter_filter.filter(before_filter)
|
|
end
|
|
end
|
|
|
|
test "filter should return mask option when value is filtered" do
|
|
mask = Object.new.freeze
|
|
test_hashes = [
|
|
[{ "foo" => "bar" }, { "foo" => "bar" }, %w'food'],
|
|
[{ "foo" => "bar" }, { "foo" => mask }, %w'foo'],
|
|
[{ "foo" => "bar", "bar" => "foo" }, { "foo" => mask, "bar" => "foo" }, %w'foo baz'],
|
|
[{ "foo" => "bar", "baz" => "foo" }, { "foo" => mask, "baz" => mask }, %w'foo baz'],
|
|
[{ "bar" => { "foo" => "bar", "bar" => "foo" } }, { "bar" => { "foo" => mask, "bar" => "foo" } }, %w'fo'],
|
|
[{ "foo" => { "foo" => "bar", "bar" => "foo" } }, { "foo" => mask }, %w'f banana'],
|
|
[{ "deep" => { "cc" => { "code" => "bar", "bar" => "foo" }, "ss" => { "code" => "bar" } } }, { "deep" => { "cc" => { "code" => mask, "bar" => "foo" }, "ss" => { "code" => "bar" } } }, %w'deep.cc.code'],
|
|
[{ "baz" => [{ "foo" => "baz" }, "1"] }, { "baz" => [{ "foo" => mask }, "1"] }, [/foo/]]]
|
|
|
|
test_hashes.each do |before_filter, after_filter, filter_words|
|
|
parameter_filter = ActiveSupport::ParameterFilter.new(filter_words, mask: mask)
|
|
assert_equal after_filter, parameter_filter.filter(before_filter)
|
|
|
|
filter_words << "blah"
|
|
filter_words << lambda { |key, value|
|
|
value.reverse! if /bargain/.match?(key)
|
|
}
|
|
filter_words << lambda { |key, value, original_params|
|
|
value.replace("world!") if original_params["barg"]["blah"] == "bar" && key == "hello"
|
|
}
|
|
|
|
parameter_filter = ActiveSupport::ParameterFilter.new(filter_words, mask: mask)
|
|
before_filter["barg"] = { :bargain => "gain", "blah" => "bar", "bar" => { "bargain" => { "blah" => "foo", "hello" => "world" } } }
|
|
after_filter["barg"] = { :bargain => "niag", "blah" => mask, "bar" => { "bargain" => { "blah" => mask, "hello" => "world!" } } }
|
|
|
|
assert_equal after_filter, parameter_filter.filter(before_filter)
|
|
end
|
|
end
|
|
|
|
test "filter_param" do
|
|
parameter_filter = ActiveSupport::ParameterFilter.new(["foo", /bar/])
|
|
assert_equal "[FILTERED]", parameter_filter.filter_param("food", "secret value")
|
|
assert_equal "[FILTERED]", parameter_filter.filter_param("baz.foo", "secret value")
|
|
assert_equal "[FILTERED]", parameter_filter.filter_param("barbar", "secret value")
|
|
assert_equal "non secret value", parameter_filter.filter_param("baz", "non secret value")
|
|
end
|
|
|
|
test "filter_param can work with empty filters" do
|
|
parameter_filter = ActiveSupport::ParameterFilter.new
|
|
assert_equal "bar", parameter_filter.filter_param("foo", "bar")
|
|
end
|
|
|
|
test "parameter filter should maintain hash with indifferent access" do
|
|
test_hashes = [
|
|
[{ "foo" => "bar" }.with_indifferent_access, ["blah"]],
|
|
[{ "foo" => "bar" }.with_indifferent_access, []]
|
|
]
|
|
|
|
test_hashes.each do |before_filter, filter_words|
|
|
parameter_filter = ActiveSupport::ParameterFilter.new(filter_words)
|
|
assert_instance_of ActiveSupport::HashWithIndifferentAccess,
|
|
parameter_filter.filter(before_filter)
|
|
end
|
|
end
|
|
|
|
test "filter_param should return mask option when value is filtered" do
|
|
mask = Object.new.freeze
|
|
parameter_filter = ActiveSupport::ParameterFilter.new(["foo", /bar/], mask: mask)
|
|
assert_equal mask, parameter_filter.filter_param("food", "secret value")
|
|
assert_equal mask, parameter_filter.filter_param("baz.foo", "secret value")
|
|
assert_equal mask, parameter_filter.filter_param("barbar", "secret value")
|
|
assert_equal "non secret value", parameter_filter.filter_param("baz", "non secret value")
|
|
end
|
|
|
|
test "process parameter filter with hash having integer keys" do
|
|
test_hashes = [
|
|
[{ 13 => "bar" }, { 13 => "[FILTERED]" }, %w'13'],
|
|
[{ 20 => "bar" }, { 20 => "bar" }, %w'13'],
|
|
]
|
|
|
|
test_hashes.each do |before_filter, after_filter, filter_words|
|
|
parameter_filter = ActiveSupport::ParameterFilter.new(filter_words)
|
|
assert_equal after_filter, parameter_filter.filter(before_filter)
|
|
end
|
|
end
|
|
|
|
test "precompile_filters" do
|
|
patterns = [/A.a/, /b.B/i, "ccC", :ddD]
|
|
keys = ["Aaa", "Bbb", "Ccc", "Ddd"]
|
|
deep_patterns = [/A\.a/, /b\.B/i, "c.C", :"d.D"]
|
|
deep_keys = ["A.a", "B.b", "C.c", "D.d"]
|
|
procs = [proc { }, proc { }]
|
|
|
|
precompiled = ActiveSupport::ParameterFilter.precompile_filters([*patterns, *deep_patterns, *procs])
|
|
|
|
assert_equal 2, precompiled.grep(Regexp).length
|
|
assert_equal 2 + procs.length, precompiled.length
|
|
|
|
regexp = precompiled.find { |filter| filter.to_s.include?(patterns.first.to_s) }
|
|
keys.each { |key| assert_match regexp, key }
|
|
assert_no_match regexp, keys.first.swapcase
|
|
|
|
deep_regexp = precompiled.find { |filter| filter.to_s.include?(deep_patterns.first.to_s) }
|
|
deep_keys.each { |deep_key| assert_match deep_regexp, deep_key }
|
|
assert_no_match deep_regexp, deep_keys.first.swapcase
|
|
|
|
assert_not_equal regexp, deep_regexp
|
|
assert_equal procs, precompiled & procs
|
|
end
|
|
end
|