foreman_openscap 13.1.3 → 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: 55a7bad4db31425c475beef24f3e4b59bc75012eac8aa688c44fa6160cea8830
4
- data.tar.gz: d3eec8f2d7b76787f7ca02ede989f9a9774e09600fa62afe5e9f3d2249d4d605
3
+ metadata.gz: 2c87d1f5b2553990c35d7307969efd773d1a29a29f7871f91aafca638789c746
4
+ data.tar.gz: c9bebdf6c2008237cad80975e5c675e708b031f4167c1b75abcf28a1d77d7e21
5
5
  SHA512:
6
- metadata.gz: 4bca688480801ce9ba18c1d6cb9ddab0c5e50a89e8a34689bdcf3a21d40fd1a6cc0bb1380c0cdedf5f5adf05476affcffd75179bb7fedcb0ddb89e4109dae5e6
7
- data.tar.gz: 3b3caeaa4a54b5b004814228e2212505ee7121a3fce4e1d7e34a6da645b8e4c23f353a44d4a57b0e5b6e4b02cd74b71b778e77955429abe6e5eb509c12c14d13
6
+ metadata.gz: bb4f58ffa8e0d4b7ee9142be81f4193fce8f72f130ebef4ff0d1fc2f54b980983b27835cbe020b1bc2ba11cf5e89d3e56bb2dcdfce50b567d46f5162c275733b
7
+ data.tar.gz: 4773b95b988c447e2db42f6974b0dc893ce8fcb4350615f124698db83a203f26cdc428fcebd25591635b0841ad5d7441130548fd6f3b85457979e9f5d624dc27
@@ -7,9 +7,10 @@ module Api::V2
7
7
  render_error(:custom_error, :status => :unprocessable_entity, :locals => { :message => exception.message })
8
8
  end
9
9
 
10
- before_action :find_editable_hosts, only: [:change_openscap_proxy]
10
+ before_action :find_editable_hosts, only: [:change_openscap_proxy, :assign_compliance_policy, :unassign_compliance_policy]
11
11
  before_action :find_openscap_proxy, only: [:change_openscap_proxy]
12
12
  before_action :validate_openscap_proxy_feature, only: [:change_openscap_proxy]
13
+ before_action :find_compliance_policy, only: [:assign_compliance_policy, :unassign_compliance_policy]
13
14
 
14
15
  def_param_group :bulk_host_ids do
15
16
  param :included, Hash, :desc => N_("Hosts to include in the action"), :required => true, :action_aware => true do
@@ -23,6 +24,33 @@ module Api::V2
23
24
  end
24
25
  end
25
26
 
27
+ api :PUT, "/compliance/hosts/bulk/assign_compliance_policy", N_("Assign compliance policy to multiple hosts")
28
+ param_group :bulk_host_ids
29
+ param :policy_id, :number, :required => true, :desc => N_("ID of the compliance policy to assign to the hosts")
30
+ def assign_compliance_policy
31
+ @policy.host_ids = @policy.host_ids + @hosts.pluck(:id)
32
+ if @policy.save
33
+ message = _("Assigned with compliance policy: %s") % @policy.name
34
+ process_response(true, {
35
+ :message => n_("Updated host: #{message}", "Updated hosts: #{message}", @hosts.count),
36
+ })
37
+ else
38
+ render_error(:custom_error, :status => :unprocessable_entity,
39
+ :locals => { :message => @policy.errors.full_messages.to_sentence })
40
+ end
41
+ end
42
+
43
+ api :PUT, "/compliance/hosts/bulk/unassign_compliance_policy", N_("Unassign compliance policy from multiple hosts")
44
+ param_group :bulk_host_ids
45
+ param :policy_id, :number, :required => true, :desc => N_("ID of the compliance policy to unassign from the hosts")
46
+ def unassign_compliance_policy
47
+ @policy.unassign_hosts(@hosts)
48
+ message = _("Unassigned from compliance policy '%s'") % @policy.name
49
+ process_response(true, {
50
+ :message => n_("Updated host: #{message}", "Updated hosts: #{message}", @hosts.count),
51
+ })
52
+ end
53
+
26
54
  api :PUT, "/hosts/bulk/change_openscap_proxy", N_("Assign OpenSCAP Proxy to multiple hosts")
27
55
  param_group :bulk_host_ids
28
56
  param :openscap_proxy_id, :number, :required => true, :desc => N_("ID of the OpenSCAP Proxy to assign to the hosts")
@@ -84,6 +112,15 @@ module Api::V2
84
112
  render_error(:custom_error, :status => :unprocessable_entity,
85
113
  :locals => { :message => _("The selected OpenSCAP Proxy does not have the OpenSCAP feature enabled.") })
86
114
  end
115
+
116
+ def find_compliance_policy
117
+ @policy = ::ForemanOpenscap::Policy.authorized(:assign_policies)
118
+ .find_by(:id => params.require(:policy_id))
119
+ return if @policy
120
+
121
+ render_error(:custom_error, :status => :unprocessable_entity,
122
+ :locals => { :message => _("Compliance policy with id %s not found") % params[:policy_id] })
123
+ end
87
124
  end
88
125
  end
89
126
  end
@@ -83,7 +83,7 @@ class PoliciesController < ApplicationController
83
83
  if policy.save
84
84
  success _("Updated hosts: Assigned with compliance policy: %s") % policy.name
85
85
  # We prefer to go back as this does not lose the current search
86
- return redirect_to hosts_path
86
+ redirect_to hosts_path
87
87
  else
88
88
  return process_error :object => policy, :redirect => hosts_path
89
89
  end
data/config/routes.rb CHANGED
@@ -105,6 +105,8 @@ Rails.application.routes.draw do
105
105
  end
106
106
 
107
107
  match 'hosts/bulk/change_openscap_proxy', :to => 'compliance/hosts_bulk_actions#change_openscap_proxy', :via => [:put]
108
+ match 'compliance/hosts/bulk/assign_compliance_policy', :to => 'compliance/hosts_bulk_actions#assign_compliance_policy', :via => [:put]
109
+ match 'compliance/hosts/bulk/unassign_compliance_policy', :to => 'compliance/hosts_bulk_actions#unassign_compliance_policy', :via => [:put]
108
110
  end
109
111
  end
110
112
  end
@@ -76,7 +76,9 @@ module ForemanOpenscap
76
76
  :resource_type => 'ForemanOpenscap::Policy'
77
77
  permission :assign_policies, { :policies => %i[select_multiple_hosts update_multiple_hosts
78
78
  disassociate_multiple_hosts
79
- remove_policy_from_multiple_hosts] },
79
+ remove_policy_from_multiple_hosts],
80
+ 'api/v2/compliance/hosts_bulk_actions' => %i[assign_compliance_policy
81
+ unassign_compliance_policy] },
80
82
  :resource_type => 'ForemanOpenscap::Policy'
81
83
  permission :view_scap_contents, { :scap_contents => %i[index show auto_complete_search],
82
84
  'api/v2/compliance/scap_contents' => %i[index show xml],
@@ -1,3 +1,3 @@
1
1
  module ForemanOpenscap
2
- VERSION = '13.1.3'.freeze
2
+ VERSION = '13.2.0'.freeze
3
3
  end
@@ -3,7 +3,7 @@ require 'test_plugin_helper'
3
3
  class Api::V2::Compliance::HostsBulkActionsControllerTest < ActionController::TestCase
4
4
  tests Api::V2::Compliance::HostsBulkActionsController
5
5
 
6
- def setup
6
+ setup do
7
7
  as_admin do
8
8
  @organization = FactoryBot.create(:organization)
9
9
  @location = FactoryBot.create(:location)
@@ -17,6 +17,10 @@ class Api::V2::Compliance::HostsBulkActionsControllerTest < ActionController::Te
17
17
  :organization => @organization,
18
18
  :location => @location)
19
19
  @host_ids = [@host1.id, @host2.id]
20
+ @policy = FactoryBot.create(:policy,
21
+ :organizations => [@organization],
22
+ :locations => [@location])
23
+ @policy.scap_content.update(:organization_ids => [@organization.id], :location_ids => [@location.id])
20
24
  end
21
25
  end
22
26
 
@@ -129,4 +133,174 @@ class Api::V2::Compliance::HostsBulkActionsControllerTest < ActionController::Te
129
133
  refute_match(/Successfully updated/, response['error']['message'])
130
134
  assert_equal @host_ids.sort, response['error']['failed_host_ids'].sort
131
135
  end
136
+
137
+ test "should assign compliance policy to selected hosts" do
138
+ put :assign_compliance_policy,
139
+ params: valid_bulk_params.merge(:policy_id => @policy.id),
140
+ session: set_session_user
141
+
142
+ assert_response :success
143
+ response = ActiveSupport::JSON.decode(@response.body)
144
+ assert_match(/Updated hosts: Assigned with compliance policy/, response['message'])
145
+ assert_includes response['message'], @policy.name
146
+
147
+ @policy.reload
148
+ assert_includes @policy.hosts.map(&:id), @host1.id
149
+ assert_includes @policy.hosts.map(&:id), @host2.id
150
+ end
151
+
152
+ test "should assign compliance policy to a single host" do
153
+ put :assign_compliance_policy,
154
+ params: valid_bulk_params([@host1.id]).merge(:policy_id => @policy.id),
155
+ session: set_session_user
156
+
157
+ assert_response :success
158
+ response = ActiveSupport::JSON.decode(@response.body)
159
+ assert_match(/Updated host: Assigned with compliance policy/, response['message'])
160
+
161
+ @policy.reload
162
+ assert_includes @policy.hosts.map(&:id), @host1.id
163
+ refute_includes @policy.hosts.map(&:id), @host2.id
164
+ end
165
+
166
+ test "should require policy_id for assign" do
167
+ put :assign_compliance_policy,
168
+ params: valid_bulk_params,
169
+ session: set_session_user
170
+
171
+ assert_response :unprocessable_entity
172
+ response = ActiveSupport::JSON.decode(@response.body)
173
+ assert_match(/policy_id/, response['error']['message'])
174
+ end
175
+
176
+ test "should return error when policy is not found for assign" do
177
+ put :assign_compliance_policy,
178
+ params: valid_bulk_params.merge(:policy_id => 0),
179
+ session: set_session_user
180
+
181
+ assert_response :unprocessable_entity
182
+ response = ActiveSupport::JSON.decode(@response.body)
183
+ assert_match(/Compliance policy with id .* not found/, response['error']['message'])
184
+ end
185
+
186
+ test "should unassign compliance policy from selected hosts" do
187
+ as_admin do
188
+ @host1.policies = [@policy]
189
+ @host1.save!
190
+ @host2.policies = [@policy]
191
+ @host2.save!
192
+ end
193
+
194
+ put :unassign_compliance_policy,
195
+ params: valid_bulk_params.merge(:policy_id => @policy.id),
196
+ session: set_session_user
197
+
198
+ assert_response :success
199
+ response = ActiveSupport::JSON.decode(@response.body)
200
+ assert_match(/Updated hosts: Unassigned from compliance policy/, response['message'])
201
+ assert_includes response['message'], @policy.name
202
+
203
+ @policy.reload
204
+ refute_includes @policy.hosts.map(&:id), @host1.id
205
+ refute_includes @policy.hosts.map(&:id), @host2.id
206
+ end
207
+
208
+ test "should unassign compliance policy from a single host" do
209
+ as_admin do
210
+ @host1.policies = [@policy]
211
+ @host1.save!
212
+ @host2.policies = [@policy]
213
+ @host2.save!
214
+ end
215
+
216
+ put :unassign_compliance_policy,
217
+ params: valid_bulk_params([@host1.id]).merge(:policy_id => @policy.id),
218
+ session: set_session_user
219
+
220
+ assert_response :success
221
+ response = ActiveSupport::JSON.decode(@response.body)
222
+ assert_match(/Updated host: Unassigned from compliance policy/, response['message'])
223
+
224
+ @policy.reload
225
+ refute_includes @policy.hosts.map(&:id), @host1.id
226
+ assert_includes @policy.hosts.map(&:id), @host2.id
227
+ end
228
+
229
+ test "should require policy_id for unassign" do
230
+ put :unassign_compliance_policy,
231
+ params: valid_bulk_params,
232
+ session: set_session_user
233
+
234
+ assert_response :unprocessable_entity
235
+ response = ActiveSupport::JSON.decode(@response.body)
236
+ assert_match(/policy_id/, response['error']['message'])
237
+ end
238
+
239
+ test "should return error when policy is not found for unassign" do
240
+ put :unassign_compliance_policy,
241
+ params: valid_bulk_params.merge(:policy_id => 0),
242
+ session: set_session_user
243
+
244
+ assert_response :unprocessable_entity
245
+ response = ActiveSupport::JSON.decode(@response.body)
246
+ assert_match(/Compliance policy with id .* not found/, response['error']['message'])
247
+ end
248
+
249
+ context "forbidden user" do
250
+ setup do
251
+ @user = FactoryBot.create(:user, :admin => false,
252
+ :organizations => [@organization],
253
+ :locations => [@location])
254
+ end
255
+
256
+ test "should forbid assign compliance policy without assign_policies permission" do
257
+ setup_user('edit', 'hosts', nil, @user)
258
+
259
+ put :assign_compliance_policy,
260
+ params: valid_bulk_params.merge(:policy_id => @policy.id),
261
+ session: set_session_user(@user)
262
+
263
+ assert_forbidden_missing_permission('assign_policies')
264
+ end
265
+
266
+ test "should forbid unassign compliance policy without assign_policies permission" do
267
+ setup_user('edit', 'hosts', nil, @user)
268
+
269
+ put :unassign_compliance_policy,
270
+ params: valid_bulk_params.merge(:policy_id => @policy.id),
271
+ session: set_session_user(@user)
272
+
273
+ assert_forbidden_missing_permission('assign_policies')
274
+ end
275
+
276
+ test "should forbid change openscap proxy without edit_hosts permission" do
277
+ setup_user('assign', 'policies', nil, @user)
278
+
279
+ put :change_openscap_proxy,
280
+ params: valid_bulk_params.merge(:openscap_proxy_id => @proxy.id),
281
+ session: set_session_user(@user)
282
+
283
+ assert_forbidden_missing_permission('edit_hosts')
284
+ end
285
+
286
+ test "should forbid assign compliance policy when user cannot edit hosts" do
287
+ setup_user('assign', 'policies', nil, @user)
288
+
289
+ put :assign_compliance_policy,
290
+ params: valid_bulk_params.merge(:policy_id => @policy.id),
291
+ session: set_session_user(@user)
292
+
293
+ assert_response :forbidden
294
+ response = ActiveSupport::JSON.decode(@response.body)
295
+ assert_match(/No hosts matched search, or action unauthorized for selected hosts/, response['error']['message'])
296
+ end
297
+ end
298
+
299
+ private
300
+
301
+ def assert_forbidden_missing_permission(permission)
302
+ assert_response :forbidden
303
+ response = ActiveSupport::JSON.decode(@response.body)
304
+ assert_includes response['error']['missing_permissions'], permission
305
+ end
132
306
  end
@@ -0,0 +1,49 @@
1
+ import React, { useContext } from 'react';
2
+ import { MenuItem } from '@patternfly/react-core';
3
+ import { translate as __ } from 'foremanReact/common/I18n';
4
+ import { openBulkModal } from 'foremanReact/common/BulkModalStateHelper';
5
+ import { usePermissions } from 'foremanReact/common/hooks/Permissions/permissionHooks';
6
+ import { EDIT_HOSTS } from 'foremanReact/permissions';
7
+ import { ForemanHostsIndexActionsBarContext } from 'foremanReact/components/HostsIndex';
8
+ import {
9
+ ASSIGN_POLICY_MODAL_ID,
10
+ UNASSIGN_POLICY_MODAL_ID,
11
+ ASSIGN_POLICIES,
12
+ } from './constants';
13
+
14
+ const BulkCompliancePolicyMenuItems = () => {
15
+ const { selectedCount } = useContext(ForemanHostsIndexActionsBarContext);
16
+
17
+ const hasPermission = usePermissions([ASSIGN_POLICIES, EDIT_HOSTS]);
18
+
19
+ const handleAssignClick = () => openBulkModal(ASSIGN_POLICY_MODAL_ID, true);
20
+ const handleUnassignClick = () =>
21
+ openBulkModal(UNASSIGN_POLICY_MODAL_ID, true);
22
+
23
+ if (!hasPermission) {
24
+ return null;
25
+ }
26
+
27
+ return (
28
+ <>
29
+ <MenuItem
30
+ itemId="assign-compliance-policy-dropdown-item"
31
+ key="assign-compliance-policy-dropdown-item"
32
+ onClick={handleAssignClick}
33
+ isDisabled={selectedCount === 0}
34
+ >
35
+ {__('Assign compliance policy')}
36
+ </MenuItem>
37
+ <MenuItem
38
+ itemId="unassign-compliance-policy-dropdown-item"
39
+ key="unassign-compliance-policy-dropdown-item"
40
+ onClick={handleUnassignClick}
41
+ isDisabled={selectedCount === 0}
42
+ >
43
+ {__('Unassign compliance policy')}
44
+ </MenuItem>
45
+ </>
46
+ );
47
+ };
48
+
49
+ export default BulkCompliancePolicyMenuItems;
@@ -0,0 +1,304 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import PropTypes from 'prop-types';
3
+ import { FormattedMessage } from 'react-intl';
4
+ import { useDispatch, useSelector } from 'react-redux';
5
+ import {
6
+ Modal,
7
+ Button,
8
+ TextContent,
9
+ Text,
10
+ Select,
11
+ SelectOption,
12
+ SelectList,
13
+ MenuToggle,
14
+ Spinner,
15
+ EmptyState,
16
+ EmptyStateHeader,
17
+ EmptyStateIcon,
18
+ EmptyStateBody,
19
+ Flex,
20
+ FlexItem,
21
+ } from '@patternfly/react-core';
22
+ import { ExclamationCircleIcon, SearchIcon } from '@patternfly/react-icons';
23
+ import { STATUS } from 'foremanReact/constants';
24
+ import { addToast } from 'foremanReact/components/ToastsList/slice';
25
+ import { translate as __ } from 'foremanReact/common/I18n';
26
+ import {
27
+ selectAPIStatus,
28
+ selectAPIResponse,
29
+ } from 'foremanReact/redux/API/APISelectors';
30
+ import {
31
+ buildBulkRequestBody,
32
+ bulkErrorToastParams,
33
+ } from 'foremanReact/components/HostsIndex/BulkActions/helpers';
34
+ import { fetchPolicies, bulkAssignPolicy, bulkUnassignPolicy } from './actions';
35
+ import {
36
+ POLICIES_KEY,
37
+ BULK_ASSIGN_POLICY_KEY,
38
+ BULK_UNASSIGN_POLICY_KEY,
39
+ } from './constants';
40
+
41
+ const BulkCompliancePolicyModal = ({
42
+ isOpen,
43
+ closeModal,
44
+ mode,
45
+ selectAllHostsMode,
46
+ selectedCount,
47
+ fetchBulkParams,
48
+ organizationId,
49
+ locationId,
50
+ onSuccess: onSuccessCallback,
51
+ }) => {
52
+ const dispatch = useDispatch();
53
+ const [policyId, setPolicyId] = useState('');
54
+ const [policySelectOpen, setPolicySelectOpen] = useState(false);
55
+
56
+ useEffect(() => {
57
+ if (isOpen) {
58
+ dispatch(fetchPolicies());
59
+ }
60
+ }, [dispatch, isOpen]);
61
+
62
+ const isAssignMode = mode === 'assign';
63
+ const actionKey = isAssignMode
64
+ ? BULK_ASSIGN_POLICY_KEY
65
+ : BULK_UNASSIGN_POLICY_KEY;
66
+
67
+ const policies = useSelector(state => selectAPIResponse(state, POLICIES_KEY));
68
+ const policiesStatus = useSelector(state =>
69
+ selectAPIStatus(state, POLICIES_KEY)
70
+ );
71
+ const policiesError = useSelector(
72
+ state => state.API?.[POLICIES_KEY]?.error?.message
73
+ );
74
+ const hostUpdateStatus = useSelector(state =>
75
+ selectAPIStatus(state, actionKey)
76
+ );
77
+
78
+ const onToggleClick = () => {
79
+ setPolicySelectOpen(!policySelectOpen);
80
+ };
81
+
82
+ const handlePolicySelect = (event, selection) => {
83
+ setPolicyId(selection);
84
+ setPolicySelectOpen(false);
85
+ };
86
+
87
+ const getPolicyLabel = id =>
88
+ policies?.results?.find(p => p.id.toString() === id)?.name ||
89
+ __('Select a policy');
90
+
91
+ const toggle = toggleRef => (
92
+ <MenuToggle
93
+ ref={toggleRef}
94
+ ouiaId="bulk-compliance-policy-toggle"
95
+ onClick={onToggleClick}
96
+ isExpanded={policySelectOpen}
97
+ style={{ width: '100%' }}
98
+ >
99
+ {policyId ? getPolicyLabel(policyId) : __('Select a policy')}
100
+ </MenuToggle>
101
+ );
102
+
103
+ const handleModalClose = () => {
104
+ setPolicyId('');
105
+ setPolicySelectOpen(false);
106
+ closeModal();
107
+ };
108
+
109
+ const handleError = error => {
110
+ handleModalClose();
111
+ dispatch(addToast(bulkErrorToastParams(error, actionKey)));
112
+ };
113
+
114
+ const handleSuccess = response => {
115
+ dispatch(
116
+ addToast({
117
+ type: 'success',
118
+ message: response.data.message,
119
+ })
120
+ );
121
+ if (onSuccessCallback) onSuccessCallback();
122
+ handleModalClose();
123
+ };
124
+
125
+ const handleConfirm = () => {
126
+ const requestBody = buildBulkRequestBody({
127
+ fetchBulkParams,
128
+ organizationId,
129
+ locationId,
130
+ policy_id: policyId,
131
+ });
132
+
133
+ if (mode === 'assign') {
134
+ dispatch(bulkAssignPolicy(requestBody, handleSuccess, handleError));
135
+ } else {
136
+ dispatch(bulkUnassignPolicy(requestBody, handleSuccess, handleError));
137
+ }
138
+ };
139
+
140
+ const title = isAssignMode
141
+ ? __('Assign Compliance Policy')
142
+ : __('Unassign Compliance Policy');
143
+ const confirmButtonText = isAssignMode
144
+ ? __('Assign policy')
145
+ : __('Unassign policy');
146
+
147
+ const modalActions = [
148
+ <Button
149
+ key="confirm"
150
+ ouiaId="bulk-compliance-policy-modal-confirm-button"
151
+ variant="primary"
152
+ onClick={handleConfirm}
153
+ isDisabled={hostUpdateStatus === STATUS.PENDING || policyId === ''}
154
+ isLoading={hostUpdateStatus === STATUS.PENDING}
155
+ >
156
+ {confirmButtonText}
157
+ </Button>,
158
+ <Button
159
+ key="cancel"
160
+ ouiaId="bulk-compliance-policy-modal-cancel-button"
161
+ variant="link"
162
+ onClick={handleModalClose}
163
+ >
164
+ {__('Cancel')}
165
+ </Button>,
166
+ ];
167
+
168
+ const warningMessage = isAssignMode
169
+ ? __(
170
+ 'The selected policy will be assigned to the selected hosts. Some hosts may already be assigned to this policy.'
171
+ )
172
+ : __(
173
+ 'The selected policy will be unassigned from the selected hosts. Some hosts may not be assigned to this policy.'
174
+ );
175
+
176
+ return (
177
+ <Modal
178
+ isOpen={isOpen}
179
+ onClose={handleModalClose}
180
+ onEscapePress={handleModalClose}
181
+ title={title}
182
+ variant="medium"
183
+ position="top"
184
+ actions={modalActions}
185
+ id="bulk-compliance-policy-modal"
186
+ key="bulk-compliance-policy-modal"
187
+ ouiaId="bulk-compliance-policy-modal"
188
+ >
189
+ <TextContent>
190
+ <Text ouiaId="bulk-compliance-policy-count">
191
+ {selectAllHostsMode ? (
192
+ <FormattedMessage
193
+ id="bulk-compliance-policy-all-hosts"
194
+ defaultMessage="{action} will affect {boldCount} selected hosts."
195
+ values={{
196
+ action: isAssignMode ? __('Assigning') : __('Unassigning'),
197
+ boldCount: <strong>{__('All')}</strong>,
198
+ }}
199
+ />
200
+ ) : (
201
+ <FormattedMessage
202
+ id="bulk-compliance-policy-selected-hosts"
203
+ defaultMessage="{action} will affect {boldCount} selected {count, plural, one {host} other {hosts}}."
204
+ values={{
205
+ action: isAssignMode ? __('Assigning') : __('Unassigning'),
206
+ count: selectedCount,
207
+ boldCount: <strong>{selectedCount}</strong>,
208
+ }}
209
+ />
210
+ )}
211
+ </Text>
212
+ <Text ouiaId="bulk-compliance-policy-warning">{warningMessage}</Text>
213
+ </TextContent>
214
+
215
+ {policiesStatus === STATUS.PENDING && (
216
+ <Flex
217
+ direction={{ default: 'column' }}
218
+ alignItems={{ default: 'alignItemsCenter' }}
219
+ spaceItems={{ default: 'spaceItemsSm' }}
220
+ >
221
+ <FlexItem>
222
+ <Spinner size="lg" aria-label={__('Loading policies')} />
223
+ </FlexItem>
224
+ <FlexItem>
225
+ <Text ouiaId="loading-policies-text">
226
+ {__('Loading policies...')}
227
+ </Text>
228
+ </FlexItem>
229
+ </Flex>
230
+ )}
231
+
232
+ {policiesStatus === STATUS.ERROR && (
233
+ <EmptyState>
234
+ <EmptyStateHeader
235
+ titleText={__('Failed to load policies')}
236
+ icon={<EmptyStateIcon icon={ExclamationCircleIcon} />}
237
+ headingLevel="h4"
238
+ />
239
+ <EmptyStateBody>
240
+ {policiesError || __('An error occurred while fetching policies.')}
241
+ </EmptyStateBody>
242
+ </EmptyState>
243
+ )}
244
+
245
+ {policiesStatus === STATUS.RESOLVED && policies?.results?.length === 0 && (
246
+ <EmptyState>
247
+ <EmptyStateHeader
248
+ titleText={__('No policies available')}
249
+ icon={<EmptyStateIcon icon={SearchIcon} />}
250
+ headingLevel="h4"
251
+ />
252
+ <EmptyStateBody>
253
+ {__(
254
+ 'There are no compliance policies available in your current organization and location.'
255
+ )}
256
+ </EmptyStateBody>
257
+ </EmptyState>
258
+ )}
259
+
260
+ {policiesStatus === STATUS.RESOLVED && policies?.results?.length > 0 && (
261
+ <Select
262
+ id="compliance-policy-select"
263
+ isOpen={policySelectOpen}
264
+ selected={policyId}
265
+ onSelect={handlePolicySelect}
266
+ onOpenChange={isSelectOpen => setPolicySelectOpen(isSelectOpen)}
267
+ toggle={toggle}
268
+ shouldFocusToggleOnSelect
269
+ ouiaId="bulk-compliance-policy-select"
270
+ >
271
+ <SelectList>
272
+ {policies.results.map(policy => (
273
+ <SelectOption key={policy.id} value={policy.id.toString()}>
274
+ {policy.name}
275
+ </SelectOption>
276
+ ))}
277
+ </SelectList>
278
+ </Select>
279
+ )}
280
+ </Modal>
281
+ );
282
+ };
283
+
284
+ BulkCompliancePolicyModal.propTypes = {
285
+ isOpen: PropTypes.bool,
286
+ closeModal: PropTypes.func,
287
+ mode: PropTypes.oneOf(['assign', 'unassign']).isRequired,
288
+ fetchBulkParams: PropTypes.func.isRequired,
289
+ selectedCount: PropTypes.number.isRequired,
290
+ selectAllHostsMode: PropTypes.bool.isRequired,
291
+ organizationId: PropTypes.number,
292
+ locationId: PropTypes.number,
293
+ onSuccess: PropTypes.func,
294
+ };
295
+
296
+ BulkCompliancePolicyModal.defaultProps = {
297
+ isOpen: false,
298
+ closeModal: () => {},
299
+ organizationId: undefined,
300
+ locationId: undefined,
301
+ onSuccess: undefined,
302
+ };
303
+
304
+ export default BulkCompliancePolicyModal;
@@ -0,0 +1,65 @@
1
+ import React, { useContext } from 'react';
2
+ import { ForemanActionsBarContext } from 'foremanReact/components/HostDetails/ActionsBar';
3
+ import { useBulkModalOpen } from 'foremanReact/common/BulkModalStateHelper';
4
+ import BulkCompliancePolicyModal from './BulkCompliancePolicyModal';
5
+ import { ASSIGN_POLICY_MODAL_ID, UNASSIGN_POLICY_MODAL_ID } from './constants';
6
+
7
+ export const BulkAssignPolicyModalScene = () => {
8
+ const {
9
+ selectAllHostsMode,
10
+ selectedCount,
11
+ fetchBulkParams,
12
+ organizationId,
13
+ locationId,
14
+ refreshTableData,
15
+ } = useContext(ForemanActionsBarContext) || {};
16
+
17
+ const { isOpen, close: closeModal } = useBulkModalOpen(
18
+ ASSIGN_POLICY_MODAL_ID
19
+ );
20
+
21
+ return (
22
+ <BulkCompliancePolicyModal
23
+ key="bulk-assign-compliance-policy-modal"
24
+ selectAllHostsMode={selectAllHostsMode}
25
+ selectedCount={selectedCount}
26
+ fetchBulkParams={fetchBulkParams}
27
+ organizationId={organizationId}
28
+ locationId={locationId}
29
+ isOpen={isOpen}
30
+ closeModal={closeModal}
31
+ mode="assign"
32
+ onSuccess={refreshTableData}
33
+ />
34
+ );
35
+ };
36
+
37
+ export const BulkUnassignPolicyModalScene = () => {
38
+ const {
39
+ selectAllHostsMode,
40
+ selectedCount,
41
+ fetchBulkParams,
42
+ organizationId,
43
+ locationId,
44
+ refreshTableData,
45
+ } = useContext(ForemanActionsBarContext) || {};
46
+
47
+ const { isOpen, close: closeModal } = useBulkModalOpen(
48
+ UNASSIGN_POLICY_MODAL_ID
49
+ );
50
+
51
+ return (
52
+ <BulkCompliancePolicyModal
53
+ key="bulk-unassign-compliance-policy-modal"
54
+ selectAllHostsMode={selectAllHostsMode}
55
+ selectedCount={selectedCount}
56
+ fetchBulkParams={fetchBulkParams}
57
+ organizationId={organizationId}
58
+ locationId={locationId}
59
+ isOpen={isOpen}
60
+ closeModal={closeModal}
61
+ mode="unassign"
62
+ onSuccess={refreshTableData}
63
+ />
64
+ );
65
+ };
@@ -0,0 +1,121 @@
1
+ import { APIActions } from 'foremanReact/redux/API';
2
+
3
+ import {
4
+ fetchPolicies,
5
+ bulkAssignPolicy,
6
+ bulkUnassignPolicy,
7
+ } from '../actions';
8
+ import {
9
+ POLICIES_KEY,
10
+ BULK_ASSIGN_POLICY_KEY,
11
+ BULK_UNASSIGN_POLICY_KEY,
12
+ } from '../constants';
13
+
14
+ jest.mock('foremanReact/redux/API', () => ({
15
+ APIActions: {
16
+ get: jest.fn(params => ({
17
+ type: 'API_GET',
18
+ params,
19
+ })),
20
+ put: jest.fn(params => ({
21
+ type: 'API_PUT',
22
+ params,
23
+ })),
24
+ },
25
+ }));
26
+
27
+ jest.mock('foremanReact/common/helpers', () => ({
28
+ foremanUrl: jest.fn(path => path),
29
+ }));
30
+
31
+ jest.mock('foremanReact/common/I18n', () => ({
32
+ translate: jest.fn(text => text),
33
+ }));
34
+
35
+ describe('Compliance Policy Actions', () => {
36
+ beforeEach(() => {
37
+ jest.clearAllMocks();
38
+ APIActions.put.mockReturnValue({ type: 'API_PUT' });
39
+ });
40
+
41
+ describe('fetchPolicies', () => {
42
+ it('dispatches GET request to compliance policies API', () => {
43
+ const action = fetchPolicies();
44
+
45
+ expect(APIActions.get).toHaveBeenCalledWith({
46
+ url: '/api/v2/compliance/policies',
47
+ key: POLICIES_KEY,
48
+ params: { per_page: 'all' },
49
+ });
50
+ expect(action.type).toBe('API_GET');
51
+ });
52
+ });
53
+
54
+ describe('bulkAssignPolicy', () => {
55
+ const requestBody = {
56
+ included: {
57
+ search: 'id ^ (1,2,3)',
58
+ },
59
+ policy_id: '5',
60
+ organization_id: 1,
61
+ location_id: 2,
62
+ };
63
+ const handleSuccess = jest.fn();
64
+ const handleError = jest.fn();
65
+
66
+ it('dispatches PUT request to assign policy endpoint', () => {
67
+ bulkAssignPolicy(requestBody, handleSuccess, handleError);
68
+
69
+ expect(APIActions.put).toHaveBeenCalledTimes(1);
70
+ const callArgs = APIActions.put.mock.calls[0][0];
71
+
72
+ expect(callArgs.key).toBe(BULK_ASSIGN_POLICY_KEY);
73
+ expect(callArgs.url).toContain(
74
+ '/api/v2/compliance/hosts/bulk/assign_compliance_policy'
75
+ );
76
+ expect(callArgs.params).toEqual(requestBody);
77
+ expect(callArgs.handleSuccess).toBe(handleSuccess);
78
+ expect(callArgs.handleError).toBe(handleError);
79
+ });
80
+
81
+ it('passes error handler callback', () => {
82
+ bulkAssignPolicy(requestBody, handleSuccess, handleError);
83
+ const callArgs = APIActions.put.mock.calls[0][0];
84
+ expect(callArgs.handleError).toBe(handleError);
85
+ });
86
+ });
87
+
88
+ describe('bulkUnassignPolicy', () => {
89
+ const requestBody = {
90
+ included: {
91
+ search: 'name ~ test',
92
+ },
93
+ policy_id: '3',
94
+ organization_id: 1,
95
+ location_id: 2,
96
+ };
97
+ const handleSuccess = jest.fn();
98
+ const handleError = jest.fn();
99
+
100
+ it('dispatches PUT request to unassign policy endpoint', () => {
101
+ bulkUnassignPolicy(requestBody, handleSuccess, handleError);
102
+
103
+ expect(APIActions.put).toHaveBeenCalledTimes(1);
104
+ const callArgs = APIActions.put.mock.calls[0][0];
105
+
106
+ expect(callArgs.key).toBe(BULK_UNASSIGN_POLICY_KEY);
107
+ expect(callArgs.url).toContain(
108
+ '/api/v2/compliance/hosts/bulk/unassign_compliance_policy'
109
+ );
110
+ expect(callArgs.params).toEqual(requestBody);
111
+ expect(callArgs.handleSuccess).toBe(handleSuccess);
112
+ expect(callArgs.handleError).toBe(handleError);
113
+ });
114
+
115
+ it('passes error handler callback', () => {
116
+ bulkUnassignPolicy(requestBody, handleSuccess, handleError);
117
+ const callArgs = APIActions.put.mock.calls[0][0];
118
+ expect(callArgs.handleError).toBe(handleError);
119
+ });
120
+ });
121
+ });
@@ -0,0 +1,32 @@
1
+ import { APIActions } from 'foremanReact/redux/API';
2
+ import { foremanUrl } from 'foremanReact/common/helpers';
3
+ import {
4
+ POLICIES_KEY,
5
+ BULK_ASSIGN_POLICY_KEY,
6
+ BULK_UNASSIGN_POLICY_KEY,
7
+ } from './constants';
8
+
9
+ export const fetchPolicies = () =>
10
+ APIActions.get({
11
+ url: foremanUrl('/api/v2/compliance/policies'),
12
+ key: POLICIES_KEY,
13
+ params: { per_page: 'all' },
14
+ });
15
+
16
+ export const bulkAssignPolicy = (params, handleSuccess, handleError) =>
17
+ APIActions.put({
18
+ url: foremanUrl('/api/v2/compliance/hosts/bulk/assign_compliance_policy'),
19
+ key: BULK_ASSIGN_POLICY_KEY,
20
+ params,
21
+ handleSuccess,
22
+ handleError,
23
+ });
24
+
25
+ export const bulkUnassignPolicy = (params, handleSuccess, handleError) =>
26
+ APIActions.put({
27
+ url: foremanUrl('/api/v2/compliance/hosts/bulk/unassign_compliance_policy'),
28
+ key: BULK_UNASSIGN_POLICY_KEY,
29
+ params,
30
+ handleSuccess,
31
+ handleError,
32
+ });
@@ -0,0 +1,8 @@
1
+ export const POLICIES_KEY = 'COMPLIANCE_POLICIES';
2
+ export const BULK_ASSIGN_POLICY_KEY = 'BULK_ASSIGN_COMPLIANCE_POLICY';
3
+ export const BULK_UNASSIGN_POLICY_KEY = 'BULK_UNASSIGN_COMPLIANCE_POLICY';
4
+
5
+ export const ASSIGN_POLICY_MODAL_ID = 'assign-compliance-policy-modal';
6
+ export const UNASSIGN_POLICY_MODAL_ID = 'unassign-compliance-policy-modal';
7
+
8
+ export const ASSIGN_POLICIES = 'assign_policies';
@@ -4,10 +4,15 @@ import HostKebabItems from './components/HostExtentions/HostKebabItems';
4
4
  import BulkChangeOpenscapProxyModalScene, {
5
5
  ChangeOpenscapProxyMenuItem,
6
6
  } from './components/HostsIndex/ChangeOpenscapProxyAction';
7
+ import BulkCompliancePolicyMenuItems from './components/BulkActions/compliancePolicy/BulkCompliancePolicyMenuItems';
8
+ import {
9
+ BulkAssignPolicyModalScene,
10
+ BulkUnassignPolicyModalScene,
11
+ } from './components/BulkActions/compliancePolicy/BulkCompliancePolicyModalScene';
7
12
 
8
13
  const HOST_ASSOCIATIONS_WEIGHT = 1212;
9
14
  const BULK_MODAL_WEIGHT = 100;
10
-
15
+ const BULK_POLICY_MODAL_WEIGHT = 200;
11
16
  const OPENSCAP_KEBAB_WEIGHT = 400;
12
17
 
13
18
  addGlobalFill(
@@ -30,3 +35,24 @@ addGlobalFill(
30
35
  <BulkChangeOpenscapProxyModalScene key="bulk-change-openscap-proxy-modal" />,
31
36
  BULK_MODAL_WEIGHT
32
37
  );
38
+
39
+ addGlobalFill(
40
+ '_host-associations',
41
+ 'compliance-policy-bulk-actions',
42
+ <BulkCompliancePolicyMenuItems key="compliance-policy-menu-items" />,
43
+ BULK_POLICY_MODAL_WEIGHT
44
+ );
45
+
46
+ addGlobalFill(
47
+ '_all-hosts-modals',
48
+ 'assign-compliance-policy-modal',
49
+ <BulkAssignPolicyModalScene key="bulk-assign-policy-modal" />,
50
+ BULK_POLICY_MODAL_WEIGHT
51
+ );
52
+
53
+ addGlobalFill(
54
+ '_all-hosts-modals',
55
+ 'unassign-compliance-policy-modal',
56
+ <BulkUnassignPolicyModalScene key="bulk-unassign-policy-modal" />,
57
+ BULK_POLICY_MODAL_WEIGHT
58
+ );
metadata CHANGED
@@ -1,7 +1,7 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: foreman_openscap
3
3
  version: !ruby/object:Gem::Version
4
- version: 13.1.3
4
+ version: 13.2.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - slukasik@redhat.com
@@ -366,6 +366,12 @@ files:
366
366
  - test/unit/services/lookup_key_overrider_test.rb
367
367
  - test/unit/services/report_dashboard/data_test.rb
368
368
  - test/unit/tailoring_file_test.rb
369
+ - webpack/components/BulkActions/compliancePolicy/BulkCompliancePolicyMenuItems.js
370
+ - webpack/components/BulkActions/compliancePolicy/BulkCompliancePolicyModal.js
371
+ - webpack/components/BulkActions/compliancePolicy/BulkCompliancePolicyModalScene.js
372
+ - webpack/components/BulkActions/compliancePolicy/__tests__/actions.test.js
373
+ - webpack/components/BulkActions/compliancePolicy/actions.js
374
+ - webpack/components/BulkActions/compliancePolicy/constants.js
369
375
  - webpack/components/EmptyState.js
370
376
  - webpack/components/HostExtentions/HostKebabItems.js
371
377
  - webpack/components/HostsIndex/BulkActions/changeOpenscapProxy/BulkChangeOpenscapProxyModal.js