aboutsummaryrefslogtreecommitdiffstats
path: root/activesupport/lib/active_support/parameter_filter.rb
blob: 59945e9daaadbcaba3b45d2b81683e9575a69cc7 (plain) (blame)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
# frozen_string_literal: true

require "active_support/core_ext/object/duplicable"
require "active_support/core_ext/array/extract"

module ActiveSupport
  # +ParameterFilter+ allows you to specify keys for sensitive data from
  # hash-like object and replace corresponding value. Filtering only certain
  # sub-keys from a hash is possible by using the dot notation:
  # 'credit_card.number'. If a proc is given, each key and value of a hash and
  # all sub-hashes are passed to it, where the value or the key can be replaced
  # using String#replace or similar methods.
  #
  #   ActiveSupport::ParameterFilter.new([:password])
  #   => replaces the value to all keys matching /password/i with "[FILTERED]"
  #
  #   ActiveSupport::ParameterFilter.new([:foo, "bar"])
  #   => replaces the value to all keys matching /foo|bar/i with "[FILTERED]"
  #
  #   ActiveSupport::ParameterFilter.new(["credit_card.code"])
  #   => replaces { credit_card: {code: "xxxx"} } with "[FILTERED]", does not
  #   change { file: { code: "xxxx"} }
  #
  #   ActiveSupport::ParameterFilter.new([-> (k, v) do
  #     v.reverse! if k =~ /secret/i
  #   end])
  #   => reverses the value to all keys matching /secret/i
  class ParameterFilter
    FILTERED = "[FILTERED]" # :nodoc:

    def initialize(filters = [])
      @filters = filters
    end

    def filter(params)
      compiled_filter.call(params)
    end

  private

    def compiled_filter
      @compiled_filter ||= CompiledFilter.compile(@filters)
    end

    class CompiledFilter # :nodoc:
      def self.compile(filters)
        return lambda { |params| params.dup } if filters.empty?

        strings, regexps, blocks = [], [], []

        filters.each do |item|
          case item
          when Proc
            blocks << item
          when Regexp
            regexps << item
          else
            strings << Regexp.escape(item.to_s)
          end
        end

        deep_regexps = regexps.extract! { |r| r.to_s.include?("\\.") }
        deep_strings = strings.extract! { |s| s.include?("\\.") }

        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
      end

      attr_reader :regexps, :deep_regexps, :blocks

      def initialize(regexps, deep_regexps, blocks)
        @regexps = regexps
        @deep_regexps = deep_regexps.any? ? deep_regexps : nil
        @blocks = blocks
      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
        end

        filtered_params
      end
    end
  end
end