foreman_remote_execution 3.3.7 → 4.0.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 (45) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +1 -1
  3. data/app/controllers/job_invocations_controller.rb +22 -8
  4. data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
  5. data/app/lib/actions/remote_execution/run_hosts_job.rb +1 -1
  6. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +35 -0
  7. data/app/models/job_invocation.rb +6 -3
  8. data/app/models/job_invocation_composer.rb +2 -2
  9. data/app/models/remote_execution_provider.rb +2 -2
  10. data/app/models/setting/remote_execution.rb +2 -2
  11. data/app/models/ssh_execution_provider.rb +1 -1
  12. data/app/views/job_invocations/_form.html.erb +1 -1
  13. data/app/views/job_invocations/_tab_hosts.html.erb +1 -20
  14. data/app/views/job_invocations/show.html.erb +3 -0
  15. data/app/views/job_invocations/show.json.erb +2 -1
  16. data/db/migrate/20200623073022_rename_sudo_password_to_effective_user_password.rb +34 -0
  17. data/db/seeds.d/20-permissions.rb +9 -0
  18. data/lib/foreman_remote_execution/engine.rb +4 -10
  19. data/lib/foreman_remote_execution/version.rb +1 -1
  20. data/test/functional/api/v2/job_invocations_controller_test.rb +64 -1
  21. data/test/functional/job_invocations_controller_test.rb +71 -0
  22. data/test/support/remote_execution_helper.rb +5 -0
  23. data/test/unit/actions/run_host_job_test.rb +3 -3
  24. data/test/unit/actions/run_hosts_job_test.rb +1 -1
  25. data/test/unit/job_invocation_composer_test.rb +5 -5
  26. data/test/unit/remote_execution_provider_test.rb +6 -6
  27. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +2 -0
  28. data/webpack/__mocks__/foremanReact/components/SearchBar.js +2 -0
  29. data/webpack/__mocks__/foremanReact/constants.js +21 -0
  30. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +2 -0
  31. data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors.js +1 -0
  32. data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +21 -15
  33. data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +10 -0
  34. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +62 -0
  35. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +6 -0
  36. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +10 -2
  37. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsPage.test.js +9 -0
  38. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +26 -0
  39. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +17 -2
  40. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +68 -0
  41. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +11 -0
  42. data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +35 -19
  43. data/webpack/react_app/components/TargetingHosts/index.js +73 -13
  44. metadata +17 -3
  45. data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +0 -8
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 7c5854e373426c0a630f0a643f88522425c6482fb88fee1888fc6f61114217c3
4
- data.tar.gz: f333b2504dc7db6e1119d418c89fcd2cd35d5dcbd72e97d393b57a16c9d305a6
3
+ metadata.gz: 93860edb8974a0691383b3a199a1a2d079d10401d7780b99aa6275b75125eb58
4
+ data.tar.gz: d333f8eb85a42b36694dee932447d4364b57ed2c095ad5a055d1e7adf69a9866
5
5
  SHA512:
6
- metadata.gz: 3df47233fbc5b7bb2628a2bb49906c284e8e7c821bb8a026659e0473f9bc8760d5ed3c19f27183f5fd32ab39d5b06cdb0e9345a1e9e4a4629a269adb3121370a
7
- data.tar.gz: 33d40d8f6449814f23648dbe20c6a376a152135b4a4c5de583a73e1db03ab69ecb39b1a2ce3a1cabad717be898a683f3d58fe5e1aaf50438011f6ce4cfbbe2b4
6
+ metadata.gz: 70c300e719d6587639719a4d95aa23da2d3f3a16d51692392c789b70cd381b31320789db0f45b57c129e4b90d36bd1a8ddadd29db4f38f2d9e40a39219b1d130
7
+ data.tar.gz: 1feab0c5cfd3fee6054a004eba812b7257df80a2dce50aa200d15dd20797f24f8a614d802668ac6daf043c189b7caa8e8d5f0d4a997726764c80c91173d9437b
@@ -30,7 +30,7 @@ jobs:
30
30
  strategy:
31
31
  fail-fast: false
32
32
  matrix:
33
- foreman-core-branch: [2.1-stable, develop]
33
+ foreman-core-branch: [develop]
34
34
  ruby-version: [2.5, 2.6]
35
35
  node-version: [12]
36
36
  steps:
@@ -52,16 +52,18 @@ class JobInvocationsController < ApplicationController
52
52
 
53
53
  def show
54
54
  @job_invocation = resource_base.includes(:template_invocations => :run_host_job_task).find(params[:id])
55
- @auto_refresh = @job_invocation.task.try(:pending?)
56
- @resource_base = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
57
- # There's no need to do the joining if we're not filtering
58
- unless params[:search].nil?
59
- @resource_base = @resource_base.joins(:template_invocations)
60
- .where(:template_invocations => { :job_invocation_id => @job_invocation.id})
61
- end
62
- @hosts = resource_base_search_and_page
63
55
  @job_organization = Taxonomy.find_by(id: @job_invocation.task.input[:current_organization_id])
64
56
  @job_location = Taxonomy.find_by(id: @job_invocation.task.input[:current_location_id])
57
+ @auto_refresh = @job_invocation.task.try(:pending?)
58
+
59
+ respond_to do |format|
60
+ format.json do
61
+ targeting_hosts_resources
62
+ end
63
+
64
+ format.html
65
+ format.js
66
+ end
65
67
  end
66
68
 
67
69
  def index
@@ -153,4 +155,16 @@ class JobInvocationsController < ApplicationController
153
155
  JobInvocationComposer.from_ui_params(with_triggering)
154
156
  end
155
157
  end
158
+
159
+ def targeting_hosts_resources
160
+ @auto_refresh = @job_invocation.task.try(:pending?)
161
+ @resource_base = @job_invocation.targeting.hosts.authorized(:view_hosts, Host)
162
+
163
+ unless params[:search].nil?
164
+ @resource_base = @resource_base.joins(:template_invocations)
165
+ .where(:template_invocations => { :job_invocation_id => @job_invocation.id})
166
+ end
167
+ @hosts = resource_base_search_and_page
168
+ @total_hosts = resource_base_with_search.size
169
+ end
156
170
  end
@@ -60,7 +60,7 @@ module Actions
60
60
  def secrets(host, job_invocation, provider)
61
61
  job_secrets = { :ssh_password => job_invocation.password,
62
62
  :key_passphrase => job_invocation.key_passphrase,
63
- :sudo_password => job_invocation.sudo_password }
63
+ :effective_user_password => job_invocation.effective_user_password }
64
64
 
65
65
  job_secrets.merge(provider.secrets(host)) { |_key, job_secret, provider_secret| job_secret || provider_secret }
66
66
  end
@@ -47,7 +47,7 @@ module Actions
47
47
  end
48
48
 
49
49
  def finalize
50
- job_invocation.password = job_invocation.key_passphrase = job_invocation.sudo_password = nil
50
+ job_invocation.password = job_invocation.key_passphrase = job_invocation.effective_user_password = nil
51
51
  job_invocation.save!
52
52
 
53
53
  Rails.logger.debug "cleaning cache for keys that begin with 'job_invocation_#{job_invocation.id}'"
@@ -3,14 +3,33 @@ module ForemanRemoteExecution
3
3
  module Scope
4
4
  class Input < ::Foreman::Renderer::Scope::Template
5
5
  include Foreman::Renderer::Scope::Macros::HostTemplate
6
+ extend ApipieDSL::Class
6
7
 
7
8
  attr_reader :template, :host, :invocation, :input_template_instance, :current_user
8
9
  delegate :input, to: :input_template_instance
9
10
 
11
+ apipie :class, 'Macros related to template rendering' do
12
+ name 'Template Input Render'
13
+ sections only: %w[all jobs]
14
+ end
15
+
16
+ apipie :method, 'Always raises an error with a description provided as an argument' do
17
+ desc 'This method is useful for aborting script execution if some of the conditions are not met'
18
+ required :message, String, desc: 'Description for the error'
19
+ raises error: ::InputTemplateRenderer::RenderError, desc: 'The error is always being raised'
20
+ returns nil, desc: "Doesn't return anything"
21
+ example "<%
22
+ @host.operatingsystem #=> nil
23
+ render_error(N_('Unsupported or no operating system found for this host.')) unless @host.operatingsystem #=> InputTemplateRenderer::RenderError is raised and the execution of the script is aborted
24
+ %>"
25
+ end
10
26
  def render_error(message)
11
27
  raise ::InputTemplateRenderer::RenderError.new(message)
12
28
  end
13
29
 
30
+ apipie :method, 'Check whether the template in preview mode or not' do
31
+ returns one_of: [true, false], desc: 'Returns true if the template in preview mode, false otherwise'
32
+ end
14
33
  def preview?
15
34
  !!@preview
16
35
  end
@@ -23,6 +42,16 @@ module ForemanRemoteExecution
23
42
  Rails.cache.fetch(cache_key, &block)
24
43
  end
25
44
 
45
+ # rubocop:disable Lint/InterpolationCheck
46
+ apipie :method, 'Render template by given name' do
47
+ required :template_name, String, desc: 'name of the template to render'
48
+ optional :input_values, Hash, desc: 'key:value list of input values for the template'
49
+ optional :options, Hash, desc: 'Additional options such as :with_foreign_input_set. Set to true if a foreign input set should be considered when rendering the template'
50
+ raises error: StandardError, desc: 'raises an error if there is no template or template input with such name'
51
+ returns String, desc: 'Rendered template'
52
+ example '<%= render_template("Run Command - Ansible Default", command: "yum -y group install #{input("package")}") %>'
53
+ end
54
+ # rubocop:enable Lint/InterpolationCheck
26
55
  def render_template(template_name, input_values = {}, options = {})
27
56
  options.assert_valid_keys(:with_foreign_input_set)
28
57
  with_foreign_input_set = options.fetch(:with_foreign_input_set, true)
@@ -59,6 +88,12 @@ module ForemanRemoteExecution
59
88
  input_values.merge(overrides).with_indifferent_access
60
89
  end
61
90
 
91
+ apipie :method, 'Returns the value of template input' do
92
+ required :name, String, desc: 'name of the template input'
93
+ raises error: UndefinedInput, desc: 'when there is no input with such name defined for the current template'
94
+ returns Object, desc: 'The value of template input'
95
+ example 'input("Include Facts") #=> "yes"'
96
+ end
62
97
  def input(name)
63
98
  return template_input_values[name.to_s] if template_input_values.key?(name.to_s)
64
99
 
@@ -41,7 +41,10 @@ class JobInvocation < ApplicationRecord
41
41
  has_many :template_invocation_tasks, :through => :template_invocations,
42
42
  :class_name => 'ForemanTasks::Task',
43
43
  :source => 'run_host_job_task'
44
-
44
+ has_one :user, through: :task
45
+ scoped_search relation: :user, on: :login, rename: 'user', complete_value: true,
46
+ value_translation: ->(value) { value == 'current_user' ? User.current.login : value },
47
+ special_values: [:current_user], aliases: ['owner'], :only_explicit => true
45
48
  scoped_search :relation => :task, :on => :started_at, :rename => 'started_at', :complete_value => true
46
49
  scoped_search :relation => :task, :on => :start_at, :rename => 'start_at', :complete_value => true
47
50
  scoped_search :relation => :task, :on => :ended_at, :rename => 'ended_at', :complete_value => true
@@ -70,7 +73,7 @@ class JobInvocation < ApplicationRecord
70
73
 
71
74
  delegate :start_at, :to => :task, :allow_nil => true
72
75
 
73
- encrypts :password, :key_passphrase, :sudo_password
76
+ encrypts :password, :key_passphrase, :effective_user_password
74
77
 
75
78
  def self.search_by_status(key, operator, value)
76
79
  conditions = HostStatus::ExecutionStatus::ExecutionTaskStatusMapper.sql_conditions_for(value)
@@ -139,7 +142,7 @@ class JobInvocation < ApplicationRecord
139
142
  invocation.pattern_template_invocations = self.pattern_template_invocations.map(&:deep_clone)
140
143
  invocation.password = self.password
141
144
  invocation.key_passphrase = self.key_passphrase
142
- invocation.sudo_password = self.sudo_password
145
+ invocation.effective_user_password = self.effective_user_password
143
146
  end
144
147
  end
145
148
 
@@ -15,7 +15,7 @@ class JobInvocationComposer
15
15
  :description_format => job_invocation_base[:description_format],
16
16
  :password => blank_to_nil(job_invocation_base[:password]),
17
17
  :key_passphrase => blank_to_nil(job_invocation_base[:key_passphrase]),
18
- :sudo_password => blank_to_nil(job_invocation_base[:sudo_password]),
18
+ :effective_user_password => blank_to_nil(job_invocation_base[:effective_user_password]),
19
19
  :concurrency_control => concurrency_control_params,
20
20
  :execution_timeout_interval => execution_timeout_interval,
21
21
  :template_invocations => template_invocations_params }.with_indifferent_access
@@ -348,7 +348,7 @@ class JobInvocationComposer
348
348
  job_invocation.execution_timeout_interval = params[:execution_timeout_interval]
349
349
  job_invocation.password = params[:password]
350
350
  job_invocation.key_passphrase = params[:key_passphrase]
351
- job_invocation.sudo_password = params[:sudo_password]
351
+ job_invocation.effective_user_password = params[:effective_user_password]
352
352
 
353
353
  if @reruns && job_invocation.targeting.static?
354
354
  job_invocation.targeting.host_ids = JobInvocation.find(@reruns).targeting.host_ids
@@ -57,8 +57,8 @@ class RemoteExecutionProvider
57
57
  [true, 'true', 'True', 'TRUE', '1'].include?(setting)
58
58
  end
59
59
 
60
- def sudo_password(host)
61
- host_setting(host, :remote_execution_sudo_password)
60
+ def effective_user_password(host)
61
+ host_setting(host, :remote_execution_effective_user_password)
62
62
  end
63
63
 
64
64
  def effective_interfaces(host)
@@ -1,6 +1,6 @@
1
1
  class Setting::RemoteExecution < Setting
2
2
 
3
- ::Setting::BLANK_ATTRS.concat %w{remote_execution_ssh_password remote_execution_ssh_key_passphrase remote_execution_sudo_password remote_execution_cockpit_url remote_execution_form_job_template}
3
+ ::Setting::BLANK_ATTRS.concat %w{remote_execution_ssh_password remote_execution_ssh_key_passphrase remote_execution_sudo_password remote_execution_effective_user_password remote_execution_cockpit_url remote_execution_form_job_template}
4
4
 
5
5
  def self.default_settings
6
6
  [
@@ -27,7 +27,7 @@ class Setting::RemoteExecution < Setting
27
27
  N_('Effective User Method'),
28
28
  nil,
29
29
  { :collection => proc { Hash[SSHExecutionProvider::EFFECTIVE_USER_METHODS.map { |method| [method, method] }] } }),
30
- self.set('remote_execution_sudo_password', N_("Sudo password"), '', N_("Sudo password"), nil, {:encrypted => true}),
30
+ self.set('remote_execution_effective_user_password', N_("Effective user password"), '', N_("Effective user password"), nil, {:encrypted => true}),
31
31
  self.set('remote_execution_sync_templates',
32
32
  N_('Whether we should sync templates from disk when running db:seed.'),
33
33
  true,
@@ -32,7 +32,7 @@ class SSHExecutionProvider < RemoteExecutionProvider
32
32
  {
33
33
  :ssh_password => ssh_password(host),
34
34
  :key_passphrase => ssh_key_passphrase(host),
35
- :sudo_password => sudo_password(host),
35
+ :effective_user_password => effective_user_password(host),
36
36
  }
37
37
  end
38
38
 
@@ -95,7 +95,7 @@
95
95
  <div class="advanced hidden">
96
96
  <%= password_f f, :password, :placeholder => '*****', :label => _('Password'), :label_help => N_('Password is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.') %>
97
97
  <%= password_f f, :key_passphrase, :placeholder => '*****', :label => _('Private key passphrase'), :label_help => N_('Key passhprase is only applicable for SSH provider. Other providers ignore this field. <br> Passphrase is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.') %>
98
- <%= password_f f, :sudo_password, :placeholder => '*****', :label => _('Sudo password'), :label_help => N_('Sudo password is only applicable for SSH provider. Other providers ignore this field. <br> Password is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.') %>
98
+ <%= password_f f, :effective_user_password, :placeholder => '*****', :label => _('Effective user password'), :label_help => N_('Effective user password is only applicable for SSH provider. Other providers ignore this field. <br> Password is stored encrypted in DB until the job finishes. For future or recurring executions, it is removed after the last execution.') %>
99
99
  </div>
100
100
 
101
101
  <div class="advanced hidden">
@@ -1,24 +1,5 @@
1
1
  <% if job_invocation.resolved? %>
2
- <%= form_with url: job_invocation_path(job_invocation), method: "get", id: "search-form" do |f| %>
3
- <div class="row">
4
- <div class="title_filter col-md-6">
5
- <div class="input-group">
6
- <%= autocomplete_f(f, :search, value: params[:search].try(:squeeze, " "), placeholder: _("Filter") + ' ...', path: hosts_path, only_input: true) %>
7
- <span class="input-group-btn">
8
- <button class="btn btn-default" type="submit">
9
- <%= icon_text('search', content_tag(:span, _('Search'), :class => 'hidden-xs', :kind => 'fa')) %>
10
- </button>
11
- </span>
12
- </div>
13
- </div>
14
- </div>
15
- <% end %>
16
- <br>
17
-
18
- <div id="targeting_hosts">
19
- <%= mount_react_component('TargetingHosts', '#targeting_hosts') %>
20
- </div>
21
- <%= will_paginate_with_info @hosts, :container => true %>
2
+ <%= react_component('TargetingHosts' ) %>
22
3
  <% else %>
23
4
  <div class="alert alert-warning">
24
5
  <%=
@@ -2,6 +2,9 @@
2
2
  <% stylesheet 'foreman_remote_execution/foreman_remote_execution' %>
3
3
  <% javascript 'charts', 'foreman_remote_execution/template_invocation' %>
4
4
  <% javascript *webpack_asset_paths('foreman_remote_execution', :extension => 'js') %>
5
+ <% content_for(:stylesheets) do %>
6
+ <%= webpacked_plugins_css_for :foreman_remote_execution %>
7
+ <% end %>
5
8
 
6
9
  <%= breadcrumbs name_field: 'description' %>
7
10
 
@@ -1,4 +1,5 @@
1
1
  {
2
2
  "autoRefresh": "<%= @auto_refresh %>",
3
- "hosts": <%= targeting_hosts(@job_invocation, @hosts).to_json.html_safe %>
3
+ "hosts": <%= targeting_hosts(@job_invocation, @hosts).to_json.html_safe %>,
4
+ "total_hosts": <%= @total_hosts %>
4
5
  }
@@ -0,0 +1,34 @@
1
+ class RenameSudoPasswordToEffectiveUserPassword < ActiveRecord::Migration[6.0]
2
+ def up
3
+ rename_column :job_invocations, :sudo_password, :effective_user_password
4
+
5
+ Parameter.where(name: 'remote_execution_sudo_password').each do |parameter|
6
+ record = Parameter.find_by(type: parameter.type, reference_id: parameter.reference_id, name: "remote_execution_effective_user_password")
7
+ if record.nil?
8
+ parameter.update(name: "remote_execution_effective_user_password")
9
+ end
10
+ end
11
+
12
+ return unless (password = Setting.find_by(:name => 'remote_execution_sudo_password').try(:value))
13
+
14
+ Setting.find_by(:name => 'remote_execution_effective_user_password').update(value: password)
15
+
16
+ Setting.find_by(:name => 'remote_execution_sudo_password').delete
17
+ end
18
+
19
+ def down
20
+ rename_column :job_invocations, :effective_user_password, :sudo_password
21
+
22
+ Parameter.where(name: 'remote_execution_effective_user_password').each do |parameter|
23
+ record = Parameter.find_by(type: parameter.type, reference_id: parameter.reference_id, name: "remote_execution_sudo_password")
24
+ if record.nil?
25
+ parameter.update(name: "remote_execution_sudo_password")
26
+ end
27
+ end
28
+
29
+ return unless (password = Setting.find_by(:name => 'remote_execution_effective_user_password').try(:value))
30
+
31
+ Setting.create!(name: 'remote_execution_sudo_password', value: password, description: 'Sudo password', category: 'Setting::RemoteExecution', settings_type: 'string', full_name: 'Sudo password',encrypted: true, default: nil)
32
+ Setting.find_by(:name => 'remote_execution_effective_user_password').delete
33
+ end
34
+ end
@@ -0,0 +1,9 @@
1
+ view_permission = Permission.find_by(name: "view_job_invocations", resource_type: 'JobInvocation')
2
+ default_role = Role.default
3
+
4
+ # the view_permissions can be nil in tests: skipping in that case
5
+ if view_permission && !default_role.permissions.include?(view_permission)
6
+ default_role.filters.create(:search => 'user = current_user') do |filter|
7
+ filter.filterings.build { |f| f.permission = view_permission }
8
+ end
9
+ end
@@ -32,15 +32,6 @@ module ForemanRemoteExecution
32
32
  end
33
33
  end
34
34
 
35
- # A workaround for https://projects.theforeman.org/issues/30685
36
- initializer 'foreman_remote_execution.rails_loading_workaround' do
37
- # Without this, in production environment the module gets prepended too
38
- # late and the extensions do not get applied
39
- # TODO: Remove this and from config.to_prepare once there is an extension
40
- # point in Foreman
41
- ProvisioningTemplatesHelper.prepend ForemanRemoteExecution::JobTemplatesExtensions
42
- end
43
-
44
35
  initializer 'foreman_remote_execution.apipie' do
45
36
  Apipie.configuration.checksum_path += ['/api/']
46
37
  end
@@ -53,9 +44,12 @@ module ForemanRemoteExecution
53
44
 
54
45
  initializer 'foreman_remote_execution.register_plugin', before: :finisher_hook do |_app|
55
46
  Foreman::Plugin.register :foreman_remote_execution do
56
- requires_foreman '>= 1.25'
47
+ requires_foreman '>= 2.2'
57
48
 
58
49
  apipie_documented_controllers ["#{ForemanRemoteExecution::Engine.root}/app/controllers/api/v2/*.rb"]
50
+ ApipieDSL.configuration.dsl_classes_matchers += [
51
+ "#{ForemanRemoteExecution::Engine.root}/app/lib/foreman_remote_execution/renderer/**/*.rb",
52
+ ]
59
53
 
60
54
  automatic_assets(false)
61
55
  precompile_assets(*assets_to_precompile)
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '3.3.7'.freeze
2
+ VERSION = '4.0.0'.freeze
3
3
  end
@@ -38,10 +38,11 @@ module Api
38
38
 
39
39
  test 'should see only permitted hosts' do
40
40
  @user = FactoryBot.create(:user, admin: false)
41
+ @invocation.task.update(user: @user)
41
42
  setup_user('view', 'job_invocations', nil, @user)
42
43
  setup_user('view', 'hosts', 'name ~ nope.example.com', @user)
43
44
 
44
- get :show, params: { :id => @invocation.id }, session: set_session_user(@user)
45
+ get :show, params: { :id => @invocation.id }, session: prepare_user(@user)
45
46
  assert_response :success
46
47
  response = ActiveSupport::JSON.decode(@response.body)
47
48
  assert_equal response['targeting']['hosts'], []
@@ -298,6 +299,68 @@ module Api
298
299
  post :rerun, params: { :id => @invocation.id }
299
300
  assert_response 404
300
301
  end
302
+
303
+ describe 'restricted access' do
304
+ setup do
305
+ @admin = FactoryBot.create(:user, mail: 'admin@test.foreman.com', admin: true)
306
+ @user = FactoryBot.create(:user, mail: 'user@test.foreman.com', admin: false)
307
+ @invocation = FactoryBot.create(:job_invocation, :with_template, :with_task, :with_unplanned_host)
308
+ @invocation2 = FactoryBot.create(:job_invocation, :with_template, :with_task, :with_unplanned_host)
309
+
310
+ @invocation.task.update(user: @admin)
311
+ @invocation2.task.update(user: @user)
312
+
313
+ setup_user 'view', 'hosts', nil, @user
314
+ setup_user 'view', 'job_invocations', 'user = current_user', @user
315
+ setup_user 'create', 'job_invocations', 'user = current_user', @user
316
+ setup_user 'cancel', 'job_invocations', 'user = current_user', @user
317
+ end
318
+
319
+ let(:host) { @invocation.targeting.hosts.first }
320
+ let(:host2) { @invocation2.targeting.hosts.first }
321
+
322
+ context 'without user filter' do
323
+ test '#index' do
324
+ get :index, session: prepare_user(@admin)
325
+ assert_response :success
326
+ assert JSON.parse(@response.body)['results'].size >= 2
327
+ end
328
+
329
+ test '#show' do
330
+ get :show, params: { id: @invocation2.id }, session: prepare_user(@admin)
331
+ assert_response :success
332
+ end
333
+
334
+ test '#output' do
335
+ get :output, params: { job_invocation_id: @invocation2.id, host_id: host2.id }, session: prepare_user(@admin)
336
+ assert_response :success
337
+ end
338
+ end
339
+
340
+ context 'with user filter' do
341
+ test '#index' do
342
+ get :index, session: prepare_user(@user)
343
+ assert_response :success
344
+ assert_equal 1, JSON.parse(@response.body)['results'].size
345
+ end
346
+
347
+ test '#show' do
348
+ get :show, params: { id: @invocation.id }, session: prepare_user(@user)
349
+ assert_response :not_found
350
+ end
351
+
352
+ test '#output' do
353
+ get :output, params: { job_invocation_id: @invocation.id, host_id: host.id }, session: prepare_user(@user)
354
+ assert_response :not_found
355
+ assert_includes @response.body, 'Job invocation not found'
356
+ end
357
+ end
358
+ end
359
+
360
+ def prepare_user(user)
361
+ User.current = user
362
+ set_session_user(user)
363
+ end
301
364
  end
302
365
  end
303
366
  end