foreman_openbolt 1.2.0 → 1.3.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 +4 -4
- data/README.md +1 -0
- data/Rakefile +32 -0
- data/app/controllers/api/v2/openbolt_jobs_controller.rb +77 -0
- data/app/controllers/api/v2/openbolt_tasks_controller.rb +83 -0
- data/app/controllers/concerns/foreman_openbolt/common.rb +121 -0
- data/app/controllers/concerns/foreman_openbolt/jobs.rb +84 -0
- data/app/controllers/concerns/foreman_openbolt/tasks.rb +140 -0
- data/app/controllers/foreman_openbolt/task_controller.rb +12 -291
- data/app/lib/actions/foreman_openbolt/cleanup_proxy_artifacts.rb +4 -2
- data/app/lib/actions/foreman_openbolt/poll_task_status.rb +48 -29
- data/app/models/foreman_openbolt/task_job.rb +9 -2
- data/config/routes.rb +39 -4
- data/db/migrate/20260824000000_remove_view_smart_proxies_openbolt_permission.rb +18 -0
- data/lib/foreman_openbolt/engine.rb +15 -7
- data/lib/foreman_openbolt/version.rb +1 -1
- data/lib/proxy_api/openbolt.rb +64 -13
- data/package.json +1 -1
- data/test/acceptance/acceptance_helper.rb +13 -0
- data/test/acceptance/api_acceptance_helper.rb +109 -0
- data/test/acceptance/tests/api/authn_authz_test.rb +82 -0
- data/test/acceptance/tests/api/jobs_test.rb +77 -0
- data/test/acceptance/tests/api/launch_test.rb +79 -0
- data/test/acceptance/tests/api/proxy_tasks_test.rb +51 -0
- data/test/unit/controllers/api/v2/openbolt_jobs_controller_test.rb +251 -0
- data/test/unit/controllers/api/v2/openbolt_tasks_controller_test.rb +438 -0
- data/test/unit/controllers/task_controller_test.rb +197 -39
- data/test/unit/docker/Dockerfile +1 -0
- data/test/unit/lib/actions/poll_task_status_test.rb +112 -34
- data/test/unit/lib/proxy_api/openbolt_test.rb +121 -6
- data/test/unit/models/task_job_test.rb +11 -0
- data/webpack/src/Components/LaunchTask/__tests__/LaunchTask.test.js +44 -1
- data/webpack/src/Components/LaunchTask/hooks/__tests__/useOpenBoltOptions.test.js +3 -0
- data/webpack/src/Components/LaunchTask/hooks/__tests__/useTasksData.test.js +5 -4
- data/webpack/src/Components/LaunchTask/hooks/useOpenBoltOptions.js +1 -1
- data/webpack/src/Components/LaunchTask/hooks/useTasksData.js +4 -1
- data/webpack/src/Components/LaunchTask/index.js +21 -6
- data/webpack/src/Components/TaskExecution/__tests__/TaskExecution.test.js +1 -1
- data/webpack/src/Components/TaskExecution/hooks/__tests__/useJobPolling.test.js +8 -8
- data/webpack/src/Components/TaskExecution/hooks/useJobPolling.js +3 -3
- data/webpack/src/Components/TaskHistory/__tests__/TaskHistory.test.js +6 -6
- data/webpack/src/Components/TaskHistory/index.js +3 -3
- metadata +17 -4
|
@@ -0,0 +1,251 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
module Api
|
|
6
|
+
module V2
|
|
7
|
+
class OpenboltJobsControllerTest < ActionController::TestCase
|
|
8
|
+
setup do
|
|
9
|
+
@proxy = FactoryBot.create(:smart_proxy)
|
|
10
|
+
@session = set_session_user
|
|
11
|
+
WebMock.reset!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
teardown do
|
|
15
|
+
WebMock.reset!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
context 'jobs' do
|
|
19
|
+
test 'returns empty results with pagination envelope when no jobs exist' do
|
|
20
|
+
get :jobs, session: @session
|
|
21
|
+
assert_response :success
|
|
22
|
+
body = JSON.parse(response.body)
|
|
23
|
+
assert_equal [], body['results']
|
|
24
|
+
assert_equal 0, body['total']
|
|
25
|
+
assert_equal 1, body['page']
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
test 'returns paginated results with kind on each entry' do
|
|
29
|
+
3.times { FactoryBot.create(:task_job, smart_proxy: @proxy) }
|
|
30
|
+
|
|
31
|
+
get :jobs, params: { page: 1, per_page: 2 }, session: @session
|
|
32
|
+
assert_response :success
|
|
33
|
+
body = JSON.parse(response.body)
|
|
34
|
+
assert_equal 2, body['results'].length
|
|
35
|
+
assert_equal 3, body['total']
|
|
36
|
+
assert_equal 1, body['page']
|
|
37
|
+
assert_equal 2, body['per_page']
|
|
38
|
+
assert(body['results'].all? { |row| row['kind'] == 'task' })
|
|
39
|
+
|
|
40
|
+
row = body['results'].first
|
|
41
|
+
%w[job_id name description parameters targets status
|
|
42
|
+
submitted_at completed_at duration].each do |field|
|
|
43
|
+
assert row.key?(field), "expected results[0] to contain '#{field}'"
|
|
44
|
+
end
|
|
45
|
+
assert_equal @proxy.id, row['smart_proxy']['id']
|
|
46
|
+
assert_equal @proxy.name, row['smart_proxy']['name']
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
test 'caps per_page at 100' do
|
|
50
|
+
get :jobs, params: { per_page: 500 }, session: @session
|
|
51
|
+
assert_response :success
|
|
52
|
+
body = JSON.parse(response.body)
|
|
53
|
+
assert_equal 100, body['per_page']
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
test 'floors per_page at 1 when zero is requested' do
|
|
57
|
+
get :jobs, params: { per_page: 0 }, session: @session
|
|
58
|
+
assert_response :success
|
|
59
|
+
body = JSON.parse(response.body)
|
|
60
|
+
assert_equal 1, body['per_page']
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
test 'floors per_page at 1 when a negative value is requested' do
|
|
64
|
+
get :jobs, params: { per_page: -5 }, session: @session
|
|
65
|
+
assert_response :success
|
|
66
|
+
body = JSON.parse(response.body)
|
|
67
|
+
assert_equal 1, body['per_page']
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
test 'defaults per_page to 20' do
|
|
71
|
+
get :jobs, session: @session
|
|
72
|
+
assert_response :success
|
|
73
|
+
body = JSON.parse(response.body)
|
|
74
|
+
assert_equal 20, body['per_page']
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
test 'per_page=all returns all jobs in one page' do
|
|
78
|
+
5.times { FactoryBot.create(:task_job, smart_proxy: @proxy) }
|
|
79
|
+
get :jobs, params: { per_page: 'all' }, session: @session
|
|
80
|
+
assert_response :success
|
|
81
|
+
body = JSON.parse(response.body)
|
|
82
|
+
assert_equal 5, body['results'].length
|
|
83
|
+
assert_equal 5, body['per_page']
|
|
84
|
+
end
|
|
85
|
+
|
|
86
|
+
test 'per_page=all on empty DB does not 500' do
|
|
87
|
+
get :jobs, params: { per_page: 'all' }, session: @session
|
|
88
|
+
assert_response :success
|
|
89
|
+
body = JSON.parse(response.body)
|
|
90
|
+
assert_equal [], body['results']
|
|
91
|
+
assert_equal 0, body['total']
|
|
92
|
+
end
|
|
93
|
+
|
|
94
|
+
test 'floors page at 1 when zero is requested' do
|
|
95
|
+
get :jobs, params: { page: 0 }, session: @session
|
|
96
|
+
assert_response :success
|
|
97
|
+
body = JSON.parse(response.body)
|
|
98
|
+
assert_equal 1, body['page']
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
test 'floors page at 1 when a negative value is requested' do
|
|
102
|
+
get :jobs, params: { page: -5 }, session: @session
|
|
103
|
+
assert_response :success
|
|
104
|
+
body = JSON.parse(response.body)
|
|
105
|
+
assert_equal 1, body['page']
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
test 'floors page at 1 when a non-numeric value is requested' do
|
|
109
|
+
get :jobs, params: { page: 'bogus' }, session: @session
|
|
110
|
+
assert_response :success
|
|
111
|
+
body = JSON.parse(response.body)
|
|
112
|
+
assert_equal 1, body['page']
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
test 'page beyond the last page returns empty results' do
|
|
116
|
+
FactoryBot.create(:task_job, smart_proxy: @proxy)
|
|
117
|
+
get :jobs, params: { page: 99 }, session: @session
|
|
118
|
+
assert_response :success
|
|
119
|
+
body = JSON.parse(response.body)
|
|
120
|
+
assert_equal [], body['results']
|
|
121
|
+
assert_equal 1, body['total']
|
|
122
|
+
end
|
|
123
|
+
|
|
124
|
+
test 'returns rows in DESC submitted_at order' do
|
|
125
|
+
oldest = FactoryBot.create(:task_job, smart_proxy: @proxy, submitted_at: 3.hours.ago)
|
|
126
|
+
middle = FactoryBot.create(:task_job, smart_proxy: @proxy, submitted_at: 2.hours.ago)
|
|
127
|
+
newest = FactoryBot.create(:task_job, smart_proxy: @proxy, submitted_at: 1.hour.ago)
|
|
128
|
+
|
|
129
|
+
get :jobs, session: @session
|
|
130
|
+
assert_response :success
|
|
131
|
+
body = JSON.parse(response.body)
|
|
132
|
+
assert_equal([newest.job_id, middle.job_id, oldest.job_id],
|
|
133
|
+
body['results'].map { |row| row['job_id'] })
|
|
134
|
+
end
|
|
135
|
+
end
|
|
136
|
+
|
|
137
|
+
context 'job_status' do
|
|
138
|
+
test 'returns job status with kind' do
|
|
139
|
+
job = FactoryBot.create(:task_job, :running, smart_proxy: @proxy)
|
|
140
|
+
|
|
141
|
+
get :job_status, params: { job_id: job.job_id }, session: @session
|
|
142
|
+
assert_response :success
|
|
143
|
+
body = JSON.parse(response.body)
|
|
144
|
+
assert_equal 'task', body['kind']
|
|
145
|
+
assert_equal 'running', body['status']
|
|
146
|
+
assert_equal job.task_name, body['name']
|
|
147
|
+
assert_equal job.targets, body['targets']
|
|
148
|
+
assert_equal @proxy.id, body['smart_proxy']['id']
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
test 'returns not_found when job does not exist' do
|
|
152
|
+
get :job_status, params: { job_id: 'nonexistent' }, session: @session
|
|
153
|
+
assert_response :not_found
|
|
154
|
+
assert_match(/Job nonexistent not found/,
|
|
155
|
+
JSON.parse(response.body)['error']['message'])
|
|
156
|
+
end
|
|
157
|
+
end
|
|
158
|
+
|
|
159
|
+
context 'job_result' do
|
|
160
|
+
test 'returns result fields with kind for completed job' do
|
|
161
|
+
ForemanTasks.stubs(:async_task)
|
|
162
|
+
job = FactoryBot.create(:task_job, :success, smart_proxy: @proxy)
|
|
163
|
+
|
|
164
|
+
get :job_result, params: { job_id: job.job_id }, session: @session
|
|
165
|
+
assert_response :success
|
|
166
|
+
body = JSON.parse(response.body)
|
|
167
|
+
assert_equal 'task', body['kind']
|
|
168
|
+
assert_equal 'success', body['status']
|
|
169
|
+
assert_equal job.result, body['value']
|
|
170
|
+
assert_equal job.log, body['log']
|
|
171
|
+
assert_equal job.command, body['command']
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
test 'returns not_found when job does not exist' do
|
|
175
|
+
get :job_result, params: { job_id: 'nonexistent' }, session: @session
|
|
176
|
+
assert_response :not_found
|
|
177
|
+
assert_match(/Job nonexistent not found/,
|
|
178
|
+
JSON.parse(response.body)['error']['message'])
|
|
179
|
+
end
|
|
180
|
+
end
|
|
181
|
+
|
|
182
|
+
context 'authorization' do
|
|
183
|
+
test 'forbids unprivileged users from listing jobs' do
|
|
184
|
+
reset_api_credentials
|
|
185
|
+
unprivileged = FactoryBot.create(:user)
|
|
186
|
+
User.current = unprivileged
|
|
187
|
+
|
|
188
|
+
get :jobs, session: set_session_user(unprivileged)
|
|
189
|
+
assert_response :forbidden
|
|
190
|
+
end
|
|
191
|
+
|
|
192
|
+
test 'forbids unprivileged users from reading job status' do
|
|
193
|
+
job = FactoryBot.create(:task_job, smart_proxy: @proxy)
|
|
194
|
+
reset_api_credentials
|
|
195
|
+
unprivileged = FactoryBot.create(:user)
|
|
196
|
+
User.current = unprivileged
|
|
197
|
+
|
|
198
|
+
get :job_status, params: { job_id: job.job_id },
|
|
199
|
+
session: set_session_user(unprivileged)
|
|
200
|
+
assert_response :forbidden
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
test 'forbids unprivileged users from reading job result' do
|
|
204
|
+
job = FactoryBot.create(:task_job, :success, smart_proxy: @proxy)
|
|
205
|
+
reset_api_credentials
|
|
206
|
+
unprivileged = FactoryBot.create(:user)
|
|
207
|
+
User.current = unprivileged
|
|
208
|
+
|
|
209
|
+
get :job_result, params: { job_id: job.job_id },
|
|
210
|
+
session: set_session_user(unprivileged)
|
|
211
|
+
assert_response :forbidden
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
test 'execute_openbolt without view_smart_proxies still lists jobs' do
|
|
215
|
+
FactoryBot.create(:task_job, smart_proxy: @proxy)
|
|
216
|
+
reset_api_credentials
|
|
217
|
+
granted = FactoryBot.create(:user)
|
|
218
|
+
setup_user('execute', 'openbolt', nil, granted)
|
|
219
|
+
User.current = granted
|
|
220
|
+
|
|
221
|
+
get :jobs, session: set_session_user(granted)
|
|
222
|
+
assert_response :success
|
|
223
|
+
body = JSON.parse(response.body)
|
|
224
|
+
assert_equal 1, body['results'].length
|
|
225
|
+
end
|
|
226
|
+
|
|
227
|
+
test 'execute_openbolt without view_smart_proxies still reads job status' do
|
|
228
|
+
job = FactoryBot.create(:task_job, :running, smart_proxy: @proxy)
|
|
229
|
+
reset_api_credentials
|
|
230
|
+
granted = FactoryBot.create(:user)
|
|
231
|
+
setup_user('execute', 'openbolt', nil, granted)
|
|
232
|
+
User.current = granted
|
|
233
|
+
|
|
234
|
+
get :job_status, params: { job_id: job.job_id }, session: set_session_user(granted)
|
|
235
|
+
assert_response :success
|
|
236
|
+
end
|
|
237
|
+
|
|
238
|
+
test 'execute_openbolt without view_smart_proxies still reads job result' do
|
|
239
|
+
job = FactoryBot.create(:task_job, :success, smart_proxy: @proxy)
|
|
240
|
+
reset_api_credentials
|
|
241
|
+
granted = FactoryBot.create(:user)
|
|
242
|
+
setup_user('execute', 'openbolt', nil, granted)
|
|
243
|
+
User.current = granted
|
|
244
|
+
|
|
245
|
+
get :job_result, params: { job_id: job.job_id }, session: set_session_user(granted)
|
|
246
|
+
assert_response :success
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
end
|
|
250
|
+
end
|
|
251
|
+
end
|
|
@@ -0,0 +1,438 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'test_plugin_helper'
|
|
4
|
+
|
|
5
|
+
module Api
|
|
6
|
+
module V2
|
|
7
|
+
class OpenboltTasksControllerTest < ActionController::TestCase
|
|
8
|
+
setup do
|
|
9
|
+
@proxy = FactoryBot.create(:smart_proxy)
|
|
10
|
+
@session = set_session_user
|
|
11
|
+
WebMock.reset!
|
|
12
|
+
end
|
|
13
|
+
|
|
14
|
+
teardown do
|
|
15
|
+
WebMock.reset!
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
context 'tasks' do
|
|
19
|
+
test 'returns tasks list from the proxy as JSON' do
|
|
20
|
+
tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
|
|
21
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks")
|
|
22
|
+
.to_return(status: 200, body: tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
23
|
+
|
|
24
|
+
get :tasks, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
25
|
+
assert_response :success
|
|
26
|
+
assert_equal tasks, JSON.parse(response.body)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
test 'returns not_found when smart_proxy does not exist' do
|
|
30
|
+
get :tasks, params: { smart_proxy_id: -1 }, session: @session
|
|
31
|
+
assert_response :not_found
|
|
32
|
+
assert_match(/not found/, JSON.parse(response.body)['error']['message'])
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
test 'returns bad_gateway when proxy returns invalid JSON' do
|
|
36
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks")
|
|
37
|
+
.to_return(status: 200, body: 'not valid json',
|
|
38
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
39
|
+
|
|
40
|
+
get :tasks, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
41
|
+
assert_response :bad_gateway
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
test 'returns bad_gateway when ProxyAPI::Openbolt.new raises' do
|
|
45
|
+
::ProxyAPI::Openbolt.expects(:new).raises(StandardError, 'ssl setup failed')
|
|
46
|
+
|
|
47
|
+
get :tasks, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
48
|
+
assert_response :bad_gateway
|
|
49
|
+
assert_match(/Failed to connect to Smart Proxy/,
|
|
50
|
+
JSON.parse(response.body)['error']['message'])
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
context 'reload_tasks' do
|
|
55
|
+
test 'returns reloaded task list' do
|
|
56
|
+
tasks = { 'new::task' => {} }
|
|
57
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/reload")
|
|
58
|
+
.to_return(status: 200, body: tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
59
|
+
|
|
60
|
+
post :reload_tasks, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
61
|
+
assert_response :success
|
|
62
|
+
assert_equal tasks, JSON.parse(response.body)
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
context 'task_options' do
|
|
67
|
+
test 'returns options with Foreman setting defaults merged in' do
|
|
68
|
+
Setting['openbolt_transport'] = 'winrm'
|
|
69
|
+
|
|
70
|
+
proxy_options = {
|
|
71
|
+
'transport' => { 'type' => %w[ssh winrm] },
|
|
72
|
+
'verbose' => { 'type' => 'boolean' },
|
|
73
|
+
}
|
|
74
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
|
|
75
|
+
.to_return(status: 200, body: proxy_options.to_json,
|
|
76
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
77
|
+
|
|
78
|
+
get :task_options, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
79
|
+
assert_response :success
|
|
80
|
+
body = JSON.parse(response.body)
|
|
81
|
+
assert_equal 'winrm', body['transport']['default']
|
|
82
|
+
assert_equal false, body['verbose']['default']
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
test 'shows encrypted placeholder instead of the saved secret value' do
|
|
86
|
+
Setting['openbolt_password'] = 'real-secret-value'
|
|
87
|
+
|
|
88
|
+
proxy_options = {
|
|
89
|
+
'password' => { 'type' => 'string', 'sensitive' => true },
|
|
90
|
+
}
|
|
91
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks/options")
|
|
92
|
+
.to_return(status: 200, body: proxy_options.to_json,
|
|
93
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
94
|
+
|
|
95
|
+
get :task_options, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
96
|
+
assert_response :success
|
|
97
|
+
body = JSON.parse(response.body)
|
|
98
|
+
assert_equal '[Use saved encrypted default]', body['password']['default']
|
|
99
|
+
assert_no_match(/real-secret-value/, response.body)
|
|
100
|
+
end
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
context 'launch_task' do
|
|
104
|
+
setup do
|
|
105
|
+
@tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
|
|
106
|
+
stub_request(:get, "#{@proxy.url}/openbolt/tasks")
|
|
107
|
+
.to_return(status: 200, body: @tasks.to_json, headers: { 'Content-Type' => 'application/json' })
|
|
108
|
+
stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
|
|
109
|
+
.to_return(status: 200, body: { 'id' => 'api-job-1' }.to_json,
|
|
110
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
111
|
+
ForemanTasks.stubs(:async_task)
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
test 'launches task and returns job_id with kind' do
|
|
115
|
+
post :launch_task, params: {
|
|
116
|
+
smart_proxy_id: @proxy.id,
|
|
117
|
+
task_name: 'mymod::install',
|
|
118
|
+
targets: 'host1.example.com',
|
|
119
|
+
}, session: @session
|
|
120
|
+
|
|
121
|
+
assert_response :success
|
|
122
|
+
body = JSON.parse(response.body)
|
|
123
|
+
assert_equal 'api-job-1', body['job_id']
|
|
124
|
+
assert_equal 'task', body['kind']
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
test 'schedules polling after launch' do
|
|
128
|
+
ForemanTasks.expects(:async_task).with(
|
|
129
|
+
Actions::ForemanOpenbolt::PollTaskStatus,
|
|
130
|
+
'api-job-1',
|
|
131
|
+
@proxy.id
|
|
132
|
+
)
|
|
133
|
+
|
|
134
|
+
post :launch_task, params: {
|
|
135
|
+
smart_proxy_id: @proxy.id,
|
|
136
|
+
task_name: 'mymod::install',
|
|
137
|
+
targets: 'host1.example.com',
|
|
138
|
+
}, session: @session
|
|
139
|
+
|
|
140
|
+
assert_response :success
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
test 'creates a TaskJob record' do
|
|
144
|
+
assert_difference('ForemanOpenbolt::TaskJob.count', 1) do
|
|
145
|
+
post :launch_task, params: {
|
|
146
|
+
smart_proxy_id: @proxy.id,
|
|
147
|
+
task_name: 'mymod::install',
|
|
148
|
+
targets: 'host1.example.com,host2.example.com',
|
|
149
|
+
}, session: @session
|
|
150
|
+
end
|
|
151
|
+
job = ForemanOpenbolt::TaskJob.find_by(job_id: 'api-job-1')
|
|
152
|
+
assert_equal 'mymod::install', job.task_name
|
|
153
|
+
assert_equal %w[host1.example.com host2.example.com], job.targets
|
|
154
|
+
end
|
|
155
|
+
|
|
156
|
+
test 'returns bad_request when task_name and targets are missing' do
|
|
157
|
+
post :launch_task, params: { smart_proxy_id: @proxy.id }, session: @session
|
|
158
|
+
assert_response :bad_request
|
|
159
|
+
end
|
|
160
|
+
|
|
161
|
+
test 'returns bad_request and persists nothing when proxy returns error' do
|
|
162
|
+
stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
|
|
163
|
+
.to_return(status: 200, body: { 'error' => 'Task not found' }.to_json,
|
|
164
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
165
|
+
ForemanTasks.expects(:async_task).never
|
|
166
|
+
|
|
167
|
+
assert_no_difference('ForemanOpenbolt::TaskJob.count') do
|
|
168
|
+
post :launch_task, params: {
|
|
169
|
+
smart_proxy_id: @proxy.id,
|
|
170
|
+
task_name: 'missing::task',
|
|
171
|
+
targets: 'host1',
|
|
172
|
+
}, session: @session
|
|
173
|
+
end
|
|
174
|
+
assert_response :bad_request
|
|
175
|
+
end
|
|
176
|
+
|
|
177
|
+
test 'returns bad_request and persists nothing when proxy returns no job id' do
|
|
178
|
+
stub_request(:post, "#{@proxy.url}/openbolt/launch/task")
|
|
179
|
+
.to_return(status: 200, body: { 'status' => 'ok' }.to_json,
|
|
180
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
181
|
+
ForemanTasks.expects(:async_task).never
|
|
182
|
+
|
|
183
|
+
assert_no_difference('ForemanOpenbolt::TaskJob.count') do
|
|
184
|
+
post :launch_task, params: {
|
|
185
|
+
smart_proxy_id: @proxy.id,
|
|
186
|
+
task_name: 'test::task',
|
|
187
|
+
targets: 'host1',
|
|
188
|
+
}, session: @session
|
|
189
|
+
end
|
|
190
|
+
assert_response :bad_request
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
test 'returns bad_request when smart_proxy_id is missing' do
|
|
194
|
+
post :launch_task, params: { task_name: 'test::task', targets: 'host1' }, session: @session
|
|
195
|
+
assert_response :bad_request
|
|
196
|
+
body = JSON.parse(response.body)
|
|
197
|
+
assert_kind_of Hash, body['error']
|
|
198
|
+
assert_match(/Smart Proxy ID is required/, body['error']['message'])
|
|
199
|
+
end
|
|
200
|
+
|
|
201
|
+
test 'returns bad_request and persists nothing when encrypted placeholder is used for nonexistent setting' do
|
|
202
|
+
ForemanTasks.expects(:async_task).never
|
|
203
|
+
|
|
204
|
+
assert_no_difference('ForemanOpenbolt::TaskJob.count') do
|
|
205
|
+
post :launch_task, params: {
|
|
206
|
+
smart_proxy_id: @proxy.id,
|
|
207
|
+
task_name: 'mymod::install',
|
|
208
|
+
targets: 'host1.example.com',
|
|
209
|
+
options: { 'nonexistent-option' => '[Use saved encrypted default]' },
|
|
210
|
+
}, session: @session
|
|
211
|
+
end
|
|
212
|
+
assert_response :bad_request
|
|
213
|
+
assert_match(/No saved value for encrypted option/,
|
|
214
|
+
JSON.parse(response.body)['error']['message'])
|
|
215
|
+
end
|
|
216
|
+
|
|
217
|
+
test 'sends literal encrypted-option value to proxy but scrubs it in the database' do
|
|
218
|
+
post :launch_task, params: {
|
|
219
|
+
smart_proxy_id: @proxy.id,
|
|
220
|
+
task_name: 'mymod::install',
|
|
221
|
+
targets: 'host1.example.com',
|
|
222
|
+
options: { 'password' => 'literal-typed-secret', 'transport' => 'ssh' },
|
|
223
|
+
}, session: @session
|
|
224
|
+
|
|
225
|
+
assert_response :success
|
|
226
|
+
launch_url = "#{@proxy.url}/openbolt/launch/task"
|
|
227
|
+
assert_requested(:post, launch_url) do |req|
|
|
228
|
+
sent = JSON.parse(req.body)
|
|
229
|
+
sent['options']['password'] == 'literal-typed-secret'
|
|
230
|
+
end
|
|
231
|
+
|
|
232
|
+
job = ForemanOpenbolt::TaskJob.find_by(job_id: 'api-job-1')
|
|
233
|
+
assert_equal '*****', job.openbolt_options['password']
|
|
234
|
+
assert_equal 'ssh', job.openbolt_options['transport']
|
|
235
|
+
end
|
|
236
|
+
|
|
237
|
+
test 'sends real encrypted value to proxy and scrubs it in the database' do
|
|
238
|
+
Setting['openbolt_password'] = 'real-secret-password'
|
|
239
|
+
|
|
240
|
+
post :launch_task, params: {
|
|
241
|
+
smart_proxy_id: @proxy.id,
|
|
242
|
+
task_name: 'mymod::install',
|
|
243
|
+
targets: 'host1.example.com',
|
|
244
|
+
options: { 'password' => '[Use saved encrypted default]', 'transport' => 'ssh' },
|
|
245
|
+
}, session: @session
|
|
246
|
+
|
|
247
|
+
assert_response :success
|
|
248
|
+
launch_url = "#{@proxy.url}/openbolt/launch/task"
|
|
249
|
+
assert_requested(:post, launch_url) do |req|
|
|
250
|
+
sent = JSON.parse(req.body)
|
|
251
|
+
sent['options']['password'] == 'real-secret-password' &&
|
|
252
|
+
sent['options']['transport'] == 'ssh'
|
|
253
|
+
end
|
|
254
|
+
|
|
255
|
+
job = ForemanOpenbolt::TaskJob.find_by(job_id: 'api-job-1')
|
|
256
|
+
assert_equal '*****', job.openbolt_options['password']
|
|
257
|
+
assert_equal 'ssh', job.openbolt_options['transport']
|
|
258
|
+
end
|
|
259
|
+
|
|
260
|
+
test 'returns 500 and marks the row exception when async_task scheduling fails' do
|
|
261
|
+
ForemanTasks.stubs(:async_task).raises(StandardError, 'Dynflow executor unavailable')
|
|
262
|
+
|
|
263
|
+
post :launch_task, params: {
|
|
264
|
+
smart_proxy_id: @proxy.id,
|
|
265
|
+
task_name: 'mymod::install',
|
|
266
|
+
targets: 'host1.example.com',
|
|
267
|
+
}, session: @session
|
|
268
|
+
|
|
269
|
+
assert_response :internal_server_error
|
|
270
|
+
assert_match(/background polling could not be scheduled/,
|
|
271
|
+
JSON.parse(response.body)['error']['message'])
|
|
272
|
+
|
|
273
|
+
job = ForemanOpenbolt::TaskJob.find_by(job_id: 'api-job-1')
|
|
274
|
+
assert_not_nil job, 'TaskJob row should be persisted even when polling scheduling fails'
|
|
275
|
+
assert_equal 'exception', job.status
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
test 'logs the persisted status when the exception flip itself fails' do
|
|
279
|
+
ForemanTasks.stubs(:async_task).raises(StandardError, 'Dynflow executor unavailable')
|
|
280
|
+
ForemanOpenbolt::TaskJob.any_instance.stubs(:update!).raises(
|
|
281
|
+
ActiveRecord::RecordInvalid.new(ForemanOpenbolt::TaskJob.new)
|
|
282
|
+
)
|
|
283
|
+
|
|
284
|
+
Foreman::Logging.stubs(:exception)
|
|
285
|
+
Foreman::Logging.expects(:exception)
|
|
286
|
+
.with(regexp_matches(/Row will remain in 'pending' state/), anything)
|
|
287
|
+
.at_least_once
|
|
288
|
+
|
|
289
|
+
post :launch_task, params: {
|
|
290
|
+
smart_proxy_id: @proxy.id,
|
|
291
|
+
task_name: 'mymod::install',
|
|
292
|
+
targets: 'host1.example.com',
|
|
293
|
+
}, session: @session
|
|
294
|
+
|
|
295
|
+
assert_response :internal_server_error
|
|
296
|
+
assert_match(/background polling could not be scheduled/,
|
|
297
|
+
JSON.parse(response.body)['error']['message'])
|
|
298
|
+
|
|
299
|
+
job = ForemanOpenbolt::TaskJob.find_by(job_id: 'api-job-1')
|
|
300
|
+
assert_not_nil job, 'TaskJob row should still be persisted'
|
|
301
|
+
assert_equal 'pending', job.status
|
|
302
|
+
end
|
|
303
|
+
|
|
304
|
+
test 'persists with empty description when metadata fetch fails with ProxyException' do
|
|
305
|
+
tasks_url = "#{@proxy.url}/openbolt/tasks"
|
|
306
|
+
::ProxyAPI::Openbolt.any_instance.stubs(:tasks).raises(
|
|
307
|
+
::ProxyAPI::ProxyException.new(tasks_url, RuntimeError.new('boom'), 'proxy down post-launch')
|
|
308
|
+
)
|
|
309
|
+
|
|
310
|
+
post :launch_task, params: {
|
|
311
|
+
smart_proxy_id: @proxy.id,
|
|
312
|
+
task_name: 'mymod::install',
|
|
313
|
+
targets: 'host1.example.com',
|
|
314
|
+
}, session: @session
|
|
315
|
+
|
|
316
|
+
assert_response :success
|
|
317
|
+
job = ForemanOpenbolt::TaskJob.find_by(job_id: 'api-job-1')
|
|
318
|
+
assert_not_nil job
|
|
319
|
+
assert_equal '', job.task_description
|
|
320
|
+
assert_equal 'pending', job.status
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
test 'persists with empty description when metadata fetch fails with transport error' do
|
|
324
|
+
::ProxyAPI::Openbolt.any_instance.stubs(:tasks).raises(Errno::ECONNREFUSED)
|
|
325
|
+
|
|
326
|
+
post :launch_task, params: {
|
|
327
|
+
smart_proxy_id: @proxy.id,
|
|
328
|
+
task_name: 'mymod::install',
|
|
329
|
+
targets: 'host1.example.com',
|
|
330
|
+
}, session: @session
|
|
331
|
+
|
|
332
|
+
assert_response :success
|
|
333
|
+
job = ForemanOpenbolt::TaskJob.find_by(job_id: 'api-job-1')
|
|
334
|
+
assert_not_nil job
|
|
335
|
+
assert_equal '', job.task_description
|
|
336
|
+
assert_equal 'pending', job.status
|
|
337
|
+
end
|
|
338
|
+
|
|
339
|
+
test 'returns 500 with the proxy job id when TaskJob persistence fails post-launch' do
|
|
340
|
+
ForemanOpenbolt::TaskJob.stubs(:create_from_execution!)
|
|
341
|
+
.raises(ActiveRecord::RecordInvalid.new(ForemanOpenbolt::TaskJob.new))
|
|
342
|
+
|
|
343
|
+
post :launch_task, params: {
|
|
344
|
+
smart_proxy_id: @proxy.id,
|
|
345
|
+
task_name: 'mymod::install',
|
|
346
|
+
targets: 'host1.example.com',
|
|
347
|
+
}, session: @session
|
|
348
|
+
|
|
349
|
+
assert_response :internal_server_error
|
|
350
|
+
body = JSON.parse(response.body)
|
|
351
|
+
assert_match(/api-job-1/, body['error']['message'])
|
|
352
|
+
assert_match(/could not record/i, body['error']['message'])
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
test 'forwards parameters body field to the proxy' do
|
|
356
|
+
post :launch_task, params: {
|
|
357
|
+
smart_proxy_id: @proxy.id,
|
|
358
|
+
task_name: 'mymod::install',
|
|
359
|
+
targets: 'host1.example.com',
|
|
360
|
+
parameters: { 'name' => 'nginx', 'version' => '1.21' },
|
|
361
|
+
}, session: @session
|
|
362
|
+
|
|
363
|
+
assert_response :success
|
|
364
|
+
launch_url = "#{@proxy.url}/openbolt/launch/task"
|
|
365
|
+
assert_requested(:post, launch_url) do |req|
|
|
366
|
+
sent = JSON.parse(req.body)
|
|
367
|
+
sent['parameters'] == { 'name' => 'nginx', 'version' => '1.21' }
|
|
368
|
+
end
|
|
369
|
+
|
|
370
|
+
job = ForemanOpenbolt::TaskJob.find_by(job_id: 'api-job-1')
|
|
371
|
+
assert_equal({ 'name' => 'nginx', 'version' => '1.21' }, job.task_parameters)
|
|
372
|
+
end
|
|
373
|
+
end
|
|
374
|
+
|
|
375
|
+
context 'authorization' do
|
|
376
|
+
test 'forbids users without execute_openbolt permission' do
|
|
377
|
+
reset_api_credentials
|
|
378
|
+
unprivileged = FactoryBot.create(:user)
|
|
379
|
+
User.current = unprivileged
|
|
380
|
+
|
|
381
|
+
get :tasks, params: { smart_proxy_id: @proxy.id },
|
|
382
|
+
session: set_session_user(unprivileged)
|
|
383
|
+
assert_response :forbidden
|
|
384
|
+
end
|
|
385
|
+
|
|
386
|
+
test 'forbids unprivileged users from launching tasks' do
|
|
387
|
+
reset_api_credentials
|
|
388
|
+
unprivileged = FactoryBot.create(:user)
|
|
389
|
+
User.current = unprivileged
|
|
390
|
+
|
|
391
|
+
post :launch_task, params: {
|
|
392
|
+
smart_proxy_id: @proxy.id,
|
|
393
|
+
task_name: 'mymod::install',
|
|
394
|
+
targets: 'host1.example.com',
|
|
395
|
+
}, session: set_session_user(unprivileged)
|
|
396
|
+
assert_response :forbidden
|
|
397
|
+
end
|
|
398
|
+
|
|
399
|
+
test 'allows users granted execute_openbolt via setup_user' do
|
|
400
|
+
tasks_url = "#{@proxy.url}/openbolt/tasks"
|
|
401
|
+
tasks = { 'mymod::install' => { 'description' => 'Install a package' } }
|
|
402
|
+
stub_request(:get, tasks_url)
|
|
403
|
+
.to_return(status: 200, body: tasks.to_json,
|
|
404
|
+
headers: { 'Content-Type' => 'application/json' })
|
|
405
|
+
reset_api_credentials
|
|
406
|
+
granted = FactoryBot.create(:user)
|
|
407
|
+
setup_user('execute', 'openbolt', nil, granted)
|
|
408
|
+
setup_user('view', 'smart_proxies', nil, granted)
|
|
409
|
+
User.current = granted
|
|
410
|
+
|
|
411
|
+
get :tasks, params: { smart_proxy_id: @proxy.id },
|
|
412
|
+
session: set_session_user(granted)
|
|
413
|
+
assert_response :success
|
|
414
|
+
end
|
|
415
|
+
|
|
416
|
+
test 'forbids unprivileged users from fetching task options' do
|
|
417
|
+
reset_api_credentials
|
|
418
|
+
unprivileged = FactoryBot.create(:user)
|
|
419
|
+
User.current = unprivileged
|
|
420
|
+
|
|
421
|
+
get :task_options, params: { smart_proxy_id: @proxy.id },
|
|
422
|
+
session: set_session_user(unprivileged)
|
|
423
|
+
assert_response :forbidden
|
|
424
|
+
end
|
|
425
|
+
|
|
426
|
+
test 'forbids unprivileged users from reloading the task cache' do
|
|
427
|
+
reset_api_credentials
|
|
428
|
+
unprivileged = FactoryBot.create(:user)
|
|
429
|
+
User.current = unprivileged
|
|
430
|
+
|
|
431
|
+
post :reload_tasks, params: { smart_proxy_id: @proxy.id },
|
|
432
|
+
session: set_session_user(unprivileged)
|
|
433
|
+
assert_response :forbidden
|
|
434
|
+
end
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
end
|
|
438
|
+
end
|