foreman_remote_execution 3.3.2 → 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 (59) hide show
  1. checksums.yaml +4 -4
  2. data/.github/workflows/ci.yml +2 -1
  3. data/app/assets/stylesheets/foreman_remote_execution/job_invocations.scss +6 -5
  4. data/app/controllers/api/v2/job_invocations_controller.rb +3 -2
  5. data/app/controllers/job_invocations_controller.rb +22 -8
  6. data/app/lib/actions/remote_execution/run_host_job.rb +1 -1
  7. data/app/lib/actions/remote_execution/run_hosts_job.rb +1 -1
  8. data/app/lib/foreman_remote_execution/renderer/scope/input.rb +35 -0
  9. data/app/models/concerns/foreman_remote_execution/orchestration/ssh.rb +11 -4
  10. data/app/models/job_invocation.rb +6 -3
  11. data/app/models/job_invocation_composer.rb +2 -2
  12. data/app/models/remote_execution_provider.rb +2 -2
  13. data/app/models/setting/remote_execution.rb +2 -2
  14. data/app/models/ssh_execution_provider.rb +1 -1
  15. data/app/services/default_proxy_proxy_selector.rb +3 -1
  16. data/app/views/api/v2/job_invocations/main.json.rabl +2 -2
  17. data/app/views/job_invocations/_card_target_hosts.html.erb +1 -1
  18. data/app/views/job_invocations/_form.html.erb +1 -1
  19. data/app/views/job_invocations/_tab_hosts.html.erb +1 -20
  20. data/app/views/job_invocations/index.html.erb +2 -1
  21. data/app/views/job_invocations/show.html.erb +9 -0
  22. data/app/views/job_invocations/show.js.erb +5 -0
  23. data/app/views/job_invocations/show.json.erb +2 -1
  24. data/app/views/templates/ssh/package_action.erb +1 -0
  25. data/app/views/templates/ssh/puppet_agent_disable.erb +3 -0
  26. data/app/views/templates/ssh/puppet_agent_enable.erb +3 -0
  27. data/app/views/templates/ssh/puppet_install_modules_from_forge.erb +3 -0
  28. data/app/views/templates/ssh/puppet_run_once.erb +3 -0
  29. data/db/migrate/20200623073022_rename_sudo_password_to_effective_user_password.rb +34 -0
  30. data/db/seeds.d/20-permissions.rb +9 -0
  31. data/lib/foreman_remote_execution/engine.rb +4 -1
  32. data/lib/foreman_remote_execution/version.rb +1 -1
  33. data/test/functional/api/v2/job_invocations_controller_test.rb +65 -2
  34. data/test/functional/job_invocations_controller_test.rb +71 -0
  35. data/test/models/orchestration/ssh_test.rb +1 -1
  36. data/test/support/remote_execution_helper.rb +5 -0
  37. data/test/unit/actions/run_host_job_test.rb +3 -3
  38. data/test/unit/actions/run_hosts_job_test.rb +1 -1
  39. data/test/unit/job_invocation_composer_test.rb +5 -5
  40. data/test/unit/remote_execution_provider_test.rb +6 -6
  41. data/webpack/__mocks__/foremanReact/components/Pagination/PaginationWrapper.js +2 -0
  42. data/webpack/__mocks__/foremanReact/components/SearchBar.js +2 -0
  43. data/webpack/__mocks__/foremanReact/constants.js +21 -0
  44. data/webpack/__mocks__/foremanReact/redux/API/APISelectors.js +2 -0
  45. data/webpack/__mocks__/foremanReact/redux/middlewares/IntervalMiddleware/IntervalSelectors.js +1 -0
  46. data/webpack/react_app/components/TargetingHosts/TargetingHosts.js +21 -15
  47. data/webpack/react_app/components/TargetingHosts/TargetingHostsHelpers.js +10 -0
  48. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.js +62 -0
  49. data/webpack/react_app/components/TargetingHosts/TargetingHostsPage.scss +6 -0
  50. data/webpack/react_app/components/TargetingHosts/TargetingHostsSelectors.js +10 -2
  51. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsPage.test.js +9 -0
  52. data/webpack/react_app/components/TargetingHosts/__tests__/TargetingHostsSelectors.test.js +26 -0
  53. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHosts.test.js.snap +16 -1
  54. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsPage.test.js.snap +68 -0
  55. data/webpack/react_app/components/TargetingHosts/__tests__/__snapshots__/TargetingHostsSelectors.test.js.snap +11 -0
  56. data/webpack/react_app/components/TargetingHosts/__tests__/fixtures.js +35 -19
  57. data/webpack/react_app/components/TargetingHosts/index.js +73 -13
  58. metadata +18 -3
  59. data/webpack/react_app/components/TargetingHosts/TargetingHostsActions.js +0 -8
@@ -7,4 +7,7 @@ snippet: false
7
7
  provider_type: SSH
8
8
  kind: job_template
9
9
  -%>
10
+ <% if @host.operatingsystem.family == 'Debian' -%>
11
+ export PATH=/opt/puppetlabs/bin:$PATH
12
+ <% end -%>
10
13
  puppet agent --enable
@@ -33,4 +33,7 @@ template_inputs:
33
33
  provider_type: SSH
34
34
  kind: job_template
35
35
  -%>
36
+ <% if @host.operatingsystem.family == 'Debian' -%>
37
+ export PATH=/opt/puppetlabs/bin:$PATH
38
+ <% end -%>
36
39
  puppet module install <%= input('puppet_module') %> <%= "--target-dir #{input('target_dir')}" if input('target_dir').present? %> <%= "--version #{input('version')}" if input('version').present? %> <%= "--force" if input('force') == "true" %> <%= "--ignore-dependencies" if input('ignore_dependencies') == "true" %>
@@ -11,4 +11,7 @@ template_inputs:
11
11
  input_type: user
12
12
  required: false
13
13
  %>
14
+ <% if @host.operatingsystem.family == 'Debian' -%>
15
+ export PATH=/opt/puppetlabs/bin:$PATH
16
+ <% end -%>
14
17
  puppet agent --onetime --no-usecacheonfailure --no-daemonize <%= input("puppet_options") %>
@@ -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
@@ -44,9 +44,12 @@ module ForemanRemoteExecution
44
44
 
45
45
  initializer 'foreman_remote_execution.register_plugin', before: :finisher_hook do |_app|
46
46
  Foreman::Plugin.register :foreman_remote_execution do
47
- requires_foreman '>= 1.25'
47
+ requires_foreman '>= 2.2'
48
48
 
49
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
+ ]
50
53
 
51
54
  automatic_assets(false)
52
55
  precompile_assets(*assets_to_precompile)
@@ -1,3 +1,3 @@
1
1
  module ForemanRemoteExecution
2
- VERSION = '3.3.2'.freeze
2
+ VERSION = '4.0.0'.freeze
3
3
  end
@@ -38,13 +38,14 @@ 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
- assert_empty response['targeting']['hosts']
48
+ assert_equal response['targeting']['hosts'], []
48
49
  end
49
50
  end
50
51
 
@@ -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
@@ -1,6 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require 'test_plugin_helper'
4
+ require_relative '../support/remote_execution_helper'
4
5
 
5
6
  class JobInvocationsControllerTest < ActionController::TestCase
6
7
  test 'should parse inputs coming from the URL params' do
@@ -58,4 +59,74 @@ class JobInvocationsControllerTest < ActionController::TestCase
58
59
  post :new, params: params, session: set_session_user
59
60
  assert_response :success
60
61
  end
62
+
63
+ context 'restricted access' do
64
+ setup do
65
+ @admin = users(:admin)
66
+ @user = FactoryBot.create(:user, mail: 'test23@test.foreman.com', admin: false)
67
+ @invocation = FactoryBot.create(:job_invocation, :with_template, :with_task)
68
+ @invocation2 = FactoryBot.create(:job_invocation, :with_template, :with_task)
69
+
70
+ @invocation.task.update(user: @admin)
71
+ @invocation2.task.update(user: @user)
72
+
73
+ setup_user 'view', 'hosts', nil, @user
74
+ setup_user 'view', 'job_invocations', 'user = current_user', @user
75
+ setup_user 'create', 'job_invocations', 'user = current_user', @user
76
+ setup_user 'cancel', 'job_invocations', 'user = current_user', @user
77
+ end
78
+
79
+ context 'without user filter' do
80
+ test '#index' do
81
+ get :index, session: prepare_user(@admin)
82
+ assert_response :success
83
+ assert 2, assigns(:job_invocations).size
84
+ end
85
+
86
+ test '#show' do
87
+ get :show, params: { id: @invocation2.id }, session: prepare_user(@admin)
88
+ assert_response :success
89
+ end
90
+
91
+ test '#rerun' do
92
+ get :rerun, params: { id: @invocation2.id }, session: prepare_user(@admin)
93
+ assert_response :success
94
+ end
95
+
96
+ test '#cancel' do
97
+ ForemanTasks::Task.any_instance.expects(:cancel).returns(true)
98
+ post :cancel, params: { id: @invocation2.id }, session: prepare_user(@admin)
99
+ assert_response :redirect
100
+ end
101
+ end
102
+
103
+ context 'with user filter' do
104
+ test '#index' do
105
+ get :index, session: prepare_user(@user)
106
+ assert_response :success
107
+ assert_equal 1, assigns(:job_invocations).size
108
+ assert_equal @invocation2, assigns(:job_invocations)[0]
109
+ end
110
+
111
+ test '#show' do
112
+ get :show, params: { id: @invocation.id }, session: prepare_user(@user)
113
+ assert_response :not_found
114
+ end
115
+
116
+ test '#rerun' do
117
+ get :rerun, params: { id: @invocation.id }, session: prepare_user(@user)
118
+ assert_response :not_found
119
+ end
120
+
121
+ test 'cancel' do
122
+ post :cancel, params: { id: @invocation.id }, session: prepare_user(@user)
123
+ assert_response :not_found
124
+ end
125
+ end
126
+ end
127
+
128
+ def prepare_user(user)
129
+ User.current = user
130
+ set_session_user(user)
131
+ end
61
132
  end
@@ -30,7 +30,7 @@ class SSHOrchestrationTest < ActiveSupport::TestCase
30
30
 
31
31
  it 'does not fail on 404 from the smart proxy' do
32
32
  host.stubs(:skip_orchestration?).returns false
33
- SmartProxy.any_instance.expects(:drop_host_from_known_hosts).raises(RestClient::ResourceNotFound).twice
33
+ ::ProxyAPI::RemoteExecutionSSH.any_instance.expects(:delete).raises(RestClient::ResourceNotFound).twice
34
34
  host.build = true
35
35
  host.save!
36
36
  ids = ["ssh_remove_known_hosts_interface_#{interface.ip}_#{proxy.id}",
@@ -0,0 +1,5 @@
1
+ module RemoteExecutionHelper
2
+ def job_invocation_task_buttons(task)
3
+ return []
4
+ end
5
+ end
@@ -12,7 +12,7 @@ module ForemanRemoteExecution
12
12
  let(:provider) do
13
13
  provider = ::SSHExecutionProvider
14
14
  provider.expects(:ssh_password).with(host).returns('sshpass')
15
- provider.expects(:sudo_password).with(host).returns('sudopass')
15
+ provider.expects(:effective_user_password).with(host).returns('sudopass')
16
16
  provider.expects(:ssh_key_passphrase).with(host).returns('keypass')
17
17
  provider
18
18
  end
@@ -21,7 +21,7 @@ module ForemanRemoteExecution
21
21
  secrets = subject.secrets(host, job_invocation, provider)
22
22
 
23
23
  assert_equal 'sshpass', secrets[:ssh_password]
24
- assert_equal 'sudopass', secrets[:sudo_password]
24
+ assert_equal 'sudopass', secrets[:effective_user_password]
25
25
  assert_equal 'keypass', secrets[:key_passphrase]
26
26
  end
27
27
 
@@ -31,7 +31,7 @@ module ForemanRemoteExecution
31
31
  secrets = subject.secrets(host, job_invocation, provider)
32
32
 
33
33
  assert_equal 'jobsshpass', secrets[:ssh_password]
34
- assert_equal 'sudopass', secrets[:sudo_password]
34
+ assert_equal 'sudopass', secrets[:effective_user_password]
35
35
  assert_equal 'jobkeypass', secrets[:key_passphrase]
36
36
  end
37
37
  end
@@ -14,7 +14,7 @@ module ForemanRemoteExecution
14
14
  invocation.description = 'Some short description'
15
15
  invocation.password = 'changeme'
16
16
  invocation.key_passphrase = 'changemetoo'
17
- invocation.sudo_password = 'sudopassword'
17
+ invocation.effective_user_password = 'sudopassword'
18
18
  invocation.save
19
19
  end
20
20
  end
@@ -523,15 +523,15 @@ class JobInvocationComposerTest < ActiveSupport::TestCase
523
523
  end
524
524
  end
525
525
 
526
- describe '#sudo_password' do
527
- let(:sudo_password) { 'password' }
526
+ describe '#effective_user_password' do
527
+ let(:effective_user_password) { 'password' }
528
528
  let(:params) do
529
- { :job_invocation => { :sudo_password => sudo_password }}
529
+ { :job_invocation => { :effective_user_password => effective_user_password }}
530
530
  end
531
531
 
532
- it 'sets the sudo password properly' do
532
+ it 'sets the effective_user_password password properly' do
533
533
  composer
534
- _(composer.job_invocation.sudo_password).must_equal sudo_password
534
+ _(composer.job_invocation.effective_user_password).must_equal effective_user_password
535
535
  end
536
536
  end
537
537
 
@@ -78,12 +78,12 @@ class RemoteExecutionProviderTest < ActiveSupport::TestCase
78
78
  end
79
79
  end
80
80
 
81
- describe 'sudo password' do
82
- it 'uses the remote_execution_sudo_password on the host param' do
83
- host.params['remote_execution_sudo_password'] = 'mypassword'
84
- host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_sudo_password', :value => 'mypassword')
85
- assert_not proxy_options.key?(:sudo_password)
86
- _(secrets[:sudo_password]).must_equal 'mypassword'
81
+ describe 'effective user password' do
82
+ it 'uses the remote_execution_effective_user_password on the host param' do
83
+ host.params['remote_execution_effective_user_password'] = 'mypassword'
84
+ host.host_parameters << FactoryBot.create(:host_parameter, :host => host, :name => 'remote_execution_effective_user_password', :value => 'mypassword')
85
+ assert_not proxy_options.key?(:effective_user_password)
86
+ _(secrets[:effective_user_password]).must_equal 'mypassword'
87
87
  end
88
88
  end
89
89
 
@@ -0,0 +1,2 @@
1
+ const PaginationWrapper = () => jest.fn();
2
+ export default PaginationWrapper;
@@ -0,0 +1,2 @@
1
+ const SearchBar = () => jest.fn();
2
+ export default SearchBar;
@@ -1,3 +1,24 @@
1
1
  export const STATUS = {
2
+ PENDING: 'PENDING',
3
+ RESOLVED: 'RESOLVED',
2
4
  ERROR: 'ERROR',
3
5
  };
6
+
7
+ export const getControllerSearchProps = (
8
+ controller,
9
+ id = 'searchBar',
10
+ canCreate = true
11
+ ) => ({
12
+ controller,
13
+ autocomplete: {
14
+ id,
15
+ searchQuery: '',
16
+ url: `${controller}/auto_complete_search`,
17
+ useKeyShortcuts: true,
18
+ },
19
+ bookmarks: {
20
+ url: '/api/bookmarks',
21
+ canCreate,
22
+ documentationUrl: `4.1.5Searching`,
23
+ },
24
+ });
@@ -0,0 +1,2 @@
1
+ export const selectAPIStatus = () => 'RESOLVED';
2
+ export const selectAPIResponse = state => state;
@@ -0,0 +1 @@
1
+ export const selectDoesIntervalExist = () => false;
@@ -5,8 +5,8 @@ import { LoadingState, Alert } from 'patternfly-react';
5
5
  import { STATUS } from 'foremanReact/constants';
6
6
  import HostItem from './components/HostItem';
7
7
 
8
- const TargetingHosts = ({ status, items }) => {
9
- if (status === STATUS.ERROR) {
8
+ const TargetingHosts = ({ apiStatus, items }) => {
9
+ if (apiStatus === STATUS.ERROR) {
10
10
  return (
11
11
  <Alert type="error">
12
12
  {__(
@@ -16,8 +16,24 @@ const TargetingHosts = ({ status, items }) => {
16
16
  );
17
17
  }
18
18
 
19
+ const tableBodyRows = items.length ? (
20
+ items.map(({ name, link, status, actions }) => (
21
+ <HostItem
22
+ key={name}
23
+ name={name}
24
+ link={link}
25
+ status={status}
26
+ actions={actions}
27
+ />
28
+ ))
29
+ ) : (
30
+ <tr>
31
+ <td colSpan="3">{__('No hosts found.')}</td>
32
+ </tr>
33
+ );
34
+
19
35
  return (
20
- <LoadingState loading={!items.length}>
36
+ <LoadingState loading={!items.length && apiStatus === STATUS.PENDING}>
21
37
  <div>
22
38
  <table className="table table-bordered table-striped table-hover">
23
39
  <thead>
@@ -27,17 +43,7 @@ const TargetingHosts = ({ status, items }) => {
27
43
  <th>{__('Actions')}</th>
28
44
  </tr>
29
45
  </thead>
30
- <tbody>
31
- {items.map(host => (
32
- <HostItem
33
- key={host.name}
34
- name={host.name}
35
- link={host.link}
36
- status={host.status}
37
- actions={host.actions}
38
- />
39
- ))}
40
- </tbody>
46
+ <tbody>{tableBodyRows}</tbody>
41
47
  </table>
42
48
  </div>
43
49
  </LoadingState>
@@ -45,7 +51,7 @@ const TargetingHosts = ({ status, items }) => {
45
51
  };
46
52
 
47
53
  TargetingHosts.propTypes = {
48
- status: PropTypes.string.isRequired,
54
+ apiStatus: PropTypes.string.isRequired,
49
55
  items: PropTypes.array.isRequired,
50
56
  };
51
57