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 +4 -4
- data/app/controllers/insights_cloud/ui_requests_controller.rb +6 -3
- data/app/services/foreman_rh_cloud/insights_api_forwarder.rb +163 -7
- data/app/services/foreman_rh_cloud/tags_auth.rb +13 -3
- data/lib/foreman_rh_cloud/plugin.rb +36 -2
- data/lib/foreman_rh_cloud/version.rb +1 -1
- data/package.json +1 -1
- data/test/controllers/insights_cloud/ui_requests_controller_test.rb +26 -0
- data/test/unit/services/foreman_rh_cloud/insights_api_forwarder_test.rb +194 -17
- data/test/unit/services/foreman_rh_cloud/tags_auth_test.rb +14 -0
- data/webpack/CVEsHostDetailsTab/CVEsHostDetailsTab.js +13 -9
- data/webpack/CVEsHostDetailsTab/__tests__/CVEsHostDetailsTab.test.js +9 -0
- data/webpack/CveDetailsPage/CveDetailsPage.js +4 -2
- data/webpack/CveDetailsPage/CveDetailsPage.test.js +9 -0
- data/webpack/InsightsCloudSync/InsightsCloudSync.js +10 -6
- data/webpack/InsightsHostDetailsTab/NewHostDetailsTab.js +10 -6
- data/webpack/InsightsVulnerability/InsightsVulnerabilityListPage.js +10 -6
- data/webpack/InsightsVulnerability/InsightsVulnerabilityListPage.test.js +9 -0
- data/webpack/IopRecommendationDetails/IopRecommendationDetails.js +10 -6
- data/webpack/common/Hooks/PermissionsHooks.js +57 -0
- data/webpack/common/ScalprumModule/ScalprumContext.js +21 -2
- data/webpack/common/ScalprumModule/__tests__/ScalprumContext.test.js +120 -0
- metadata +3 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 5707d35255f855b6e11f49080a65c0ca44c2a75d09183edbefb78ca7ceb06023
|
|
4
|
+
data.tar.gz: f52cff55dc3dfa16e9dc5a151a0866227f37ad99c55016cc42c29eeb1411e151
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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, :
|
|
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
|
|
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
|
-
|
|
9
|
-
{
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
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
|
-
|
|
101
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
"
|
|
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
|
-
|
|
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
|
|
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,
|
data/package.json
CHANGED
|
@@ -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 {
|
|
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
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
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 {
|
|
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 {...
|
|
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 {
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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 {
|
|
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
|
-
|
|
129
|
-
|
|
130
|
-
|
|
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 {
|
|
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
|
-
|
|
17
|
-
|
|
18
|
-
|
|
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 {
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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.
|
|
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
|