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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 637a2b5c3eb9d907b2c20a3921b728efe617dd2dccb6cd1f99135784f2721bed
4
- data.tar.gz: 2b254832b522c4c60c6f11ff8e727a890c3b0632d1df3e0abc024657ad8a0343
3
+ metadata.gz: '058ab42a9ca04ad514bb6b70da9fef474f1d632db028c878ca74c683d9e779b7'
4
+ data.tar.gz: 680c4b6007f94101da65041c00ed0ad789534c113a3f900a520c0a24244cc7a8
5
5
  SHA512:
6
- metadata.gz: 9e3c2a143ad9a3e7ff76ddf83440655c0fd0c558f423acc0dac4ddbb88e9245850d39f270e7f455849d58fd2e37904804a0e1611641f7f8438ca2a79e15e1e36
7
- data.tar.gz: 22886acec69b9b059802e3ba750bb43bda9132990c4c104e07a49ed6852f37b54db2302ead5acfeb973e136ba435256437ef786e396253420e89322946d929e7
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]
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '15.0.0'.freeze
2
+ VERSION = '15.0.1'.freeze
3
3
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "15.0.0",
3
+ "version": "15.0.1",
4
4
  "description": "Inventory Upload =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -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
- {__('Configure')}
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.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
@@ -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