foreman_openscap 13.1.2 → 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/assets/javascripts/foreman_openscap/locale/de/foreman_openscap.js +5 -5
- data/app/assets/javascripts/foreman_openscap/locale/en/foreman_openscap.js +1 -1
- data/app/assets/javascripts/foreman_openscap/locale/en_GB/foreman_openscap.js +1 -1
- data/app/assets/javascripts/foreman_openscap/locale/es/foreman_openscap.js +1 -1
- data/app/assets/javascripts/foreman_openscap/locale/fr/foreman_openscap.js +26 -24
- data/app/assets/javascripts/foreman_openscap/locale/gl/foreman_openscap.js +1 -1
- data/app/assets/javascripts/foreman_openscap/locale/it/foreman_openscap.js +1 -1
- data/app/assets/javascripts/foreman_openscap/locale/ja/foreman_openscap.js +26 -28
- data/app/assets/javascripts/foreman_openscap/locale/ka/foreman_openscap.js +5 -5
- data/app/assets/javascripts/foreman_openscap/locale/ko/foreman_openscap.js +26 -28
- data/app/assets/javascripts/foreman_openscap/locale/pt_BR/foreman_openscap.js +1 -1
- data/app/assets/javascripts/foreman_openscap/locale/ru/foreman_openscap.js +1 -1
- data/app/assets/javascripts/foreman_openscap/locale/sv_SE/foreman_openscap.js +1 -1
- data/app/assets/javascripts/foreman_openscap/locale/zh_CN/foreman_openscap.js +22 -24
- data/app/assets/javascripts/foreman_openscap/locale/zh_TW/foreman_openscap.js +1 -1
- data/app/controllers/api/v2/compliance/hosts_bulk_actions_controller.rb +38 -1
- data/app/controllers/policies_controller.rb +1 -1
- data/app/models/concerns/foreman_openscap/host_extensions.rb +10 -4
- data/app/models/concerns/foreman_openscap/hostgroup_extensions.rb +7 -0
- data/app/models/foreman_openscap/asset.rb +21 -1
- data/app/models/foreman_openscap/compliance_status.rb +19 -7
- data/app/models/foreman_openscap/policy.rb +43 -3
- data/app/services/foreman_openscap/compliance_status_resetter.rb +26 -0
- data/config/routes.rb +2 -0
- data/lib/foreman_openscap/engine.rb +3 -1
- data/lib/foreman_openscap/version.rb +1 -1
- data/locale/de/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/de/foreman_openscap.po +5 -5
- data/locale/en/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/en/foreman_openscap.po +1 -1
- data/locale/en_GB/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/en_GB/foreman_openscap.po +1 -1
- data/locale/es/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/es/foreman_openscap.po +1 -1
- data/locale/foreman_openscap.pot +2 -2
- data/locale/fr/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/fr/foreman_openscap.po +26 -24
- data/locale/gl/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/gl/foreman_openscap.po +1 -1
- data/locale/it/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/it/foreman_openscap.po +1 -1
- data/locale/ja/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/ja/foreman_openscap.po +26 -28
- data/locale/ka/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/ka/foreman_openscap.po +5 -5
- data/locale/ko/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/ko/foreman_openscap.po +26 -28
- data/locale/pt_BR/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/pt_BR/foreman_openscap.po +1 -1
- data/locale/ru/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/ru/foreman_openscap.po +1 -1
- data/locale/sv_SE/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/sv_SE/foreman_openscap.po +1 -1
- data/locale/zh_CN/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/zh_CN/foreman_openscap.po +22 -24
- data/locale/zh_TW/LC_MESSAGES/foreman_openscap.mo +0 -0
- data/locale/zh_TW/foreman_openscap.po +1 -1
- data/test/functional/api/v2/compliance/hosts_bulk_actions_controller_test.rb +175 -1
- data/test/test_plugin_helper.rb +7 -0
- data/test/unit/compliance_status_resetter_test.rb +48 -0
- data/test/unit/compliance_status_test.rb +53 -4
- data/test/unit/concerns/host_extensions_test.rb +55 -0
- data/test/unit/concerns/hostgroup_extensions_test.rb +64 -0
- data/test/unit/policy_test.rb +90 -0
- data/webpack/components/BulkActions/compliancePolicy/BulkCompliancePolicyMenuItems.js +49 -0
- data/webpack/components/BulkActions/compliancePolicy/BulkCompliancePolicyModal.js +304 -0
- data/webpack/components/BulkActions/compliancePolicy/BulkCompliancePolicyModalScene.js +65 -0
- data/webpack/components/BulkActions/compliancePolicy/__tests__/actions.test.js +121 -0
- data/webpack/components/BulkActions/compliancePolicy/actions.js +32 -0
- data/webpack/components/BulkActions/compliancePolicy/constants.js +8 -0
- data/webpack/global_index.js +27 -1
- metadata +10 -1
|
@@ -74,12 +74,48 @@ class ComplianceStatusTest < ActiveSupport::TestCase
|
|
|
74
74
|
status = ForemanOpenscap::ComplianceStatus.new
|
|
75
75
|
host = FactoryBot.create(:compliance_host, :policies => [@policy_a])
|
|
76
76
|
|
|
77
|
+
host.expects(:combined_policies).never
|
|
77
78
|
status.host = host
|
|
78
79
|
|
|
79
80
|
assert status.relevant?
|
|
80
81
|
end
|
|
81
82
|
|
|
82
|
-
test 'relevant? should be
|
|
83
|
+
test 'relevant? should be false for host without policies' do
|
|
84
|
+
status = ForemanOpenscap::ComplianceStatus.new
|
|
85
|
+
host = FactoryBot.create(:host)
|
|
86
|
+
|
|
87
|
+
status.host = host
|
|
88
|
+
|
|
89
|
+
refute status.relevant?
|
|
90
|
+
end
|
|
91
|
+
|
|
92
|
+
test 'relevant? should be false for host with hostgroup but no policies' do
|
|
93
|
+
status = ForemanOpenscap::ComplianceStatus.new
|
|
94
|
+
host = FactoryBot.create(:host, :with_hostgroup)
|
|
95
|
+
|
|
96
|
+
status.host = host
|
|
97
|
+
|
|
98
|
+
refute status.relevant?
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
test 'relevant? should be true for hostgroup policies without loading combined_policies' do
|
|
102
|
+
status = ForemanOpenscap::ComplianceStatus.new
|
|
103
|
+
host = FactoryBot.create(:host, :with_hostgroup)
|
|
104
|
+
|
|
105
|
+
assert_empty host.policies
|
|
106
|
+
@policy_a.hostgroup_ids = [host.hostgroup.id]
|
|
107
|
+
assert @policy_a.save
|
|
108
|
+
|
|
109
|
+
host.reload
|
|
110
|
+
assert_includes host.combined_policies, @policy_a
|
|
111
|
+
|
|
112
|
+
status.host = host
|
|
113
|
+
host.expects(:combined_policies).never
|
|
114
|
+
|
|
115
|
+
assert status.relevant?
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
test 'to_status should use hostgroup policies' do
|
|
83
119
|
status = ForemanOpenscap::ComplianceStatus.new
|
|
84
120
|
host = FactoryBot.create(:host, :with_hostgroup)
|
|
85
121
|
othered_status = { :passed => 0, :failed => 0, :othered => 1 }.with_indifferent_access
|
|
@@ -95,12 +131,10 @@ class ComplianceStatusTest < ActiveSupport::TestCase
|
|
|
95
131
|
FactoryBot.create(:policy_arf_report, :policy_id => @policy_a.id, :arf_report_id => report.id)
|
|
96
132
|
|
|
97
133
|
status.host = host
|
|
98
|
-
|
|
99
|
-
assert status.relevant?
|
|
100
134
|
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, status.to_status
|
|
101
135
|
end
|
|
102
136
|
|
|
103
|
-
test 'relevant? should be true for inherited parent hostgroup policies' do
|
|
137
|
+
test 'relevant? should be true for inherited parent hostgroup policies without loading combined_policies' do
|
|
104
138
|
status = ForemanOpenscap::ComplianceStatus.new
|
|
105
139
|
host = FactoryBot.create(:host, :with_hostgroup)
|
|
106
140
|
hostgroup = host.hostgroup
|
|
@@ -116,7 +150,22 @@ class ComplianceStatusTest < ActiveSupport::TestCase
|
|
|
116
150
|
assert_includes host.combined_policies, @policy_a
|
|
117
151
|
|
|
118
152
|
status.host = host
|
|
153
|
+
host.expects(:combined_policies).never
|
|
119
154
|
|
|
120
155
|
assert status.relevant?
|
|
121
156
|
end
|
|
157
|
+
|
|
158
|
+
test 'to_label returns label based on stored status' do
|
|
159
|
+
status = ForemanOpenscap::ComplianceStatus.new
|
|
160
|
+
status.status = ForemanOpenscap::ComplianceStatus::COMPLIANT
|
|
161
|
+
status.expects(:to_status).never
|
|
162
|
+
assert_equal 'Compliant', status.to_label
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
test 'to_global returns global status based on stored status' do
|
|
166
|
+
status = ForemanOpenscap::ComplianceStatus.new
|
|
167
|
+
status.status = ForemanOpenscap::ComplianceStatus::COMPLIANT
|
|
168
|
+
status.expects(:to_status).never
|
|
169
|
+
assert_equal ::HostStatus::Global::OK, status.to_global
|
|
170
|
+
end
|
|
122
171
|
end
|
|
@@ -162,6 +162,61 @@ class HostExtensionsTest < ActiveSupport::TestCase
|
|
|
162
162
|
assert_empty ForemanOpenscap::Asset.where(:assetable_id => host.id, :assetable_type => 'Host::Base')
|
|
163
163
|
end
|
|
164
164
|
|
|
165
|
+
test 'should reset compliance status when assigning policy through host policies' do
|
|
166
|
+
host = FactoryBot.create(:compliance_host)
|
|
167
|
+
set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
168
|
+
|
|
169
|
+
host.policies = [@policy]
|
|
170
|
+
|
|
171
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, host.reload.compliance_status
|
|
172
|
+
end
|
|
173
|
+
|
|
174
|
+
test 'should reset compliance status when removing policy through host policies' do
|
|
175
|
+
host = FactoryBot.create(:compliance_host, :policies => [@policy])
|
|
176
|
+
set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
177
|
+
|
|
178
|
+
host.policies = []
|
|
179
|
+
|
|
180
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, host.reload.compliance_status
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
test 'should reset compliance status when host is moved to a different hostgroup' do
|
|
184
|
+
old_hostgroup = FactoryBot.create(:hostgroup)
|
|
185
|
+
new_hostgroup = FactoryBot.create(:hostgroup)
|
|
186
|
+
host = FactoryBot.create(:compliance_host, :hostgroup_id => old_hostgroup.id)
|
|
187
|
+
set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
188
|
+
|
|
189
|
+
host.update!(:hostgroup_id => new_hostgroup.id)
|
|
190
|
+
|
|
191
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, host.reload.compliance_status
|
|
192
|
+
end
|
|
193
|
+
|
|
194
|
+
test 'should not reset compliance status when host is saved without hostgroup change' do
|
|
195
|
+
hostgroup = FactoryBot.create(:hostgroup)
|
|
196
|
+
host = FactoryBot.create(:compliance_host, :hostgroup_id => hostgroup.id)
|
|
197
|
+
set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
198
|
+
|
|
199
|
+
host.update!(:comment => 'unrelated change')
|
|
200
|
+
|
|
201
|
+
assert_equal ForemanOpenscap::ComplianceStatus::COMPLIANT, host.reload.compliance_status
|
|
202
|
+
end
|
|
203
|
+
|
|
204
|
+
test 'compliance_status returns stored persisted status without recalculation' do
|
|
205
|
+
set_compliance_status(@host, ForemanOpenscap::ComplianceStatus::INCONCLUSIVE)
|
|
206
|
+
status = @host.reload.get_status(ForemanOpenscap::ComplianceStatus)
|
|
207
|
+
status.expects(:to_status).never
|
|
208
|
+
|
|
209
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, @host.compliance_status
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
test 'compliance_status_label returns label from stored persisted status without recalculation' do
|
|
213
|
+
set_compliance_status(@host, ForemanOpenscap::ComplianceStatus::INCOMPLIANT)
|
|
214
|
+
status = @host.reload.get_status(ForemanOpenscap::ComplianceStatus)
|
|
215
|
+
status.expects(:to_status).never
|
|
216
|
+
|
|
217
|
+
assert_equal 'Incompliant', @host.compliance_status_label
|
|
218
|
+
end
|
|
219
|
+
|
|
165
220
|
private
|
|
166
221
|
|
|
167
222
|
def setup_hosts_with_policy
|
|
@@ -7,4 +7,68 @@ class HostgroupExtensionsTest < ActiveSupport::TestCase
|
|
|
7
7
|
asset_scope = ::ForemanOpenscap::Asset.where(:assetable_id => hostgroup.id, :assetable_type => 'Hostgroup')
|
|
8
8
|
assert_difference("asset_scope.count", -3) { hostgroup.destroy }
|
|
9
9
|
end
|
|
10
|
+
|
|
11
|
+
test "should reset compliance status for subtree when assigning policy through hostgroup asset policies" do
|
|
12
|
+
parent = FactoryBot.create(:hostgroup)
|
|
13
|
+
child = FactoryBot.create(:hostgroup, :ancestry => parent.id.to_s)
|
|
14
|
+
parent_host = FactoryBot.create(:compliance_host, :hostgroup_id => parent.id)
|
|
15
|
+
child_host = FactoryBot.create(:compliance_host, :hostgroup_id => child.id)
|
|
16
|
+
policy = FactoryBot.create(:policy)
|
|
17
|
+
asset = FactoryBot.create(:asset, :assetable_id => parent.id, :assetable_type => 'Hostgroup')
|
|
18
|
+
|
|
19
|
+
set_compliance_status(parent_host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
20
|
+
set_compliance_status(child_host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
21
|
+
|
|
22
|
+
asset.policies = [policy]
|
|
23
|
+
|
|
24
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, parent_host.reload.compliance_status
|
|
25
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, child_host.reload.compliance_status
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
test "should reset compliance status for subtree when removing policy through hostgroup asset policies" do
|
|
29
|
+
parent = FactoryBot.create(:hostgroup)
|
|
30
|
+
child = FactoryBot.create(:hostgroup, :ancestry => parent.id.to_s)
|
|
31
|
+
parent_host = FactoryBot.create(:compliance_host, :hostgroup_id => parent.id)
|
|
32
|
+
child_host = FactoryBot.create(:compliance_host, :hostgroup_id => child.id)
|
|
33
|
+
policy = FactoryBot.create(:policy)
|
|
34
|
+
asset = FactoryBot.create(:asset, :assetable_id => parent.id, :assetable_type => 'Hostgroup')
|
|
35
|
+
asset.policies = [policy]
|
|
36
|
+
|
|
37
|
+
set_compliance_status(parent_host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
38
|
+
set_compliance_status(child_host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
39
|
+
|
|
40
|
+
asset.policies = []
|
|
41
|
+
|
|
42
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, parent_host.reload.compliance_status
|
|
43
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, child_host.reload.compliance_status
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
test "should reset compliance status for subtree when hostgroup is reparented" do
|
|
47
|
+
old_parent = FactoryBot.create(:hostgroup)
|
|
48
|
+
new_parent = FactoryBot.create(:hostgroup)
|
|
49
|
+
moved = FactoryBot.create(:hostgroup, :ancestry => old_parent.id.to_s)
|
|
50
|
+
child = FactoryBot.create(:hostgroup, :ancestry => "#{old_parent.id}/#{moved.id}")
|
|
51
|
+
moved_host = FactoryBot.create(:compliance_host, :hostgroup_id => moved.id)
|
|
52
|
+
child_host = FactoryBot.create(:compliance_host, :hostgroup_id => child.id)
|
|
53
|
+
unrelated_host = FactoryBot.create(:compliance_host, :hostgroup_id => old_parent.id)
|
|
54
|
+
[moved_host, child_host, unrelated_host].each do |host|
|
|
55
|
+
set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
moved.update!(:parent_id => new_parent.id)
|
|
59
|
+
|
|
60
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, moved_host.reload.compliance_status
|
|
61
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, child_host.reload.compliance_status
|
|
62
|
+
assert_equal ForemanOpenscap::ComplianceStatus::COMPLIANT, unrelated_host.reload.compliance_status
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
test "should not reset compliance status for hostgroup save without ancestry change" do
|
|
66
|
+
hostgroup = FactoryBot.create(:hostgroup)
|
|
67
|
+
host = FactoryBot.create(:compliance_host, :hostgroup_id => hostgroup.id)
|
|
68
|
+
set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
69
|
+
|
|
70
|
+
hostgroup.update!(:name => 'renamed')
|
|
71
|
+
|
|
72
|
+
assert_equal ForemanOpenscap::ComplianceStatus::COMPLIANT, host.reload.compliance_status
|
|
73
|
+
end
|
|
10
74
|
end
|
data/test/unit/policy_test.rb
CHANGED
|
@@ -46,6 +46,90 @@ class PolicyTest < ActiveSupport::TestCase
|
|
|
46
46
|
assert_equal 1, policy.hosts.count
|
|
47
47
|
end
|
|
48
48
|
|
|
49
|
+
test "should reset compliance status when assigning policy to host" do
|
|
50
|
+
host = FactoryBot.create(:compliance_host)
|
|
51
|
+
unrelated_host = FactoryBot.create(:compliance_host)
|
|
52
|
+
set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
53
|
+
set_compliance_status(unrelated_host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
54
|
+
policy = FactoryBot.create(:policy, :scap_content => @scap_content, :scap_content_profile => @scap_profile)
|
|
55
|
+
|
|
56
|
+
policy.host_ids = [host.id]
|
|
57
|
+
policy.save!
|
|
58
|
+
|
|
59
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(host)
|
|
60
|
+
assert_equal ForemanOpenscap::ComplianceStatus::COMPLIANT, compliance_status_for(unrelated_host)
|
|
61
|
+
end
|
|
62
|
+
|
|
63
|
+
test "should create inconclusive compliance status when assigning policy to host without status" do
|
|
64
|
+
host = FactoryBot.create(:compliance_host)
|
|
65
|
+
policy = FactoryBot.create(:policy, :scap_content => @scap_content, :scap_content_profile => @scap_profile)
|
|
66
|
+
|
|
67
|
+
policy.host_ids = [host.id]
|
|
68
|
+
policy.save!
|
|
69
|
+
|
|
70
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(host)
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
test "should reset compliance status when removing policy from host assignment" do
|
|
74
|
+
host = FactoryBot.create(:compliance_host)
|
|
75
|
+
asset = FactoryBot.create(:asset, :assetable_id => host.id, :assetable_type => 'Host::Base')
|
|
76
|
+
policy = FactoryBot.create(:policy, :assets => [asset], :scap_content => @scap_content, :scap_content_profile => @scap_profile)
|
|
77
|
+
set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
78
|
+
|
|
79
|
+
policy.host_ids = []
|
|
80
|
+
policy.save!
|
|
81
|
+
|
|
82
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(host)
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
test "should reset compliance status when unassigning host from policy" do
|
|
86
|
+
host = FactoryBot.create(:compliance_host)
|
|
87
|
+
asset = FactoryBot.create(:asset, :assetable_id => host.id, :assetable_type => 'Host::Base')
|
|
88
|
+
policy = FactoryBot.create(:policy, :assets => [asset], :scap_content => @scap_content, :scap_content_profile => @scap_profile)
|
|
89
|
+
set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
90
|
+
|
|
91
|
+
policy.unassign_hosts([host])
|
|
92
|
+
|
|
93
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(host)
|
|
94
|
+
end
|
|
95
|
+
|
|
96
|
+
test "should reset compliance status for hostgroup subtree when assigning policy to hostgroup" do
|
|
97
|
+
parent = FactoryBot.create(:hostgroup)
|
|
98
|
+
child = FactoryBot.create(:hostgroup, :ancestry => parent.id.to_s)
|
|
99
|
+
parent_host = FactoryBot.create(:compliance_host, :hostgroup_id => parent.id)
|
|
100
|
+
child_host = FactoryBot.create(:compliance_host, :hostgroup_id => child.id)
|
|
101
|
+
unrelated_host = FactoryBot.create(:compliance_host)
|
|
102
|
+
[parent_host, child_host, unrelated_host].each do |host|
|
|
103
|
+
set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
104
|
+
end
|
|
105
|
+
policy = FactoryBot.create(:policy, :scap_content => @scap_content, :scap_content_profile => @scap_profile)
|
|
106
|
+
|
|
107
|
+
policy.hostgroup_ids = [parent.id]
|
|
108
|
+
policy.save!
|
|
109
|
+
|
|
110
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(parent_host)
|
|
111
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(child_host)
|
|
112
|
+
assert_equal ForemanOpenscap::ComplianceStatus::COMPLIANT, compliance_status_for(unrelated_host)
|
|
113
|
+
end
|
|
114
|
+
|
|
115
|
+
test "should reset compliance status for hostgroup subtree when removing policy from hostgroup" do
|
|
116
|
+
parent = FactoryBot.create(:hostgroup)
|
|
117
|
+
child = FactoryBot.create(:hostgroup, :ancestry => parent.id.to_s)
|
|
118
|
+
parent_host = FactoryBot.create(:compliance_host, :hostgroup_id => parent.id)
|
|
119
|
+
child_host = FactoryBot.create(:compliance_host, :hostgroup_id => child.id)
|
|
120
|
+
asset = FactoryBot.create(:asset, :assetable_id => parent.id, :assetable_type => 'Hostgroup')
|
|
121
|
+
policy = FactoryBot.create(:policy, :assets => [asset], :scap_content => @scap_content, :scap_content_profile => @scap_profile)
|
|
122
|
+
[parent_host, child_host].each do |host|
|
|
123
|
+
set_compliance_status(host, ForemanOpenscap::ComplianceStatus::COMPLIANT)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
policy.hostgroup_ids = []
|
|
127
|
+
policy.save!
|
|
128
|
+
|
|
129
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(parent_host)
|
|
130
|
+
assert_equal ForemanOpenscap::ComplianceStatus::INCONCLUSIVE, compliance_status_for(child_host)
|
|
131
|
+
end
|
|
132
|
+
|
|
49
133
|
test "should delete assets when unassigning hosts" do
|
|
50
134
|
host1 = FactoryBot.create(:compliance_host)
|
|
51
135
|
host2 = FactoryBot.create(:compliance_host)
|
|
@@ -331,4 +415,10 @@ class PolicyTest < ActiveSupport::TestCase
|
|
|
331
415
|
assert_equal policy.deploy_by, 'manual'
|
|
332
416
|
assert_equal policy.name, name
|
|
333
417
|
end
|
|
418
|
+
|
|
419
|
+
private
|
|
420
|
+
|
|
421
|
+
def compliance_status_for(host)
|
|
422
|
+
ForemanOpenscap::ComplianceStatus.find_by(:host_id => host.id).try(:status)
|
|
423
|
+
end
|
|
334
424
|
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;
|