Skip to content

Commit 6b6a4cb

Browse files
committed
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.
1 parent 9fa76f0 commit 6b6a4cb

12 files changed

Lines changed: 117 additions & 8 deletions

File tree

README.md

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,8 @@ 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 job argument keys that you want to filter out in the UI. This is useful for hiding sensitive user data. You can also override this option for each application. Currently, only root-level hash keys are supported, and it only works with the `Resque` and `SolidQueue` adapters. See the [Advanced configuration](#advanced-configuration) section for an example.
118+
```ruby
117119

118120
This library extends Active Job with a querying interface and the following setting:
119121
- `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`.
@@ -184,7 +186,9 @@ SERVERS_BY_APP.each do |app, servers|
184186
# [ server, [ queue_adapter, BacktraceCleaner.new ]] # with optional backtrace cleaner
185187
end.to_h
186188
187-
MissionControl::Jobs.applications.add(app, queue_adapters_by_name)
189+
filter_arguments = %i[ author ]
190+
191+
MissionControl::Jobs.applications.add(app, queue_adapters_by_name, filter_arguments)
188192
end
189193
```
190194

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+
ActiveJob::JobArgumentFilter.filter_argument_hash(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: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
class ActiveJob::JobArgumentFilter
2+
FILTERED = "[FILTERED]"
3+
4+
class << self
5+
def filter_arguments(arguments)
6+
arguments.each do |argument|
7+
if argument.is_a?(Hash)
8+
filter_argument_hash(argument)
9+
end
10+
end
11+
end
12+
13+
def filter_argument_hash(argument)
14+
return argument if filters.blank?
15+
16+
argument.each do |key, value|
17+
if filters.include?(key.to_s)
18+
argument[key] = FILTERED
19+
end
20+
end
21+
end
22+
23+
private
24+
def filters
25+
MissionControl::Jobs::Current.application&.filter_arguments
26+
end
27+
end
28+
end

lib/active_job/queue_adapters/resque_ext.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -188,13 +188,19 @@ def deserialize_resque_job(resque_job_hash, index)
188188
args_hash = resque_job_hash.dig("payload", "args") || resque_job_hash.dig("args")
189189
ActiveJob::JobProxy.new(args_hash&.first).tap do |job|
190190
job.last_execution_error = execution_error_from_resque_job(resque_job_hash)
191-
job.raw_data = resque_job_hash
191+
job.raw_data = filter_raw_data_arguments(resque_job_hash)
192192
job.position = jobs_relation.offset_value + index
193193
job.failed_at = resque_job_hash["failed_at"]&.to_datetime&.utc
194194
job.status = job.failed_at.present? ? :failed : :pending
195195
end
196196
end
197197

198+
def filter_raw_data_arguments(raw_data)
199+
args = raw_data.dig("payload", "args")
200+
ActiveJob::JobArgumentFilter.filter_arguments(args.first["arguments"]) if args
201+
raw_data
202+
end
203+
198204
def execution_error_from_resque_job(resque_job_hash)
199205
if resque_job_hash["exception"].present?
200206
ActiveJob::ExecutionError.new \

lib/active_job/queue_adapters/solid_queue_ext.rb

Lines changed: 7 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -98,7 +98,7 @@ def deserialize_and_proxy_solid_queue_job(solid_queue_job, job_status = nil)
9898
ActiveJob::JobProxy.new(solid_queue_job.arguments).tap do |job|
9999
job.status = job_status
100100
job.last_execution_error = execution_error_from_solid_queue_job(solid_queue_job) if job_status == :failed
101-
job.raw_data = solid_queue_job.as_json
101+
job.raw_data = filter_raw_data_arguments(solid_queue_job.as_json)
102102
job.failed_at = solid_queue_job&.failed_execution&.created_at if job_status == :failed
103103
job.finished_at = solid_queue_job.finished_at
104104
job.blocked_by = solid_queue_job.concurrency_key
@@ -109,6 +109,12 @@ def deserialize_and_proxy_solid_queue_job(solid_queue_job, job_status = nil)
109109
end
110110
end
111111

112+
def filter_raw_data_arguments(raw_data)
113+
arguments = raw_data.dig("arguments", "arguments")
114+
ActiveJob::JobArgumentFilter.filter_arguments(arguments)
115+
raw_data
116+
end
117+
112118
def status_from_solid_queue_job(solid_queue_job)
113119
SolidQueueJobs::STATUS_MAP.invert[solid_queue_job.status]
114120
end

lib/mission_control/jobs.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@ module Jobs
2626
mattr_accessor :show_console_help, default: true
2727
mattr_accessor :backtrace_cleaner
2828

29+
mattr_accessor :filter_arguments, default: []
30+
2931
mattr_accessor :importmap, default: Importmap::Map.new
3032

3133
mattr_accessor :http_basic_auth_user

lib/mission_control/jobs/application.rb

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,11 +2,12 @@
22
class MissionControl::Jobs::Application
33
include MissionControl::Jobs::IdentifiedByName
44

5-
attr_reader :servers
5+
attr_reader :servers, :filter_arguments
66

77
def initialize(name:)
88
super
99
@servers = MissionControl::Jobs::IdentifiedElements.new
10+
@filter_arguments = []
1011
end
1112

1213
def add_servers(queue_adapters_by_name)
@@ -17,4 +18,8 @@ def add_servers(queue_adapters_by_name)
1718
backtrace_cleaner: cleaner, application: self)
1819
end
1920
end
21+
22+
def filter_arguments=(arguments)
23+
@filter_arguments = Array(arguments).map(&:to_s)
24+
end
2025
end
Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,9 @@
11
# A container to register applications
22
class MissionControl::Jobs::Applications < MissionControl::Jobs::IdentifiedElements
3-
def add(name, queue_adapters_by_name = {})
3+
def add(name, queue_adapters_by_name = {}, filter_arguments = [])
44
self << MissionControl::Jobs::Application.new(name: name).tap do |application|
55
application.add_servers(queue_adapters_by_name)
6+
application.filter_arguments = filter_arguments.presence || MissionControl::Jobs.filter_arguments
67
end
78
end
89
end
Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,38 @@
1+
require "test_helper"
2+
3+
class ActiveJob::JobArgumentFilterTest < ActiveSupport::TestCase
4+
setup do
5+
@application = MissionControl::Jobs::Application.new(name: "BC4")
6+
MissionControl::Jobs::Current.application = @application
7+
end
8+
9+
test "filter_arguments" do
10+
arguments = [
11+
"deliver",
12+
{
13+
email_address: "jorge@37signals.com",
14+
profile: { name: "Jorge Manrubia" },
15+
message: "Hello!"
16+
}
17+
]
18+
@application.filter_arguments = %i[ email_address message ]
19+
20+
filtered = ActiveJob::JobArgumentFilter.filter_arguments(arguments)
21+
22+
assert_equal "deliver", filtered[0]
23+
assert_equal({ email_address: "[FILTERED]", profile: { name: "Jorge Manrubia" }, message: "[FILTERED]" }, filtered[1])
24+
end
25+
26+
test "filter_argument_hash" do
27+
argument = {
28+
email_address: "jorge@37signals.com",
29+
message: "Hello!"
30+
}
31+
filtered = ActiveJob::JobArgumentFilter.filter_argument_hash(argument)
32+
assert_equal({ email_address: "jorge@37signals.com", message: "Hello!" }, filtered)
33+
34+
@application.filter_arguments = %i[ message ]
35+
filtered = ActiveJob::JobArgumentFilter.filter_argument_hash(argument)
36+
assert_equal({ email_address: "jorge@37signals.com", message: "[FILTERED]" }, filtered)
37+
end
38+
end

test/dummy/config/initializers/mission_control_jobs.rb

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,5 +26,7 @@ def redis_connection_for(app, server)
2626
[ server, queue_adapter ]
2727
end.to_h
2828

29-
MissionControl::Jobs.applications.add(app, queue_adapters_by_name)
29+
filter_arguments = %i[ author ]
30+
31+
MissionControl::Jobs.applications.add(app, queue_adapters_by_name, filter_arguments)
3032
end

0 commit comments

Comments
 (0)