foreman_rh_cloud 13.1.0 → 13.2.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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: a37e0d6adc3c8b44e73f636a10ee9012c2e14ef450e1a88d65bca7bbcd7c69cc
4
- data.tar.gz: f53d45a393d00773a1ac42ffc2ca08f43fc52b861fba0eab2d1d8a4e1226600e
3
+ metadata.gz: 5707d35255f855b6e11f49080a65c0ca44c2a75d09183edbefb78ca7ceb06023
4
+ data.tar.gz: f52cff55dc3dfa16e9dc5a151a0866227f37ad99c55016cc42c29eeb1411e151
5
5
  SHA512:
6
- metadata.gz: 6ceaf758e5d3068fb4540399bc45cc0387f6e32a32b7cddcf38404a86b3279a7c0728707c7363996ab73303407b8977ace52641a40ec066c00d83b02a02d29ba
7
- data.tar.gz: 67b81d211aa7f0aba5ea6e81dc3349add6e003b39a27f6b097eed42b9baf5011222a48ebf1e0b9ed30999a84e8fdd4bbeb583dadd6e0043b147818fd48841efc
6
+ metadata.gz: '0239185072ee7994fc76aef806ac99aaab740b72da7c9a81e699e26df9af5d276b969d9c3279ab0e9a796996eb289739f09cb6d170a932528348c6e7f2972512'
7
+ data.tar.gz: 577bb25d4bfd6d59f30b6e74b2e6689e38441272841b989f28df1b7e445cc80bfcb30e47287ed9b54649dfeaa265cfdadaa3c847f1b08a2b405690b701ec3a92
@@ -2,7 +2,7 @@ module InsightsCloud
2
2
  class UIRequestsController < ::ApplicationController
3
3
  layout false
4
4
 
5
- before_action :ensure_org, :ensure_loc, :only => [:forward_request]
5
+ before_action :ensure_org, :find_location, :only => [:forward_request]
6
6
 
7
7
  # The method that "proxies" requests over to Cloud
8
8
  def forward_request
@@ -18,6 +18,10 @@ module InsightsCloud
18
18
  @organization,
19
19
  @location
20
20
  )
21
+ rescue ::Foreman::PermissionMissingException => e
22
+ logger.warn("Permission denied for forwarding request: #{e}")
23
+ message = e.message
24
+ return render json: { message: message, error: message }, status: :forbidden
21
25
  rescue RestClient::Exceptions::Timeout => e
22
26
  response_obj = e.response.presence || e.exception
23
27
  return render json: { message: response_obj.to_s, error: response_obj.to_s }, status: :gateway_timeout
@@ -93,9 +97,8 @@ module InsightsCloud
93
97
  return render_message 'Organization not found or invalid', :status => 400 unless @organization
94
98
  end
95
99
 
96
- def ensure_loc
100
+ def find_location
97
101
  @location = Location.current
98
- return render_message 'Location not found or invalid', :status => 400 unless @location
99
102
  end
100
103
 
101
104
  def base_url
@@ -4,17 +4,144 @@ module ForemanRhCloud
4
4
  class InsightsApiForwarder
5
5
  include ForemanRhCloud::CertAuth
6
6
 
7
+ # Permission mapping for API paths:
8
+ #
9
+ # Foreman Permission | Paths
10
+ # ---------------------|--------------------------------------------------
11
+ # view_vulnerability | GET /api/inventory/v1/hosts(/*)
12
+ # view_vulnerability | GET /api/vulnerability/v1/*
13
+ # | POST /api/vulnerability/v1/vulnerabilities/cves
14
+ # edit_vulnerability | PATCH /api/vulnerability/v1/status
15
+ # edit_vulnerability | PATCH /api/vulnerability/v1/cves/status
16
+ # | PATCH /api/vulnerability/v1/cves/business_risk
17
+ # edit_vulnerability | PATCH /api/vulnerability/v1/systems/opt_out
18
+ #
19
+ # view_advisor | GET /api/insights/v1/*
20
+ # edit_advisor | POST /api/insights/v1/ack/
21
+ # | DELETE /api/insights/v1/ack/{rule_id}/
22
+ # | POST /api/insights/v1/hostack/
23
+ # | DELETE /api/insights/v1/hostack/{id}/
24
+ # | POST /api/insights/v1/rule/{rule_id}/unack_hosts/
25
+ #
7
26
  SCOPED_REQUESTS = [
8
- { test: %r{api/vulnerability/v1/vulnerabilities/cves}, tag_name: :tags },
9
- { test: %r{api/vulnerability/v1/dashbar}, tag_name: :tags },
10
- { test: %r{api/vulnerability/v1/cves/[^/]+/affected_systems}, tag_name: :tags },
11
- { test: %r{api/vulnerability/v1/systems/[^/]+/cves}, tag_name: :tags },
12
- { test: %r{api/insights/.*}, tag_name: :tags },
27
+ # Inventory hosts - requires view_vulnerability for GET
28
+ {
29
+ test: %r{api/inventory/v1/hosts(/.*)?$},
30
+ tag_name: :tags,
31
+ permissions: {
32
+ 'GET' => :view_vulnerability,
33
+ },
34
+ },
35
+ # Vulnerability CVEs list - POST requires view_vulnerability (no tags support per OpenAPI spec)
36
+ {
37
+ test: %r{api/vulnerability/v1/vulnerabilities/cves},
38
+ permissions: {
39
+ 'POST' => :view_vulnerability,
40
+ },
41
+ },
42
+ # Vulnerability status - PATCH requires edit_vulnerability
43
+ # Note: GET /status does not support tags parameter per OpenAPI spec
44
+ {
45
+ test: %r{api/vulnerability/v1/status},
46
+ permissions: {
47
+ 'PATCH' => :edit_vulnerability,
48
+ },
49
+ },
50
+ # CVE status - PATCH requires edit_vulnerability (no GET endpoint)
51
+ {
52
+ test: %r{api/vulnerability/v1/cves/status},
53
+ permissions: {
54
+ 'PATCH' => :edit_vulnerability,
55
+ },
56
+ },
57
+ # CVE business risk - PATCH requires edit_vulnerability (no GET endpoint)
58
+ {
59
+ test: %r{api/vulnerability/v1/cves/business_risk},
60
+ permissions: {
61
+ 'PATCH' => :edit_vulnerability,
62
+ },
63
+ },
64
+ # Systems opt out - PATCH requires edit_vulnerability (no GET endpoint)
65
+ {
66
+ test: %r{api/vulnerability/v1/systems/opt_out},
67
+ permissions: {
68
+ 'PATCH' => :edit_vulnerability,
69
+ },
70
+ },
71
+ # Endpoints without tags support (per OpenAPI spec) - still require view_vulnerability for GET
72
+ {
73
+ test: %r{api/vulnerability/v1/(apistatus|version|business_risk|announcement)$},
74
+ permissions: {
75
+ 'GET' => :view_vulnerability,
76
+ },
77
+ },
78
+ {
79
+ test: %r{api/vulnerability/v1/cves/[^/]+$},
80
+ permissions: {
81
+ 'GET' => :view_vulnerability,
82
+ },
83
+ },
84
+ {
85
+ test: %r{api/vulnerability/v1/(playbooks|report)/},
86
+ permissions: {
87
+ 'GET' => :view_vulnerability,
88
+ },
89
+ },
90
+ # Other vulnerability endpoints - GET requires view_vulnerability (with tags support)
91
+ {
92
+ test: %r{api/vulnerability/v1/.*},
93
+ tag_name: :tags,
94
+ permissions: {
95
+ 'GET' => :view_vulnerability,
96
+ },
97
+ },
98
+ # Advisor ack endpoints - POST/DELETE require edit_advisor
99
+ {
100
+ test: %r{api/insights/v1/ack(/[^/]*)?$},
101
+ tag_name: :tags,
102
+ permissions: {
103
+ 'POST' => :edit_advisor,
104
+ 'DELETE' => :edit_advisor,
105
+ },
106
+ },
107
+ # Advisor hostack endpoints - POST/DELETE require edit_advisor
108
+ {
109
+ test: %r{api/insights/v1/hostack(/[^/]*)?$},
110
+ tag_name: :tags,
111
+ permissions: {
112
+ 'POST' => :edit_advisor,
113
+ 'DELETE' => :edit_advisor,
114
+ },
115
+ },
116
+ # Advisor rule unack_hosts - POST requires edit_advisor
117
+ {
118
+ test: %r{api/insights/v1/rule/[^/]+/unack_hosts},
119
+ tag_name: :tags,
120
+ permissions: {
121
+ 'POST' => :edit_advisor,
122
+ },
123
+ },
124
+ # Other Advisor/Insights endpoints - GET requires view_advisor
125
+ {
126
+ test: %r{api/insights/v1/.*},
127
+ tag_name: :tags,
128
+ permissions: {
129
+ 'GET' => :view_advisor,
130
+ },
131
+ },
132
+ # Other API endpoints (tagging only, no permission enforcement)
13
133
  { test: %r{api/inventory/.*}, tag_name: :tags },
14
134
  { test: %r{api/tasks/.*}, tag_name: :tags },
15
135
  ].freeze
16
136
 
17
137
  def forward_request(original_request, path, controller_name, user, organization, location)
138
+ # Check permissions before forwarding
139
+ permission = required_permission_for(path, original_request.request_method)
140
+ if permission && !user&.can?(permission)
141
+ logger.warn("User #{user&.login || 'anonymous'} lacks permission #{permission} for #{original_request.request_method} #{path}")
142
+ raise ::Foreman::PermissionMissingException.new(N_("You do not have permission to perform this action"))
143
+ end
144
+
18
145
  TagsAuth.new(user, organization, location, logger).update_tag if scope_request?(original_request, path)
19
146
 
20
147
  forward_params = prepare_forward_params(original_request, path, user: user, organization: organization, location: location).to_a
@@ -97,8 +224,15 @@ module ForemanRhCloud
97
224
  def scope_request?(original_request, path)
98
225
  return nil unless original_request.get?
99
226
 
100
- request_pattern = SCOPED_REQUESTS.find { |pattern| pattern[:test].match?(path) }
101
- request_pattern[:tag_name] if request_pattern
227
+ # Only consider patterns that define tag_name - this ensures patterns without
228
+ # tag_name (permission-only entries) cannot override tag-supporting patterns
229
+ matching_patterns = SCOPED_REQUESTS.select { |pattern| pattern[:tag_name] && pattern[:test].match?(path) }
230
+ return nil if matching_patterns.empty?
231
+
232
+ # Choose the most specific pattern by regex source length for consistency
233
+ # with required_permission_for behavior
234
+ request_pattern = matching_patterns.max_by { |pattern| pattern[:test].source.length }
235
+ request_pattern[:tag_name]
102
236
  end
103
237
 
104
238
  def core_app_name
@@ -116,5 +250,27 @@ module ForemanRhCloud
116
250
  def logger
117
251
  Foreman::Logging.logger('app')
118
252
  end
253
+
254
+ # Returns the required permission for the given path and HTTP method
255
+ # Resolves overlapping patterns by choosing the most specific matcher,
256
+ # defined as the one with the longest regex source that matches the path.
257
+ # This avoids permission changes caused by reordering SCOPED_REQUESTS.
258
+ # @param path [String] The request path
259
+ # @param http_method [String] The HTTP method (GET, POST, etc.)
260
+ # @return [Symbol, nil] The required permission symbol or nil if no permission required
261
+ def required_permission_for(path, http_method)
262
+ # Collect all matching patterns
263
+ matching_patterns = SCOPED_REQUESTS.select { |pattern| pattern[:test].match?(path) }
264
+ return nil if matching_patterns.empty?
265
+
266
+ # Choose the most specific pattern: longest regex source wins.
267
+ # This makes overlapping patterns deterministic and independent of array order.
268
+ request_pattern = matching_patterns.max_by { |pattern| pattern[:test].source.length }
269
+
270
+ permissions = request_pattern[:permissions]
271
+ return nil unless permissions
272
+
273
+ permissions[http_method]
274
+ end
119
275
  end
120
276
  end
@@ -20,7 +20,8 @@ module ForemanRhCloud
20
20
  end
21
21
 
22
22
  def update_tag
23
- logger.debug("Updating tags for user: #{@user}, org: #{@org.name}, loc: #{@loc.name}")
23
+ loc_name = location_name_for_tag
24
+ logger.debug("Updating tags for user: #{@user}, org: #{@org.name}, loc: #{loc_name}")
24
25
 
25
26
  payload = tags_query_payload
26
27
  params = {
@@ -36,7 +37,9 @@ module ForemanRhCloud
36
37
  end
37
38
 
38
39
  def allowed_hosts
39
- Host.authorized_as(@user, nil, nil).where(organization: @org, location: @loc).joins(:subscription_facet).pluck('katello_subscription_facets.uuid')
40
+ query = Host.authorized_as(@user, nil, nil).where(organization: @org)
41
+ query = query.where(location: @loc) if @loc
42
+ query.joins(:subscription_facet).pluck('katello_subscription_facets.uuid')
40
43
  end
41
44
 
42
45
  def tags_query_payload
@@ -47,11 +50,18 @@ module ForemanRhCloud
47
50
  end
48
51
 
49
52
  def tag_value
50
- "U:\"#{@user.login}\"O:\"#{@org.name}\"L:\"#{@loc.name}\""
53
+ location_part = "L:\"#{location_name_for_tag}\""
54
+ "U:\"#{@user.login}\"O:\"#{@org.name}\"#{location_part}"
51
55
  end
52
56
 
53
57
  def auth_tag
54
58
  "#{TAG_NAME}=#{tag_value}"
55
59
  end
60
+
61
+ private
62
+
63
+ def location_name_for_tag
64
+ @loc ? @loc.name : '*'
65
+ end
56
66
  end
57
67
  end
@@ -71,12 +71,46 @@ module ForemanRhCloud
71
71
  :control_organization_insights,
72
72
  'insights_cloud/settings': [:set_org_parameter]
73
73
  )
74
+ # Insights Vulnerability permissions
75
+ permission(
76
+ :view_vulnerability,
77
+ {},
78
+ :resource_type => 'ForemanRhCloud'
79
+ )
80
+ permission(
81
+ :edit_vulnerability,
82
+ {},
83
+ :resource_type => 'ForemanRhCloud'
84
+ )
85
+ # Insights Advisor permissions
86
+ permission(
87
+ :view_advisor,
88
+ {},
89
+ :resource_type => 'ForemanRhCloud'
90
+ )
91
+ permission(
92
+ :edit_advisor,
93
+ {},
94
+ :resource_type => 'ForemanRhCloud'
95
+ )
74
96
  end
75
97
 
76
- plugin_permissions = [:view_foreman_rh_cloud, :generate_foreman_rh_cloud, :view_insights_hits, :dispatch_cloud_requests, :control_organization_insights]
98
+ # Core RH Cloud permissions for inventory upload and sync
99
+ rh_cloud_permissions = [:view_foreman_rh_cloud, :generate_foreman_rh_cloud, :view_insights_hits, :dispatch_cloud_requests, :control_organization_insights]
100
+
101
+ # Insights application permissions (Vulnerability, Advisor)
102
+ insights_permissions = [:view_vulnerability, :edit_vulnerability, :view_advisor, :edit_advisor]
103
+
104
+ plugin_permissions = rh_cloud_permissions + insights_permissions
105
+
106
+ read_only_permissions = [:view_foreman_rh_cloud, :view_insights_hits, :view_vulnerability, :view_advisor]
77
107
 
78
108
  role 'ForemanRhCloud', plugin_permissions, 'Role granting permissions to view the hosts inventory,
79
- generate a report, upload it to the cloud and download it locally'
109
+ generate a report, upload it to the cloud, download it locally,
110
+ and manage Insights Vulnerability and Advisor features'
111
+
112
+ role 'ForemanRhCloud Read Only', read_only_permissions, 'Role granting read-only permissions to view
113
+ Insights Vulnerability, Advisor, and host inventory'
80
114
 
81
115
  add_permissions_to_default_roles Role::ORG_ADMIN => plugin_permissions,
82
116
  Role::MANAGER => plugin_permissions,
@@ -1,3 +1,3 @@
1
1
  module ForemanRhCloud
2
- VERSION = '13.1.0'.freeze
2
+ VERSION = '13.2.0'.freeze
3
3
  end
data/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "foreman_rh_cloud",
3
- "version": "13.1.0",
3
+ "version": "13.2.0",
4
4
  "description": "Inventory Upload =============",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -180,6 +180,32 @@ module InsightsCloud
180
180
  assert_equal 'Cloud request failed', JSON.parse(@response.body)['message']
181
181
  assert_match(/#{@body}/, JSON.parse(@response.body)['response'])
182
182
  end
183
+
184
+ test "should allow forward_request with nil location (Any location)" do
185
+ net_http_resp = Net::HTTPResponse.new(1.0, 200, "OK")
186
+ res = RestClient::Response.create(@body, net_http_resp, @http_req)
187
+ ::ForemanRhCloud::InsightsApiForwarder.any_instance.stubs(:forward_request).returns(res)
188
+
189
+ # Set session with nil location_id to simulate "Any location"
190
+ session_with_nil_location = set_session_user.merge(
191
+ organization_id: @org.id,
192
+ location_id: nil
193
+ )
194
+
195
+ get :forward_request, params: { "controller" => "vulnerabilities", "path" => "api/vulnerability/v1/cves" }, session: session_with_nil_location
196
+ assert_equal 200, @response.status
197
+ assert_equal @body, @response.body
198
+ end
199
+
200
+ test "should allow forward_request with location set" do
201
+ net_http_resp = Net::HTTPResponse.new(1.0, 200, "OK")
202
+ res = RestClient::Response.create(@body, net_http_resp, @http_req)
203
+ ::ForemanRhCloud::InsightsApiForwarder.any_instance.stubs(:forward_request).returns(res)
204
+
205
+ get :forward_request, params: { "controller" => "vulnerabilities", "path" => "api/vulnerability/v1/cves" }, session: set_session
206
+ assert_equal 200, @response.status
207
+ assert_equal @body, @response.body
208
+ end
183
209
  end
184
210
 
185
211
  def set_session
@@ -29,6 +29,7 @@ class UIRequestForwarderTest < ActiveSupport::TestCase
29
29
  'action_dispatch.request.query_parameters' => params
30
30
  )
31
31
 
32
+ @user.stubs(:can?).with(:view_vulnerability).returns(true)
32
33
  ::ForemanRhCloud::TagsAuth.any_instance.expects(:update_tag)
33
34
  @forwarder.expects(:execute_cloud_request).with do |actual_params|
34
35
  actual = actual_params[:headers][:params]
@@ -37,9 +38,6 @@ class UIRequestForwarderTest < ActiveSupport::TestCase
37
38
  end
38
39
 
39
40
  @forwarder.forward_request(req, '/api/vulnerability/v1/cves/abc-123/affected_systems', 'test_controller', @user, @organization, @location)
40
-
41
- # This test asserts the parameters that are sent to the execute_cloud_request method.
42
- # This is done by setting the expectation before the actual call.
43
41
  end
44
42
 
45
43
  test 'should not scope GET requests for unknown uris' do
@@ -62,9 +60,6 @@ class UIRequestForwarderTest < ActiveSupport::TestCase
62
60
  end
63
61
 
64
62
  @forwarder.forward_request(req, '/api/vulnerability/foo/bar', 'test_controller', @user, @organization, @location)
65
-
66
- # This test asserts the parameters that are sent to the execute_cloud_request method.
67
- # This is done by setting the expectation before the actual call.
68
63
  end
69
64
 
70
65
  test 'should merge URI params in GET requests' do
@@ -79,6 +74,7 @@ class UIRequestForwarderTest < ActiveSupport::TestCase
79
74
  'action_dispatch.request.query_parameters' => params
80
75
  )
81
76
 
77
+ @user.stubs(:can?).with(:view_vulnerability).returns(true)
82
78
  ::ForemanRhCloud::TagsAuth.any_instance.expects(:update_tag)
83
79
  @forwarder.expects(:execute_cloud_request).with do |actual_params|
84
80
  actual = actual_params[:headers][:params]
@@ -89,8 +85,6 @@ class UIRequestForwarderTest < ActiveSupport::TestCase
89
85
  end
90
86
 
91
87
  @forwarder.forward_request(req, '/api/vulnerability/v1/cves/abc-123/affected_systems', 'test_controller', @user, @organization, @location)
92
- # This test asserts the parameters that are sent to the execute_cloud_request method.
93
- # This is done by setting the expectation before the actual call.
94
88
  end
95
89
 
96
90
  test 'should not scope POST requests' do
@@ -110,9 +104,6 @@ class UIRequestForwarderTest < ActiveSupport::TestCase
110
104
  end
111
105
 
112
106
  @forwarder.forward_request(req, '/api/vulnerability/v1/cves', 'test_controller', @user, @organization, @location)
113
-
114
- # This test asserts the parameters that are sent to the execute_cloud_request method.
115
- # This is done by setting the expectation before the actual call.
116
107
  end
117
108
 
118
109
  test 'should not scope PUT requests' do
@@ -132,9 +123,6 @@ class UIRequestForwarderTest < ActiveSupport::TestCase
132
123
  end
133
124
 
134
125
  @forwarder.forward_request(req, '/api/vulnerability/v1/cves', 'test_controller', @user, @organization, @location)
135
-
136
- # This test asserts the parameters that are sent to the execute_cloud_request method.
137
- # This is done by setting the expectation before the actual call.
138
126
  end
139
127
 
140
128
  test 'should not scope PATCH requests' do
@@ -155,9 +143,6 @@ class UIRequestForwarderTest < ActiveSupport::TestCase
155
143
  end
156
144
 
157
145
  @forwarder.forward_request(req, '/api/vulnerability/v1/cves', 'test_controller', @user, @organization, @location)
158
-
159
- # This test asserts the parameters that are sent to the execute_cloud_request method.
160
- # This is done by setting the expectation before the actual call.
161
146
  end
162
147
 
163
148
  test 'scope_request? should return tag_name for scoped requests' do
@@ -214,4 +199,196 @@ class UIRequestForwarderTest < ActiveSupport::TestCase
214
199
  tag_string = CGI.unescape(param_value)
215
200
  tag_string.split('=')[0]
216
201
  end
202
+
203
+ # Helper to build test requests with minimal boilerplate
204
+ def build_request(method:, uri:, params: {}, data: nil)
205
+ env = {
206
+ 'REQUEST_URI' => uri,
207
+ 'REQUEST_METHOD' => method,
208
+ 'rack.input' => ::Puma::NullIO.new,
209
+ 'action_dispatch.request.query_parameters' => params,
210
+ }
211
+ env['RAW_POST_DATA'] = data if data
212
+ env['action_dispatch.request.path_parameters'] = { format: 'json' } if method == 'PATCH'
213
+ ActionDispatch::Request.new(env)
214
+ end
215
+
216
+ # Permission enforcement tests
217
+
218
+ # GET /api/inventory/v1/hosts requires view_vulnerability
219
+ test 'should allow GET request to inventory hosts when user has view_vulnerability permission' do
220
+ user_agent = { :foo => :bar }
221
+ params = {}
222
+
223
+ req = ActionDispatch::Request.new(
224
+ 'REQUEST_URI' => '/api/inventory/v1/hosts',
225
+ 'REQUEST_METHOD' => 'GET',
226
+ 'HTTP_USER_AGENT' => user_agent,
227
+ 'rack.input' => ::Puma::NullIO.new,
228
+ 'action_dispatch.request.query_parameters' => params
229
+ )
230
+
231
+ @user.stubs(:can?).with(:view_vulnerability).returns(true)
232
+ ::ForemanRhCloud::TagsAuth.any_instance.expects(:update_tag)
233
+ @forwarder.expects(:execute_cloud_request).returns(true)
234
+
235
+ @forwarder.forward_request(req, 'api/inventory/v1/hosts', 'test_controller', @user, @organization, @location)
236
+ end
237
+
238
+ test 'should deny GET request to inventory hosts when user lacks view_vulnerability permission' do
239
+ user_agent = { :foo => :bar }
240
+ params = {}
241
+
242
+ req = ActionDispatch::Request.new(
243
+ 'REQUEST_URI' => '/api/inventory/v1/hosts',
244
+ 'REQUEST_METHOD' => 'GET',
245
+ 'HTTP_USER_AGENT' => user_agent,
246
+ 'rack.input' => ::Puma::NullIO.new,
247
+ 'action_dispatch.request.query_parameters' => params
248
+ )
249
+
250
+ @user.stubs(:can?).with(:view_vulnerability).returns(false)
251
+
252
+ assert_raises(::Foreman::PermissionMissingException) do
253
+ @forwarder.forward_request(req, 'api/inventory/v1/hosts', 'test_controller', @user, @organization, @location)
254
+ end
255
+ end
256
+
257
+ # POST /api/vulnerability/v1/vulnerabilities/cves requires view_vulnerability
258
+ test 'should deny POST request to vulnerabilities cves when user lacks view_vulnerability permission' do
259
+ post_data = '{"test": "data"}'
260
+ req = ActionDispatch::Request.new(
261
+ 'REQUEST_URI' => '/api/vulnerability/v1/vulnerabilities/cves',
262
+ 'REQUEST_METHOD' => 'POST',
263
+ 'rack.input' => ::Puma::NullIO.new,
264
+ 'RAW_POST_DATA' => post_data
265
+ )
266
+
267
+ @user.stubs(:can?).with(:view_vulnerability).returns(false)
268
+
269
+ assert_raises(::Foreman::PermissionMissingException) do
270
+ @forwarder.forward_request(req, 'api/vulnerability/v1/vulnerabilities/cves', 'test_controller', @user, @organization, @location)
271
+ end
272
+ end
273
+
274
+ # PATCH /api/vulnerability/v1/status requires edit_vulnerability
275
+ test 'should allow PATCH request to vulnerability status when user has edit_vulnerability permission' do
276
+ patch_data = '{"status": "resolved"}'
277
+ req = ActionDispatch::Request.new(
278
+ 'REQUEST_URI' => '/api/vulnerability/v1/status',
279
+ 'REQUEST_METHOD' => 'PATCH',
280
+ 'rack.input' => ::Puma::NullIO.new,
281
+ 'RAW_POST_DATA' => patch_data,
282
+ "action_dispatch.request.path_parameters" => { :format => "json" }
283
+ )
284
+
285
+ @user.stubs(:can?).with(:edit_vulnerability).returns(true)
286
+ @forwarder.expects(:execute_cloud_request).returns(true)
287
+
288
+ @forwarder.forward_request(req, 'api/vulnerability/v1/status', 'test_controller', @user, @organization, @location)
289
+ end
290
+
291
+ test 'should deny PATCH request to vulnerability status when user lacks edit_vulnerability permission' do
292
+ patch_data = '{"status": "resolved"}'
293
+ req = ActionDispatch::Request.new(
294
+ 'REQUEST_URI' => '/api/vulnerability/v1/status',
295
+ 'REQUEST_METHOD' => 'PATCH',
296
+ 'rack.input' => ::Puma::NullIO.new,
297
+ 'RAW_POST_DATA' => patch_data,
298
+ "action_dispatch.request.path_parameters" => { :format => "json" }
299
+ )
300
+
301
+ @user.stubs(:can?).with(:edit_vulnerability).returns(false)
302
+
303
+ assert_raises(::Foreman::PermissionMissingException) do
304
+ @forwarder.forward_request(req, 'api/vulnerability/v1/status', 'test_controller', @user, @organization, @location)
305
+ end
306
+ end
307
+
308
+ # Data-driven tests for required_permission_for
309
+ PERMISSION_MAPPINGS = [
310
+ # Vulnerability endpoints
311
+ { path: 'api/inventory/v1/hosts', method: 'GET', expected: :view_vulnerability },
312
+ { path: 'api/inventory/v1/hosts/abc-123', method: 'GET', expected: :view_vulnerability },
313
+ { path: 'api/vulnerability/v1/vulnerabilities/cves', method: 'POST', expected: :view_vulnerability },
314
+ { path: 'api/vulnerability/v1/status', method: 'PATCH', expected: :edit_vulnerability },
315
+ { path: 'api/vulnerability/v1/cves/status', method: 'PATCH', expected: :edit_vulnerability },
316
+ { path: 'api/vulnerability/v1/cves/business_risk', method: 'PATCH', expected: :edit_vulnerability },
317
+ { path: 'api/vulnerability/v1/systems/opt_out', method: 'PATCH', expected: :edit_vulnerability },
318
+ { path: 'api/vulnerability/v1/dashbar', method: 'GET', expected: :view_vulnerability },
319
+ { path: 'api/vulnerability/v1/cves/CVE-2024-1234/affected_systems', method: 'GET', expected: :view_vulnerability },
320
+ # Endpoints without tags support (still require view_vulnerability)
321
+ { path: 'api/vulnerability/v1/apistatus', method: 'GET', expected: :view_vulnerability },
322
+ { path: 'api/vulnerability/v1/version', method: 'GET', expected: :view_vulnerability },
323
+ { path: 'api/vulnerability/v1/business_risk', method: 'GET', expected: :view_vulnerability },
324
+ { path: 'api/vulnerability/v1/announcement', method: 'GET', expected: :view_vulnerability },
325
+ { path: 'api/vulnerability/v1/cves/CVE-2024-1234', method: 'GET', expected: :view_vulnerability },
326
+ { path: 'api/vulnerability/v1/playbooks/abc-123', method: 'GET', expected: :view_vulnerability },
327
+ { path: 'api/vulnerability/v1/report/abc-123', method: 'GET', expected: :view_vulnerability },
328
+ # Advisor endpoints
329
+ { path: 'api/insights/v1/stats/systems', method: 'GET', expected: :view_advisor },
330
+ { path: 'api/insights/v1/ack/', method: 'POST', expected: :edit_advisor },
331
+ { path: 'api/insights/v1/ack/rule_id', method: 'DELETE', expected: :edit_advisor },
332
+ { path: 'api/insights/v1/hostack/', method: 'POST', expected: :edit_advisor },
333
+ { path: 'api/insights/v1/hostack/123', method: 'DELETE', expected: :edit_advisor },
334
+ { path: 'api/insights/v1/rule/test_rule/unack_hosts', method: 'POST', expected: :edit_advisor },
335
+ # Unknown endpoints
336
+ { path: 'api/unknown/endpoint', method: 'GET', expected: nil },
337
+ ].freeze
338
+
339
+ PERMISSION_MAPPINGS.each do |mapping|
340
+ test "required_permission_for returns #{mapping[:expected].inspect} for #{mapping[:method]} #{mapping[:path]}" do
341
+ permission = @forwarder.send(:required_permission_for, mapping[:path], mapping[:method])
342
+ assert_equal mapping[:expected], permission
343
+ end
344
+ end
345
+
346
+ # Advisor permission integration tests
347
+ test 'should allow GET request to insights endpoint when user has view_advisor permission' do
348
+ req = build_request(method: 'GET', uri: '/api/insights/v1/stats/systems')
349
+
350
+ @user.stubs(:can?).with(:view_advisor).returns(true)
351
+ ::ForemanRhCloud::TagsAuth.any_instance.expects(:update_tag)
352
+ @forwarder.expects(:execute_cloud_request).returns(true)
353
+
354
+ @forwarder.forward_request(req, 'api/insights/v1/stats/systems', 'test_controller', @user, @organization, @location)
355
+ end
356
+
357
+ test 'should deny GET request to insights endpoint when user lacks view_advisor permission' do
358
+ req = build_request(method: 'GET', uri: '/api/insights/v1/stats/systems')
359
+
360
+ @user.stubs(:can?).with(:view_advisor).returns(false)
361
+
362
+ assert_raises(::Foreman::PermissionMissingException) do
363
+ @forwarder.forward_request(req, 'api/insights/v1/stats/systems', 'test_controller', @user, @organization, @location)
364
+ end
365
+ end
366
+
367
+ test 'should allow POST request to insights ack when user has edit_advisor permission' do
368
+ req = build_request(method: 'POST', uri: '/api/insights/v1/ack/', data: '{"rule_id": "test|RULE"}')
369
+
370
+ @user.stubs(:can?).with(:edit_advisor).returns(true)
371
+ @forwarder.expects(:execute_cloud_request).returns(true)
372
+
373
+ @forwarder.forward_request(req, 'api/insights/v1/ack/', 'test_controller', @user, @organization, @location)
374
+ end
375
+
376
+ test 'should deny POST request to insights ack when user lacks edit_advisor permission' do
377
+ req = build_request(method: 'POST', uri: '/api/insights/v1/ack/', data: '{"rule_id": "test|RULE"}')
378
+
379
+ @user.stubs(:can?).with(:edit_advisor).returns(false)
380
+
381
+ assert_raises(::Foreman::PermissionMissingException) do
382
+ @forwarder.forward_request(req, 'api/insights/v1/ack/', 'test_controller', @user, @organization, @location)
383
+ end
384
+ end
385
+
386
+ # Edge case: anonymous user
387
+ test 'should deny request when user is nil' do
388
+ req = build_request(method: 'GET', uri: '/api/insights/v1/stats/systems')
389
+
390
+ assert_raises(::Foreman::PermissionMissingException) do
391
+ @forwarder.forward_request(req, 'api/insights/v1/stats/systems', 'test_controller', nil, @organization, @location)
392
+ end
393
+ end
217
394
  end
@@ -40,4 +40,18 @@ class TagsAuthTest < ActiveSupport::TestCase
40
40
 
41
41
  @auth.update_tag
42
42
  end
43
+
44
+ test 'Generates tags with wildcard location when location is nil' do
45
+ auth_with_nil_loc = ::ForemanRhCloud::TagsAuth.new(@user, @org, nil, @logger)
46
+ uuid1 = 'test_uuid1'
47
+
48
+ auth_with_nil_loc.expects(:allowed_hosts).returns([uuid1])
49
+ auth_with_nil_loc.expects(:execute_cloud_request).with do |actual_params|
50
+ actual = JSON.parse(actual_params[:payload])
51
+ assert_includes actual['host_id_list'], uuid1
52
+ assert_equal "U:\"#{@user.login}\"O:\"#{@org.name}\"L:\"*\"", actual['tags'].first['value']
53
+ end
54
+
55
+ auth_with_nil_loc.update_tag
56
+ end
43
57
  end
@@ -1,7 +1,8 @@
1
1
  import React from 'react';
2
2
  import PropTypes from 'prop-types';
3
3
  import { ScalprumComponent, ScalprumProvider } from '@scalprum/react-core';
4
- import { providerOptions } from '../common/ScalprumModule/ScalprumContext';
4
+ import { createProviderOptions } from '../common/ScalprumModule/ScalprumContext';
5
+ import { useInsightsPermissions } from '../common/Hooks/PermissionsHooks';
5
6
  import './CVEsHostDetailsTab.scss';
6
7
 
7
8
  const CVEsHostDetailsTab = ({ systemId }) => {
@@ -18,14 +19,17 @@ CVEsHostDetailsTab.propTypes = {
18
19
  systemId: PropTypes.string.isRequired,
19
20
  };
20
21
 
21
- const CVEsHostDetailsTabWrapper = ({ response }) => (
22
- <ScalprumProvider {...providerOptions}>
23
- <CVEsHostDetailsTab
24
- // eslint-disable-next-line camelcase
25
- systemId={response?.subscription_facet_attributes?.uuid}
26
- />
27
- </ScalprumProvider>
28
- );
22
+ const CVEsHostDetailsTabWrapper = ({ response }) => {
23
+ const permissions = useInsightsPermissions();
24
+ return (
25
+ <ScalprumProvider {...createProviderOptions(permissions)}>
26
+ <CVEsHostDetailsTab
27
+ // eslint-disable-next-line camelcase
28
+ systemId={response?.subscription_facet_attributes?.uuid}
29
+ />
30
+ </ScalprumProvider>
31
+ );
32
+ };
29
33
 
30
34
  CVEsHostDetailsTabWrapper.propTypes = {
31
35
  response: PropTypes.shape({
@@ -2,6 +2,15 @@ import React from 'react';
2
2
  import { render } from '@testing-library/react';
3
3
  import CVEsHostDetailsTabWrapper from '../CVEsHostDetailsTab';
4
4
 
5
+ jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
6
+ useForemanContext: () => ({
7
+ metadata: {
8
+ permissions: new Set(['view_vulnerability']),
9
+ },
10
+ }),
11
+ useForemanPermissions: () => new Set(['view_vulnerability']),
12
+ }));
13
+
5
14
  jest.mock('@scalprum/react-core', () => ({
6
15
  ScalprumComponent: jest.fn(props => (
7
16
  <div data-testid="mock-scalprum-component">{JSON.stringify(props)}</div>
@@ -1,15 +1,17 @@
1
1
  import React from 'react';
2
2
  import { useParams } from 'react-router-dom';
3
3
  import { ScalprumComponent, ScalprumProvider } from '@scalprum/react-core';
4
- import { providerOptions } from '../common/ScalprumModule/ScalprumContext';
4
+ import { createProviderOptions } from '../common/ScalprumModule/ScalprumContext';
5
+ import { useInsightsPermissions } from '../common/Hooks/PermissionsHooks';
5
6
 
6
7
  const CveDetailsPage = () => {
7
8
  const { cveId } = useParams();
9
+ const permissions = useInsightsPermissions();
8
10
  const scope = 'vulnerability';
9
11
  const module = './CveDetailPage';
10
12
 
11
13
  return (
12
- <ScalprumProvider {...providerOptions}>
14
+ <ScalprumProvider {...createProviderOptions(permissions)}>
13
15
  <div className="rh-cloud-cve-details-page vulnerability">
14
16
  <ScalprumComponent scope={scope} module={module} cveId={cveId} />
15
17
  </div>
@@ -8,6 +8,15 @@ jest.mock('react-router-dom', () => ({
8
8
  useParams: jest.fn(() => ({ cveId: 'CVE-2021-1234' })),
9
9
  }));
10
10
 
11
+ jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
12
+ useForemanContext: () => ({
13
+ metadata: {
14
+ permissions: new Set(['view_vulnerability']),
15
+ },
16
+ }),
17
+ useForemanPermissions: () => new Set(['view_vulnerability']),
18
+ }));
19
+
11
20
  jest.mock('@scalprum/react-core', () => ({
12
21
  ScalprumComponent: jest.fn(props => (
13
22
  <div data-testid="mock-scalprum-component">{JSON.stringify(props)}</div>
@@ -14,7 +14,8 @@ import './InsightsCloudSync.scss';
14
14
  import Pagination from './Components/InsightsTable/Pagination';
15
15
  import ToolbarDropdown from './Components/ToolbarDropdown';
16
16
  import InsightsSettings from './Components/InsightsSettings';
17
- import { providerOptions } from '../common/ScalprumModule/ScalprumContext';
17
+ import { createProviderOptions } from '../common/ScalprumModule/ScalprumContext';
18
+ import { useInsightsPermissions } from '../common/Hooks/PermissionsHooks';
18
19
 
19
20
  // Hosted Insights advisor
20
21
  const InsightsCloudSync = ({ syncInsights, query, fetchInsights }) => {
@@ -78,11 +79,14 @@ const IopRecommendationsPage = props => (
78
79
  </div>
79
80
  );
80
81
 
81
- const IopRecommendationsPageWrapped = props => (
82
- <ScalprumProvider {...providerOptions}>
83
- <IopRecommendationsPage {...props} />
84
- </ScalprumProvider>
85
- );
82
+ const IopRecommendationsPageWrapped = props => {
83
+ const permissions = useInsightsPermissions();
84
+ return (
85
+ <ScalprumProvider {...createProviderOptions(permissions)}>
86
+ <IopRecommendationsPage {...props} />
87
+ </ScalprumProvider>
88
+ );
89
+ };
86
90
 
87
91
  const RecommendationsPage = props => {
88
92
  const isIop = useIopConfig();
@@ -23,7 +23,8 @@ import {
23
23
  import { redHatAdvisorSystems } from '../InsightsCloudSync/InsightsCloudSyncHelpers';
24
24
  import { useIopConfig } from '../common/Hooks/ConfigHooks';
25
25
  import { generateRuleUrl } from '../InsightsCloudSync/InsightsCloudSync';
26
- import { providerOptions } from '../common/ScalprumModule/ScalprumContext';
26
+ import { createProviderOptions } from '../common/ScalprumModule/ScalprumContext';
27
+ import { useInsightsPermissions } from '../common/Hooks/PermissionsHooks';
27
28
 
28
29
  // Hosted Insights advisor
29
30
  const NewHostDetailsTab = ({ hostName, router }) => {
@@ -124,11 +125,14 @@ const IopInsightsTab = props => (
124
125
  </div>
125
126
  );
126
127
 
127
- const IopInsightsTabWrapped = props => (
128
- <ScalprumProvider {...providerOptions}>
129
- <IopInsightsTab {...props} />
130
- </ScalprumProvider>
131
- );
128
+ const IopInsightsTabWrapped = props => {
129
+ const permissions = useInsightsPermissions();
130
+ return (
131
+ <ScalprumProvider {...createProviderOptions(permissions)}>
132
+ <IopInsightsTab {...props} />
133
+ </ScalprumProvider>
134
+ );
135
+ };
132
136
 
133
137
  const InsightsTab = props => {
134
138
  const isIop = useIopConfig();
@@ -1,6 +1,7 @@
1
1
  import React from 'react';
2
2
  import { ScalprumComponent, ScalprumProvider } from '@scalprum/react-core';
3
- import { providerOptions } from '../common/ScalprumModule/ScalprumContext';
3
+ import { createProviderOptions } from '../common/ScalprumModule/ScalprumContext';
4
+ import { useInsightsPermissions } from '../common/Hooks/PermissionsHooks';
4
5
 
5
6
  const InsightsVulnerabilityListPage = () => {
6
7
  const scope = 'vulnerability';
@@ -12,10 +13,13 @@ const InsightsVulnerabilityListPage = () => {
12
13
  );
13
14
  };
14
15
 
15
- const InsightsVulnerabilityListPageWrap = () => (
16
- <ScalprumProvider {...providerOptions}>
17
- <InsightsVulnerabilityListPage />
18
- </ScalprumProvider>
19
- );
16
+ const InsightsVulnerabilityListPageWrap = () => {
17
+ const permissions = useInsightsPermissions();
18
+ return (
19
+ <ScalprumProvider {...createProviderOptions(permissions)}>
20
+ <InsightsVulnerabilityListPage />
21
+ </ScalprumProvider>
22
+ );
23
+ };
20
24
 
21
25
  export default InsightsVulnerabilityListPageWrap;
@@ -3,6 +3,15 @@ import { render } from '@testing-library/react';
3
3
  import '@testing-library/jest-dom';
4
4
  import InsightsVulnerabilityListPage from './InsightsVulnerabilityListPage';
5
5
 
6
+ jest.mock('foremanReact/Root/Context/ForemanContext', () => ({
7
+ useForemanContext: () => ({
8
+ metadata: {
9
+ permissions: new Set(['view_vulnerability']),
10
+ },
11
+ }),
12
+ useForemanPermissions: () => new Set(['view_vulnerability']),
13
+ }));
14
+
6
15
  jest.mock('@scalprum/react-core', () => ({
7
16
  ScalprumComponent: jest.fn(props => (
8
17
  <div data-testid="mock-scalprum-component">{JSON.stringify(props)}</div>
@@ -3,7 +3,8 @@ import { useRouteMatch } from 'react-router-dom';
3
3
  import { ScalprumComponent, ScalprumProvider } from '@scalprum/react-core';
4
4
 
5
5
  import RemediationModal from '../InsightsCloudSync/Components/RemediationModal';
6
- import { providerOptions } from '../common/ScalprumModule/ScalprumContext';
6
+ import { createProviderOptions } from '../common/ScalprumModule/ScalprumContext';
7
+ import { useInsightsPermissions } from '../common/Hooks/PermissionsHooks';
7
8
 
8
9
  const scope = 'advisor';
9
10
  const module = './RecommendationDetailsWrapped';
@@ -25,10 +26,13 @@ const IopRecommendationDetails = props => {
25
26
  );
26
27
  };
27
28
 
28
- const IopRecommendationDetailsWrapped = props => (
29
- <ScalprumProvider {...providerOptions}>
30
- <IopRecommendationDetails {...props} />
31
- </ScalprumProvider>
32
- );
29
+ const IopRecommendationDetailsWrapped = props => {
30
+ const permissions = useInsightsPermissions();
31
+ return (
32
+ <ScalprumProvider {...createProviderOptions(permissions)}>
33
+ <IopRecommendationDetails {...props} />
34
+ </ScalprumProvider>
35
+ );
36
+ };
33
37
 
34
38
  export default IopRecommendationDetailsWrapped;
@@ -0,0 +1,57 @@
1
+ import { useMemo } from 'react';
2
+ import { useForemanPermissions } from 'foremanReact/Root/Context/ForemanContext';
3
+
4
+ /**
5
+ * Mapping from Foreman permissions to Insights Chrome API permissions.
6
+ * Used to convert Foreman's permission names to the format expected by
7
+ * Scalprum-loaded Insights apps (vulnerability-ui, advisor).
8
+ *
9
+ * When adding new permissions, update both:
10
+ * - This mapping (for front-end RBAC in Scalprum apps)
11
+ * - The SCOPED_REQUESTS in app/services/foreman_rh_cloud/insights_api_forwarder.rb (for backend API enforcement)
12
+ */
13
+ const PERMISSION_MAPPING = {
14
+ view_vulnerability: [
15
+ 'inventory:hosts:read',
16
+ 'vulnerability:vulnerability_results:read',
17
+ 'vulnerability:system.opt_out:read',
18
+ 'vulnerability:report_and_export:read',
19
+ 'vulnerability:advanced_report:read',
20
+ ],
21
+ edit_vulnerability: [
22
+ 'vulnerability:system.cve.status:write',
23
+ 'vulnerability:cve.business_risk_and_status:write',
24
+ 'vulnerability:system.opt_out:write',
25
+ ],
26
+ view_advisor: ['advisor:recommendation-results:read', 'advisor:exports:read'],
27
+ edit_advisor: ['advisor:disable-recommendations:write'],
28
+ };
29
+
30
+ /**
31
+ * Hook to access Insights permissions in Chrome API format.
32
+ * Reads Foreman permissions from context and converts them to the format
33
+ * expected by Scalprum-loaded Insights apps.
34
+ *
35
+ * Uses ForemanContext.metadata.permissions (added in Foreman PR #10338).
36
+ * Falls back to empty permissions if not available (older Foreman versions).
37
+ *
38
+ * @see https://github.com/theforeman/foreman/blob/develop/developer_docs/handling_user_permissions.asciidoc
39
+ * @returns {Array<{permission: string, resourceDefinitions: Array}>} User's Insights permissions
40
+ */
41
+ export const useInsightsPermissions = () => {
42
+ const userPermissions = useForemanPermissions() || new Set();
43
+
44
+ return useMemo(
45
+ () =>
46
+ Object.entries(PERMISSION_MAPPING).flatMap(
47
+ ([foremanPerm, insightsPerms]) =>
48
+ userPermissions.has(foremanPerm)
49
+ ? insightsPerms.map(perm => ({
50
+ permission: perm,
51
+ resourceDefinitions: [],
52
+ }))
53
+ : []
54
+ ),
55
+ [userPermissions]
56
+ );
57
+ };
@@ -39,7 +39,25 @@ export const mockUser = {
39
39
  },
40
40
  };
41
41
 
42
- export const providerOptions = {
42
+ /**
43
+ * Creates getUserPermissions function for Chrome API.
44
+ * @param {Array} permissions - Permissions array from ForemanContext
45
+ * @returns {Function} getUserPermissions function
46
+ */
47
+ const createGetUserPermissions = permissions => async (app, _bypassCache) =>
48
+ app
49
+ ? permissions.filter(p => p.permission?.startsWith(`${app}:`))
50
+ : permissions;
51
+
52
+ /**
53
+ * Creates provider options with the given permissions.
54
+ * Call this from wrapper components with permissions from useInsightsPermissions().
55
+ *
56
+ * @see https://github.com/theforeman/foreman/blob/develop/developer_docs/foreman-context.asciidoc
57
+ * @param {Array} permissions - Permissions from useInsightsPermissions()
58
+ * @returns {Object} Provider options for ScalprumProvider
59
+ */
60
+ export const createProviderOptions = (permissions = []) => ({
43
61
  pluginSDKOptions: {
44
62
  pluginLoaderOptions: {
45
63
  transformPluginManifest: manifest => {
@@ -66,8 +84,9 @@ export const providerOptions = {
66
84
  on: () => {},
67
85
  auth: {
68
86
  getUser: () => Promise.resolve(mockUser),
87
+ getUserPermissions: createGetUserPermissions(permissions),
69
88
  },
70
89
  },
71
90
  },
72
91
  config: modulesConfig,
73
- };
92
+ });
@@ -0,0 +1,120 @@
1
+ import {
2
+ modulesConfig,
3
+ mockUser,
4
+ createProviderOptions,
5
+ } from '../ScalprumContext';
6
+
7
+ describe('ScalprumContext', () => {
8
+ describe('modulesConfig', () => {
9
+ it('should have vulnerability module config', () => {
10
+ expect(modulesConfig.vulnerability).toBeDefined();
11
+ expect(modulesConfig.vulnerability.name).toBe('vulnerability');
12
+ });
13
+
14
+ it('should have advisor module config', () => {
15
+ expect(modulesConfig.advisor).toBeDefined();
16
+ expect(modulesConfig.advisor.name).toBe('advisor');
17
+ });
18
+
19
+ it('should have inventory module config', () => {
20
+ expect(modulesConfig.inventory).toBeDefined();
21
+ expect(modulesConfig.inventory.name).toBe('inventory');
22
+ });
23
+ });
24
+
25
+ describe('mockUser', () => {
26
+ it('should have identity with org_id FOREMAN', () => {
27
+ expect(mockUser.identity.org_id).toBe('FOREMAN');
28
+ });
29
+ });
30
+
31
+ describe('createProviderOptions', () => {
32
+ it('should have isBeta function', () => {
33
+ const options = createProviderOptions([]);
34
+ expect(options.api.chrome.isBeta).toBeInstanceOf(Function);
35
+ expect(options.api.chrome.isBeta()).toBe(false);
36
+ });
37
+
38
+ it('should have auth.getUser that resolves to mockUser', async () => {
39
+ const options = createProviderOptions([]);
40
+ expect(options.api.chrome.auth.getUser).toBeInstanceOf(Function);
41
+ const user = await options.api.chrome.auth.getUser();
42
+ expect(user).toEqual(mockUser);
43
+ });
44
+
45
+ it('should return empty array when no permissions provided', async () => {
46
+ const options = createProviderOptions([]);
47
+ const permissions = await options.api.chrome.auth.getUserPermissions();
48
+ expect(permissions).toEqual([]);
49
+ });
50
+
51
+ describe('getUserPermissions', () => {
52
+ const testPermissions = [
53
+ { permission: 'inventory:hosts:read', resourceDefinitions: [] },
54
+ {
55
+ permission: 'vulnerability:vulnerability_results:read',
56
+ resourceDefinitions: [],
57
+ },
58
+ {
59
+ permission: 'vulnerability:system.opt_out:read',
60
+ resourceDefinitions: [],
61
+ },
62
+ ];
63
+
64
+ it('should filter vulnerability permissions by app prefix', async () => {
65
+ const options = createProviderOptions(testPermissions);
66
+ const permissions = await options.api.chrome.auth.getUserPermissions(
67
+ 'vulnerability'
68
+ );
69
+
70
+ expect(permissions).toHaveLength(2);
71
+ expect(permissions[0].permission).toBe(
72
+ 'vulnerability:vulnerability_results:read'
73
+ );
74
+ });
75
+
76
+ it('should filter inventory permissions by app prefix', async () => {
77
+ const options = createProviderOptions(testPermissions);
78
+ const permissions = await options.api.chrome.auth.getUserPermissions(
79
+ 'inventory'
80
+ );
81
+
82
+ expect(permissions).toHaveLength(1);
83
+ expect(permissions[0].permission).toBe('inventory:hosts:read');
84
+ });
85
+
86
+ it('should return all permissions when no app filter is provided', async () => {
87
+ const options = createProviderOptions(testPermissions);
88
+ const permissions = await options.api.chrome.auth.getUserPermissions();
89
+
90
+ expect(permissions).toHaveLength(3);
91
+ });
92
+
93
+ it('should return permissions in Chrome API format', async () => {
94
+ const options = createProviderOptions(testPermissions);
95
+ const permissions = await options.api.chrome.auth.getUserPermissions(
96
+ 'vulnerability'
97
+ );
98
+
99
+ expect(permissions[0]).toHaveProperty('permission');
100
+ expect(permissions[0]).toHaveProperty('resourceDefinitions');
101
+ expect(Array.isArray(permissions[0].resourceDefinitions)).toBe(true);
102
+ });
103
+
104
+ it('should handle permissions with missing permission field', async () => {
105
+ const malformedPermissions = [
106
+ { permission: 'inventory:hosts:read', resourceDefinitions: [] },
107
+ { resourceDefinitions: [] }, // missing permission field
108
+ { permission: null, resourceDefinitions: [] },
109
+ ];
110
+ const options = createProviderOptions(malformedPermissions);
111
+
112
+ const permissions = await options.api.chrome.auth.getUserPermissions(
113
+ 'inventory'
114
+ );
115
+ expect(permissions).toHaveLength(1);
116
+ expect(permissions[0].permission).toBe('inventory:hosts:read');
117
+ });
118
+ });
119
+ });
120
+ });
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: 13.1.0
4
+ version: 13.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Foreman Red Hat Cloud team
@@ -631,7 +631,9 @@ files:
631
631
  - webpack/common/ForemanTasks/ForemanTasksHelpers.js
632
632
  - webpack/common/ForemanTasks/index.js
633
633
  - webpack/common/Hooks/ConfigHooks.js
634
+ - webpack/common/Hooks/PermissionsHooks.js
634
635
  - webpack/common/ScalprumModule/ScalprumContext.js
636
+ - webpack/common/ScalprumModule/__tests__/ScalprumContext.test.js
635
637
  - webpack/common/Switcher/HelpLabel.js
636
638
  - webpack/common/Switcher/SwitcherPF4.js
637
639
  - webpack/common/Switcher/SwitcherPF4.scss