rails/activesupport/test/parameter_filter_test.rb
Jonathan Hefner e5693c56c6 Add AS::ParameterFilter.precompile_filters
`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.
2022-11-24 10:26:54 -06:00

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