diff --git a/ee/app/services/vulnerabilities/bulk_dismiss_service.rb b/ee/app/services/vulnerabilities/bulk_dismiss_service.rb index 488a5d1898bfd5e86b85d992e2de0e8f74bbea08..3f97fc20d9a97c784868f7708c8630896e92f2ae 100644 --- a/ee/app/services/vulnerabilities/bulk_dismiss_service.rb +++ b/ee/app/services/vulnerabilities/bulk_dismiss_service.rb @@ -43,14 +43,12 @@ def vulnerabilities_to_update(ids) Vulnerability.id_in(ids) end - def update_support_tables(vulnerabilities, db_attributes) - Vulnerabilities::StateTransition.insert_all!(db_attributes[:state_transitions]) - # The `insert_or_update_vulnerability_reads` database trigger does not - # update the dismissal_reason and we are moving away from using - # database triggers to keep tables up to date. - Vulnerabilities::Read - .by_vulnerabilities(vulnerabilities) - .update_all(dismissal_reason: dismissal_reason, auto_resolved: false) + def update_support_tables(_vulnerabilities, db_attributes) + Vulnerabilities::BulkModify.modify do + Vulnerabilities::StateTransition.insert_all!(db_attributes[:state_transitions]) + + { Vulnerabilities::StateTransition => db_attributes[:state_transitions] } + end end def vulnerabilities_attributes(vulnerabilities) diff --git a/ee/app/services/vulnerabilities/bulk_modify.rb b/ee/app/services/vulnerabilities/bulk_modify.rb new file mode 100644 index 0000000000000000000000000000000000000000..f64fcdcc766798755186b66bc0eeee09dddf11fc --- /dev/null +++ b/ee/app/services/vulnerabilities/bulk_modify.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +module Vulnerabilities + class BulkModify + @after_save = {} + + def self.modify + Thread.current['deliberate_modification'] = true + + result = yield + apply_callbacks(result) + + result + ensure + Thread.current['deliberate_modification'] = false + end + + def self.after_save(method_name, if_condition = nil) + @after_save ||= {} + @after_save[method_name] = if_condition + end + + def self.apply_callbacks(changes) + (@after_save || {}).each do |method_name, condition| + next unless condition.nil? || condition.call(changes) + + # rubocop:disable GitlabSecurity/PublicSend -- Doing otherwise here would require a redudnant case statement + send(method_name, changes) + # rubocop:enable GitlabSecurity/PublicSend + end + end + + def self.update_vulnerability_reads_dismissal_reason(changes) + changes = changes[Vulnerabilities::StateTransition] + + Vulnerabilities::Read.by_vulnerabilities( + # rubocop:disable CodeReuse/ActiveRecord -- changes is a hash, not a relation + # rubocop:disable Database/AvoidUsingPluckWithoutLimit -- changes is a hash, not a relation + changes.pluck(:vulnerability_id) + # rubocop:enable CodeReuse/ActiveRecord + # rubocop:enable Database/AvoidUsingPluckWithoutLimit + ).update_all(dismissal_reason: changes.first[:dismissal_reason]) + end + + # If the changed records are state transitions, where the dismissal_reason was bulk set to the same value, + # we want to propogate this to the vulnerability reads for those state transitions + after_save :update_vulnerability_reads_dismissal_reason, ->(changes) { + dismissal_reason_changes = changes[Vulnerabilities::StateTransition]&.pluck(:dismissal_reason) + + dismissal_reason_changes.uniq.compact.size == 1 + } + end +end