Commit d90de582 authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by Jan Provaznik

Add filtering vulnerabilities visible by user in Vulnerabilities Finder

This change extends VulnerabilitiesFinder to accept current_user
parameter that is used in FinderMethods to filter only vulnerabilities
that are accessible by the user.
parent 61a5d261
# frozen_string_literal: true
module Autocomplete
class VulnerabilitiesAutocompleteFinder
attr_reader :current_user, :vulnerable, :params
# current_user - the User object of the user that wants to view the list of Vulnerabilities
#
# vulnerable - any object that has a #vulnerabilities method that returns a collection of vulnerabilitie
# params - a Hash containing additional parameters to set
#
# The supported parameters are those supported by
# `Security::VulnerabilitiesFinder`.
def initialize(current_user, vulnerable, params = {})
@current_user = current_user
@vulnerable = vulnerable
@params = params
end
DEFAULT_AUTOCOMPLETE_LIMIT = 5
def execute
return [] unless vulnerable.feature_available?(:security_dashboard)
::Security::VulnerabilitiesFinder # rubocop: disable CodeReuse/Finder
.new(vulnerable, params)
.execute
.autocomplete_search(params[:search].to_s)
.with_limit(DEFAULT_AUTOCOMPLETE_LIMIT)
.order_id_desc
.visible_to_user_and_access_level(current_user, ::Gitlab::Access::DEVELOPER)
end
end
end
...@@ -12,6 +12,7 @@ module EE ...@@ -12,6 +12,7 @@ module EE
include ::Awardable include ::Awardable
include ::Referable include ::Referable
include ::Presentable include ::Presentable
include ::Gitlab::SQL::Pattern
TooManyDaysError = Class.new(StandardError) TooManyDaysError = Class.new(StandardError)
...@@ -71,6 +72,7 @@ module EE ...@@ -71,6 +72,7 @@ module EE
scope :with_findings_scanner_and_identifiers, -> { includes(findings: [:scanner, :identifiers, finding_identifiers: :identifier]) } scope :with_findings_scanner_and_identifiers, -> { includes(findings: [:scanner, :identifiers, finding_identifiers: :identifier]) }
scope :with_created_issue_links_and_issues, -> { includes(created_issue_links: :issue) } scope :with_created_issue_links_and_issues, -> { includes(created_issue_links: :issue) }
scope :visible_to_user_and_access_level, -> (user, access_level) { where(project_id: ::Project.visible_to_user_and_access_level(user, access_level)) }
scope :for_projects, -> (project_ids) { where(project_id: project_ids) } scope :for_projects, -> (project_ids) { where(project_id: project_ids) }
scope :with_report_types, -> (report_types) { where(report_type: report_types) } scope :with_report_types, -> (report_types) { where(report_type: report_types) }
scope :with_severities, -> (severities) { where(severity: severities) } scope :with_severities, -> (severities) { where(severity: severities) }
...@@ -86,12 +88,24 @@ module EE ...@@ -86,12 +88,24 @@ module EE
where(exist_query, ::Vulnerabilities::IssueLink.select(1).where(issue_links[:vulnerability_id].eq(arel_table[:id]))) where(exist_query, ::Vulnerabilities::IssueLink.select(1).where(issue_links[:vulnerability_id].eq(arel_table[:id])))
end end
scope :autocomplete_search, -> (query) do
return self if query.blank?
id_as_text = Arel::Nodes::NamedFunction.new('CAST', [arel_table[:id].as('TEXT')])
fuzzy_search(query, [:title])
.or(where(id_as_text.matches("%#{sanitize_sql_like(query.squish)}%")))
end
scope :order_severity_asc, -> { reorder(severity: :asc, id: :desc) } scope :order_severity_asc, -> { reorder(severity: :asc, id: :desc) }
scope :order_severity_desc, -> { reorder(severity: :desc, id: :desc) } scope :order_severity_desc, -> { reorder(severity: :desc, id: :desc) }
scope :order_title_asc, -> { reorder(title: :asc, id: :desc) } scope :order_title_asc, -> { reorder(title: :asc, id: :desc) }
scope :order_title_desc, -> { reorder(title: :desc, id: :desc) } scope :order_title_desc, -> { reorder(title: :desc, id: :desc) }
scope :order_created_at_asc, -> { reorder(created_at: :asc, id: :desc) } scope :order_created_at_asc, -> { reorder(created_at: :asc, id: :desc) }
scope :order_created_at_desc, -> { reorder(created_at: :desc, id: :desc) } scope :order_created_at_desc, -> { reorder(created_at: :desc, id: :desc) }
scope :order_id_desc, -> { reorder(id: :desc) }
scope :with_limit, -> (maximum) { limit(maximum) }
delegate :scanner_name, :scanner_external_id, :metadata, :message, :cve, :description, delegate :scanner_name, :scanner_external_id, :metadata, :message, :cve, :description,
to: :finding, prefix: true, allow_nil: true to: :finding, prefix: true, allow_nil: true
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Autocomplete::VulnerabilitiesAutocompleteFinder do
describe '#execute' do
let_it_be(:group, refind: true) { create(:group) }
let_it_be(:project, refind: true) { create(:project, group: group) }
let_it_be(:vulnerability) { create(:vulnerability, project: project) }
let(:params) { {} }
let_it_be(:user) { create(:user) }
subject { described_class.new(user, vulnerable, params).execute }
shared_examples 'autocomplete vulnerabilities finder' do
context 'when user does not have access to project' do
it { is_expected.to be_empty }
end
context 'when user has access to project' do
before do
vulnerable.add_developer(user)
end
context 'when security dashboards are not enabled' do
it { is_expected.to be_empty }
end
context 'when security dashboards are enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
it { is_expected.to match_array([vulnerability]) }
context 'when multiple vulnerabilities are found' do
before do
create_list(:vulnerability, 10, project: project)
end
it 'returns max 5 items' do
expect(subject.count).to eq(5)
end
it 'is sorted descending by id' do
expect(subject).to be_sorted(:id, :desc)
end
end
context 'when search is provided in params' do
context 'and it matches ID of vulnerability' do
let(:params) { { search: vulnerability.id.to_s } }
it { is_expected.to match_array([vulnerability]) }
end
context 'and it matches title of vulnerability' do
let(:params) { { search: vulnerability.title } }
it { is_expected.to match_array([vulnerability]) }
end
context 'and it does not match neither title or id of vulnerability' do
let(:params) { { search: non_existing_record_id.to_s } }
it { is_expected.to be_empty }
end
end
end
end
end
context 'when vulnerable is project' do
let(:vulnerable) { project }
it_behaves_like 'autocomplete vulnerabilities finder'
end
context 'when vulnerable is group' do
let(:vulnerable) { group }
it_behaves_like 'autocomplete vulnerabilities finder'
end
end
end
...@@ -92,6 +92,70 @@ RSpec.describe Vulnerability do ...@@ -92,6 +92,70 @@ RSpec.describe Vulnerability do
end end
end end
describe '.visible_to_user_and_access_level' do
let(:project1) { create(:project) }
let(:project2) { create(:project) }
let!(:vulnerability1) { create(:vulnerability, project: project1) }
let!(:vulnerability2) { create(:vulnerability, project: project2) }
let(:user) { create(:user) }
before do
project1.add_developer(user)
end
subject { described_class.visible_to_user_and_access_level(user, ::Gitlab::Access::DEVELOPER) }
it 'returns vulnerabilities visible for given user with provided access level' do
is_expected.to contain_exactly(vulnerability1)
end
end
describe '.with_limit' do
let(:project) { create(:project) }
let!(:vulnerabilities) { create_list(:vulnerability, 10, project: project) }
subject { described_class.with_limit(5) }
it 'returns vulnerabilities limited by provided value' do
expect(subject.count).to eq(5)
end
end
describe '.autocomplete_search' do
using RSpec::Parameterized::TableSyntax
let(:project) { create(:project) }
let!(:vulnerability_1) { create(:vulnerability, title: 'Predictable pseudorandom number generator') }
let!(:vulnerability_2) { create(:vulnerability, title: 'Use of pseudorandom MD2, MD4, or MD5 hash function.') }
subject { described_class.autocomplete_search(search) }
where(:search, :filtered_vulnerabilities) do
'PSEUDORANDOM' | [:vulnerability_1, :vulnerability_2]
'Predictable PSEUDORANDOM' | [:vulnerability_1]
'mD2' | [:vulnerability_2]
end
with_them do
it 'returns the vulnerabilities filtered' do
expect(subject).to match_array(filtered_vulnerabilities.map { |name| public_send(name) })
end
end
context 'when id is used in search params' do
let(:search) { vulnerability_1.id.to_s }
it { is_expected.to match_array([vulnerability_1]) }
end
context 'when query is empty' do
let(:search) { '' }
it { is_expected.to match_array([vulnerability_1, vulnerability_2]) }
end
end
describe '.for_projects' do describe '.for_projects' do
let(:project1) { create(:project) } let(:project1) { create(:project) }
let(:project2) { create(:project) } let(:project2) { create(:project) }
...@@ -225,6 +289,18 @@ RSpec.describe Vulnerability do ...@@ -225,6 +289,18 @@ RSpec.describe Vulnerability do
end end
end end
describe '.order_id_desc' do
subject { described_class.order_id_desc }
before do
create_list(:vulnerability, 10)
end
it 'returns vulnerabilities ordered by id' do
is_expected.to be_sorted(:id, :desc)
end
end
describe '.with_resolution' do describe '.with_resolution' do
let_it_be(:vulnerability_with_resolution) { create(:vulnerability, resolved_on_default_branch: true) } let_it_be(:vulnerability_with_resolution) { create(:vulnerability, resolved_on_default_branch: true) }
let_it_be(:vulnerability_without_resolution) { create(:vulnerability, resolved_on_default_branch: false) } let_it_be(:vulnerability_without_resolution) { create(:vulnerability, resolved_on_default_branch: false) }
......
# frozen_string_literal: true
# Assert that this collection is sorted by argument and order
#
# By default, this checks that the collection is sorted ascending
# but you can check order by specific field and order by passing
# them, eg:
#
# ```
# expect(collection).to be_sorted(:field, :desc)
# ```
RSpec::Matchers.define :be_sorted do |by, order = :asc|
match do |actual|
next true unless actual.present? # emtpy collection is sorted
actual
.then { |collection| by ? collection.sort_by(&by) : collection.sort }
.then { |sorted_collection| order.to_sym == :desc ? sorted_collection.reverse : sorted_collection }
.then { |sorted_collection| sorted_collection == actual }
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