Commit 7d7b1b07 authored by Victor Zagorodny's avatar Victor Zagorodny Committed by Dmitriy Zaporozhets

Add VulnerabilityIssueLinks API w/ "get list" op

Add API endpoints to get the list of
VulnerabilityIssueLink entities for a
Vulnerability. Extract some common DB and auth
related helpers into a reusable module to be
used by VulnerabilityIssueLinks and
Vulnerabilities API.
parent e25dfd65
...@@ -24,7 +24,12 @@ class Vulnerability < ApplicationRecord ...@@ -24,7 +24,12 @@ class Vulnerability < ApplicationRecord
has_many :findings, class_name: 'Vulnerabilities::Occurrence', inverse_of: :vulnerability has_many :findings, class_name: 'Vulnerabilities::Occurrence', inverse_of: :vulnerability
has_many :issue_links, class_name: 'Vulnerabilities::IssueLink', inverse_of: :vulnerability has_many :issue_links, class_name: 'Vulnerabilities::IssueLink', inverse_of: :vulnerability
has_many :related_issues, through: :issue_links, source: :issue has_many :related_issues, through: :issue_links, source: :issue do
def with_vulnerability_links
select('issues.*, vulnerability_issue_links.id AS vulnerability_link_id, '\
'vulnerability_issue_links.link_type AS vulnerability_link_type')
end
end
enum state: { opened: 1, closed: 2, resolved: 3 } enum state: { opened: 1, closed: 2, resolved: 3 }
enum severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS, _prefix: :severity enum severity: Vulnerabilities::Occurrence::SEVERITY_LEVELS, _prefix: :severity
......
# frozen_string_literal: true
module API
module Helpers
module VulnerabilitiesHelpers
def find_and_authorize_vulnerability!(action)
find_vulnerability!.tap do |vulnerability|
authorize! action, vulnerability.project
end
end
end
end
end
# frozen_string_literal: true
module API
module Helpers
module VulnerabilitiesHooks
extend ActiveSupport::Concern
included do
before do
not_found! unless Feature.enabled?(:first_class_vulnerabilities)
authenticate!
end
end
end
end
end
...@@ -2,8 +2,11 @@ ...@@ -2,8 +2,11 @@
module API module API
class Vulnerabilities < Grape::API class Vulnerabilities < Grape::API
include ::API::Helpers::VulnerabilitiesHooks
include PaginationParams include PaginationParams
helpers ::API::Helpers::VulnerabilitiesHelpers
helpers do helpers do
def vulnerabilities_by(project) def vulnerabilities_by(project)
Security::VulnerabilitiesFinder.new(project).execute Security::VulnerabilitiesFinder.new(project).execute
...@@ -17,12 +20,6 @@ module API ...@@ -17,12 +20,6 @@ module API
authorize! action, vulnerability.project authorize! action, vulnerability.project
end end
def find_and_authorize_vulnerability!(action)
find_vulnerability!.tap do |vulnerability|
authorize_vulnerability!(vulnerability, action)
end
end
def render_vulnerability(vulnerability) def render_vulnerability(vulnerability)
if vulnerability.valid? if vulnerability.valid?
present vulnerability, with: EE::API::Entities::Vulnerability present vulnerability, with: EE::API::Entities::Vulnerability
...@@ -32,12 +29,6 @@ module API ...@@ -32,12 +29,6 @@ module API
end end
end end
before do
not_found! unless Feature.enabled?(:first_class_vulnerabilities)
authenticate!
end
params do params do
requires :id, type: String, desc: 'The ID of a vulnerability' requires :id, type: String, desc: 'The ID of a vulnerability'
end end
......
# frozen_string_literal: true
module API
class VulnerabilityIssueLinks < Grape::API
include ::API::Helpers::VulnerabilitiesHooks
helpers ::API::Helpers::VulnerabilitiesHelpers
helpers do
def find_vulnerability!
Vulnerability.find(params[:id])
end
end
params do
requires :id, type: Integer, desc: 'The ID of a vulnerability'
end
resource :vulnerabilities do
desc 'Get related issues for a vulnerability' do
success EE::API::Entities::VulnerabilityRelatedIssue
end
get ':id/issue_links' do
vulnerability = find_and_authorize_vulnerability!(:read_vulnerability)
present vulnerability
.related_issues
.with_api_entity_associations
.with_vulnerability_links,
with: EE::API::Entities::VulnerabilityRelatedIssue
end
end
end
end
...@@ -41,6 +41,7 @@ module EE ...@@ -41,6 +41,7 @@ module EE
mount ::API::ProjectApprovals mount ::API::ProjectApprovals
mount ::API::Vulnerabilities mount ::API::Vulnerabilities
mount ::API::VulnerabilityFindings mount ::API::VulnerabilityFindings
mount ::API::VulnerabilityIssueLinks
mount ::API::MergeRequestApprovals mount ::API::MergeRequestApprovals
mount ::API::MergeRequestApprovalRules mount ::API::MergeRequestApprovalRules
mount ::API::ProjectAliases mount ::API::ProjectAliases
......
...@@ -938,6 +938,14 @@ module EE ...@@ -938,6 +938,14 @@ module EE
expose :resolved_at expose :resolved_at
expose :closed_at expose :closed_at
end end
class VulnerabilityRelatedIssue < ::API::Entities::IssueBasic
# vulnerability_link_* attributes come from joined Vulnerabilities::IssueLink record
expose :vulnerability_link_id
expose :vulnerability_link_type do |related_issue|
::Vulnerabilities::IssueLink.link_types.key(related_issue.vulnerability_link_type)
end
end
end end
end end
end end
...@@ -34,5 +34,13 @@ FactoryBot.define do ...@@ -34,5 +34,13 @@ FactoryBot.define do
project: vulnerability.project) project: vulnerability.project)
end end
end end
trait :with_issue_links do
after(:create) do |vulnerability|
create_list(:issue, 2).each do |issue|
create(:vulnerabilities_issue_link, vulnerability: vulnerability, issue: issue)
end
end
end
end end
end end
{
"type": "object",
"allOf": [
{ "$ref": "../../../../../../../spec/fixtures/api/schemas/public_api/v4/issue.json" },
{
"required": ["vulnerability_link_id", "vulnerability_link_type"],
"properties": {
"vulnerability_link_id": { "type": "integer" },
"vulnerability_link_type": {
"type": "string",
"enum": ["created", "related"]
}
}
}
]
}
{
"type": "array",
"items": { "$ref": "vulnerability_related_issue.json" }
}
...@@ -12,42 +12,6 @@ describe API::Vulnerabilities do ...@@ -12,42 +12,6 @@ describe API::Vulnerabilities do
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:project_vulnerabilities_path) { "/projects/#{project.id}/vulnerabilities" } let(:project_vulnerabilities_path) { "/projects/#{project.id}/vulnerabilities" }
shared_examples 'forbids actions on vulnerability in case of disabled features' do
context 'when "first-class vulnerabilities" feature is disabled' do
before do
stub_feature_flags(first_class_vulnerabilities: false)
end
it 'responds with "not found"' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
context 'when security dashboard feature is not available' do
before do
stub_licensed_features(security_dashboard: false)
end
it 'responds with 403 Forbidden' do
subject
expect(response).to have_gitlab_http_status(403)
end
end
end
shared_examples 'responds with "not found" for an unknown vulnerability ID' do
let(:vulnerability_id) { 0 }
it do
subject
expect(response).to have_gitlab_http_status(404)
end
end
describe 'GET /projects/:id/vulnerabilities' do describe 'GET /projects/:id/vulnerabilities' do
let_it_be(:project) { create(:project, :with_vulnerabilities) } let_it_be(:project) { create(:project, :with_vulnerabilities) }
......
# frozen_string_literal: true
require 'spec_helper'
describe API::VulnerabilityIssueLinks do
include AccessMatchersForRequest
before do
stub_licensed_features(security_dashboard: true)
end
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
describe 'GET /vulnerabilities/:id/issue_links' do
let_it_be(:vulnerability) { create(:vulnerability, :with_issue_links, project: project) }
let_it_be(:vulnerability_id) { vulnerability.id }
let(:vulnerability_issue_links_path) { "/vulnerabilities/#{vulnerability_id}/issue_links" }
subject(:get_issue_links) { get api(vulnerability_issue_links_path, user) }
context 'with an authorized user with proper permissions' do
before do
project.add_developer(user)
end
it 'gets the list of vulnerabilities' do
get_issue_links
expect(response).to have_gitlab_http_status(200)
expect(response).to match_response_schema('public_api/v4/vulnerability_related_issues', dir: 'ee')
expect(json_response.map { |link| link['id'] }).to match_array(vulnerability.related_issues.map(&:id))
expect(json_response.map { |link| link['vulnerability_link_id'] }).to(
match_array(vulnerability.issue_links.map(&:id)))
expect(json_response.map { |link| link['vulnerability_link_type'] }).to all eq 'related'
end
it_behaves_like 'responds with "not found" for an unknown vulnerability ID'
it_behaves_like 'forbids actions on vulnerability in case of disabled features'
end
describe 'permissions' do
it { expect { get_issue_links }.to be_allowed_for(:admin) }
it { expect { get_issue_links }.to be_allowed_for(:owner).of(project) }
it { expect { get_issue_links }.to be_allowed_for(:maintainer).of(project) }
it { expect { get_issue_links }.to be_allowed_for(:developer).of(project) }
it { expect { get_issue_links }.to be_allowed_for(:auditor) }
it { expect { get_issue_links }.to be_denied_for(:reporter).of(project) }
it { expect { get_issue_links }.to be_denied_for(:guest).of(project) }
it { expect { get_issue_links }.to be_denied_for(:anonymous) }
end
end
end
# frozen_string_literal: true
shared_examples 'forbids actions on vulnerability in case of disabled features' do
context 'when "first-class vulnerabilities" feature is disabled' do
before do
stub_feature_flags(first_class_vulnerabilities: false)
end
it 'responds with "not found"' do
subject
expect(response).to have_gitlab_http_status(404)
end
end
context 'when security dashboard feature is not available' do
before do
stub_licensed_features(security_dashboard: false)
end
it 'responds with 403 Forbidden' do
subject
expect(response).to have_gitlab_http_status(403)
end
end
end
shared_examples 'responds with "not found" for an unknown vulnerability ID' do
let(:vulnerability_id) { 0 }
it do
subject
expect(response).to have_gitlab_http_status(404)
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