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
def execute
raise Gitlab::Access::AccessDeniedError unless authorized?
@vulnerability.tap do |vulnerability|
update_with_note(vulnerability, state: Vulnerability.states[:confirmed], confirmed_by: @user, confirmed_at: Time.current)
@vulnerability.transaction do
DestroyDismissalFeedbackService.new(@user, @vulnerability).execute
update_with_note(@vulnerability, state: Vulnerability.states[:confirmed], confirmed_by: @user, confirmed_at: Time.current)
end
@vulnerability
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
def execute
raise Gitlab::Access::AccessDeniedError unless authorized?
@vulnerability.tap do |vulnerability|
update_with_note(vulnerability, state: Vulnerability.states[:resolved], resolved_by: @user, resolved_at: Time.current)
@vulnerability.transaction do
DestroyDismissalFeedbackService.new(@user, @vulnerability).execute
update_with_note(@vulnerability, state: Vulnerability.states[:resolved], resolved_by: @user, resolved_at: Time.current)
end
@vulnerability
end
end
end
......@@ -10,46 +10,12 @@ module Vulnerabilities
raise Gitlab::Access::AccessDeniedError unless authorized?
@vulnerability.transaction do
revert_result = revert_findings_to_detected_state
raise ActiveRecord::Rollback unless revert_result
DestroyDismissalFeedbackService.new(@user, @vulnerability).execute
update_with_note(@vulnerability, state: Vulnerability.states[:detected], **REVERT_PARAMS)
end
@vulnerability
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
......@@ -2,15 +2,34 @@
module VulnerabilityFeedback
class DestroyService < ::BaseService
def initialize(project, user, vulnerability_feedback)
@project, @current_user, @vulnerability_feedback = project, user, vulnerability_feedback
include Gitlab::Utils::StrongMemoize
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
def execute
# 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
......@@ -22,6 +22,7 @@ RSpec.describe Vulnerabilities::ConfirmService do
end
it_behaves_like 'calls vulnerability statistics utility services in order'
it_behaves_like 'removes dismissal feedback from associated findings'
it 'confirms a vulnerability' 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
end
it_behaves_like 'calls vulnerability statistics utility services in order'
it_behaves_like 'removes dismissal feedback from associated findings'
it 'resolves a vulnerability' do
Timecop.freeze do
......
......@@ -23,7 +23,6 @@ RSpec.describe Vulnerabilities::RevertToDetectedService do
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))
expect(vulnerability.findings).to all not_have_vulnerability_dismissal_feedback
end
end
......@@ -34,6 +33,7 @@ RSpec.describe Vulnerabilities::RevertToDetectedService do
end
it_behaves_like 'calls vulnerability statistics utility services in order'
it_behaves_like 'removes dismissal feedback from associated findings'
end
context 'with an authorized user with proper permissions' do
......@@ -59,23 +59,6 @@ RSpec.describe Vulnerabilities::RevertToDetectedService do
include_examples 'reverts vulnerability'
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
before do
stub_licensed_features(security_dashboard: false)
......
......@@ -3,32 +3,67 @@
require 'spec_helper'
RSpec.describe VulnerabilityFeedback::DestroyService, '#execute' do
let(:group) { create(:group) }
let(:project) { create(:project, :public, :repository, namespace: group) }
let(:project) { create(:project, :public, :repository) }
let(:user) { create(:user) }
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
group.add_developer(user)
project.add_developer(user)
stub_licensed_features(security_dashboard: true)
end
subject { described_class.new(project, user, vulnerability_feedback).execute }
subject(:destroy_feedback) { service_object.execute }
context 'when feedback_type is dismissal' do
let(:feedback_type) { :dismissal }
it 'destroys the feedback' do
subject
context 'when the user is authorized' do
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
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
expect { described_class.new(project, unauthorized_user, vulnerability_feedback).execute }
.to raise_error(Gitlab::Access::AccessDeniedError)
expect { destroy_feedback }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
end
......@@ -37,8 +72,7 @@ RSpec.describe VulnerabilityFeedback::DestroyService, '#execute' do
let(:feedback_type) { :issue }
it 'raise error as this type of feedback can not be destroyed' do
expect { described_class.new(project, user, vulnerability_feedback).execute }
.to raise_error(Gitlab::Access::AccessDeniedError)
expect { destroy_feedback }.to raise_error(Gitlab::Access::AccessDeniedError)
end
end
......@@ -46,8 +80,7 @@ RSpec.describe VulnerabilityFeedback::DestroyService, '#execute' do
let(:feedback_type) { :merge_request }
it 'raise error as this type of feedback can not be destroyed' do
expect { described_class.new(project, user, vulnerability_feedback).execute }
.to raise_error(Gitlab::Access::AccessDeniedError)
expect { destroy_feedback }.to raise_error(Gitlab::Access::AccessDeniedError)
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