foreman_rh_cloud 15.0.0 → 15.0.1
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/app/controllers/insights_cloud/ui_requests_controller.rb +11 -0
- data/lib/foreman_rh_cloud/version.rb +1 -1
- data/package.json +1 -1
- data/test/controllers/insights_cloud/ui_requests_controller_test.rb +147 -2
- data/webpack/ForemanInventoryUpload/SubscriptionsPageExtension/InventoryAutoUpload/InventoryAutoUpload.js +3 -1
- data/webpack/ForemanInventoryUpload/SubscriptionsPageExtension/InventoryAutoUpload/__tests__/InventoryAutoUpload.test.js +23 -0
- metadata +1 -2
- data/lib/tasks/hybrid_cloud.rake +0 -132
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: '058ab42a9ca04ad514bb6b70da9fef474f1d632db028c878ca74c683d9e779b7'
|
|
4
|
+
data.tar.gz: 680c4b6007f94101da65041c00ed0ad789534c113a3f900a520c0a24244cc7a8
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: f53bc0a6402a59d7e92c0a1ffffaa961fd09eea5bec29204151b68ed50500860f5d39526bd0ebd8874ffb00ae5f8beaafba93d99b307c207ea3d62a6f70295ef
|
|
7
|
+
data.tar.gz: 61e0cba5b33a973607e915ff50b7da93db3ed042a5bbc71dc7df7bed7cad922a1be4281ec2aa7bc4f87ed54e32afb266bdfeed95bd01b917cb13e9a0f8fa7135
|
|
@@ -1,5 +1,16 @@
|
|
|
1
1
|
module InsightsCloud
|
|
2
2
|
class UIRequestsController < ::ApplicationController
|
|
3
|
+
# This controller proxies both browser (session + CSRF token) and
|
|
4
|
+
# token-authenticated API requests (e.g. Robottelo). ApplicationController's
|
|
5
|
+
# unconditional protect_from_forgery rejects the latter with a 422, so swap
|
|
6
|
+
# in the same session-only CSRF check Foreman's API base controller uses.
|
|
7
|
+
skip_forgery_protection
|
|
8
|
+
include Foreman::Controller::ApiCsrfProtection
|
|
9
|
+
|
|
10
|
+
protect_from_forgery with: :exception,
|
|
11
|
+
if: :protect_api_from_forgery?,
|
|
12
|
+
prepend: true
|
|
13
|
+
|
|
3
14
|
layout false
|
|
4
15
|
|
|
5
16
|
before_action :ensure_org, :find_location, :only => [:forward_request]
|
data/package.json
CHANGED
|
@@ -3,8 +3,6 @@ require 'rest-client'
|
|
|
3
3
|
|
|
4
4
|
module InsightsCloud
|
|
5
5
|
class UIRequestsControllerTest < ActionController::TestCase
|
|
6
|
-
include KatelloCVEHelper
|
|
7
|
-
|
|
8
6
|
setup do
|
|
9
7
|
FactoryBot.create(:common_parameter, name: InsightsCloud.enable_client_param, key_type: 'boolean', value: true)
|
|
10
8
|
end
|
|
@@ -222,6 +220,153 @@ module InsightsCloud
|
|
|
222
220
|
end
|
|
223
221
|
end
|
|
224
222
|
|
|
223
|
+
context "#forward_request CSRF protection" do
|
|
224
|
+
setup do
|
|
225
|
+
ActionController::Base.allow_forgery_protection = true
|
|
226
|
+
as_admin do
|
|
227
|
+
@org = FactoryBot.create(:organization)
|
|
228
|
+
@loc = FactoryBot.create(:location)
|
|
229
|
+
end
|
|
230
|
+
User.current = nil
|
|
231
|
+
@forwarder = ForemanRhCloud::InsightsApiForwarder.any_instance
|
|
232
|
+
@forwarder.stubs(:forward_request).returns(successful_cloud_response)
|
|
233
|
+
@controller.stubs(:authorized).returns(true)
|
|
234
|
+
end
|
|
235
|
+
|
|
236
|
+
teardown do
|
|
237
|
+
ActionController::Base.allow_forgery_protection = false
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
test 'rejects a browser session without a CSRF token' do
|
|
241
|
+
@forwarder.expects(:forward_request).never
|
|
242
|
+
|
|
243
|
+
assert_raises(ActionController::InvalidAuthenticityToken) do
|
|
244
|
+
request_with(:post, session: set_session)
|
|
245
|
+
end
|
|
246
|
+
end
|
|
247
|
+
|
|
248
|
+
test 'rejects a browser session with an invalid CSRF token' do
|
|
249
|
+
@forwarder.expects(:forward_request).never
|
|
250
|
+
|
|
251
|
+
assert_raises(ActionController::InvalidAuthenticityToken) do
|
|
252
|
+
request_with(:post, session: set_session, headers: { 'X-CSRF-Token' => 'invalid' })
|
|
253
|
+
end
|
|
254
|
+
end
|
|
255
|
+
|
|
256
|
+
test 'forwards a browser session with a valid CSRF token' do
|
|
257
|
+
token = @controller.send(:form_authenticity_token)
|
|
258
|
+
|
|
259
|
+
request_with(:post, params: { authenticity_token: token }, session: set_session,
|
|
260
|
+
headers: { 'X-CSRF-Token' => token })
|
|
261
|
+
|
|
262
|
+
assert_response :success
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
test 'forwards valid basic credentials without a session or CSRF token' do
|
|
266
|
+
set_basic_auth(users(:admin), 'secret')
|
|
267
|
+
|
|
268
|
+
request_with(:post)
|
|
269
|
+
|
|
270
|
+
assert_response :success
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
test 'forwards valid personal access token without a session or CSRF token' do
|
|
274
|
+
user = users(:admin)
|
|
275
|
+
token_value = as_admin do
|
|
276
|
+
token = FactoryBot.create(:personal_access_token, user: user)
|
|
277
|
+
value = token.generate_token
|
|
278
|
+
token.save!
|
|
279
|
+
value
|
|
280
|
+
end
|
|
281
|
+
set_basic_auth(user, token_value)
|
|
282
|
+
|
|
283
|
+
request_with(:post)
|
|
284
|
+
|
|
285
|
+
assert_response :success
|
|
286
|
+
assert_equal user.id, session[:user]
|
|
287
|
+
assert session[:api_authenticated_session]
|
|
288
|
+
end
|
|
289
|
+
|
|
290
|
+
test 'rejects invalid credentials without a session or CSRF token' do
|
|
291
|
+
@forwarder.expects(:forward_request).never
|
|
292
|
+
set_basic_auth('admin', 'invalid')
|
|
293
|
+
|
|
294
|
+
request_with(:post)
|
|
295
|
+
|
|
296
|
+
assert_response :unauthorized
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
test 'rejects a browser session with authorization and without a CSRF token' do
|
|
300
|
+
@forwarder.expects(:forward_request).never
|
|
301
|
+
set_basic_auth(users(:admin), 'secret')
|
|
302
|
+
|
|
303
|
+
assert_raises(ActionController::InvalidAuthenticityToken) do
|
|
304
|
+
request_with(:post, session: set_session)
|
|
305
|
+
end
|
|
306
|
+
end
|
|
307
|
+
|
|
308
|
+
test 'rejects a browser session with authorization and an invalid CSRF token' do
|
|
309
|
+
@forwarder.expects(:forward_request).never
|
|
310
|
+
set_basic_auth(users(:admin), 'secret')
|
|
311
|
+
|
|
312
|
+
assert_raises(ActionController::InvalidAuthenticityToken) do
|
|
313
|
+
request_with(:post, session: set_session, headers: { 'X-CSRF-Token' => 'invalid' })
|
|
314
|
+
end
|
|
315
|
+
end
|
|
316
|
+
|
|
317
|
+
test 'rejects DELETE from a browser session without a CSRF token' do
|
|
318
|
+
@forwarder.expects(:forward_request).never
|
|
319
|
+
|
|
320
|
+
assert_raises(ActionController::InvalidAuthenticityToken) do
|
|
321
|
+
request_with(:delete, session: set_session)
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
test 'forwards DELETE with valid basic credentials without a session or CSRF token' do
|
|
326
|
+
set_basic_auth(users(:admin), 'secret')
|
|
327
|
+
|
|
328
|
+
request_with(:delete)
|
|
329
|
+
|
|
330
|
+
assert_response :success
|
|
331
|
+
end
|
|
332
|
+
|
|
333
|
+
test 'forwards DELETE with a valid personal access token without a session or CSRF token' do
|
|
334
|
+
user = users(:admin)
|
|
335
|
+
token_value = as_admin do
|
|
336
|
+
token = FactoryBot.create(:personal_access_token, user: user)
|
|
337
|
+
value = token.generate_token
|
|
338
|
+
token.save!
|
|
339
|
+
value
|
|
340
|
+
end
|
|
341
|
+
set_basic_auth(user, token_value)
|
|
342
|
+
|
|
343
|
+
request_with(:delete)
|
|
344
|
+
|
|
345
|
+
assert_response :success
|
|
346
|
+
assert_equal user.id, session[:user]
|
|
347
|
+
assert session[:api_authenticated_session]
|
|
348
|
+
end
|
|
349
|
+
|
|
350
|
+
private
|
|
351
|
+
|
|
352
|
+
def request_with(method, params: {}, session: nil, headers: {})
|
|
353
|
+
headers.each { |name, value| @request.headers[name] = value }
|
|
354
|
+
public_send(method, :forward_request,
|
|
355
|
+
params: forward_request_params.merge(params), session: session)
|
|
356
|
+
end
|
|
357
|
+
|
|
358
|
+
def forward_request_params
|
|
359
|
+
{ controller: 'vulnerabilities', path: 'api/vulnerability/v1/cves', organization_id: @org.id }
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
def successful_cloud_response
|
|
363
|
+
request = RestClient::Request.new(method: 'POST', url: 'http://test.theforeman.org')
|
|
364
|
+
response = Net::HTTPResponse.new(1.0, 200, 'OK')
|
|
365
|
+
response['content-type'] = 'application/json'
|
|
366
|
+
RestClient::Response.create('{}', response, request)
|
|
367
|
+
end
|
|
368
|
+
end
|
|
369
|
+
|
|
225
370
|
def set_session
|
|
226
371
|
set_session_user.merge(
|
|
227
372
|
organization_id: @org.id,
|
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
import { InfoAltIcon, CaretRightIcon } from '@patternfly/react-icons';
|
|
14
14
|
import { translate as __ } from 'foremanReact/common/I18n';
|
|
15
15
|
import { foremanUrl } from '../../../ForemanRhCloudHelpers';
|
|
16
|
+
import { useIopConfig } from '../../../common/Hooks/ConfigHooks';
|
|
16
17
|
import SwitcherPF4 from '../../../common/Switcher/SwitcherPF4';
|
|
17
18
|
import { settingsDict } from '../../Components/InventorySettings/AdvancedSetting/AdvancedSettingsConstants';
|
|
18
19
|
import InventorySettings from '../../Components/InventorySettings/InventorySettings';
|
|
@@ -22,6 +23,7 @@ const InventoryAutoUploadSwitcher = ({
|
|
|
22
23
|
setSetting,
|
|
23
24
|
getSettings,
|
|
24
25
|
}) => {
|
|
26
|
+
const isIop = useIopConfig();
|
|
25
27
|
useEffect(() => {
|
|
26
28
|
getSettings();
|
|
27
29
|
}, [getSettings]);
|
|
@@ -80,7 +82,7 @@ const InventoryAutoUploadSwitcher = ({
|
|
|
80
82
|
rel="noopener noreferrer"
|
|
81
83
|
>
|
|
82
84
|
<strong>
|
|
83
|
-
{__('
|
|
85
|
+
{isIop ? __('Administer') : __('Insights')}
|
|
84
86
|
{' > '}
|
|
85
87
|
{__('Inventory Upload')}
|
|
86
88
|
</strong>
|
|
@@ -2,6 +2,11 @@ import React from 'react';
|
|
|
2
2
|
import { render, screen } from '@testing-library/react';
|
|
3
3
|
import InventoryAutoUpload from '../InventoryAutoUpload';
|
|
4
4
|
|
|
5
|
+
let mockIsIop = false;
|
|
6
|
+
jest.mock('../../../../common/Hooks/ConfigHooks', () => ({
|
|
7
|
+
useIopConfig: () => mockIsIop,
|
|
8
|
+
}));
|
|
9
|
+
|
|
5
10
|
jest.mock(
|
|
6
11
|
'../../../Components/InventorySettings/InventorySettings',
|
|
7
12
|
() => () => <div data-testid="inventory-settings">InventorySettings</div>
|
|
@@ -15,6 +20,10 @@ describe('InventoryAutoUpload', () => {
|
|
|
15
20
|
...overrides,
|
|
16
21
|
});
|
|
17
22
|
|
|
23
|
+
beforeEach(() => {
|
|
24
|
+
mockIsIop = false;
|
|
25
|
+
});
|
|
26
|
+
|
|
18
27
|
it('renders the heading', () => {
|
|
19
28
|
render(<InventoryAutoUpload {...buildProps()} />);
|
|
20
29
|
expect(screen.getByText('Red Hat Cloud Inventory')).toBeTruthy();
|
|
@@ -39,4 +48,18 @@ describe('InventoryAutoUpload', () => {
|
|
|
39
48
|
render(<InventoryAutoUpload {...props} />);
|
|
40
49
|
expect(props.getSettings).toHaveBeenCalled();
|
|
41
50
|
});
|
|
51
|
+
|
|
52
|
+
it('points to the Insights menu in regular mode', () => {
|
|
53
|
+
mockIsIop = false;
|
|
54
|
+
render(<InventoryAutoUpload {...buildProps()} />);
|
|
55
|
+
expect(screen.getByText(/Insights/)).toBeTruthy();
|
|
56
|
+
expect(screen.queryByText(/Administer/)).toBeNull();
|
|
57
|
+
});
|
|
58
|
+
|
|
59
|
+
it('points to the Administer menu in IoP mode', () => {
|
|
60
|
+
mockIsIop = true;
|
|
61
|
+
render(<InventoryAutoUpload {...buildProps()} />);
|
|
62
|
+
expect(screen.getByText(/Administer/)).toBeTruthy();
|
|
63
|
+
expect(screen.queryByText(/Insights/)).toBeNull();
|
|
64
|
+
});
|
|
42
65
|
});
|
metadata
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: foreman_rh_cloud
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 15.0.
|
|
4
|
+
version: 15.0.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Foreman Red Hat Cloud team
|
|
@@ -200,7 +200,6 @@ files:
|
|
|
200
200
|
- lib/inventory_sync/async/inventory_self_host_sync.rb
|
|
201
201
|
- lib/inventory_sync/async/query_inventory_job.rb
|
|
202
202
|
- lib/tasks/foreman_rh_cloud_tasks.rake
|
|
203
|
-
- lib/tasks/hybrid_cloud.rake
|
|
204
203
|
- lib/tasks/insights.rake
|
|
205
204
|
- lib/tasks/rh_cloud_inventory.rake
|
|
206
205
|
- locale/Makefile
|
data/lib/tasks/hybrid_cloud.rake
DELETED
|
@@ -1,132 +0,0 @@
|
|
|
1
|
-
require 'io/console'
|
|
2
|
-
require 'uri'
|
|
3
|
-
|
|
4
|
-
def logger
|
|
5
|
-
@logger ||= Logger.new(STDOUT)
|
|
6
|
-
end
|
|
7
|
-
|
|
8
|
-
namespace :rh_cloud do
|
|
9
|
-
desc 'Register Satellite Organization with Hybrid Cloud API.'
|
|
10
|
-
# This task registers the Satellite Organization with the Hybrid Cloud API.
|
|
11
|
-
# It requires the user to input their organization ID, Insights URL, and token.
|
|
12
|
-
# The task will then send a POST request to the Hybrid Cloud API to register the organization.
|
|
13
|
-
# The response will be logged, and any errors will be caught and logged as well.
|
|
14
|
-
# The task will exit with an error message if the organization does not have a manifest imported or if the token is not entered.
|
|
15
|
-
# The task will also log a warning if the custom URL is not set and the default one is used.
|
|
16
|
-
task hybridcloud_register: [:environment] do
|
|
17
|
-
include ::ForemanRhCloud::CertAuth
|
|
18
|
-
include ::InsightsCloud::CandlepinCache
|
|
19
|
-
|
|
20
|
-
def default_registrations_url
|
|
21
|
-
URI.join(ForemanRhCloud.base_url, '/api/identity/certificate/registrations').to_s
|
|
22
|
-
end
|
|
23
|
-
|
|
24
|
-
# Helper method to get the registrations URL, with a warning for default usage
|
|
25
|
-
def registrations_url(custom_url)
|
|
26
|
-
if custom_url.empty?
|
|
27
|
-
logger.warn("Custom url is not set, using the default one: #{default_registrations_url}")
|
|
28
|
-
default_registrations_url
|
|
29
|
-
else
|
|
30
|
-
if URI(custom_url).scheme.nil?
|
|
31
|
-
logger.warn("Custom URL lacks a scheme; prepending https:// prefix.")
|
|
32
|
-
custom_url = "https://" + custom_url
|
|
33
|
-
end
|
|
34
|
-
custom_url
|
|
35
|
-
end
|
|
36
|
-
end
|
|
37
|
-
|
|
38
|
-
def get_organization(user_org_id)
|
|
39
|
-
maybe_organization = Organization.find_by(id: user_org_id)
|
|
40
|
-
if maybe_organization.nil?
|
|
41
|
-
logger.error("Organization with ID '#{user_org_id}' not found.")
|
|
42
|
-
exit(1)
|
|
43
|
-
end
|
|
44
|
-
maybe_organization
|
|
45
|
-
end
|
|
46
|
-
|
|
47
|
-
def get_uid(organization)
|
|
48
|
-
maybe_uid = cp_owner_id(organization)
|
|
49
|
-
if maybe_uid.nil?
|
|
50
|
-
logger.error("Organization '#{organization}' does not have a manifest imported.")
|
|
51
|
-
exit(1)
|
|
52
|
-
end
|
|
53
|
-
maybe_uid
|
|
54
|
-
end
|
|
55
|
-
|
|
56
|
-
# --- Input Collection ---
|
|
57
|
-
puts "Paste in your organization ID, this can be retrieved with the command: hammer organization list"
|
|
58
|
-
loop do
|
|
59
|
-
input = STDIN.gets.chomp
|
|
60
|
-
if input.match?(/^\d+$/) # Checks if input consists only of digits
|
|
61
|
-
@user_org_id = input.to_i
|
|
62
|
-
break
|
|
63
|
-
else
|
|
64
|
-
puts "Invalid input. Please enter a numeric organization ID."
|
|
65
|
-
end
|
|
66
|
-
end
|
|
67
|
-
|
|
68
|
-
puts "\n" + "-" * 50 + "\n\n"
|
|
69
|
-
puts "Paste in your custom Insights URL. If nothing is entered, the default will be used (#{default_registrations_url})."
|
|
70
|
-
insights_user_input = STDIN.gets.chomp
|
|
71
|
-
|
|
72
|
-
puts "\n" + "-" * 50 + "\n\n"
|
|
73
|
-
puts 'Paste in your Hybrid Cloud API token, output will be hidden.'
|
|
74
|
-
puts 'This token can be retrieved from the Hybrid Cloud console.'
|
|
75
|
-
token = STDIN.noecho(&:gets).chomp
|
|
76
|
-
if token.empty?
|
|
77
|
-
logger.error('Token was not entered.')
|
|
78
|
-
exit(1)
|
|
79
|
-
end
|
|
80
|
-
|
|
81
|
-
# --- Data Preparation ---
|
|
82
|
-
|
|
83
|
-
organization = get_organization(@user_org_id)
|
|
84
|
-
uid = get_uid(organization)
|
|
85
|
-
hostname = ForemanRhCloud.foreman_host_name
|
|
86
|
-
insights_url = registrations_url(insights_user_input)
|
|
87
|
-
|
|
88
|
-
# --- API Request ---
|
|
89
|
-
|
|
90
|
-
headers = {
|
|
91
|
-
Authorization: "Bearer #{token}",
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
payload = {
|
|
95
|
-
'uid': uid,
|
|
96
|
-
"display_name": "#{hostname}+#{organization.label}",
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
begin
|
|
100
|
-
response = execute_cloud_request(
|
|
101
|
-
organization: organization,
|
|
102
|
-
method: :post,
|
|
103
|
-
url: insights_url,
|
|
104
|
-
headers: headers,
|
|
105
|
-
payload: payload.to_json
|
|
106
|
-
)
|
|
107
|
-
logger.debug("Cloud request completed: status=#{response.code}, body_preview=#{response.body&.slice(0, 200)}")
|
|
108
|
-
rescue RestClient::Unauthorized => _ex
|
|
109
|
-
# Add a more specific rescue for 401 Unauthorized errors
|
|
110
|
-
logger.error('Registration failed: Your token is invalid or unauthorized. Please check your token and try again.')
|
|
111
|
-
# Optionally, you can still log the full debug info if helpful for advanced troubleshooting
|
|
112
|
-
# logger.debug(ex.backtrace.join("\n"))
|
|
113
|
-
exit(1)
|
|
114
|
-
rescue RestClient::ExceptionWithResponse => ex
|
|
115
|
-
# This catches any RestClient exception that has a response (like 400, 403, 404, 500, etc.)
|
|
116
|
-
status_code = begin
|
|
117
|
-
ex.response.code
|
|
118
|
-
rescue StandardError
|
|
119
|
-
"unknown"
|
|
120
|
-
end
|
|
121
|
-
logger.error("Registration failed with HTTP status #{status_code}: #{ex.message}")
|
|
122
|
-
logger.debug("Response body (if available): #{ex.response.body}")
|
|
123
|
-
exit(1)
|
|
124
|
-
rescue StandardError => ex
|
|
125
|
-
# This is the catch-all for any other unexpected errors
|
|
126
|
-
logger.error("An unexpected error occurred during registration: #{ex.message}")
|
|
127
|
-
exit(1)
|
|
128
|
-
end
|
|
129
|
-
|
|
130
|
-
logger.info("Satellite Organization '#{organization.label}' (ID: #{@user_org_id}) successfully registered.")
|
|
131
|
-
end
|
|
132
|
-
end
|