Skip to content

Commit 8bedf36

Browse files
authored
Merge pull request #5527 from rmosolgo/ar-backend-fixes
DetailedTrace: improve generator, filter hashes before logging
2 parents 26419d4 + b19946d commit 8bedf36

File tree

14 files changed

+32484
-31095
lines changed

14 files changed

+32484
-31095
lines changed

lib/generators/graphql/detailed_trace_generator.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -67,6 +67,9 @@ def install_detailed_traces
6767
6868
RUBY
6969
end
70+
71+
gem("google-protobuf")
72+
7073
end
7174
end
7275
end

lib/generators/graphql/templates/create_graphql_detailed_traces.erb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,9 @@
11
class CreateGraphqlDetailedTraces < ActiveRecord::Migration[<%= ActiveRecord::Migration.current_version %>]
22
def change
33
create_table :graphql_detailed_traces, force: true do |t|
4-
t.integer :begin_ms, null: false
4+
t.bigint :begin_ms, null: false
55
t.float :duration_ms, null: false
6-
t.text :trace_data, null: false
6+
t.binary :trace_data, null: false
77
t.string :operation_name, null: false
88
end
99
end

lib/graphql/dashboard.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -120,7 +120,7 @@ def show
120120
end
121121

122122
class StaticsController < ApplicationController
123-
skip_after_action :verify_same_origin_request
123+
skip_forgery_protection
124124
# Use an explicit list of files to avoid any chance of reading other files from disk
125125
STATICS = {}
126126

lib/graphql/tracing/detailed_trace/active_record_backend.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def save_trace(operation_name, duration_ms, begin_ms, trace_data)
4646
begin_ms: begin_ms,
4747
operation_name: operation_name,
4848
duration_ms: duration_ms,
49-
trace_data: Base64.encode64(trace_data),
49+
trace_data: trace_data,
5050
)
5151
if @limit
5252
@model_class
@@ -64,7 +64,7 @@ def record_to_stored_trace(gdt)
6464
begin_ms: gdt.begin_ms,
6565
operation_name: gdt.operation_name,
6666
duration_ms: gdt.duration_ms,
67-
trace_data: Base64.decode64(gdt.trace_data)
67+
trace_data: gdt.trace_data
6868
)
6969

7070
end

lib/graphql/tracing/perfetto_trace.rb

Lines changed: 47 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,6 +91,21 @@ 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::ParameterFilter)
97+
fp = if defined?(Rails) && Rails.application && (app_config = Rails.application.config.filter_parameters).present? && !app_config.empty?
98+
app_config
99+
elsif ActiveSupport.respond_to?(:filter_parameters)
100+
ActiveSupport.filter_parameters
101+
else
102+
EmptyObjects::EMPTY_ARRAY
103+
end
104+
ActiveSupport::ParameterFilter.new(fp, mask: ArgumentsFilter::FILTERED)
105+
else
106+
ArgumentsFilter.new
107+
end
108+
94109
Fiber[:graphql_flow_stack] = nil
95110
@sequence_id = object_id
96111
@pid = Process.pid
@@ -590,6 +605,22 @@ def fid
590605
Fiber.current.object_id
591606
end
592607

608+
class ArgumentsFilter
609+
# From Rails defaults
610+
# https://github.com/rails/rails/blob/main/railties/lib/rails/generators/rails/app/templates/config/initializers/filter_parameter_logging.rb.tt#L6-L8
611+
SENSITIVE_KEY = /passw|token|crypt|email|_key|salt|certificate|secret|ssn|cvv|cvc|otp/i
612+
FILTERED = "[FILTERED]"
613+
614+
def filter_param(key, value)
615+
if (key.is_a?(String) && SENSITIVE_KEY.match?(key)) ||
616+
(key.is_a?(Symbol) && SENSITIVE_KEY.match?(key.name))
617+
FILTERED
618+
else
619+
value
620+
end
621+
end
622+
end
623+
593624
def debug_annotation(iid, value_key, value)
594625
if iid
595626
DebugAnnotation.new(name_iid: iid, value_key => value)
@@ -634,7 +665,22 @@ def payload_to_debug(k, v, iid: nil, intern_value: false)
634665
when Array
635666
debug_annotation(iid, :array_values, v.each_with_index.map { |v2, idx| payload_to_debug((k ? "#{k}.#{idx}" : String(idx)), v2, intern_value: intern_value) }.compact)
636667
when Hash
637-
debug_annotation(iid, :dict_entries, v.map { |k2, v2| payload_to_debug(k2, v2, intern_value: intern_value) }.compact)
668+
debug_v = v.map { |k2, v2|
669+
debug_k = case k2
670+
when String
671+
k2
672+
when Symbol
673+
k2.name
674+
else
675+
String(k2)
676+
end
677+
filtered_v2 = @arguments_filter.filter_param(debug_k, v2)
678+
payload_to_debug(debug_k, filtered_v2, intern_value: intern_value)
679+
}
680+
debug_v.compact!
681+
debug_annotation(iid, :dict_entries, debug_v)
682+
when GraphQL::Schema::InputObject
683+
payload_to_debug(k, v.to_h, iid: iid, intern_value: intern_value)
638684
else
639685
class_name_iid = @interned_da_string_values[v.class.name]
640686
da = [

spec/dummy/test/controllers/dashboard/statics_controller_test.rb

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,15 @@ def test_it_serves_assets
88
assert_equal response.headers["Cache-Control"], "max-age=31556952, public"
99
end
1010

11+
def test_it_doesnt_trigger_csrf_failure
12+
original_forgery_protection = ActionController::Base.allow_forgery_protection
13+
ActionController::Base.allow_forgery_protection = true
14+
get graphql_dashboard.static_path("dashboard.js")
15+
assert_equal 200, response.status
16+
ensure
17+
ActionController::Base.allow_forgery_protection = original_forgery_protection
18+
end
19+
1120
def test_it_responds_404_for_others
1221
get graphql_dashboard.static_path("other.rb")
1322
assert_equal 404, response.status

spec/graphql/tracing/detailed_trace/active_record_backend_spec.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,7 @@ def new_backend(**kwargs)
1212
class DummyModel
1313
def self.find_by(id:)
1414
OpenStruct.new(
15-
trace_data: Base64.encode64("DummyModel##{id}")
15+
trace_data: "DummyModel##{id}"
1616
)
1717
end
1818
end

spec/graphql/tracing/perfetto_trace_spec.rb

Lines changed: 103 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,11 @@
66
describe GraphQL::Tracing::PerfettoTrace do
77
include PerfettoSnapshot
88

9+
def trace_includes?(json_str, test_str)
10+
json_str.include?(Base64.encode64(test_str).strip) ||
11+
json_str.include?(test_str)
12+
end
13+
914
class PerfettoSchema < GraphQL::Schema
1015
class BaseObject < GraphQL::Schema::Object
1116
end
@@ -46,7 +51,7 @@ def self.authorized?(obj, ctx)
4651
end
4752

4853
def user
49-
dataload_record(::User, object.user_id)
54+
dataload_record(::User, object[:user_id])
5055
end
5156
end
5257

@@ -57,7 +62,7 @@ class Book < BaseObject
5762
field :author, "PerfettoSchema::Author"
5863
field :other_book, Book
5964
def reviews
60-
object.reviews.limit(2)
65+
object.reviews.limit(2).map { |r| { stars: r.stars, user_id: r.user } }
6166
end
6267

6368
def average_review
@@ -105,6 +110,27 @@ def thing(id:)
105110
def crash
106111
raise "Crash the query"
107112
end
113+
114+
class SecretInput < GraphQL::Schema::InputObject
115+
argument :password, String
116+
end
117+
118+
class SecretThing < GraphQL::Schema::Object
119+
field :greeting, String
120+
end
121+
field :secret_field, SecretThing do
122+
argument :cipher, String, required: false
123+
argument :password, String, required: false
124+
argument :input, [[SecretInput]], required: false
125+
end
126+
127+
def secret_field(cipher: nil, password: nil, input: nil)
128+
{
129+
greeting: "Hello!",
130+
cipher: cipher || "FALLBACK_CIPHER",
131+
password: password || (input ? input[0][0][:password] : "FALLBACK_PASSWORD"),
132+
}
133+
end
108134
end
109135

110136
query(Query)
@@ -159,6 +185,81 @@ def self.detailed_trace?(q)
159185
check_snapshot(data, "example-rails-#{Rails::VERSION::MAJOR}-#{Rails::VERSION::MINOR}.json")
160186
end
161187

188+
it "filters params with Rails.application.config.filter_parameters" do
189+
query_str = 'query getStuff { secretField(cipher: "abcdef") { greeting } }'
190+
res = PerfettoSchema.execute(query_str)
191+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
192+
assert trace_includes?(json, "abcdef")
193+
refute trace_includes?(json, "FILTERED")
194+
195+
if Rails.application.present?
196+
prev_fp = Rails.application.config.filter_parameters
197+
Rails.application.config.filter_parameters = ["ciph"]
198+
else
199+
Rails.application = OpenStruct.new(config: OpenStruct.new(filter_parameters: ["ciph"]))
200+
end
201+
res = PerfettoSchema.execute(query_str)
202+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
203+
refute trace_includes?(json, "abcdef")
204+
assert trace_includes?(json, "FILTERED")
205+
ensure
206+
if prev_fp
207+
Rails.application.config.filter_parameters = prev_fp
208+
else
209+
Rails.application = nil
210+
end
211+
end
212+
213+
it "filters params with ActiveSupport" do
214+
query_str = 'query getStuff { secretField(cipher: "abcdef") { greeting } }'
215+
res = PerfettoSchema.execute(query_str)
216+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
217+
assert trace_includes?(json, "abcdef")
218+
refute trace_includes?(json, "FILTERED")
219+
220+
query_str = 'query getStuff { secretField(cipher: "abcdef") { greeting } }'
221+
res = PerfettoSchema.execute(query_str)
222+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
223+
assert trace_includes?(json, "abcdef")
224+
refute trace_includes?(json, "FILTERED")
225+
226+
if ActiveSupport.respond_to?(:filter_parameters=)
227+
begin
228+
prev_fp = ActiveSupport.filter_parameters
229+
ActiveSupport.filter_parameters = ["cipher"]
230+
res = PerfettoSchema.execute(query_str)
231+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
232+
refute trace_includes?(json, "abcdef")
233+
assert trace_includes?(json, "[FILTERED]")
234+
235+
ActiveSupport.filter_parameters = ["password"]
236+
res = PerfettoSchema.execute('query getStuff { secretField(input: [[{ password: "jklmn" }]]) { greeting } }')
237+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
238+
assert trace_includes?(json, "password"), "Name is retained"
239+
refute trace_includes?(json, "jklmn"), "Value is removed"
240+
assert_includes json, "[FILTERED]"
241+
ensure
242+
ActiveSupport.filter_parameters = prev_fp
243+
end
244+
end
245+
end
246+
247+
it "filters params without ActiveSupport" do
248+
query_str = 'query getStuff { secretField(password: "qrstuv") { greeting } }'
249+
res = PerfettoSchema.execute(query_str, context: { detailed_trace_filter: GraphQL::Tracing::PerfettoTrace::ArgumentsFilter.new })
250+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
251+
assert trace_includes?(json, "FILTERED"), "The replacement string is present"
252+
assert trace_includes?(json, "FALLBACK_CIPHER"), "Unfiltered values are present"
253+
refute trace_includes?(json, "qrstuv"), "The password is obscured"
254+
255+
query_str = 'query getStuff { secretField(input: [[{ password: "lmnop" }]]) { greeting } }'
256+
res = PerfettoSchema.execute(query_str, context: { detailed_trace_filter: GraphQL::Tracing::PerfettoTrace::ArgumentsFilter.new })
257+
json = res.context.query.current_trace.write(file: nil, debug_json: true)
258+
assert trace_includes?(json, "password"), "Name is retained"
259+
refute trace_includes?(json, "lmnop"), "The password is obscured"
260+
assert trace_includes?(json, "[FILTERED]"), "The replacement string is present"
261+
end
262+
162263
it "provides an error when google-protobuf isn't available" do
163264
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;'|)
164265
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)