From 32b03b46150b0161eba2321ccac7678511e3d58e Mon Sep 17 00:00:00 2001 From: Yoshiyuki Kinjo Date: Tue, 9 Oct 2018 16:14:51 +0900 Subject: Implement AR#inspect using ParamterFilter. AR instance support `filter_parameters` since #33756. Though Regex or Proc is valid as `filter_parameters`, they are not supported as AR#inspect. I also add :mask option and #filter_params to `ActiveSupport::ParameterFilter#new` to implement this. --- .../lib/active_support/parameter_filter.rb | 62 ++++++++++++++-------- activesupport/test/parameter_filter_test.rb | 54 +++++++++++++++++++ 2 files changed, 94 insertions(+), 22 deletions(-) (limited to 'activesupport') diff --git a/activesupport/lib/active_support/parameter_filter.rb b/activesupport/lib/active_support/parameter_filter.rb index 59945e9daa..1389d82523 100644 --- a/activesupport/lib/active_support/parameter_filter.rb +++ b/activesupport/lib/active_support/parameter_filter.rb @@ -28,22 +28,36 @@ module ActiveSupport class ParameterFilter FILTERED = "[FILTERED]" # :nodoc: - def initialize(filters = []) + # Create instance with given filters. Supported type of filters are +String+, +Regexp+, and +Proc+. + # Other types of filters are treated as +String+ using +to_s+. + # For +Proc+ filters, key, value, and optional original hash is passed to block arguments. + # + # ==== Options + # + # * :mask - A replaced object when filtered. Defaults to +"[FILTERED]"+ + def initialize(filters = [], mask: FILTERED) @filters = filters + @mask = mask end + # Mask value of +params+ if key matches one of filters. def filter(params) compiled_filter.call(params) end + # Returns filtered value for given key. For +Proc+ filters, third block argument is not populated. + def filter_param(key, value) + @filters.empty? ? value : compiled_filter.value_for_key(key, value) + end + private def compiled_filter - @compiled_filter ||= CompiledFilter.compile(@filters) + @compiled_filter ||= CompiledFilter.compile(@filters, mask: @mask) end class CompiledFilter # :nodoc: - def self.compile(filters) + def self.compile(filters, mask:) return lambda { |params| params.dup } if filters.empty? strings, regexps, blocks = [], [], [] @@ -65,42 +79,46 @@ module ActiveSupport regexps << Regexp.new(strings.join("|"), true) unless strings.empty? deep_regexps << Regexp.new(deep_strings.join("|"), true) unless deep_strings.empty? - new regexps, deep_regexps, blocks + new regexps, deep_regexps, blocks, mask: mask end attr_reader :regexps, :deep_regexps, :blocks - def initialize(regexps, deep_regexps, blocks) + def initialize(regexps, deep_regexps, blocks, mask:) @regexps = regexps @deep_regexps = deep_regexps.any? ? deep_regexps : nil @blocks = blocks + @mask = mask end def call(params, parents = [], original_params = params) filtered_params = params.class.new params.each do |key, value| - parents.push(key) if deep_regexps - if regexps.any? { |r| key =~ r } - value = FILTERED - elsif deep_regexps && (joined = parents.join(".")) && deep_regexps.any? { |r| joined =~ r } - value = FILTERED - elsif value.is_a?(Hash) - value = call(value, parents, original_params) - elsif value.is_a?(Array) - value = value.map { |v| v.is_a?(Hash) ? call(v, parents, original_params) : v } - elsif blocks.any? - key = key.dup if key.duplicable? - value = value.dup if value.duplicable? - blocks.each { |b| b.arity == 2 ? b.call(key, value) : b.call(key, value, original_params) } - end - parents.pop if deep_regexps - - filtered_params[key] = value + filtered_params[key] = value_for_key(key, value, parents, original_params) end filtered_params end + + def value_for_key(key, value, parents = [], original_params = nil) + parents.push(key) if deep_regexps + if regexps.any? { |r| r.match?(key) } + value = @mask + elsif deep_regexps && (joined = parents.join(".")) && deep_regexps.any? { |r| r.match?(joined) } + value = @mask + elsif value.is_a?(Hash) + value = call(value, parents, original_params) + elsif value.is_a?(Array) + value = value.map { |v| v.is_a?(Hash) ? call(v, parents, original_params) : v } + elsif blocks.any? + key = key.dup if key.duplicable? + value = value.dup if value.duplicable? + blocks.each { |b| b.arity == 2 ? b.call(key, value) : b.call(key, value, original_params) } + end + parents.pop if deep_regexps + value + end end end end diff --git a/activesupport/test/parameter_filter_test.rb b/activesupport/test/parameter_filter_test.rb index 3403a3188b..d2dc71061d 100644 --- a/activesupport/test/parameter_filter_test.rb +++ b/activesupport/test/parameter_filter_test.rb @@ -36,6 +36,51 @@ class ParameterFilterTest < ActiveSupport::TestCase 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 key =~ /bargain/ + } + 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 vlaue") + assert_equal "[FILTERED]", parameter_filter.filter_param("baz.foo", "secret vlaue") + assert_equal "[FILTERED]", parameter_filter.filter_param("barbar", "secret vlaue") + 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"]], @@ -48,4 +93,13 @@ class ParameterFilterTest < ActiveSupport::TestCase 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 vlaue") + assert_equal mask, parameter_filter.filter_param("baz.foo", "secret vlaue") + assert_equal mask, parameter_filter.filter_param("barbar", "secret vlaue") + assert_equal "non secret value", parameter_filter.filter_param("baz", "non secret value") + end end -- cgit v1.2.3