Commit fb224e3b authored by Dmytro Zaporozhets (DZ)'s avatar Dmytro Zaporozhets (DZ)

Merge branch '222483-add-special-reference-for-vulnerabilities' into 'master'

Enable Special References for Vulnerabilities

See merge request gitlab-org/gitlab!41983
parents 23aa7710 d4f0c229
......@@ -34,7 +34,7 @@ class Projects::AutocompleteSourcesController < Projects::ApplicationController
private
def autocomplete_service
@autocomplete_service ||= ::Projects::AutocompleteService.new(@project, current_user)
@autocomplete_service ||= ::Projects::AutocompleteService.new(@project, current_user, params)
end
def target
......
......@@ -159,6 +159,7 @@ module NotesHelper
members: autocomplete,
issues: autocomplete,
mergeRequests: autocomplete,
vulnerabilities: autocomplete,
epics: autocomplete,
milestones: autocomplete,
labels: autocomplete
......
......@@ -22,7 +22,7 @@ module Mentionable
def self.default_pattern
strong_memoize(:default_pattern) do
issue_pattern = Issue.reference_pattern
link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic].map(&:link_reference_pattern).compact)
link_patterns = Regexp.union([Issue, Commit, MergeRequest, Epic, Vulnerability].map(&:link_reference_pattern).compact)
reference_pattern(link_patterns, issue_pattern)
end
end
......
......@@ -5,6 +5,10 @@
class Vulnerability < ApplicationRecord
include IgnorableColumns
def self.link_reference_pattern
nil
end
def self.reference_prefix
'+'
end
......
......@@ -427,6 +427,7 @@ GFM recognizes the following:
| merge request | `!123` | `namespace/project!123` | `project!123` |
| snippet | `$123` | `namespace/project$123` | `project$123` |
| epic **(ULTIMATE)** | `&123` | `group1/subgroup&123` | |
| vulnerability **(ULTIMATE)** | `+123` | `namespace/project+123` | `project+123` |
| label by ID | `~123` | `namespace/project~123` | `project~123` |
| one-word label by name | `~bug` | `namespace/project~bug` | `project~bug` |
| multi-word label by name | `~"feature request"` | `namespace/project~"feature request"` | `project~"feature request"` |
......
......@@ -10,6 +10,12 @@ module EE
render json: autocomplete_service.epics
end
def vulnerabilities
return render_404 unless project.feature_available?(:security_dashboard)
render json: autocomplete_service.vulnerabilities
end
end
end
end
......@@ -7,6 +7,7 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
feature_category :issue_tracking, [:issues, :labels, :milestones, :commands]
feature_category :code_review, [:merge_requests]
feature_category :epics, [:epics]
feature_category :vulnerability_management, [:vulnerabilities]
def members
render json: ::Groups::ParticipantsService.new(@group, current_user).execute(target)
......@@ -31,6 +32,10 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
render json: @autocomplete_service.epics(confidential_only: params[:confidential_only])
end
def vulnerabilities
render json: issuable_serializer.represent(@autocomplete_service.vulnerabilities, parent_group: @group)
end
def commands
render json: @autocomplete_service.commands(target)
end
......@@ -42,7 +47,7 @@ class Groups::AutocompleteSourcesController < Groups::ApplicationController
private
def load_autocomplete_service
@autocomplete_service = ::Groups::AutocompleteService.new(@group, current_user)
@autocomplete_service = ::Groups::AutocompleteService.new(@group, current_user, params)
end
def issuable_serializer
......
......@@ -20,10 +20,10 @@ module Autocomplete
DEFAULT_AUTOCOMPLETE_LIMIT = 5
def execute
return [] unless vulnerable.feature_available?(:security_dashboard)
return ::Vulnerability.none unless vulnerable.feature_available?(:security_dashboard)
::Security::VulnerabilitiesFinder # rubocop: disable CodeReuse/Finder
.new(vulnerable, params)
.new(vulnerable)
.execute
.autocomplete_search(params[:search].to_s)
.with_limit(DEFAULT_AUTOCOMPLETE_LIMIT)
......
......@@ -92,13 +92,15 @@ module EE
issues: issues_group_autocomplete_sources_path(object),
mergeRequests: merge_requests_group_autocomplete_sources_path(object),
epics: epics_group_autocomplete_sources_path(object),
vulnerabilities: vulnerabilities_group_autocomplete_sources_path(object),
commands: commands_group_autocomplete_sources_path(object, type: noteable_type, type_id: params[:id]),
milestones: milestones_group_autocomplete_sources_path(object)
}
elsif object.group&.feature_available?(:epics)
{ epics: epics_project_autocomplete_sources_path(object) }.merge(super)
else
super
{
epics: object.group&.feature_available?(:epics) ? epics_project_autocomplete_sources_path(object) : nil,
vulnerabilities: object.feature_available?(:security_dashboard) ? vulnerabilities_project_autocomplete_sources_path(object) : nil
}.compact.merge(super)
end
end
......
......@@ -11,7 +11,8 @@ module EE
override :other_patterns
def other_patterns
super.unshift(
::Epic.reference_pattern
::Epic.reference_pattern,
::Vulnerability.reference_pattern
)
end
end
......
......@@ -75,6 +75,7 @@ module EE
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_ids, -> (ids) { where(id: ids) }
scope :for_projects, -> (project_ids) { where(project_id: project_ids) }
scope :with_report_types, -> (report_types) { where(report_type: report_types) }
scope :with_severities, -> (severities) { where(severity: severities) }
......@@ -168,6 +169,29 @@ module EE
end
class_methods do
def reference_pattern
@reference_pattern ||= %r{
(#{::Project.reference_pattern})?
#{Regexp.escape(reference_prefix)}(?<vulnerability>\d+)
}x
end
def link_reference_pattern
%r{
(?<url>
#{Regexp.escape(::Gitlab.config.gitlab.url)}
\/#{::Project.reference_pattern}
(?:\/\-)
\/security\/vulnerabilities
\/(?<vulnerability>\d+)
(?<path>
(\/[a-z0-9_=-]+)*\/*
)?
(?<anchor>\#[a-z0-9_-]+)?
)
}x
end
def parent_class
::Project
end
......
# frozen_string_literal: true
class GroupIssuableAutocompleteEntity < Grape::Entity
expose :iid
expose :iid, if: -> (e, _) { e.respond_to?(:iid) }
expose :id, if: -> (e, _) { !e.respond_to?(:iid) }
expose :title
expose :reference do |issuable, options|
issuable.to_reference(options[:parent_group])
......
......@@ -7,6 +7,13 @@ module EE
.new(current_user, group_id: project.group&.id, state: 'opened')
.execute.select([:iid, :title])
end
def vulnerabilities
::Autocomplete::VulnerabilitiesAutocompleteFinder
.new(current_user, project, params)
.execute
.select([:id, :title])
end
end
end
end
......@@ -37,6 +37,13 @@ module Groups
.select(:iid, :title)
end
def vulnerabilities
::Autocomplete::VulnerabilitiesAutocompleteFinder
.new(current_user, group, params)
.execute
.select([:id, :title, :project_id])
end
# rubocop: disable CodeReuse/ActiveRecord
def milestones
group_ids = group.self_and_ancestors.public_or_visible_to_user(current_user).pluck(:id)
......
---
title: Enable Special References for Vulnerabilities
merge_request: 41983
author:
type: added
......@@ -85,6 +85,7 @@ constraints(::Constraints::GroupUrlConstrainer.new) do
get 'merge_requests'
get 'labels'
get 'epics'
get 'vulnerabilities'
get 'commands'
get 'milestones'
end
......
......@@ -22,6 +22,7 @@ constraints(::Constraints::ProjectUrlConstrainer.new) do
resources :autocomplete_sources, only: [] do
collection do
get 'epics'
get 'vulnerabilities'
end
end
......
# frozen_string_literal: true
module EE
module Banzai
module Filter
# HTML filter that replaces vulnerability references with links. References to
# vulnerabilities that do not exist are ignored.
#
# This filter supports cross-project/group references.
module VulnerabilityReferenceFilter
extend ActiveSupport::Concern
class_methods do
def references_in(text, pattern = object_class.reference_pattern)
text.gsub(pattern) do |match|
symbol = $~[object_sym]
if object_class.reference_valid?(symbol)
yield match, symbol.to_i, $~[:project], $~[:namespace], $~
else
match
end
end
end
end
def unescape_link(href)
return href if href =~ object_class.reference_pattern
super
end
def url_for_object(vulnerability, project)
urls = ::Gitlab::Routing.url_helpers
urls.project_security_vulnerability_url(project, vulnerability, only_path: context[:only_path])
end
def data_attributes_for(text, project, object, link_content: false, link_reference: false)
{
original: escape_html_entities(text),
link: link_content,
link_reference: link_reference,
project: project.id,
object_sym => object.id
}
end
def parent_records(parent, ids)
return Vulnerabilities.none if ids.blank? || parent.nil?
parent.vulnerabilities.for_ids(ids.to_a)
end
def record_identifier(record)
record.id.to_i
end
private
def parent_type
:project
end
end
end
end
end
......@@ -4,15 +4,20 @@ module EE
module Banzai
module IssuableExtractor
EPIC_REFERENCE_TYPE = '@data-reference-type="epic"'.freeze
VULNERABILITY_REFERENCE_TYPE = '@data-reference-type="vulnerability"'.freeze
private
def reference_types
super.push(EPIC_REFERENCE_TYPE)
super
.push(EPIC_REFERENCE_TYPE)
.push(VULNERABILITY_REFERENCE_TYPE)
end
def parsers
super.push(::Banzai::ReferenceParser::EpicParser.new(context))
super
.push(::Banzai::ReferenceParser::EpicParser.new(context))
.push(::Banzai::ReferenceParser::VulnerabilityParser.new(context))
end
end
end
......
......@@ -18,6 +18,7 @@ module EE
[
::Banzai::Filter::EpicReferenceFilter,
::Banzai::Filter::IterationReferenceFilter,
::Banzai::Filter::VulnerabilityReferenceFilter,
*super
]
end
......
......@@ -11,6 +11,7 @@ module EE
[
::Banzai::Filter::EpicReferenceFilter,
::Banzai::Filter::IterationReferenceFilter,
::Banzai::Filter::VulnerabilityReferenceFilter,
*super
]
end
......
# frozen_string_literal: true
module EE
module Banzai
module ReferenceParser
module VulnerabilityParser
def references_relation
Vulnerability
end
# rubocop: disable CodeReuse/ActiveRecord
def records_for_nodes(nodes)
@vulnerabilities_for_nodes ||= grouped_objects_for_nodes(
nodes,
::Vulnerability.includes(
:author,
:project
),
self.class.data_attribute
)
end
# rubocop: enable CodeReuse/ActiveRecord
def can_read_reference?(user, vulnerability)
can?(user, :read_vulnerability, vulnerability)
end
end
end
end
end
......@@ -8,34 +8,67 @@ RSpec.describe Projects::AutocompleteSourcesController do
let_it_be(:project) { create(:project, :public, group: group) }
let_it_be(:epic) { create(:epic, group: group) }
let_it_be(:epic2) { create(:epic, group: group2) }
let_it_be(:vulnerability) { create(:vulnerability, project: project) }
before do
sign_in(user)
end
context 'when epics feature is disabled' do
it 'returns 404 status' do
get :epics, params: { namespace_id: project.namespace, project_id: project }
describe '#epics' do
context 'when epics feature is disabled' do
it 'returns 404 status' do
get :epics, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:not_found)
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when epics feature is enabled' do
before do
stub_licensed_features(epics: true)
end
describe '#epics' do
it 'returns the correct response' do
get :epics, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an(Array)
expect(json_response.count).to eq(1)
expect(json_response.first).to include(
'iid' => epic.iid, 'title' => epic.title
)
end
end
end
end
context 'when epics feature is enabled' do
before do
stub_licensed_features(epics: true)
describe '#vulnerabilities' do
context 'when vulnerabilities feature is disabled' do
it 'returns 404 status' do
get :vulnerabilities, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:not_found)
end
end
describe '#epics' do
it 'returns the correct response' do
get :epics, params: { namespace_id: project.namespace, project_id: project }
context 'when vulnerabilities feature is enabled' do
before do
stub_licensed_features(security_dashboard: true)
project.add_developer(user)
end
describe '#vulnerabilities' do
it 'returns the correct response', :aggregate_failures do
get :vulnerabilities, params: { namespace_id: project.namespace, project_id: project }
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an(Array)
expect(json_response.count).to eq(1)
expect(json_response.first).to include(
'iid' => epic.iid, 'title' => epic.title
)
expect(response).to have_gitlab_http_status(:ok)
expect(json_response).to be_an(Array)
expect(json_response.count).to eq(1)
expect(json_response.first).to include(
'id' => vulnerability.id, 'title' => vulnerability.title
)
end
end
end
end
......
......@@ -33,6 +33,31 @@ RSpec.describe Groups::AutocompleteSourcesController do
end
end
describe '#vulnerabilities' do
let_it_be_with_reload(:project) { create(:project, :private, group: group) }
let_it_be(:vulnerability) { create(:vulnerability, project: project) }
before do
project.add_developer(user)
stub_licensed_features(security_dashboard: true)
end
it 'returns 200 status' do
get :vulnerabilities, params: { group_id: group }
expect(response).to have_gitlab_http_status(:ok)
end
it 'returns the correct response', :aggregate_failures do
get :vulnerabilities, params: { group_id: group }
expect(json_response).to be_an(Array)
expect(json_response.first).to include(
'id' => vulnerability.id, 'title' => vulnerability.title
)
end
end
describe '#issues' do
using RSpec::Parameterized::TableSyntax
......
......@@ -103,7 +103,7 @@ RSpec.describe ApplicationHelper do
let(:noteable_type) { Epic }
it 'returns paths for autocomplete_sources_controller' do
expect_autocomplete_data_sources(object, noteable_type, [:members, :issues, :mergeRequests, :labels, :epics, :commands, :milestones])
expect_autocomplete_data_sources(object, noteable_type, [:members, :issues, :mergeRequests, :labels, :epics, :vulnerabilities, :commands, :milestones])
end
end
......@@ -127,7 +127,23 @@ RSpec.describe ApplicationHelper do
end
end
context 'when epics are disabled' do
context 'when vulnerabilities are enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
it 'returns paths for autocomplete_sources_controller for personal projects' do
expect_autocomplete_data_sources(object, noteable_type, [:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets, :vulnerabilities])
end
it 'returns paths for autocomplete_sources_controller including vulnerabilities for group projects' do
object.update_column(:namespace_id, create(:group).id)
expect_autocomplete_data_sources(object, noteable_type, [:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets, :vulnerabilities])
end
end
context 'when epics and vulnerabilities are disabled' do
it 'returns paths for autocomplete_sources_controller' do
expect_autocomplete_data_sources(object, noteable_type, [:members, :issues, :mergeRequests, :labels, :milestones, :commands, :snippets])
end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Banzai::Filter::VulnerabilityReferenceFilter do
include FilterSpecHelper
let(:urls) { Gitlab::Routing.url_helpers }
let(:project) { create(:project) }
let(:another_project) { create(:project) }
let(:vulnerability) { create(:vulnerability, project: project) }
let(:full_ref_text) { "Check #{vulnerability.project.full_path}+#{vulnerability.id}" }
def doc(reference = nil)
reference ||= "Check +#{vulnerability.id}"
context = { project: project, group: nil }
reference_filter(reference, context)
end
context 'internal reference' do
let(:reference) { "+#{vulnerability.id}" }
it 'links to a valid reference' do
expect(doc.css('a').first.attr('href')).to eq(urls.project_security_vulnerability_url(project, vulnerability))
end
it 'links with adjacent text' do
expect(doc.text).to eq("Check #{reference}")
end
it 'includes a title attribute' do
expect(doc.css('a').first.attr('title')).to eq(vulnerability.title)
end
it 'escapes the title attribute' do
vulnerability.update_column(:title, %{"></a>whatever<a title="})
expect(doc.text).to eq("Check #{reference}")
end
it 'includes default classes' do
expect(doc.css('a').first.attr('class')).to eq('gfm gfm-vulnerability has-tooltip')
end
it 'includes a data-project attribute' do
link = doc.css('a').first
expect(link).to have_attribute('data-project')
expect(link.attr('data-project')).to eq(project.id.to_s)
end
it 'includes a data-vulnerability attribute' do
link = doc.css('a').first
expect(link).to have_attribute('data-vulnerability')
expect(link.attr('data-vulnerability')).to eq(vulnerability.id.to_s)
end
it 'includes a data-original attribute' do
link = doc.css('a').first
expect(link).to have_attribute('data-original')
expect(link.attr('data-original')).to eq(CGI.escapeHTML(reference))
end
it 'ignores invalid vulnerability IDs' do
text = "Check +#{non_existing_record_id}"
expect(doc(text).to_s).to eq(ERB::Util.html_escape_once(text))
end
it 'ignores out of range vulnerability IDs' do
text = "Check &1161452270761535925900804973910297"
expect(doc(text).to_s).to eq(ERB::Util.html_escape_once(text))
end
it 'does not process links containing vulnerability numbers followed by text' do
href = "#{reference}st"
link = doc("<a href='#{href}'></a>").css('a').first.attr('href')
expect(link).to eq(href)
end
end
context 'internal escaped reference' do
let(:reference) { "+us;#{vulnerability.id}" }
it 'links to a valid reference' do
expect(doc.css('a').first.attr('href')).to eq(urls.project_security_vulnerability_url(project, vulnerability))
end
it 'includes a title attribute' do
expect(doc.css('a').first.attr('title')).to eq(vulnerability.title)
end
it 'includes default classes' do
expect(doc.css('a').first.attr('class')).to eq('gfm gfm-vulnerability has-tooltip')
end
it 'ignores invalid vulnerability IDs' do
text = "Check +#{non_existing_record_id}"
expect(doc(text).to_s).to eq(ERB::Util.html_escape_once(text))
end
end
context 'cross-reference' do
before do
vulnerability.update_column(:project_id, another_project.id)
end
it 'ignores a shorthand reference from another group' do
text = "Check +#{vulnerability.id}"
expect(doc(text).to_s).to eq(ERB::Util.html_escape_once(text))
end
it 'links to a valid reference for full reference' do
expect(doc(full_ref_text).css('a').first.attr('href')).to eq(urls.project_security_vulnerability_url(another_project, vulnerability))
end
it 'link has valid text' do
expect(doc(full_ref_text).css('a').first.text).to eq("#{vulnerability.project.full_path}+#{vulnerability.id}")
end
it 'includes default classes' do
expect(doc(full_ref_text).css('a').first.attr('class')).to eq('gfm gfm-vulnerability has-tooltip')
end
end
context 'escaped cross-reference' do
before do
vulnerability.update_column(:project_id, another_project.id)
end
it 'ignores a shorthand reference from another group' do
text = "Check +#{vulnerability.id}"
expect(doc(text).to_s).to eq(ERB::Util.html_escape_once(text))
end
it 'links to a valid reference for full reference' do
expect(doc(full_ref_text).css('a').first.attr('href')).to eq(urls.project_security_vulnerability_url(another_project, vulnerability))
end
it 'link has valid text' do
expect(doc(full_ref_text).css('a').first.text).to eq("#{vulnerability.project.full_path}+#{vulnerability.id}")
end
it 'includes default classes' do
expect(doc(full_ref_text).css('a').first.attr('class')).to eq('gfm gfm-vulnerability has-tooltip')
end
end
context 'url reference' do
let(:link) { urls.project_security_vulnerability_url(vulnerability.project, vulnerability) }
let(:text) { "Check #{link}" }
let(:project) { create(:project) }
before do
vulnerability.update_column(:project_id, another_project.id)
end
it 'links to a valid reference' do
expect(doc(text).css('a').first.attr('href')).to eq(urls.project_security_vulnerability_url(another_project, vulnerability))
end
it 'link has valid text' do
expect(doc(text).css('a').first.text).to eq(vulnerability.to_reference(project))
end
it 'includes default classes' do
expect(doc(text).css('a').first.attr('class')).to eq('gfm gfm-vulnerability has-tooltip')
end
it 'matches link reference with trailing slash' do
doc2 = reference_filter("Fixed (#{link}/.)")
expect(doc2).to match(%r{\(#{Regexp.escape(vulnerability.to_reference(project))}\.\)})
end
end
context 'url in a link href' do
let(:link) { urls.project_security_vulnerability_url(vulnerability.project, vulnerability) }
let(:text) do
ref = %{<a href="#{link}">Reference</a>}
"Check #{ref}"
end
before do
vulnerability.update_column(:project_id, another_project.id)
end
it 'links to a valid reference for link href' do
expect(doc(text).css('a').first.attr('href')).to eq(urls.project_security_vulnerability_url(another_project, vulnerability))
end
it 'link has valid text' do
expect(doc(text).css('a').first.text).to eq('Reference')
end
it 'includes default classes' do
expect(doc(text).css('a').first.attr('class')).to eq('gfm gfm-vulnerability has-tooltip')
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Banzai::ReferenceParser::VulnerabilityParser do
include ReferenceParserHelpers
def link(vulnerability_id)
link = empty_html_link
link['data-vulnerability'] = vulnerability_id.to_s
link
end
let(:user) { create(:user) }
let(:public_project) { create(:project, :public) }
let(:private_project1) { create(:project, :private) }
let(:private_project2) { create(:project, :private) }
let(:vulnerability) { create(:vulnerability, project: public_project) }
let(:vulnerability1) { create(:vulnerability, project: private_project1) }
let(:vulnerability2) { create(:vulnerability, project: private_project2) }
let(:nodes) do
[link(vulnerability.id), link(vulnerability1.id), link(vulnerability2.id)]
end
subject { described_class.new(Banzai::RenderContext.new(nil, user)) }
describe '#nodes_visible_to_user' do
before do
private_project1.add_developer(user)
end
context 'when the vulnerabilities feature is enabled' do
before do
stub_licensed_features(security_dashboard: true)
end
it 'returns the nodes the user can read for valid vulnerability nodes' do
expected_result = [nodes[1]]
expect(subject.nodes_visible_to_user(user, nodes)).to match_array(expected_result)
end
it 'returns an empty array for nodes without required data-attributes' do
expect(subject.nodes_visible_to_user(user, [empty_html_link])).to be_empty
end
end
context 'when the vulnerabilities feature is disabled' do
it 'returns an empty array' do
expect(subject.nodes_visible_to_user(user, nodes)).to be_empty
end
end
end
describe '#referenced_by' do
context 'when using an existing vulnerabilities IDs' do
it 'returns an Array of vulnerabilities' do
expected_result = [vulnerability, vulnerability1, vulnerability2]
expect(subject.referenced_by(nodes)).to match_array(expected_result)
end
it 'returns an empty Array for empty list of nodes' do
expect(subject.referenced_by([])).to be_empty
end
end
context 'when vulnerability with given ID does not exist' do
it 'returns an empty Array' do
expect(subject.referenced_by([link(non_existing_record_id)])).to be_empty
end
end
end
describe '#records_for_nodes' do
it 'returns a Hash containing the vulnerabilities for a list of nodes' do
expected_hash = {
nodes[0] => vulnerability,
nodes[1] => vulnerability1,
nodes[2] => vulnerability2
}
expect(subject.records_for_nodes(nodes)).to eq(expected_hash)
end
end
end
......@@ -25,4 +25,18 @@ RSpec.describe Gitlab::ReferenceExtractor do
expect(subject.epics).to match_array([@e0, @e1])
end
it 'accesses valid vulnerabilities' do
stub_licensed_features(security_dashboard: true)
vulnerability_0 = create(:vulnerability, project: project)
vulnerability_1 = create(:vulnerability, project: project)
vulnerability_2 = create(:vulnerability, project: create(:project, :private))
text = "#{vulnerability_0.to_reference(project, full: true)}, &#{non_existing_record_iid}, #{vulnerability_1.to_reference(project, full: true)}, #{vulnerability_2.to_reference(project, full: true)}"
subject.analyze(text, { project: project })
expect(subject.vulnerabilities).to match_array([vulnerability_0, vulnerability_1])
end
end
......@@ -157,6 +157,18 @@ RSpec.describe Vulnerability do
end
end
describe '.for_ids' do
let(:project) { create(:project) }
let!(:vulnerability1) { create(:vulnerability, project: project) }
let!(:vulnerability2) { create(:vulnerability, project: project) }
subject { described_class.for_ids([vulnerability1.id, vulnerability2.id]) }
it 'returns vulnerabilities with given IDs' do
is_expected.to contain_exactly(vulnerability1, vulnerability2)
end
end
describe '.for_projects' do
let(:project1) { create(:project) }
let(:project2) { create(:project) }
......@@ -498,6 +510,23 @@ RSpec.describe Vulnerability do
it { is_expected.to eq('+') }
end
describe '.reference_pattern' do
subject { described_class.reference_pattern }
it { is_expected.to match('+123') }
it { is_expected.to match('gitlab-ce+123') }
it { is_expected.to match('gitlab-org/gitlab-ce+123') }
end
describe '.link_reference_pattern' do
subject { described_class.link_reference_pattern }
it { is_expected.to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/-/security/vulnerabilities/123") }
it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/security/vulnerabilities/123") }
it { is_expected.not_to match("#{Gitlab.config.gitlab.url}/gitlab-org/gitlab-foss/issues/123") }
it { is_expected.not_to match("gitlab-org/gitlab-ce/milestones/123") }
end
describe '#to_reference' do
let(:namespace) { build(:namespace, path: 'sample-namespace') }
let(:project) { build(:project, name: 'sample-project', namespace: namespace) }
......
......@@ -6,12 +6,23 @@ RSpec.describe GroupIssuableAutocompleteEntity do
let(:group) { build_stubbed(:group) }
let(:project) { build_stubbed(:project, group: group) }
let(:issue) { build_stubbed(:issue, project: project) }
subject { described_class.new(issue, parent_group: group).as_json }
let(:vulnerability) { build_stubbed(:vulnerability, project: project) }
describe '#represent' do
it 'includes the iid, title, and reference' do
expect(subject).to include(:iid, :title, :reference)
context 'when issuable responds to iid' do
subject { described_class.new(issue, parent_group: group).as_json }
it 'includes the iid, title, and reference' do
expect(subject).to include(:iid, :title, :reference)
end
end
context 'when issuable does not respond to iid' do
subject { described_class.new(vulnerability, parent_group: project).as_json }
it 'includes the id, title, and reference' do
expect(subject).to include(:id, :title, :reference)
end
end
end
end
......@@ -3,8 +3,8 @@
require 'spec_helper'
RSpec.describe Groups::AutocompleteService do
let!(:group) { create(:group, :nested, :private, avatar: fixture_file_upload('spec/fixtures/dk.png')) }
let!(:sub_group) { create(:group, :private, parent: group) }
let_it_be(:group, refind: true) { create(:group, :nested, :private, avatar: fixture_file_upload('spec/fixtures/dk.png')) }
let_it_be(:sub_group) { create(:group, :private, parent: group) }
let(:user) { create(:user) }
let!(:epic) { create(:epic, group: group, author: user) }
......@@ -118,6 +118,28 @@ RSpec.describe Groups::AutocompleteService do
end
end
describe '#vulnerability' do
let_it_be(:project) { create(:project, group: group) }
let_it_be(:vulnerability) { create(:vulnerability, project: project) }
before do
stub_licensed_features(security_dashboard: true)
project.add_developer(user)
end
it 'returns nothing if not allowed' do
guest = create(:user)
vulnerabilities = described_class.new(group, guest).vulnerabilities
expect(vulnerabilities).to be_empty
end
it 'returns vulnerabilities from group' do
expect(subject.vulnerabilities.map(&:id)).to contain_exactly(vulnerability.id)
end
end
describe '#commands' do
context 'when target is an epic' do
let(:parent_epic) { create(:epic, group: group, author: user) }
......
......@@ -119,7 +119,7 @@ module Banzai
# Yields the link's URL and inner HTML whenever the node is a valid <a> tag.
def yield_valid_link(node)
link = CGI.unescape(node.attr('href').to_s)
link = unescape_link(node.attr('href').to_s)
inner_html = node.inner_html
return unless link.force_encoding('UTF-8').valid_encoding?
......@@ -127,6 +127,10 @@ module Banzai
yield link, inner_html
end
def unescape_link(href)
CGI.unescape(href)
end
def replace_text_when_pattern_matches(node, index, pattern)
return unless node.text =~ pattern
......
# frozen_string_literal: true
module Banzai
module Filter
# The actual filter is implemented in the EE mixin
class VulnerabilityReferenceFilter < IssuableReferenceFilter
self.reference_type = :vulnerability
def self.object_class
Vulnerability
end
private
def project
context[:project]
end
end
end
end
Banzai::Filter::VulnerabilityReferenceFilter.prepend_if_ee('EE::Banzai::Filter::VulnerabilityReferenceFilter')
# frozen_string_literal: true
module Banzai
module ReferenceParser
# The actual parser is implemented in the EE mixin
class VulnerabilityParser < IssuableParser
self.reference_type = :vulnerability
def records_for_nodes(_nodes)
{}
end
end
end
end
Banzai::ReferenceParser::VulnerabilityParser.prepend_if_ee('::EE::Banzai::ReferenceParser::VulnerabilityParser')
......@@ -4,7 +4,7 @@ module Gitlab
# Extract possible GFM references from an arbitrary String for further processing.
class ReferenceExtractor < Banzai::ReferenceExtractor
REFERABLES = %i(user issue label milestone mentioned_user mentioned_group mentioned_project
merge_request snippet commit commit_range directly_addressed_user epic iteration).freeze
merge_request snippet commit commit_range directly_addressed_user epic iteration vulnerability).freeze
attr_accessor :project, :current_user, :author
# This counter is increased by a number of references filtered out by
# banzai reference exctractor. Note that this counter is stateful and
......@@ -38,7 +38,7 @@ module Gitlab
end
REFERABLES.each do |type|
define_method("#{type}s") do
define_method(type.to_s.pluralize) do
@references[type] ||= references(type)
end
end
......
......@@ -296,7 +296,7 @@ RSpec.describe Gitlab::ReferenceExtractor do
end
it 'returns all supported prefixes' do
expect(prefixes.keys.uniq).to match_array(%w(@ # ~ % ! $ & *iteration:))
expect(prefixes.keys.uniq).to match_array(%w(@ # ~ % ! $ & + *iteration:))
end
it 'does not allow one prefix for multiple referables if not allowed specificly' do
......
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