Skip to content

Commit 1648c00

Browse files
authored
Filtering Job Arguments (#277)
* Filtering Job Arguments Adds a new `filter_arguments` option that allows you to filter out sensitive data from job arguments. This option can be configured globally or per application. Note: Currently, only root-level hash keys are supported. If needed, support for key paths, procs, and regular expressions could be added in the future. * SolidQueue and Resque are the main adapters supported So remove the limitation from the Readme * Leftover * Remove the per-app override * Specify that are strings * Move to MissionControl NS * Don't modify raw_args They are used for job handling logic e.g. retries. * Move .first up * It's args_arr * Extra newline * No need for the if * Extra new line * No need to memoize a fast operation * Comment * Test * Rewording * Fix cop * Rename to extract_args_hash And return an hash or nil
1 parent 9fa76f0 commit 1648c00

File tree

13 files changed

+126
-5
lines changed

13 files changed

+126
-5
lines changed

README.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,7 @@ Besides `base_controller_class`, you can also set the following for `MissionCont
114114
- `scheduled_job_delay_threshold`: the time duration before a scheduled job is considered delayed. Defaults to `1.minute` (a job is considered delayed if it hasn't transitioned from the `scheduled` status 1 minute after the scheduled time).
115115
- `show_console_help`: whether to show the console help. If you don't want the console help message, set this to `false`—defaults to `true`.
116116
- `backtrace_cleaner`: a backtrace cleaner used for optionally filtering backtraces on the Failed Jobs detail page. Defaults to `Rails::BacktraceCleaner.new`. See the [Advanced configuration](#advanced-configuration) section for how to configure/override this setting on a per application/server basis.
117+
- `filter_arguments`: an array of strings representing the job argument keys you want to filter out in the UI. This is useful for hiding sensitive user data. Currently, only root-level hash keys are supported.
117118

118119
This library extends Active Job with a querying interface and the following setting:
119120
- `config.active_job.default_page_size`: the internal batch size that Active Job will use when sending queries to the underlying adapter and the batch size for the bulk operations defined above—defaults to `1000`.

app/helpers/mission_control/jobs/jobs_helper.rb

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -67,7 +67,8 @@ def as_renderable_hash(argument)
6767
elsif argument["_aj_serialized"]
6868
ActiveJob::Arguments.deserialize([ argument ]).first
6969
else
70-
argument.without("_aj_symbol_keys", "_aj_ruby2_keywords")
70+
MissionControl::Jobs.job_arguments_filter.apply_to(argument)
71+
.without("_aj_symbol_keys", "_aj_ruby2_keywords")
7172
.transform_values { |v| as_renderable_argument(v) }
7273
.map { |k, v| "#{k}: #{v}" }
7374
.join(", ")
Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,4 @@
11
<h2 class="subtitle">Raw data</h2>
22
<pre>
3-
<%= JSON.pretty_generate(job.raw_data.without("backtrace")) %>
3+
<%= JSON.pretty_generate(job.filtered_raw_data.without("backtrace")) %>
44
</pre>

lib/active_job/job_proxy.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,8 @@ class ActiveJob::JobProxy < ActiveJob::Base
77
class UnsupportedError < StandardError; end
88

99
attr_reader :job_class_name
10+
# Raw data with the sensitive user data filtered out.
11+
attr_accessor :filtered_raw_data
1012

1113
def initialize(job_data)
1214
super

lib/active_job/queue_adapters/resque_ext.rb

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -185,16 +185,29 @@ def fetch_queue_resque_jobs
185185
end
186186

187187
def deserialize_resque_job(resque_job_hash, index)
188-
args_hash = resque_job_hash.dig("payload", "args") || resque_job_hash.dig("args")
189-
ActiveJob::JobProxy.new(args_hash&.first).tap do |job|
188+
args_hash = extract_args_hash(resque_job_hash)
189+
ActiveJob::JobProxy.new(args_hash).tap do |job|
190190
job.last_execution_error = execution_error_from_resque_job(resque_job_hash)
191191
job.raw_data = resque_job_hash
192+
job.filtered_raw_data = filter_raw_data_arguments(resque_job_hash)
192193
job.position = jobs_relation.offset_value + index
193194
job.failed_at = resque_job_hash["failed_at"]&.to_datetime&.utc
194195
job.status = job.failed_at.present? ? :failed : :pending
195196
end
196197
end
197198

199+
def filter_raw_data_arguments(raw_data)
200+
raw_data.deep_dup.tap do |filtered_data|
201+
if args_hash = extract_args_hash(filtered_data)
202+
args_hash["arguments"] = MissionControl::Jobs.job_arguments_filter.apply_to(args_hash["arguments"])
203+
end
204+
end
205+
end
206+
207+
def extract_args_hash(raw_data)
208+
(raw_data.dig("payload", "args") || raw_data.dig("args"))&.first
209+
end
210+
198211
def execution_error_from_resque_job(resque_job_hash)
199212
if resque_job_hash["exception"].present?
200213
ActiveJob::ExecutionError.new \

lib/active_job/queue_adapters/solid_queue_ext.rb

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -99,6 +99,7 @@ def deserialize_and_proxy_solid_queue_job(solid_queue_job, job_status = nil)
9999
job.status = job_status
100100
job.last_execution_error = execution_error_from_solid_queue_job(solid_queue_job) if job_status == :failed
101101
job.raw_data = solid_queue_job.as_json
102+
job.filtered_raw_data = filter_raw_data_arguments(job.raw_data)
102103
job.failed_at = solid_queue_job&.failed_execution&.created_at if job_status == :failed
103104
job.finished_at = solid_queue_job.finished_at
104105
job.blocked_by = solid_queue_job.concurrency_key
@@ -109,6 +110,12 @@ def deserialize_and_proxy_solid_queue_job(solid_queue_job, job_status = nil)
109110
end
110111
end
111112

113+
def filter_raw_data_arguments(raw_data)
114+
raw_data.deep_dup.tap do |filtered_raw_data|
115+
filtered_raw_data["arguments"]["arguments"] = MissionControl::Jobs.job_arguments_filter.apply_to(filtered_raw_data.dig("arguments", "arguments"))
116+
end
117+
end
118+
112119
def status_from_solid_queue_job(solid_queue_job)
113120
SolidQueueJobs::STATUS_MAP.invert[solid_queue_job.status]
114121
end

lib/mission_control/jobs.rb

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -31,5 +31,11 @@ module Jobs
3131
mattr_accessor :http_basic_auth_user
3232
mattr_accessor :http_basic_auth_password
3333
mattr_accessor :http_basic_auth_enabled, default: true
34+
35+
mattr_accessor :filter_arguments, default: []
36+
37+
def self.job_arguments_filter
38+
MissionControl::Jobs::ArgumentsFilter.new(filter_arguments)
39+
end
3440
end
3541
end
Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,24 @@
1+
# Replaces argument values with [FILTERED] for any keys that match a filter.
2+
class MissionControl::Jobs::ArgumentsFilter
3+
FILTERED = "[FILTERED]"
4+
5+
def initialize(filter)
6+
@filter = filter
7+
end
8+
9+
def apply_to(arguments)
10+
case arguments
11+
when Array
12+
arguments.map { |a| apply_to(a) }
13+
when Hash
14+
arguments.map do |k, v|
15+
[ k, filter.include?(k.to_s) ? FILTERED : v ]
16+
end.to_h
17+
else
18+
arguments
19+
end
20+
end
21+
22+
private
23+
attr_reader :filter
24+
end

test/active_job/queue_adapters/adapter_testing/retry_jobs.rb

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -116,4 +116,24 @@ module ActiveJob::QueueAdapters::AdapterTesting::RetryJobs
116116
failed_job.retry
117117
end
118118
end
119+
120+
test "retrying a single job with filtered arguments preserves the original arguments" do
121+
@previous_filter_arguments, MissionControl::Jobs.filter_arguments = MissionControl::Jobs.filter_arguments, %w[ author ]
122+
arguments = [ Post.create(title: "hello_world"), 1.year.ago, { author: "Jorge", price: 10 } ]
123+
FailingPostJob.perform_later(arguments)
124+
perform_enqueued_jobs
125+
126+
failed_job = ActiveJob.jobs.failed.last
127+
failed_job.retry
128+
129+
perform_enqueued_jobs
130+
131+
invocations = FailingPostJob.invocations
132+
assert_equal 2, invocations.count
133+
invocations.each do |invocation|
134+
assert_equal arguments, invocation.arguments.first
135+
end
136+
ensure
137+
MissionControl::Jobs.filter_arguments = @previous_filter_arguments
138+
end
119139
end

test/dummy/config/initializers/mission_control_jobs.rb

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ def redis_connection_for(app, server)
1515
Resque::DataStore.new redis_namespace
1616
end
1717

18+
# Filter sensitive arguments from the UI.
19+
MissionControl::Jobs.filter_arguments = %w[ author ]
20+
1821
SERVERS_BY_APP.each do |app, servers|
1922
queue_adapters_by_name = servers.collect do |server|
2023
queue_adapter = if server.start_with?("resque")

0 commit comments

Comments
 (0)