diff --git a/db/migrate/20211108204736_add_policy_idx_to_approval_project_rule.rb b/db/migrate/20211108204736_add_policy_idx_to_approval_project_rule.rb new file mode 100644 index 0000000000000000000000000000000000000000..90e5fa34817d5b7dd4eb055b55ee5b6ee54dda9a --- /dev/null +++ b/db/migrate/20211108204736_add_policy_idx_to_approval_project_rule.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddPolicyIdxToApprovalProjectRule < Gitlab::Database::Migration[1.0] + enable_lock_retries! + + def change + add_column :approval_project_rules, :orchestration_policy_idx, :integer, limit: 2 + end +end diff --git a/db/migrate/20211122215001_add_policy_idx_to_approval_merge_request_rule.rb b/db/migrate/20211122215001_add_policy_idx_to_approval_merge_request_rule.rb new file mode 100644 index 0000000000000000000000000000000000000000..b1c7bc4d5cea22cd303541a9e184e748cdbf9b70 --- /dev/null +++ b/db/migrate/20211122215001_add_policy_idx_to_approval_merge_request_rule.rb @@ -0,0 +1,9 @@ +# frozen_string_literal: true + +class AddPolicyIdxToApprovalMergeRequestRule < Gitlab::Database::Migration[1.0] + enable_lock_retries! + + def change + add_column :approval_merge_request_rules, :orchestration_policy_idx, :integer, limit: 2 + end +end diff --git a/db/schema_migrations/20211108204736 b/db/schema_migrations/20211108204736 new file mode 100644 index 0000000000000000000000000000000000000000..6d37b1b118475b85867539ec08f46f2ba14995e0 --- /dev/null +++ b/db/schema_migrations/20211108204736 @@ -0,0 +1 @@ +9e01b1817e4c578f5be7d7378dc12a8535c2bbbff5ecbc77f5a7cfdb148927f5 \ No newline at end of file diff --git a/db/schema_migrations/20211122215001 b/db/schema_migrations/20211122215001 new file mode 100644 index 0000000000000000000000000000000000000000..be0fd652eb7b5649781a5073a8192747dbe05c48 --- /dev/null +++ b/db/schema_migrations/20211122215001 @@ -0,0 +1 @@ +fc29e10717357f7dd57940042d69a6c43a0d17fdf3c951917a76eae8c1d93ba3 \ No newline at end of file diff --git a/db/structure.sql b/db/structure.sql index 9a57719ec243364b14f52be8ff2d628a7ef16eec..0c76dab7af9e972606b5db73be5ce4663646a51e 100644 --- a/db/structure.sql +++ b/db/structure.sql @@ -10547,6 +10547,7 @@ CREATE TABLE approval_merge_request_rules ( report_type smallint, section text, modified_from_project_rule boolean DEFAULT false NOT NULL, + orchestration_policy_idx smallint, CONSTRAINT check_6fca5928b2 CHECK ((char_length(section) <= 255)) ); @@ -10616,7 +10617,8 @@ CREATE TABLE approval_project_rules ( vulnerabilities_allowed smallint DEFAULT 0 NOT NULL, severity_levels text[] DEFAULT '{}'::text[] NOT NULL, report_type smallint, - vulnerability_states text[] DEFAULT '{newly_detected}'::text[] NOT NULL + vulnerability_states text[] DEFAULT '{newly_detected}'::text[] NOT NULL, + orchestration_policy_idx smallint ); CREATE TABLE approval_project_rules_groups ( diff --git a/ee/app/models/approval_project_rule.rb b/ee/app/models/approval_project_rule.rb index 55dbf72e3b8864011b40c23bdaff85a542f6e04c..fd1c95c65cbb6781252cc5f01ee042eac3e10e9e 100644 --- a/ee/app/models/approval_project_rule.rb +++ b/ee/app/models/approval_project_rule.rb @@ -83,7 +83,7 @@ def vulnerability_states_for_branch(branch = project.default_branch) def report_approver_attributes attributes - .slice('approvals_required', 'name') + .slice('approvals_required', 'name', 'orchestration_policy_idx') .merge( users: users, groups: groups, diff --git a/ee/app/models/approval_state.rb b/ee/app/models/approval_state.rb index cdde3fbd211ec0b422a714e01229c0034e8ce2e0..1aa1fa57f00a21f057c77858a611ce958c826ded 100644 --- a/ee/app/models/approval_state.rb +++ b/ee/app/models/approval_state.rb @@ -247,10 +247,10 @@ def any_approver_approval_rules def wrapped_rules strong_memoize(:wrapped_rules) do - merge_request_rules = merge_request.approval_rules.applicable_to_branch(target_branch) + grouped_merge_request_rules = merge_request.approval_rules.applicable_to_branch(target_branch).group_by(&:report_type) - merge_request_rules.map! do |rule| - ApprovalWrappedRule.wrap(merge_request, rule) + grouped_merge_request_rules.flat_map do |report_type, merge_request_rules| + Approvals::WrappedRuleSet.wrap(merge_request, merge_request_rules, report_type).wrapped_rules end end end diff --git a/ee/app/models/approval_wrapped_rule.rb b/ee/app/models/approval_wrapped_rule.rb index 4aaeb5cb8bd97a56ec5b1f855f02ca22e90ce4fc..f968f166e65c00c5c3bea5fa6eaa1aa4f2aa671f 100644 --- a/ee/app/models/approval_wrapped_rule.rb +++ b/ee/app/models/approval_wrapped_rule.rb @@ -23,7 +23,7 @@ class ApprovalWrappedRule def_delegators(:@approval_rule, :regular?, :any_approver?, :code_owner?, :report_approver?, - :overridden?, :id, :name, :users, :groups, :code_owner, + :overridden?, :id, :users, :groups, :code_owner, :source_rule, :rule_type, :approvals_required, :section, :to_global_id) def self.wrap(merge_request, rule) @@ -109,6 +109,12 @@ def unactioned_approvers approvers - approved_approvers end + def name + return approval_rule.name unless approval_rule.scan_finding? + + approval_rule.name.gsub(/\s\d+\z/, '') + end + private def filter_approvers(approvers) diff --git a/ee/app/models/approvals/scan_finding_wrapped_rule_set.rb b/ee/app/models/approvals/scan_finding_wrapped_rule_set.rb new file mode 100644 index 0000000000000000000000000000000000000000..ff18b4d6375dd0f9cddbdb33f87b09f47cac6589 --- /dev/null +++ b/ee/app/models/approvals/scan_finding_wrapped_rule_set.rb @@ -0,0 +1,29 @@ +# frozen_string_literal: true +module Approvals + class ScanFindingWrappedRuleSet < WrappedRuleSet + extend ::Gitlab::Utils::Override + + override :wrapped_rules + def wrapped_rules + strong_memoize(:wrapped_rules) do + if ::Feature.enabled?(:scan_result_policy, merge_request.project) + grouped_merge_request_rules = approval_rules.group_by(&:orchestration_policy_idx) + grouped_merge_request_rules.map do |_, merge_request_rules| + wrapped_rules_sorted_by_approval(merge_request_rules).first + end + else + [] + end + end + end + + private + + def wrapped_rules_sorted_by_approval(merge_request_rules) + merge_request_rules.map! do |rule| + ApprovalWrappedRule.wrap(merge_request, rule) + end + merge_request_rules.sort_by {|rule| rule.approved? ? 1 : 0} + end + end +end diff --git a/ee/app/models/approvals/wrapped_rule_set.rb b/ee/app/models/approvals/wrapped_rule_set.rb new file mode 100644 index 0000000000000000000000000000000000000000..f9cf1f68003d34f30727414ef1f2862f034d5408 --- /dev/null +++ b/ee/app/models/approvals/wrapped_rule_set.rb @@ -0,0 +1,30 @@ +# frozen_string_literal: true + +module Approvals + class WrappedRuleSet + include Gitlab::Utils::StrongMemoize + + attr_reader :merge_request, :approval_rules + + def self.wrap(merge_request, rules, report_type) + if report_type == Security::ScanResultPolicy::SCAN_FINDING + ScanFindingWrappedRuleSet.new(merge_request, rules) + else + WrappedRuleSet.new(merge_request, rules) + end + end + + def initialize(merge_request, approval_rules) + @merge_request = merge_request + @approval_rules = approval_rules + end + + def wrapped_rules + strong_memoize(:wrapped_rules) do + approval_rules.map do |rule| + ApprovalWrappedRule.wrap(merge_request, rule) + end + end + end + end +end diff --git a/ee/app/services/security/security_orchestration_policies/process_scan_result_policy_service.rb b/ee/app/services/security/security_orchestration_policies/process_scan_result_policy_service.rb index d0fe5d2276cf9f30da0a4fd95773e1d9aac7ee0d..8503d9a58ef8f8f0c052f2f5df521ce4273e97fb 100644 --- a/ee/app/services/security/security_orchestration_policies/process_scan_result_policy_service.rb +++ b/ee/app/services/security/security_orchestration_policies/process_scan_result_policy_service.rb @@ -5,9 +5,10 @@ module SecurityOrchestrationPolicies class ProcessScanResultPolicyService MAX_LENGTH = 25 - def initialize(policy_configuration:, policy:) + def initialize(policy_configuration:, policy:, policy_index:) @policy_configuration = policy_configuration @policy = policy + @policy_index = policy_index @project = policy_configuration.project @author = policy_configuration.policy_last_updated_by end @@ -20,7 +21,7 @@ def execute private - attr_reader :policy_configuration, :policy, :project, :author + attr_reader :policy_configuration, :policy, :project, :author, :policy_index def create_new_approval_rules action_info = policy[:actions].find { |action| action[:type] == Security::ScanResultPolicy::REQUIRE_APPROVAL } @@ -43,12 +44,13 @@ def rule_params(rule, rule_index, action_info) severity_levels: rule[:severity_levels], user_ids: project.users.get_ids_by_username(action_info[:approvers]), vulnerabilities_allowed: rule[:vulnerabilities_allowed], - report_type: :scan_finding + report_type: :scan_finding, + orchestration_policy_idx: policy_index } end def rule_name(policy_name, rule_index) - truncated = policy_name.truncate(MAX_LENGTH, omission: '') + truncated = policy_name.truncate(MAX_LENGTH) return truncated if rule_index == 0 "#{truncated} #{rule_index + 1}" diff --git a/ee/app/workers/security/create_orchestration_policy_worker.rb b/ee/app/workers/security/create_orchestration_policy_worker.rb index dee9b8105ce0e96e9fa63180d0444e40dfac555a..6313818cb1ac54b981e9d9dcaf18683074e28489 100644 --- a/ee/app/workers/security/create_orchestration_policy_worker.rb +++ b/ee/app/workers/security/create_orchestration_policy_worker.rb @@ -28,9 +28,9 @@ def perform configuration.transaction do configuration.approval_rules.scan_finding.delete_all - configuration.active_scan_result_policies.each do |policy| + configuration.active_scan_result_policies.each_with_index do |policy, policy_index| Security::SecurityOrchestrationPolicies::ProcessScanResultPolicyService - .new(policy_configuration: configuration, policy: policy) + .new(policy_configuration: configuration, policy: policy, policy_index: policy_index) .execute end end diff --git a/ee/spec/models/approval_project_rule_spec.rb b/ee/spec/models/approval_project_rule_spec.rb index dd6bfcc927e2136f4b46da777bffe4c9252ad456..d759b54cbe10bce694110de7b22a4125a5fc74b6 100644 --- a/ee/spec/models/approval_project_rule_spec.rb +++ b/ee/spec/models/approval_project_rule_spec.rb @@ -139,7 +139,7 @@ context "when there is a project rule for each report type" do with_them do - subject { create(:approval_project_rule, report_type, :requires_approval, project: project) } + subject { create(:approval_project_rule, report_type, :requires_approval, project: project, orchestration_policy_idx: 1) } let!(:result) { subject.apply_report_approver_rules_to(merge_request) } @@ -149,6 +149,7 @@ specify { expect(result.name).to be(:default_name) } specify { expect(result.rule_type).to be(:report_approver) } specify { expect(result.report_type).to be(:report_type) } + specify { expect(result.orchestration_policy_idx).to be 1 } end end end diff --git a/ee/spec/models/approval_state_spec.rb b/ee/spec/models/approval_state_spec.rb index b6c01df3e28af10e2993f81e820145950b79aea6..ec79738724e8b316e74bb37d8e944e9ea01665e2 100644 --- a/ee/spec/models/approval_state_spec.rb +++ b/ee/spec/models/approval_state_spec.rb @@ -173,6 +173,24 @@ def create_project_member(role, user_attrs = {}) end end + context 'with multiple scan_finding rules' do + before do + 2.times {create_rule({ rule_type: :report_approver, report_type: :scan_finding })} + 2.times {create_rule({ rule_type: :report_approver, report_type: :scan_finding, orchestration_policy_idx: 1 })} + end + + it 'returns one rule for each orchestration_policy_idx' do + approval_rules = subject.wrapped_approval_rules + + expect(approval_rules.count).to be(2) + expect(approval_rules).to all(be_instance_of(ApprovalWrappedRule)) + + policy_indices = approval_rules.map { |rule| rule.approval_rule.orchestration_policy_idx } + + expect(policy_indices).to contain_exactly(nil, 1) + end + end + describe '#approval_needed?' do context 'when feature not available' do it 'returns false' do diff --git a/ee/spec/models/approval_wrapped_rule_spec.rb b/ee/spec/models/approval_wrapped_rule_spec.rb index 5fd0a4559e3068c32ed04bcde5ff07a731bb5a25..361b9079c861930f6fb95abc0d16e732929d929b 100644 --- a/ee/spec/models/approval_wrapped_rule_spec.rb +++ b/ee/spec/models/approval_wrapped_rule_spec.rb @@ -201,4 +201,27 @@ def approved_approvers_for_rule_id(rule_id) expect(subject.approvals_required).to eq(19) end end + + describe '#name' do + let(:rule_name) { 'approval rule 2' } + let(:rule) { create(:approval_merge_request_rule, report_type: report_type, name: rule_name) } + + context 'with report_type set to scan_finding' do + let(:report_type) { :scan_finding } + let(:expected_rule_name) { 'approval rule' } + + it 'returns rule name without the sequential notation' do + expect(subject.name).not_to eq(rule_name) + expect(subject.name).to eq(expected_rule_name) + end + end + + context 'with report_type other than scan_finding' do + let(:report_type) { :vulnerability } + + it 'returns rule name as is' do + expect(subject.name).to eq(rule_name) + end + end + end end diff --git a/ee/spec/models/approvals/scan_finding_wrapped_rule_set_spec.rb b/ee/spec/models/approvals/scan_finding_wrapped_rule_set_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..9fa7d21e5e8ca38f7ccffecf68f2e6183a48cd59 --- /dev/null +++ b/ee/spec/models/approvals/scan_finding_wrapped_rule_set_spec.rb @@ -0,0 +1,54 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Approvals::ScanFindingWrappedRuleSet do + let(:report_type) { Security::ScanResultPolicy::SCAN_FINDING } + let_it_be(:merge_request) { create(:merge_request) } + let_it_be(:approver) { create(:user) } + let_it_be(:approval_rules) { create_list(:approval_merge_request_rule, 2, :scan_finding, merge_request: merge_request, users: [approver]) } + + let(:approval_rules_list) { approval_rules } + + subject { described_class.wrap(merge_request, approval_rules_list, report_type).wrapped_rules } + + describe '#wrapped_rules' do + context 'with feature flag disabled' do + before do + stub_feature_flags(scan_result_policy: false) + end + + it {is_expected.to be_empty } + end + + it 'returns only one rule' do + expect(subject.count).to be 1 + end + + context 'with various orchestration_policy_idx' do + let(:orchestration_policy_idx) { 0 } + let(:approval_rules_w_policy_idx) { create_list(:approval_merge_request_rule, 2, :scan_finding, merge_request: merge_request, orchestration_policy_idx: orchestration_policy_idx, users: [approver]) } + let(:approval_rules_list) { approval_rules + approval_rules_w_policy_idx } + + it 'returns one rule for each orchestration_policy_idx' do + expect(subject.count).to be 2 + + orchestration_policy_indices = subject.map { |wrapped_rule| wrapped_rule.approval_rule.orchestration_policy_idx } + + expect(orchestration_policy_indices).to contain_exactly(nil, orchestration_policy_idx) + end + + context 'with unapproved rules' do + let(:unapproved_rule) { create(:approval_merge_request_rule, :scan_finding, merge_request: merge_request, orchestration_policy_idx: orchestration_policy_idx, users: [approver], approvals_required: 5) } + let(:approval_rules_list) { approval_rules + approval_rules_w_policy_idx + [unapproved_rule] } + + it 'returns sorted based on approval' do + selected_rules = subject.select { |wrapped_rule| wrapped_rule.approval_rule.orchestration_policy_idx == orchestration_policy_idx } + + expect(selected_rules.count).to be 1 + expect(selected_rules.first.id).to be unapproved_rule.id + end + end + end + end +end diff --git a/ee/spec/models/approvals/wrapped_rule_set_spec.rb b/ee/spec/models/approvals/wrapped_rule_set_spec.rb new file mode 100644 index 0000000000000000000000000000000000000000..ebea102beecd09b06c5bc08973c5a7fff0cea23a --- /dev/null +++ b/ee/spec/models/approvals/wrapped_rule_set_spec.rb @@ -0,0 +1,37 @@ +# frozen_string_literal: true + +require 'spec_helper' + +RSpec.describe Approvals::WrappedRuleSet do + let(:report_type) { nil } + let(:merge_request) { build(:merge_request) } + let(:approval_merge_request_rule) { build(:approval_merge_request_rule, merge_request: merge_request, report_type: report_type) } + let(:grouped_approval_wrapped_rules) { described_class.wrap(merge_request, [approval_merge_request_rule], report_type) } + + describe '.wrap' do + subject { grouped_approval_wrapped_rules } + + context "with report_type set to #{Security::ScanResultPolicy::SCAN_FINDING}" do + let(:report_type) { Security::ScanResultPolicy::SCAN_FINDING } + + it { is_expected.to be_instance_of(Approvals::ScanFindingWrappedRuleSet) } + end + + context 'with any other report_type' do + it { is_expected.to be_instance_of(described_class) } + end + end + + describe '#wrapped_rules' do + subject { grouped_approval_wrapped_rules.wrapped_rules } + + it 'returns an array of ApprovalWrappedRule' do + expect(subject.count).to be 1 + expect(subject.first).to be_instance_of(ApprovalWrappedRule) + end + + it "returns ApprovalWrappedRule with attributes as provided to #{described_class.name}" do + expect(subject.first).to have_attributes(merge_request: merge_request, approval_rule: approval_merge_request_rule) + end + end +end diff --git a/ee/spec/services/security/security_orchestration_policies/process_scan_result_policy_service_spec.rb b/ee/spec/services/security/security_orchestration_policies/process_scan_result_policy_service_spec.rb index 2542510ce651f857287abf464700d9be07e969a8..ac170028f8ab1f90dad5b358e6869d4331da99e7 100644 --- a/ee/spec/services/security/security_orchestration_policies/process_scan_result_policy_service_spec.rb +++ b/ee/spec/services/security/security_orchestration_policies/process_scan_result_policy_service_spec.rb @@ -10,7 +10,7 @@ let(:policy) { build(:scan_result_policy, name: 'Test Policy') } let(:policy_yaml) { Gitlab::Config::Loader::Yaml.new(policy.to_yaml).load! } let(:project) { policy_configuration.project } - let(:service) { described_class.new(policy_configuration: policy_configuration, policy: policy) } + let(:service) { described_class.new(policy_configuration: policy_configuration, policy: policy, policy_index: 0) } before do allow(policy_configuration).to receive(:policy_last_updated_by).and_return(project.owner) diff --git a/ee/spec/workers/security/create_orchestration_policy_worker_spec.rb b/ee/spec/workers/security/create_orchestration_policy_worker_spec.rb index fbc1c91ac6c246f505012cf1c2c3f020d5bd3b13..23e4045586b21f6d1a0e582dbae9c255b96e02aa 100644 --- a/ee/spec/workers/security/create_orchestration_policy_worker_spec.rb +++ b/ee/spec/workers/security/create_orchestration_policy_worker_spec.rb @@ -63,9 +63,9 @@ end end - active_policies[:scan_result_policy].each do |policy| + active_policies[:scan_result_policy].each_with_index do |policy, policy_index| expect_next_instance_of(Security::SecurityOrchestrationPolicies::ProcessScanResultPolicyService, - policy_configuration: configuration, policy: policy) do |service| + policy_configuration: configuration, policy: policy, policy_index: policy_index) do |service| expect(service).to receive(:execute) end end