rocketjob_mission_control 3.1.0 → 3.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.
Files changed (24) hide show
  1. checksums.yaml +4 -4
  2. data/Rakefile +3 -1
  3. data/app/controllers/rocket_job_mission_control/active_workers_controller.rb +1 -0
  4. data/app/controllers/rocket_job_mission_control/application_controller.rb +15 -2
  5. data/app/controllers/rocket_job_mission_control/dirmon_entries_controller.rb +19 -2
  6. data/app/controllers/rocket_job_mission_control/jobs_controller.rb +45 -17
  7. data/app/controllers/rocket_job_mission_control/servers_controller.rb +18 -2
  8. data/app/datatables/rocket_job_mission_control/abstract_datatable.rb +0 -1
  9. data/app/datatables/rocket_job_mission_control/jobs_datatable.rb +8 -6
  10. data/app/datatables/rocket_job_mission_control/servers_datatable.rb +20 -8
  11. data/app/models/rocket_job_mission_control/access_policy.rb +50 -0
  12. data/app/models/rocket_job_mission_control/authorization.rb +22 -0
  13. data/app/views/rocket_job_mission_control/dirmon_entries/_sidebar.html.erb +5 -2
  14. data/app/views/rocket_job_mission_control/dirmon_entries/show.html.erb +8 -2
  15. data/app/views/rocket_job_mission_control/jobs/show.html.erb +9 -5
  16. data/app/views/rocket_job_mission_control/servers/index.html.erb +10 -8
  17. data/lib/rocket_job_mission_control/engine.rb +9 -0
  18. data/lib/rocket_job_mission_control/version.rb +1 -1
  19. data/test/controllers/rocket_job_mission_control/dirmon_entries_controller_test.rb +78 -1
  20. data/test/controllers/rocket_job_mission_control/jobs_controller_test.rb +52 -1
  21. data/test/controllers/rocket_job_mission_control/servers_controller_test.rb +58 -1
  22. data/test/test_helper.rb +12 -10
  23. data/vendor/assets/stylesheets/{fontawesome-all.min.css → fontawesome-all.min.css.erb} +1 -1
  24. metadata +20 -4
@@ -42,7 +42,21 @@ module RocketJobMissionControl
42
42
 
43
43
  def action_links_html(server)
44
44
  actions = '<div class="actions">'
45
- if server.stopping?
45
+ events = valid_events(server)
46
+
47
+ if events.include?(:resume) && view.can?(:resume, server)
48
+ actions += "#{ link_to "resume", resume_server_path(server), method: :patch, class: 'btn btn-default', data: {confirm: "Resume this server?"} }"
49
+ end
50
+
51
+ if events.include?(:pause) && view.can?(:pause, server)
52
+ actions += "#{ link_to "pause", pause_server_path(server), method: :patch, class: 'btn btn-default', data: {confirm: "Pause this server?"} }"
53
+ end
54
+
55
+ if events.include?(:stop) && view.can?(:stop, server)
56
+ actions += "#{ link_to "stop", stop_server_path(server), method: :patch, class: 'btn btn-danger', data: {confirm: "Stop this server?"} }"
57
+ end
58
+
59
+ if server.stopping? && view.can?(:destroy, server)
46
60
  actions += "Server is stopping..."
47
61
  confirmation = ''
48
62
  unless server.zombie?
@@ -50,16 +64,14 @@ module RocketJobMissionControl
50
64
  end
51
65
  confirmation << "Are you sure you want to destroy #{server.name} ?"
52
66
  actions += "#{ link_to "destroy", server_path(server), method: :delete, class: 'btn btn-danger', data: {confirm: confirmation} }"
53
- else
54
- if server.paused?
55
- actions += "#{ link_to "resume", resume_server_path(server), method: :patch, class: 'btn btn-default', data: {confirm: "Resume this server?"} }"
56
- else
57
- actions += "#{ link_to "pause", pause_server_path(server), method: :patch, class: 'btn btn-default', data: {confirm: "Pause this server?"} }"
58
- end
59
- actions += "#{ link_to "stop", stop_server_path(server), method: :patch, class: 'btn btn-danger', data: {confirm: "Stop this server?"} }"
60
67
  end
68
+
61
69
  actions += '</div>'
62
70
  end
63
71
 
72
+ def valid_events(server)
73
+ server.aasm.events.collect(&:name)
74
+ end
75
+
64
76
  end
65
77
  end
@@ -0,0 +1,50 @@
1
+ module RocketJobMissionControl
2
+ class AccessPolicy
3
+ include AccessGranted::Policy
4
+
5
+ def configure
6
+ # Destroy Jobs, Dirmon Entries
7
+ role :admin, {admin: true} do
8
+ can %i[create destroy], RocketJob::Job
9
+ can :destroy, RocketJob::DirmonEntry
10
+ end
11
+
12
+ # View the contents of jobs and edit the data within them.
13
+ # Including encrypted records.
14
+ role :editor, {editor: true} do
15
+ can %i[read_records update_records], RocketJob::Job
16
+ end
17
+
18
+ # Stop, Pause, Resume, Destroy (force stop) Rocket Job Servers
19
+ role :operator, {operator: true} do
20
+ can %i[stop pause resume destroy update_all], RocketJob::Server
21
+ end
22
+
23
+ # Pause, Resume, Retry, Abort, Edit Jobs
24
+ role :manager, {manager: true} do
25
+ can %i[edit pause resume retry abort fail update run_now], RocketJob::Job
26
+ end
27
+
28
+ # Create, Destroy, Enable, Disable, Edit Dirmon Entries
29
+ role :dirmon, {dirmon: true} do
30
+ can %i[create enable disable update edit], RocketJob::DirmonEntry
31
+ end
32
+
33
+ # A User can only edit their own jobs
34
+ role :user, {user: true} do
35
+ can %i[edit pause resume retry abort update], RocketJob::Job do |job, auth|
36
+ job.respond_to?(:login) && (job.login == auth.login)
37
+ end
38
+ end
39
+
40
+ # Read only access
41
+ role :view do
42
+ can :read, RocketJob::Job
43
+ can :read, RocketJob::DirmonEntry
44
+ can :read, RocketJob::Server
45
+ can :read, RocketJob::Worker
46
+ end
47
+ end
48
+ end
49
+ end
50
+
@@ -0,0 +1,22 @@
1
+ module RocketJobMissionControl
2
+ class Authorization
3
+ ROLES = %i[admin editor operator manager dirmon user view]
4
+ attr_accessor *ROLES
5
+ attr_accessor :login
6
+
7
+ def initialize(roles: [], login: nil)
8
+ @login = login
9
+ return if roles.blank?
10
+ invalid_roles = roles - ROLES
11
+ raise(ArgumentError, "Invalid Roles Supplied: #{invalid_roles.inspect}") unless invalid_roles.empty?
12
+
13
+ roles.each { |r| inherit_less_privilege_roles(r) }
14
+ end
15
+
16
+ def inherit_less_privilege_roles(role)
17
+ index = ROLES.index(role)
18
+ roles = ROLES[index..-1]
19
+ roles.each { |role| public_send("#{role}=", true) }
20
+ end
21
+ end
22
+ end
@@ -1,7 +1,10 @@
1
1
  <ul class='nav hidden-xs' id='lg-menu'>
2
2
  <li>
3
- <%= link_to(new_dirmon_entry_path) do %>
4
- <i class='fas fa-plus'></i> Create
3
+
4
+ <% if can?(:create, RocketJob::DirmonEntry) %>
5
+ <%= link_to(new_dirmon_entry_path) do %>
6
+ <i class='fas fa-plus'></i> Create
7
+ <% end %>
5
8
  <% end %>
6
9
 
7
10
  <%= link_to(dirmon_entries_path) do %>
@@ -9,21 +9,27 @@
9
9
 
10
10
  <div class='btn-toolbar job-actions pull-right'>
11
11
  <div class='btn-group'>
12
- <% if @dirmon_entry.disabled? || @dirmon_entry.pending? %>
12
+ <% if @dirmon_entry.disabled? || @dirmon_entry.pending? && can?(:enable, @dirmon_entry) %>
13
13
  <%= link_to 'Enable', enable_dirmon_entry_path(@dirmon_entry.id), method: :put, class: 'btn btn-default' %>
14
14
  <% end %>
15
15
 
16
- <% if @dirmon_entry.enabled? %>
16
+ <% if @dirmon_entry.enabled? && can?(:disable, @dirmon_entry)%>
17
17
  <%= link_to 'Disable', disable_dirmon_entry_path(@dirmon_entry.id), method: :put, class: 'btn btn-default', data: { confirm: 'Are you sure?' } %>
18
18
  <% end %>
19
19
  </div>
20
20
 
21
+
21
22
  <div class='btn-group'>
23
+ <% if can?(:destroy, @dirmon_entry)%>
22
24
  <%= link_to 'Destroy', dirmon_entry_path(@dirmon_entry), method: :delete, class: 'btn btn-default', data: { confirm: 'Are you sure?' } %>
25
+ <% end %>
23
26
  </div>
24
27
 
28
+
25
29
  <div class='btn-group'>
30
+ <% if can?(:edit, @dirmon_entry)%>
26
31
  <%= link_to 'Edit', edit_dirmon_entry_path(@dirmon_entry), class: 'btn btn-default' %>
32
+ <% end %>
27
33
  </div>
28
34
  </div>
29
35
 
@@ -31,34 +31,38 @@
31
31
  <div class='btn-group'>
32
32
  <% valid_events = @job.aasm.events.collect { |e| e.name } %>
33
33
 
34
- <% if valid_events.include?(:pause) %>
34
+ <% if valid_events.include?(:pause) && can?(:pause, @job)%>
35
35
  <%= job_action_link('Pause', rocket_job_mission_control.pause_job_path(@job), :patch) %>
36
36
  <% end %>
37
37
 
38
- <% if valid_events.include?(:resume) %>
38
+ <% if valid_events.include?(:resume) && can?(:resume, @job)%>
39
39
  <%= job_action_link('Resume', rocket_job_mission_control.resume_job_path(@job), :patch) %>
40
40
  <% end %>
41
41
 
42
- <% if valid_events.include?(:retry) %>
42
+ <% if valid_events.include?(:retry) && can?(:retry, @job)%>
43
43
  <%= job_action_link('Retry', rocket_job_mission_control.retry_job_path(@job), :patch) %>
44
44
  <% end %>
45
45
  </div>
46
46
 
47
47
  <div class='btn-group'>
48
- <% if valid_events.include?(:fail) %>
48
+ <% if valid_events.include?(:fail) && can?(:fail, @job)%>
49
49
  <%= job_action_link('Fail', rocket_job_mission_control.fail_job_path(@job), :patch) %>
50
50
  <% end %>
51
51
 
52
- <% if valid_events.include?(:abort) %>
52
+ <% if valid_events.include?(:abort) && can?(:abort, @job) %>
53
53
  <%= job_action_link('Abort', rocket_job_mission_control.abort_job_path(@job), :patch) %>
54
54
  <% end %>
55
55
 
56
+ <% if can?(:destroy, @job)%>
56
57
  <%= job_action_link('Destroy', rocket_job_mission_control.job_path(@job), :delete) %>
58
+ <% end %>
57
59
  </div>
58
60
 
59
61
  <% unless @job.completed? || @job.aborted? %>
60
62
  <div class='btn-group'>
63
+ <% if can?(:edit, @job)%>
61
64
  <%= link_to 'Edit', edit_job_path(@job), class: 'btn btn-default' %>
65
+ <% end %>
62
66
  </div>
63
67
  <% end %>
64
68
  </div>
@@ -11,14 +11,16 @@
11
11
  <div class='server-collection-actions'>
12
12
  <ol>
13
13
  <div class='btn-group'>
14
- <% Array(@actions).each do |action| %>
15
- <%= link_to(
16
- "#{action.to_s.humanize.capitalize}",
17
- rocket_job_mission_control.update_all_servers_path(server_action: action),
18
- method: :patch,
19
- data: { confirm: t(:confirm, scope: [:server, :update_all], action: action.to_s.singularize.humanize.downcase) },
20
- class: 'btn btn-default'
21
- )%>
14
+ <% if can?(:create, RocketJob::DirmonEntry) %>
15
+ <% Array(@actions).each do |action| %>
16
+ <%= link_to(
17
+ "#{action.to_s.humanize.capitalize}",
18
+ rocket_job_mission_control.update_all_servers_path(server_action: action),
19
+ method: :patch,
20
+ data: {confirm: t(:confirm, scope: [:server, :update_all], action: action.to_s.singularize.humanize.downcase)},
21
+ class: 'btn btn-default'
22
+ ) %>
23
+ <% end %>
22
24
  <% end %>
23
25
  </div>
24
26
  </ol>
@@ -1,4 +1,10 @@
1
1
  module RocketJobMissionControl
2
+
3
+ # The authorization callback
4
+ module Config
5
+ mattr_accessor :authorization_callback
6
+ end
7
+
2
8
  class Engine < ::Rails::Engine
3
9
  isolate_namespace RocketJobMissionControl
4
10
 
@@ -7,11 +13,14 @@ module RocketJobMissionControl
7
13
  require 'bootstrap-sass'
8
14
  require 'sass-rails'
9
15
  require 'jquery-datatables-rails'
16
+ require 'access-granted'
10
17
  begin
11
18
  require 'rocketjob_pro'
12
19
  rescue LoadError
13
20
  end
14
21
 
22
+ config.rocket_job_mission_control = ::RocketJobMissionControl::Config
23
+
15
24
  config.to_prepare do
16
25
  Rails.application.config.assets.precompile += %w(
17
26
  rocket_job_mission_control/rocket-icon-64x64.png
@@ -1,3 +1,3 @@
1
1
  module RocketJobMissionControl
2
- VERSION = '3.1.0'
2
+ VERSION = '3.2.0'
3
3
  end
@@ -5,6 +5,7 @@ module RocketJobMissionControl
5
5
  class DirmonEntriesControllerTest < ActionController::TestCase
6
6
  describe DirmonEntriesController do
7
7
  before do
8
+ set_role(:admin)
8
9
  RocketJob::DirmonEntry.delete_all
9
10
  end
10
11
 
@@ -188,7 +189,11 @@ module RocketJobMissionControl
188
189
  end
189
190
 
190
191
  it 'alerts the user' do
191
- assert_select 'div.message', "job_class_name: [\"job_class_name must be defined and must be derived from RocketJob::Job\"]"
192
+ if RocketJob::VERSION.to_f >= 3.5
193
+ assert_select 'div.message', "job_class_name: [\"Job FakeAndBadJob must be defined and inherit from RocketJob::Job\"]"
194
+ else
195
+ assert_select 'div.message', "job_class_name: [\"job_class_name must be defined and must be derived from RocketJob::Job\"]"
196
+ end
192
197
  end
193
198
  end
194
199
  end
@@ -436,6 +441,78 @@ module RocketJobMissionControl
436
441
  end
437
442
  end
438
443
  end
444
+
445
+ describe 'role base authentication control' do
446
+ let(:dirmon_params) do
447
+ {
448
+ name: 'Test',
449
+ pattern: '/files/*',
450
+ job_class_name: job_class_name,
451
+ properties: {description: '', priority: '42'}
452
+ }
453
+ end
454
+
455
+ %i[index disabled enabled failed pending].each do |method|
456
+ it "#{method} has read only default access" do
457
+ get method, format: :json
458
+ assert_response :success
459
+ end
460
+ end
461
+
462
+ # PATCH
463
+ %i[enable disable update].each do |method|
464
+ describe method.to_s do
465
+ %i[admin editor operator manager dirmon].each do |role|
466
+ it "allows role #{role} to access #{method}" do
467
+ set_role(role)
468
+ params = {id: existing_dirmon_entry.id}
469
+ params = {params: params} if Rails.version.to_i >= 5
470
+ patch :enable, params
471
+
472
+ assert_response(:redirect)
473
+ end
474
+ end
475
+ end
476
+ end
477
+
478
+ # POST
479
+ %i[admin editor operator manager dirmon].each do |role|
480
+ it 'creates dirmon entry' do
481
+ set_role(role)
482
+ params = {rocket_job_dirmon_entry: dirmon_params}
483
+ params = {params: params} if Rails.version.to_i >= 5
484
+ post :create, params
485
+
486
+ assert_response(:redirect)
487
+ end
488
+ end
489
+
490
+ it 'deletes dirmon entry' do
491
+ set_role(:admin)
492
+ params = {id: existing_dirmon_entry.id}
493
+ params = {params: params} if Rails.version.to_i >= 5
494
+ delete :destroy, params
495
+
496
+ assert_response(:redirect)
497
+ end
498
+
499
+ %i[editor operator manager dirmon].each do |role|
500
+ it "raises authentication error for #{role}" do
501
+ set_role(role)
502
+ assert_raises AccessGranted::AccessDenied do
503
+ params = {id: existing_dirmon_entry.id}
504
+ params = {params: params} if Rails.version.to_i >= 5
505
+ delete :destroy, params
506
+ end
507
+ end
508
+ end
509
+ end
510
+ end
511
+
512
+ def set_role(r)
513
+ Config.authorization_callback = -> do
514
+ {roles: [r]}
515
+ end
439
516
  end
440
517
  end
441
518
  end
@@ -14,6 +14,7 @@ module RocketJobMissionControl
14
14
 
15
15
  describe JobsController do
16
16
  before do
17
+ set_role(:admin)
17
18
  RocketJob::Job.delete_all
18
19
  end
19
20
 
@@ -52,7 +53,7 @@ module RocketJobMissionControl
52
53
 
53
54
  let :one_job_for_every_state do
54
55
  job_states.collect do |state|
55
- RocketJob::Jobs::SimpleJob.create!(state: state, worker_name: 'worker')
56
+ RocketJob::Jobs::SimpleJob.create!(state: state, worker_name: 'worker', started_at: (Time.now - 0.5))
56
57
  end
57
58
  end
58
59
 
@@ -315,6 +316,7 @@ module RocketJobMissionControl
315
316
 
316
317
  describe "with #{state} server" do
317
318
  before do
319
+ set_role(:admin)
318
320
  one_job_for_every_state
319
321
  get state, format: :json
320
322
  end
@@ -393,6 +395,55 @@ module RocketJobMissionControl
393
395
  end
394
396
  end
395
397
 
398
+ describe 'role base authentication control' do
399
+ %i[index aborted completed failed paused queued running scheduled].each do |method|
400
+ it "#{method} has read access as default" do
401
+ get method, format: :json
402
+ assert_response :success
403
+ end
404
+ end
405
+
406
+ %i[update abort retry pause resume run_now fail].each do |method|
407
+ describe "#{method}" do
408
+ before do
409
+ case method
410
+ when :pause, :fail, :abort
411
+ pausable_job.start!
412
+ when :resume
413
+ pausable_job.pause!
414
+ when :retry, :exception
415
+ pausable_job.fail!
416
+ end
417
+
418
+ @params = {id: pausable_job.id, job: {id: pausable_job.id, priority: pausable_job.priority}}
419
+ @params = {params: @params} if Rails.version.to_i >= 5
420
+ end
421
+
422
+ %i[admin editor operator manager].each do |role|
423
+ it "redirects with #{method} method and role #{role}" do
424
+ set_role(role)
425
+ patch method, @params
426
+ assert_response(:redirect)
427
+ end
428
+ end
429
+
430
+ %i[dirmon user].each do |role|
431
+ it "raises authentication error for #{role}" do
432
+ set_role(role)
433
+ assert_raises AccessGranted::AccessDenied do
434
+ patch method, @params
435
+ end
436
+ end
437
+ end
438
+ end
439
+ end
440
+ end
441
+ end
442
+
443
+ def set_role(r)
444
+ Config.authorization_callback = -> do
445
+ {roles: [r]}
446
+ end
396
447
  end
397
448
  end
398
449
  end