Skip to content

Commit dc91193

Browse files
committed
Add argument filtering to PerfettoTrace
1 parent 8cb72a4 commit dc91193

2 files changed

Lines changed: 148 additions & 1 deletion

File tree

lib/graphql/tracing/perfetto_trace.rb

Lines changed: 97 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,22 @@ def initialize(active_support_notifications_pattern: nil, save_profile: false, *
9191
ctx_debug
9292
end
9393

94+
@arguments_filter = if (ctx = query&.context) && (dtf = ctx[:detailed_trace_filter])
95+
dtf
96+
elsif defined?(ActiveSupport)
97+
fp = if defined?(Rails) && Rails.application && (app_config = Rails.application.config.filter_parameters)
98+
app_config
99+
elsif ActiveSupport.respond_to?(:filter_parameters)
100+
ActiveSupport.filter_parameters
101+
else
102+
[]
103+
end
104+
as_param_filter = ActiveSupport::ParameterFilter.new(fp, mask: ArgumentsFilter::FILTERED)
105+
ActiveSupportArgumentsFilter.new(as_param_filter)
106+
else
107+
ArgumentsFilter.new
108+
end
109+
94110
Fiber[:graphql_flow_stack] = nil
95111
@sequence_id = object_id
96112
@pid = Process.pid
@@ -255,7 +271,7 @@ def end_execute_field(field, object, arguments, query, app_result)
255271
start_field.track_event = dup_with(start_field.track_event,{
256272
debug_annotations: [
257273
payload_to_debug(nil, object.object, iid: DA_OBJECT_IID, intern_value: true),
258-
payload_to_debug(nil, arguments, iid: DA_ARGUMENTS_IID),
274+
payload_to_debug(nil, filter_arguments(arguments), iid: DA_ARGUMENTS_IID),
259275
payload_to_debug(nil, app_result, iid: DA_RESULT_IID, intern_value: true)
260276
]
261277
})
@@ -590,6 +606,86 @@ def fid
590606
Fiber.current.object_id
591607
end
592608

609+
def filter_arguments(args)
610+
@arguments_filter.filter(args)
611+
end
612+
613+
class ActiveSupportArgumentsFilter
614+
def initialize(parameter_filter)
615+
@parameter_filter = parameter_filter
616+
end
617+
618+
def filter(args)
619+
args_h = remove_wrappers(args)
620+
@parameter_filter.filter(args_h)
621+
end
622+
623+
private
624+
625+
def remove_wrappers(args)
626+
case args
627+
when Array
628+
args.map { |a| remove_wrappers(a)}
629+
when Hash
630+
args2 = args.dup
631+
args.each do |k, v|
632+
args2[k] = remove_wrappers(v)
633+
end
634+
args2
635+
when GraphQL::Schema::InputObject
636+
args_h = args.to_h
637+
args_h.each do |k, v|
638+
args_h[k] = remove_wrappers(v)
639+
end
640+
args_h
641+
else
642+
args
643+
end
644+
end
645+
end
646+
647+
class ArgumentsFilter
648+
# From Rails defaults
649+
# https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt#L6-L8
650+
SENSITIVE_KEY = /passw|token|crypt|email|_key|salt|certificate|secret|ssn|cvv|cvc|otp/i
651+
FILTERED = "[FILTERED]"
652+
653+
def filter(argument)
654+
case argument
655+
when GraphQL::Schema::InputObject
656+
filter(argument.to_h)
657+
when Hash
658+
target_h = nil
659+
argument.each do |k, v|
660+
if (k.is_a?(String) && SENSITIVE_KEY.match?(k)) ||
661+
(k.is_a?(Symbol) && SENSITIVE_KEY.match?(k.name))
662+
target_h ||= argument.dup
663+
target_h[k] = FILTERED
664+
else
665+
new_v = filter(v)
666+
if !v.equal?(new_v)
667+
target_h ||= argument.dup
668+
target_h[k] = new_v
669+
end
670+
end
671+
end
672+
target_h || argument
673+
when Array
674+
target_arr = nil
675+
argument.each_with_index do |inner_v, i|
676+
new_v = filter(inner_v)
677+
if !inner_v.equal?(new_v)
678+
target_arr ||= argument.dup
679+
target_arr[i] = new_v
680+
end
681+
end
682+
target_arr || argument
683+
else
684+
argument
685+
end
686+
end
687+
end
688+
593689
def debug_annotation(iid, value_key, value)
594690
if iid
595691
DebugAnnotation.new(name_iid: iid, value_key => value)

spec/graphql/tracing/perfetto_trace_spec.rb

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -105,6 +105,20 @@ def thing(id:)
105105
def crash
106106
raise "Crash the query"
107107
end
108+
109+
class SecretInput < GraphQL::Schema::InputObject
110+
argument :password, String
111+
end
112+
113+
field :secret_field, String do
114+
argument :cipher, String, required: false
115+
argument :password, String, required: false
116+
argument :input, [[SecretInput]], required: false
117+
end
118+
119+
def secret_field(cipher: nil, password: nil, input: nil)
120+
cipher || password || input[0][0][:password]
121+
end
108122
end
109123

110124
query(Query)
@@ -159,6 +173,43 @@ def self.detailed_trace?(q)
159173
check_snapshot(data, "example-rails-#{Rails::VERSION::MAJOR}-#{Rails::VERSION::MINOR}.json")
160174
end
161175

176+
it "filters params with ActiveSupport" do
177+
query_str = 'query getStuff { secretField(cipher: "abcdef") }'
178+
res = PerfettoSchema.execute(query_str, validate: false)
179+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
180+
assert_includes json, "abcdef"
181+
refute_includes json, "FILTERED"
182+
183+
prev_fp = ActiveSupport.filter_parameters
184+
ActiveSupport.filter_parameters = ["cipher"]
185+
res = PerfettoSchema.execute(query_str)
186+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
187+
refute_includes json, "abcdef"
188+
assert_includes json, "[FILTERED]"
189+
190+
ActiveSupport.filter_parameters = ["password"]
191+
res = PerfettoSchema.execute('query getStuff { secretField(input: [[{ password: "jklmn" }]]) }')
192+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
193+
refute json.include?("jklmn"), "Value is removed"
194+
assert_includes json, "[FILTERED]"
195+
ensure
196+
ActiveSupport.filter_parameters = prev_fp
197+
end
198+
199+
it "filters params without ActiveSupport" do
200+
query_str = 'query getStuff { secretField(password: "qrstuv") }'
201+
res = PerfettoSchema.execute(query_str, context: { detailed_trace_filter: GraphQL::Tracing::PerfettoTrace::ArgumentsFilter.new })
202+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
203+
assert_includes json, "[FILTERED]"
204+
refute_includes json, "qrstuv"
205+
206+
query_str = 'query getStuff { secretField(input: [[{ password: "lmnop" }]]) }'
207+
res = PerfettoSchema.execute(query_str, context: { detailed_trace_filter: GraphQL::Tracing::PerfettoTrace::ArgumentsFilter.new })
208+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
209+
refute json.include?("lmnop"), "The password is obscured"
210+
assert json.include?("[FILTERED]"), "The replacement string is present"
211+
end
212+
162213
it "provides an error when google-protobuf isn't available" do
163214
stderr_and_stdout, _status = Open3.capture2e(%|ruby -e 'require "bundler/inline"; gemfile(true) { source("https://rubygems.org"); gem("graphql", path: "./") }; class MySchema < GraphQL::Schema; trace_with(GraphQL::Tracing::PerfettoTrace); end;'|)
164215
assert_includes stderr_and_stdout, "GraphQL::Tracing::PerfettoTrace can't be used because the `google-protobuf` gem wasn't available. Add it to your project, then try again."

0 commit comments

Comments
 (0)