solid_queue_monitor 2.1.0 → 2.2.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 92ff7e193202f9653de1b877d2e5d8b2f841c1e50e6c86a9ba4c556782e1fe8b
4
- data.tar.gz: 2d180ede70f06618f676d167674238e4e7940e02f0b5af56b2667f543ab833f0
3
+ metadata.gz: 4f1ef08bb05ef11d7b29d511beea231cab0d1a8e1b16fc88d6c0d1fd540660b1
4
+ data.tar.gz: 310ff85731b0aa63432b03f909b39eb942a0eeccf59fc4956673c50561f70582
5
5
  SHA512:
6
- metadata.gz: 203a85cfcb427b2d21faf15c0203bea8c06ee8a01296ad94d318aae018ad83cd8a7c86b9ac633a300588df9a7dc012a1ba42c2a0bcc91e1b97829816e527bba2
7
- data.tar.gz: 4a3904c1f727a72d47d960cfc0e20c70ffd0f0f5c62463c652d31123653956e73874661d67b15a7cdbd14b6d36aed322b946548f2906d73860f6ef12d79c4ce2
6
+ metadata.gz: a7df976149fe809482b5a7a2299a431957174582d5134b7080942420595a469a645fae5541082a326e035b3b05fbc355f001d31eb06377b7a1275101139f735a
7
+ data.tar.gz: 4e32adbbba30b69d0eea3eb106d68172d8d35ae00a3ae111eb1f560337bee25708b63d5654466cd1c47e49a1d37e3e51979e2a51ac6e5412f975ad00be1a798e
data/README.md CHANGED
@@ -123,6 +123,9 @@ SolidQueueMonitor.setup do |config|
123
123
 
124
124
  # Disable the chart on the overview page to skip chart queries entirely
125
125
  # config.show_chart = true
126
+
127
+ # Enable CSRF protection for the dashboard's destructive actions (opt-in)
128
+ # config.csrf_protection_enabled = false
126
129
  end
127
130
 
128
131
  # Optional: inherit from a host-app controller to plug into your existing auth.
@@ -140,6 +143,27 @@ If you don't need the job activity chart, disable it to skip chart queries entir
140
143
  config.show_chart = false
141
144
  ```
142
145
 
146
+ ### CSRF Protection
147
+
148
+ The dashboard's destructive actions (retry, discard, pause, resume, execute, reject, remove/prune workers) are all `POST` requests. By default CSRF protection is **disabled**, because the gem does not assume the host application has a session store (it works in API-only apps without one).
149
+
150
+ If your host app has a session store and the dashboard is mounted on the same origin, you should enable CSRF protection:
151
+
152
+ ```ruby
153
+ config.csrf_protection_enabled = true
154
+ ```
155
+
156
+ When enabled:
157
+
158
+ - All dashboard forms embed an `authenticity_token`, and `csrf_meta_tags` are added to the layout for JS/`fetch`-driven requests.
159
+ - Unverified `POST` requests are rejected by Rails' standard `verify_authenticity_token` (returns `422 Unprocessable Entity`). Safe methods (`GET`/`HEAD`) pass through.
160
+
161
+ Requirements:
162
+
163
+ - The host app has a session store configured (e.g. `config.session_store :cookie_store`).
164
+ - `config.api_only` is not enabled (or session middleware is otherwise present).
165
+ - The dashboard is mounted on the same origin as the host app, so `form_authenticity_token` works.
166
+
143
167
  ### Authentication
144
168
 
145
169
  By default, Solid Queue Monitor does not require authentication to access the dashboard. This makes it easy to get started in development environments.
@@ -322,7 +322,9 @@
322
322
  function bulkSubmit(action, promptMsg) {
323
323
  var ids = checkedBoxes().map(function (checkbox) { return checkbox.value; });
324
324
  if (ids.length === 0 || !window.confirm(promptMsg)) return;
325
- Array.prototype.slice.call(form.querySelectorAll('input[type="hidden"]')).forEach(function (input) { input.remove(); });
325
+ // Only clear previously-appended job id inputs. Other hidden inputs
326
+ // (e.g. the CSRF authenticity_token) must be preserved.
327
+ Array.prototype.slice.call(form.querySelectorAll('input[type="hidden"][name="job_ids[]"]')).forEach(function (input) { input.remove(); });
326
328
  form.action = action;
327
329
  ids.forEach(function (id) { appendHidden('job_ids[]', id); });
328
330
  form.submit();
@@ -13,7 +13,12 @@ module SolidQueueMonitor
13
13
 
14
14
  before_action :authenticate, if: -> { SolidQueueMonitor::AuthenticationService.authentication_required? }
15
15
  layout 'solid_queue_monitor/application'
16
- skip_before_action :verify_authenticity_token
16
+
17
+ # CSRF protection is opt-in (config.csrf_protection_enabled). By default the
18
+ # token check is skipped so the dashboard works in hosts without a session
19
+ # store. When the host enables it, the standard verify_authenticity_token
20
+ # before_action runs and unverified POSTs are rejected.
21
+ skip_before_action :verify_authenticity_token, unless: -> { SolidQueueMonitor.csrf_protection_enabled }
17
22
 
18
23
  def set_flash_message(message, type)
19
24
  # Store in instance variable for access in views
@@ -4,6 +4,10 @@ module SolidQueueMonitor
4
4
  class AssetsController < ApplicationController
5
5
  skip_before_action :authenticate, raise: false
6
6
 
7
+ # Public read-only assets: exempt from CSRF so the cross-origin JavaScript
8
+ # guard doesn't reject GETs for the JS asset when csrf_protection_enabled.
9
+ skip_forgery_protection
10
+
7
11
  MIME_TYPES = { '.css' => 'text/css', '.js' => 'application/javascript' }.freeze
8
12
  FINGERPRINT_PATTERN = /\A(?<base>[A-Za-z0-9_]+)-(?<hash>[a-f0-9]+)(?<ext>\.css|\.js)\z/
9
13
 
@@ -25,6 +25,23 @@ module SolidQueueMonitor
25
25
  type.to_s == 'success' ? 'message-success' : 'message-error'
26
26
  end
27
27
 
28
+ # Hidden authenticity_token field for raw HTML POST forms.
29
+ # Renders nothing unless CSRF protection is enabled, so hosts without a
30
+ # session store are unaffected (form_authenticity_token needs a session).
31
+ def csrf_token_field_if_enabled
32
+ return ''.html_safe unless SolidQueueMonitor.csrf_protection_enabled
33
+
34
+ hidden_field_tag(:authenticity_token, form_authenticity_token)
35
+ end
36
+
37
+ # CSRF meta tags for JS/fetch-driven POSTs (defense in depth).
38
+ # Only emitted when CSRF protection is enabled, for the same reason.
39
+ def csrf_meta_tags_if_enabled
40
+ return ''.html_safe unless SolidQueueMonitor.csrf_protection_enabled
41
+
42
+ csrf_meta_tags
43
+ end
44
+
28
45
  def queue_link(queue_name, css_class: nil)
29
46
  return '-' if queue_name.blank?
30
47
 
@@ -4,6 +4,7 @@
4
4
  <title>Solid Queue Monitor - <%= content_for?(:title) ? yield(:title) : 'Dashboard' %></title>
5
5
  <meta charset="UTF-8">
6
6
  <meta name="viewport" content="width=device-width, initial-scale=1.0">
7
+ <%= csrf_meta_tags_if_enabled %>
7
8
  <%= stylesheet_link_tag asset_url_for('application.css'), nonce: content_security_policy_nonce %>
8
9
  </head>
9
10
  <body class="solid_queue_monitor"
@@ -13,12 +13,14 @@
13
13
  <td class="actions-cell">
14
14
  <div class="job-actions">
15
15
  <form method="post" action="<%= retry_failed_job_path(id: job.id) %>" class="inline-form">
16
+ <%= csrf_token_field_if_enabled %>
16
17
  <button type="submit" class="action-button retry-button">Retry</button>
17
18
  </form>
18
19
  <form method="post"
19
20
  action="<%= discard_failed_job_path(id: job.id) %>"
20
21
  class="inline-form"
21
22
  data-confirm="Are you sure you want to discard this job?">
23
+ <%= csrf_token_field_if_enabled %>
22
24
  <button type="submit" class="action-button discard-button">Discard</button>
23
25
  </form>
24
26
  </div>
@@ -15,6 +15,7 @@
15
15
  </div>
16
16
 
17
17
  <form method="post" id="failed-jobs-form">
18
+ <%= csrf_token_field_if_enabled %>
18
19
  <% columns = [
19
20
  { sort_key: nil, label: tag.input(type: 'checkbox', id: 'select-all', class: 'select-all-checkbox') },
20
21
  { sort_key: :class_name, label: 'Job' },
@@ -15,6 +15,7 @@
15
15
  <div class="job-actions">
16
16
  <% if @failed_execution %>
17
17
  <form action="<%= retry_failed_job_path(id: @failed_execution.id) %>" method="post" class="inline-form">
18
+ <%= csrf_token_field_if_enabled %>
18
19
  <input type="hidden" name="redirect_to" value="<%= job_path(@job) %>">
19
20
  <button type="submit" class="action-button retry-button">Retry</button>
20
21
  </form>
@@ -22,12 +23,14 @@
22
23
  method="post"
23
24
  class="inline-form"
24
25
  data-confirm="Are you sure you want to discard this job?">
26
+ <%= csrf_token_field_if_enabled %>
25
27
  <input type="hidden" name="redirect_to" value="<%= failed_jobs_path %>">
26
28
  <button type="submit" class="action-button discard-button">Discard</button>
27
29
  </form>
28
30
  <% end %>
29
31
  <% if @scheduled_execution %>
30
32
  <form action="<%= execute_scheduled_job_path(id: @scheduled_execution.id) %>" method="post" class="inline-form">
33
+ <%= csrf_token_field_if_enabled %>
31
34
  <input type="hidden" name="redirect_to" value="<%= scheduled_jobs_path %>">
32
35
  <button type="submit" class="action-button retry-button">Execute Now</button>
33
36
  </form>
@@ -10,6 +10,7 @@
10
10
  <% if failed_execution %>
11
11
  <div class="job-actions">
12
12
  <form method="post" action="<%= retry_failed_job_path(id: failed_execution.id) %>" class="inline-form">
13
+ <%= csrf_token_field_if_enabled %>
13
14
  <input type="hidden" name="redirect_to" value="<%= root_path %>">
14
15
  <button type="submit" class="action-button retry-button">Retry</button>
15
16
  </form>
@@ -17,6 +18,7 @@
17
18
  action="<%= discard_failed_job_path(id: failed_execution.id) %>"
18
19
  class="inline-form"
19
20
  data-confirm="Are you sure you want to discard this job?">
21
+ <%= csrf_token_field_if_enabled %>
20
22
  <input type="hidden" name="redirect_to" value="<%= root_path %>">
21
23
  <button type="submit" class="action-button discard-button">Discard</button>
22
24
  </form>
@@ -10,6 +10,7 @@
10
10
  <% if failed_execution %>
11
11
  <div class="job-actions">
12
12
  <form method="post" action="<%= retry_failed_job_path(id: failed_execution.id) %>" class="inline-form">
13
+ <%= csrf_token_field_if_enabled %>
13
14
  <input type="hidden" name="redirect_to" value="<%= queue_details_path(queue_name: @queue_name) %>">
14
15
  <button type="submit" class="action-button retry-button">Retry</button>
15
16
  </form>
@@ -17,6 +18,7 @@
17
18
  action="<%= discard_failed_job_path(id: failed_execution.id) %>"
18
19
  class="inline-form"
19
20
  data-confirm="Are you sure you want to discard this job?">
21
+ <%= csrf_token_field_if_enabled %>
20
22
  <input type="hidden" name="redirect_to" value="<%= queue_details_path(queue_name: @queue_name) %>">
21
23
  <button type="submit" class="action-button discard-button">Discard</button>
22
24
  </form>
@@ -17,6 +17,7 @@
17
17
  <td class="actions-cell">
18
18
  <% if paused %>
19
19
  <form action="<%= resume_queue_path %>" method="post" class="inline-form">
20
+ <%= csrf_token_field_if_enabled %>
20
21
  <input type="hidden" name="queue_name" value="<%= queue_name %>">
21
22
  <button type="submit" class="action-button resume-button" title="Resume queue processing">Resume</button>
22
23
  </form>
@@ -25,6 +26,7 @@
25
26
  method="post"
26
27
  class="inline-form"
27
28
  data-confirm="Are you sure you want to pause the <%= queue_name %> queue? Workers will stop processing jobs from this queue.">
29
+ <%= csrf_token_field_if_enabled %>
28
30
  <input type="hidden" name="queue_name" value="<%= queue_name %>">
29
31
  <button type="submit" class="action-button pause-button" title="Pause queue processing">Pause</button>
30
32
  </form>
@@ -11,12 +11,14 @@
11
11
  <div class="section-header-right">
12
12
  <% if @paused %>
13
13
  <form action="<%= resume_queue_path %>" method="post" class="inline-form">
14
+ <%= csrf_token_field_if_enabled %>
14
15
  <input type="hidden" name="queue_name" value="<%= @queue_name %>">
15
16
  <input type="hidden" name="redirect_to" value="<%= queue_details_path(queue_name: @queue_name) %>">
16
17
  <button type="submit" class="action-button resume-button">Resume Queue</button>
17
18
  </form>
18
19
  <% else %>
19
20
  <form action="<%= pause_queue_path %>" method="post" class="inline-form" data-confirm="Are you sure you want to pause this queue?">
21
+ <%= csrf_token_field_if_enabled %>
20
22
  <input type="hidden" name="queue_name" value="<%= @queue_name %>">
21
23
  <input type="hidden" name="redirect_to" value="<%= queue_details_path(queue_name: @queue_name) %>">
22
24
  <button type="submit" class="action-button pause-button">Pause Queue</button>
@@ -15,6 +15,7 @@
15
15
  </div>
16
16
 
17
17
  <form id="scheduled-jobs-form" method="post">
18
+ <%= csrf_token_field_if_enabled %>
18
19
  <% columns = [
19
20
  { sort_key: nil, label: tag.input(type: 'checkbox', id: 'scheduled-jobs-select-all') },
20
21
  { sort_key: :class_name, label: 'Job' },
@@ -13,6 +13,7 @@
13
13
  method="post"
14
14
  class="inline-form"
15
15
  data-confirm="Remove this dead process from the registry?">
16
+ <%= csrf_token_field_if_enabled %>
16
17
  <button type="submit" class="action-button discard-button" title="Remove dead process">Remove</button>
17
18
  </form>
18
19
  <% else %>
@@ -24,7 +24,7 @@
24
24
  data-confirm="Remove all <%= @summary[:dead] %> dead process<%= suffix %>? This will clean up processes that have stopped sending heartbeats.">
25
25
  Prune all
26
26
  </a>
27
- <form id="prune-all-form" action="<%= prune_workers_path %>" method="post" class="is-hidden"></form>
27
+ <form id="prune-all-form" action="<%= prune_workers_path %>" method="post" class="is-hidden"><%= csrf_token_field_if_enabled %></form>
28
28
  <% end %>
29
29
  </div>
30
30
  </div>
@@ -27,4 +27,11 @@ SolidQueueMonitor.setup do |config|
27
27
 
28
28
  # Disable the chart on the overview page to skip chart queries entirely.
29
29
  # config.show_chart = true
30
+
31
+ # Enable CSRF protection for the dashboard's destructive POST actions.
32
+ # Disabled by default for backward compatibility. Requires the host app to
33
+ # have a session store (e.g. cookie_store) and the dashboard mounted on the
34
+ # same origin. When enabled, all dashboard forms embed an authenticity token
35
+ # and unverified POSTs are rejected.
36
+ # config.csrf_protection_enabled = false
30
37
  end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module SolidQueueMonitor
4
- VERSION = '2.1.0'
4
+ VERSION = '2.2.0'
5
5
  end
@@ -11,7 +11,8 @@ module SolidQueueMonitor
11
11
  class << self
12
12
  attr_writer :username, :password, :base_controller_class
13
13
  attr_accessor :jobs_per_page, :authentication_enabled,
14
- :auto_refresh_enabled, :auto_refresh_interval, :show_chart
14
+ :auto_refresh_enabled, :auto_refresh_interval, :show_chart,
15
+ :csrf_protection_enabled
15
16
 
16
17
  def username
17
18
  resolve_value(@username)
@@ -39,6 +40,9 @@ module SolidQueueMonitor
39
40
  @auto_refresh_enabled = true
40
41
  @auto_refresh_interval = 30 # seconds
41
42
  @show_chart = true
43
+ # Disabled by default for backward compatibility: enabling CSRF protection
44
+ # requires a session-backed host app, which the gem does not assume.
45
+ @csrf_protection_enabled = false
42
46
 
43
47
  def self.setup
44
48
  yield self
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: solid_queue_monitor
3
3
  version: !ruby/object:Gem::Version
4
- version: 2.1.0
4
+ version: 2.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Vishal Sadriya