Commit 68c3d888 authored by Mehmet Emin INAC's avatar Mehmet Emin INAC

Synchronize dismissal state between finding and vulnerability records

We should remove the dismissal feedback entries associated with finding
records while changing the state of vulnerability records.
parent 459c7deb
...@@ -7,9 +7,13 @@ module Vulnerabilities ...@@ -7,9 +7,13 @@ module Vulnerabilities
def execute def execute
raise Gitlab::Access::AccessDeniedError unless authorized? raise Gitlab::Access::AccessDeniedError unless authorized?
@vulnerability.tap do |vulnerability| @vulnerability.transaction do
update_with_note(vulnerability, state: Vulnerability.states[:confirmed], confirmed_by: @user, confirmed_at: Time.current) DestroyDismissalFeedbackService.new(@user, @vulnerability).execute
update_with_note(@vulnerability, state: Vulnerability.states[:confirmed], confirmed_by: @user, confirmed_at: Time.current)
end end
@vulnerability
end end
end end
end end
# frozen_string_literal: true
require_dependency 'vulnerabilities/base_service'
# This service class removes all the dismissal feedback
# associated with a vulnerability through it's findings.
module Vulnerabilities
class DestroyDismissalFeedbackService < BaseService
def execute
@vulnerability.dismissed_findings.each do |finding|
unless destroy_feedback_for(finding)
handle_finding_revert_error(finding)
raise ActiveRecord::Rollback
end
end
end
private
def destroy_feedback_for(finding)
VulnerabilityFeedback::DestroyService
.new(@project, @user, finding.dismissal_feedback, revert_vulnerability_state: false)
.execute
end
def handle_finding_revert_error(finding)
@vulnerability.errors.add(
:base,
:finding_revert_to_detected_error,
message: _("failed to revert associated finding(id=%{finding_id}) to detected") %
{
finding_id: finding.id
})
end
end
end
...@@ -7,9 +7,13 @@ module Vulnerabilities ...@@ -7,9 +7,13 @@ module Vulnerabilities
def execute def execute
raise Gitlab::Access::AccessDeniedError unless authorized? raise Gitlab::Access::AccessDeniedError unless authorized?
@vulnerability.tap do |vulnerability| @vulnerability.transaction do
update_with_note(vulnerability, state: Vulnerability.states[:resolved], resolved_by: @user, resolved_at: Time.current) DestroyDismissalFeedbackService.new(@user, @vulnerability).execute
update_with_note(@vulnerability, state: Vulnerability.states[:resolved], resolved_by: @user, resolved_at: Time.current)
end end
@vulnerability
end end
end end
end end
...@@ -10,46 +10,12 @@ module Vulnerabilities ...@@ -10,46 +10,12 @@ module Vulnerabilities
raise Gitlab::Access::AccessDeniedError unless authorized? raise Gitlab::Access::AccessDeniedError unless authorized?
@vulnerability.transaction do @vulnerability.transaction do
revert_result = revert_findings_to_detected_state DestroyDismissalFeedbackService.new(@user, @vulnerability).execute
raise ActiveRecord::Rollback unless revert_result
update_with_note(@vulnerability, state: Vulnerability.states[:detected], **REVERT_PARAMS) update_with_note(@vulnerability, state: Vulnerability.states[:detected], **REVERT_PARAMS)
end end
@vulnerability @vulnerability
end end
private
def destroy_feedback_for(finding)
VulnerabilityFeedback::DestroyService
.new(@project, @user, finding.dismissal_feedback)
.execute
end
def revert_findings_to_detected_state
@vulnerability
.dismissed_findings
.each do |finding|
result = destroy_feedback_for(finding)
unless result
handle_finding_revert_error(finding)
return false
end
end
true
end
def handle_finding_revert_error(finding)
@vulnerability.errors.add(
:base,
:finding_revert_to_detected_error,
message: _("failed to revert associated finding(id=%{finding_id}) to detected") %
{
finding_id: finding.id
})
end
end end
end end
...@@ -2,15 +2,34 @@ ...@@ -2,15 +2,34 @@
module VulnerabilityFeedback module VulnerabilityFeedback
class DestroyService < ::BaseService class DestroyService < ::BaseService
def initialize(project, user, vulnerability_feedback) include Gitlab::Utils::StrongMemoize
@project, @current_user, @vulnerability_feedback = project, user, vulnerability_feedback
def initialize(project, user, vulnerability_feedback, revert_vulnerability_state: true)
@project, @current_user, @vulnerability_feedback, @revert_vulnerability_state = project, user, vulnerability_feedback, revert_vulnerability_state
end end
def execute def execute
# TODO: Add system note when destroying a dismissal feedback # TODO: Add system note when destroying a dismissal feedback
raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_vulnerability_feedback, @vulnerability_feedback) raise Gitlab::Access::AccessDeniedError unless can?(current_user, :destroy_vulnerability_feedback, vulnerability_feedback)
revert_vulnerability if revert_vulnerability_state?
vulnerability_feedback.destroy
end
private
attr_reader :vulnerability_feedback, :revert_vulnerability_state
def revert_vulnerability
Vulnerabilities::RevertToDetectedService.new(current_user, existing_vulnerability).execute
end
def revert_vulnerability_state?
revert_vulnerability_state && existing_vulnerability
end
@vulnerability_feedback.destroy def existing_vulnerability
strong_memoize(:existing_vulnerability) { vulnerability_feedback.finding&.vulnerability }
end end
end end
end end
...@@ -22,6 +22,7 @@ RSpec.describe Vulnerabilities::ConfirmService do ...@@ -22,6 +22,7 @@ RSpec.describe Vulnerabilities::ConfirmService do
end end
it_behaves_like 'calls vulnerability statistics utility services in order' it_behaves_like 'calls vulnerability statistics utility services in order'
it_behaves_like 'removes dismissal feedback from associated findings'
it 'confirms a vulnerability' do it 'confirms a vulnerability' do
Timecop.freeze do Timecop.freeze do
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Vulnerabilities::DestroyDismissalFeedbackService do
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:vulnerability) { create(:vulnerability, project: project) }
before(:all) do
finding_1 = create(:vulnerabilities_finding, project: project)
finding_2 = create(:vulnerabilities_finding, project: project)
create(:vulnerability_feedback, project: project, category: finding_1.report_type, project_fingerprint: finding_1.project_fingerprint)
create(:vulnerability_feedback, project: project, category: finding_2.report_type, project_fingerprint: finding_2.project_fingerprint)
create(:vulnerability_feedback)
vulnerability.findings << finding_1
vulnerability.findings << finding_2
end
describe '#execute' do
subject(:destroy_feedback) { described_class.new(user, vulnerability).execute }
context 'without necessary permissions' do
it 'raises `Gitlab::Access::AccessDeniedError` error' do
expect { destroy_feedback }.to raise_error(Gitlab::Access::AccessDeniedError)
.and not_change { Vulnerabilities::Feedback.count }
end
end
context 'with necessary permissions' do
before do
project.add_developer(user)
end
it 'destroys the feedback records associated with the findings of the given vulnerability' do
expect { destroy_feedback }.to change { Vulnerabilities::Feedback.count }.from(3).to(1)
end
end
end
end
...@@ -22,6 +22,7 @@ RSpec.describe Vulnerabilities::ResolveService do ...@@ -22,6 +22,7 @@ RSpec.describe Vulnerabilities::ResolveService do
end end
it_behaves_like 'calls vulnerability statistics utility services in order' it_behaves_like 'calls vulnerability statistics utility services in order'
it_behaves_like 'removes dismissal feedback from associated findings'
it 'resolves a vulnerability' do it 'resolves a vulnerability' do
Timecop.freeze do Timecop.freeze do
......
...@@ -23,7 +23,6 @@ RSpec.describe Vulnerabilities::RevertToDetectedService do ...@@ -23,7 +23,6 @@ RSpec.describe Vulnerabilities::RevertToDetectedService do
expect(vulnerability.reload).to( expect(vulnerability.reload).to(
have_attributes(state: 'detected', dismissed_by: nil, dismissed_at: nil, resolved_by: nil, resolved_at: nil, confirmed_by: nil, confirmed_at: nil)) have_attributes(state: 'detected', dismissed_by: nil, dismissed_at: nil, resolved_by: nil, resolved_at: nil, confirmed_by: nil, confirmed_at: nil))
expect(vulnerability.findings).to all not_have_vulnerability_dismissal_feedback
end end
end end
...@@ -34,6 +33,7 @@ RSpec.describe Vulnerabilities::RevertToDetectedService do ...@@ -34,6 +33,7 @@ RSpec.describe Vulnerabilities::RevertToDetectedService do
end end
it_behaves_like 'calls vulnerability statistics utility services in order' it_behaves_like 'calls vulnerability statistics utility services in order'
it_behaves_like 'removes dismissal feedback from associated findings'
end end
context 'with an authorized user with proper permissions' do context 'with an authorized user with proper permissions' do
...@@ -59,23 +59,6 @@ RSpec.describe Vulnerabilities::RevertToDetectedService do ...@@ -59,23 +59,6 @@ RSpec.describe Vulnerabilities::RevertToDetectedService do
include_examples 'reverts vulnerability' include_examples 'reverts vulnerability'
end end
context 'when there is an error' do
let(:broken_finding) { vulnerability.findings.first }
let!(:dismissal_feedback) do
create(:vulnerability_feedback, :dismissal, project: broken_finding.project, project_fingerprint: broken_finding.project_fingerprint)
end
before do
allow(service).to receive(:destroy_feedback_for).and_return(false)
end
it 'responds with error' do
expect(revert_vulnerability_to_detected.errors.messages).to eq(
base: ["failed to revert associated finding(id=#{broken_finding.id}) to detected"])
end
end
context 'when security dashboard feature is disabled' do context 'when security dashboard feature is disabled' do
before do before do
stub_licensed_features(security_dashboard: false) stub_licensed_features(security_dashboard: false)
......
...@@ -3,32 +3,67 @@ ...@@ -3,32 +3,67 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe VulnerabilityFeedback::DestroyService, '#execute' do RSpec.describe VulnerabilityFeedback::DestroyService, '#execute' do
let(:group) { create(:group) } let(:project) { create(:project, :public, :repository) }
let(:project) { create(:project, :public, :repository, namespace: group) }
let(:user) { create(:user) } let(:user) { create(:user) }
let(:vulnerability_feedback) { create(:vulnerability_feedback, feedback_type, project: project)} let(:vulnerability_feedback) { create(:vulnerability_feedback, feedback_type, project: project)}
let(:revert_vulnerability_state) { true }
let(:service_object) { described_class.new(project, user, vulnerability_feedback, revert_vulnerability_state: revert_vulnerability_state) }
before do before do
group.add_developer(user) project.add_developer(user)
stub_licensed_features(security_dashboard: true)
end end
subject { described_class.new(project, user, vulnerability_feedback).execute } subject(:destroy_feedback) { service_object.execute }
context 'when feedback_type is dismissal' do context 'when feedback_type is dismissal' do
let(:feedback_type) { :dismissal } let(:feedback_type) { :dismissal }
it 'destroys the feedback' do context 'when the user is authorized' do
subject context 'when the `revert_vulnerability_state` argument is set as true' do
context 'when the finding is not associated with a vulnerability' do
it 'destroys the feedback' do
expect { destroy_feedback }.to change { vulnerability_feedback.destroyed? }.to(true)
end
end
expect { vulnerability_feedback.reload }.to raise_error ActiveRecord::RecordNotFound context 'when the finding is associated with a vulnerability' do
let(:finding) { create(:vulnerabilities_finding, :dismissed, project: project) }
let(:vulnerability_feedback) { finding.dismissal_feedback }
it 'changes the state of the vulnerability to `detected`' do
expect { destroy_feedback }.to change { finding.vulnerability.reload.state }.from('dismissed').to('detected')
end
end
end
context 'when the `revert_vulnerability_state` argument is set as false' do
let(:revert_vulnerability_state) { false }
context 'when the finding is not associated with a vulnerability' do
it 'destroys the feedback' do
expect { destroy_feedback }.to change { vulnerability_feedback.destroyed? }.to(true)
end
end
context 'when the finding is associated with a vulnerability' do
let(:finding) { create(:vulnerabilities_finding, :dismissed, project: project) }
let(:vulnerability_feedback) { finding.dismissal_feedback }
it 'does not change the state of the vulnerability to `detected`' do
expect { destroy_feedback }.not_to change { finding.vulnerability.reload.state }
end
end
end
end end
context 'when user is not authorized' do context 'when user is not authorized' do
let(:unauthorized_user) { create(:user) } before do
project.add_guest(user)
end
it 'raise error if permission is denied' do it 'raise error if permission is denied' do
expect { described_class.new(project, unauthorized_user, vulnerability_feedback).execute } expect { destroy_feedback }.to raise_error(Gitlab::Access::AccessDeniedError)
.to raise_error(Gitlab::Access::AccessDeniedError)
end end
end end
end end
...@@ -37,8 +72,7 @@ RSpec.describe VulnerabilityFeedback::DestroyService, '#execute' do ...@@ -37,8 +72,7 @@ RSpec.describe VulnerabilityFeedback::DestroyService, '#execute' do
let(:feedback_type) { :issue } let(:feedback_type) { :issue }
it 'raise error as this type of feedback can not be destroyed' do it 'raise error as this type of feedback can not be destroyed' do
expect { described_class.new(project, user, vulnerability_feedback).execute } expect { destroy_feedback }.to raise_error(Gitlab::Access::AccessDeniedError)
.to raise_error(Gitlab::Access::AccessDeniedError)
end end
end end
...@@ -46,8 +80,7 @@ RSpec.describe VulnerabilityFeedback::DestroyService, '#execute' do ...@@ -46,8 +80,7 @@ RSpec.describe VulnerabilityFeedback::DestroyService, '#execute' do
let(:feedback_type) { :merge_request } let(:feedback_type) { :merge_request }
it 'raise error as this type of feedback can not be destroyed' do it 'raise error as this type of feedback can not be destroyed' do
expect { described_class.new(project, user, vulnerability_feedback).execute } expect { destroy_feedback }.to raise_error(Gitlab::Access::AccessDeniedError)
.to raise_error(Gitlab::Access::AccessDeniedError)
end end
end end
end end
# frozen_string_literal: true
RSpec.shared_examples 'removes dismissal feedback from associated findings' do
let(:finding) { create(:vulnerabilities_finding, vulnerability: vulnerability, project: vulnerability.project) }
before do
create(:vulnerability_feedback,
:dismissal,
project: finding.project,
category: finding.report_type,
project_fingerprint: finding.project_fingerprint)
end
context 'when there is no error' do
it 'removes dismissal feedback from associated findings' do
expect { subject }.to change { Vulnerabilities::Feedback.count }.by(-1)
end
end
context 'when there is an error' do
before do
allow_next_instance_of(VulnerabilityFeedback::DestroyService) do |destroy_service_object|
allow(destroy_service_object).to receive(:execute).and_return(false)
end
end
it 'does not remove any feedback' do
expect { subject }.not_to change { Vulnerabilities::Feedback.count }
end
it 'responds with error' do
expect(subject.errors.messages).to eq(
base: ["failed to revert associated finding(id=#{finding.id}) to detected"])
end
end
end
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment