Commit 037ee7d2 authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by Sean McGivern

Add Vulnerabilities External Link model

This change adds new model to database to store external links for
vulnerabilities (ie. information about created issues in Jira).
parent 8bb7aacd
---
title: Add Vulnerabilities External Link model
merge_request: 48465
author:
type: added
# frozen_string_literal: true
class CreateVulnerabilityExternalLinks < ActiveRecord::Migration[6.0]
include Gitlab::Database::MigrationHelpers
DOWNTIME = false
disable_ddl_transaction!
def up
with_lock_retries do
create_table :vulnerability_external_issue_links, if_not_exists: true do |t|
t.timestamps_with_timezone null: false
t.references :author, null: false, index: true, foreign_key: { to_table: :users, on_delete: :nullify }, type: :bigint
t.references :vulnerability, null: false, index: true, type: :bigint
t.integer :link_type, limit: 2, null: false, default: 1 # 'created'
t.integer :external_type, limit: 2, null: false, default: 1 # 'jira'
t.text :external_project_key, null: false
t.text :external_issue_key, null: false
t.index %i[vulnerability_id external_type external_project_key external_issue_key],
name: 'idx_vulnerability_ext_issue_links_on_vulne_id_and_ext_issue',
unique: true
t.index %i[vulnerability_id link_type],
name: 'idx_vulnerability_ext_issue_links_on_vulne_id_and_link_type',
where: 'link_type = 1',
unique: true # only one 'created' link per vulnerability is allowed
end
end
add_concurrent_foreign_key :vulnerability_external_issue_links, :vulnerabilities, column: :vulnerability_id, on_delete: :cascade
add_text_limit :vulnerability_external_issue_links, :external_project_key, 255
add_text_limit :vulnerability_external_issue_links, :external_issue_key, 255
end
def down
with_lock_retries do
drop_table :vulnerability_external_issue_links
end
end
end
6779e92fa65ff206b19bb99a5a242e3ab5fd7a8d15be89dee925d1fbb5b00632
\ No newline at end of file
......@@ -17226,6 +17226,29 @@ CREATE SEQUENCE vulnerability_exports_id_seq
ALTER SEQUENCE vulnerability_exports_id_seq OWNED BY vulnerability_exports.id;
CREATE TABLE vulnerability_external_issue_links (
id bigint NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
author_id bigint NOT NULL,
vulnerability_id bigint NOT NULL,
link_type smallint DEFAULT 1 NOT NULL,
external_type smallint DEFAULT 1 NOT NULL,
external_project_key text NOT NULL,
external_issue_key text NOT NULL,
CONSTRAINT check_3200604f5e CHECK ((char_length(external_issue_key) <= 255)),
CONSTRAINT check_68cffd19b0 CHECK ((char_length(external_project_key) <= 255))
);
CREATE SEQUENCE vulnerability_external_issue_links_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE vulnerability_external_issue_links_id_seq OWNED BY vulnerability_external_issue_links.id;
CREATE TABLE vulnerability_feedback (
id integer NOT NULL,
created_at timestamp with time zone NOT NULL,
......@@ -18424,6 +18447,8 @@ ALTER TABLE ONLY vulnerabilities ALTER COLUMN id SET DEFAULT nextval('vulnerabil
ALTER TABLE ONLY vulnerability_exports ALTER COLUMN id SET DEFAULT nextval('vulnerability_exports_id_seq'::regclass);
ALTER TABLE ONLY vulnerability_external_issue_links ALTER COLUMN id SET DEFAULT nextval('vulnerability_external_issue_links_id_seq'::regclass);
ALTER TABLE ONLY vulnerability_feedback ALTER COLUMN id SET DEFAULT nextval('vulnerability_feedback_id_seq'::regclass);
ALTER TABLE ONLY vulnerability_finding_links ALTER COLUMN id SET DEFAULT nextval('vulnerability_finding_links_id_seq'::regclass);
......@@ -19893,6 +19918,9 @@ ALTER TABLE ONLY vulnerabilities
ALTER TABLE ONLY vulnerability_exports
ADD CONSTRAINT vulnerability_exports_pkey PRIMARY KEY (id);
ALTER TABLE ONLY vulnerability_external_issue_links
ADD CONSTRAINT vulnerability_external_issue_links_pkey PRIMARY KEY (id);
ALTER TABLE ONLY vulnerability_feedback
ADD CONSTRAINT vulnerability_feedback_pkey PRIMARY KEY (id);
......@@ -20225,6 +20253,10 @@ CREATE INDEX idx_security_scans_on_scan_type ON security_scans USING btree (scan
CREATE UNIQUE INDEX idx_serverless_domain_cluster_on_clusters_applications_knative ON serverless_domain_cluster USING btree (clusters_applications_knative_id);
CREATE UNIQUE INDEX idx_vulnerability_ext_issue_links_on_vulne_id_and_ext_issue ON vulnerability_external_issue_links USING btree (vulnerability_id, external_type, external_project_key, external_issue_key);
CREATE UNIQUE INDEX idx_vulnerability_ext_issue_links_on_vulne_id_and_link_type ON vulnerability_external_issue_links USING btree (vulnerability_id, link_type) WHERE (link_type = 1);
CREATE UNIQUE INDEX idx_vulnerability_issue_links_on_vulnerability_id_and_issue_id ON vulnerability_issue_links USING btree (vulnerability_id, issue_id);
CREATE UNIQUE INDEX idx_vulnerability_issue_links_on_vulnerability_id_and_link_type ON vulnerability_issue_links USING btree (vulnerability_id, link_type) WHERE (link_type = 2);
......@@ -22391,6 +22423,10 @@ CREATE INDEX index_vulnerability_exports_on_group_id_not_null ON vulnerability_e
CREATE INDEX index_vulnerability_exports_on_project_id_not_null ON vulnerability_exports USING btree (project_id) WHERE (project_id IS NOT NULL);
CREATE INDEX index_vulnerability_external_issue_links_on_author_id ON vulnerability_external_issue_links USING btree (author_id);
CREATE INDEX index_vulnerability_external_issue_links_on_vulnerability_id ON vulnerability_external_issue_links USING btree (vulnerability_id);
CREATE INDEX index_vulnerability_feedback_on_author_id ON vulnerability_feedback USING btree (author_id);
CREATE INDEX index_vulnerability_feedback_on_comment_author_id ON vulnerability_feedback USING btree (comment_author_id);
......@@ -23421,6 +23457,9 @@ ALTER TABLE ONLY emails
ALTER TABLE ONLY clusters
ADD CONSTRAINT fk_f05c5e5a42 FOREIGN KEY (management_project_id) REFERENCES projects(id) ON DELETE SET NULL;
ALTER TABLE ONLY vulnerability_external_issue_links
ADD CONSTRAINT fk_f07bb8233d FOREIGN KEY (vulnerability_id) REFERENCES vulnerabilities(id) ON DELETE CASCADE;
ALTER TABLE ONLY epics
ADD CONSTRAINT fk_f081aa4489 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
......@@ -24639,6 +24678,9 @@ ALTER TABLE ONLY vulnerability_occurrence_identifiers
ALTER TABLE ONLY serverless_domain_cluster
ADD CONSTRAINT fk_rails_e59e868733 FOREIGN KEY (clusters_applications_knative_id) REFERENCES clusters_applications_knative(id) ON DELETE CASCADE;
ALTER TABLE ONLY vulnerability_external_issue_links
ADD CONSTRAINT fk_rails_e5ba7f7b13 FOREIGN KEY (author_id) REFERENCES users(id) ON DELETE SET NULL;
ALTER TABLE ONLY approval_merge_request_rule_sources
ADD CONSTRAINT fk_rails_e605a04f76 FOREIGN KEY (approval_merge_request_rule_id) REFERENCES approval_merge_request_rules(id) ON DELETE CASCADE;
......
......@@ -43,6 +43,7 @@ module EE
has_many :findings, class_name: '::Vulnerabilities::Finding', inverse_of: :vulnerability
has_many :dismissed_findings, -> { dismissed }, class_name: 'Vulnerabilities::Finding', inverse_of: :vulnerability
has_many :external_issue_links, class_name: '::Vulnerabilities::ExternalIssueLink', inverse_of: :vulnerability
has_many :issue_links, class_name: '::Vulnerabilities::IssueLink', inverse_of: :vulnerability
has_many :created_issue_links, -> { created }, class_name: '::Vulnerabilities::IssueLink', inverse_of: :vulnerability
has_many :related_issues, through: :issue_links, source: :issue do
......
# frozen_string_literal: true
module Vulnerabilities
class ExternalIssueLink < ApplicationRecord
self.table_name = 'vulnerability_external_issue_links'
belongs_to :author, class_name: 'User'
belongs_to :vulnerability
enum link_type: { created: 1 }
enum external_type: { jira: 1 }
validates :vulnerability, :external_issue_key, :external_type, :external_project_key, presence: true
validates :external_issue_key, uniqueness: { scope: [:vulnerability_id, :external_type, :external_project_key], message: N_('has already been linked to another vulnerability') }
validates :vulnerability_id,
uniqueness: {
conditions: -> { where(link_type: 'created') },
message: N_('already has a "created" issue link')
},
if: :created?
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :vulnerabilities_external_issue_link, class: 'Vulnerabilities::ExternalIssueLink' do
author
vulnerability
external_issue_key { 'GV-100' }
external_project_key { '10001' }
external_type { :jira }
trait :created do
link_type { :created }
end
transient do
project { nil }
end
after(:build) do |link, evaluator|
if evaluator.project
link.vulnerability = create(:vulnerability, project: evaluator.project)
end
end
end
end
......@@ -35,6 +35,7 @@ RSpec.describe Vulnerability do
it { is_expected.to belong_to(:epic) }
it { is_expected.to have_many(:findings).class_name('Vulnerabilities::Finding').inverse_of(:vulnerability) }
it { is_expected.to have_many(:dismissed_findings).class_name('Vulnerabilities::Finding').inverse_of(:vulnerability) }
it { is_expected.to have_many(:external_issue_links).class_name('Vulnerabilities::ExternalIssueLink').inverse_of(:vulnerability) }
it { is_expected.to have_many(:issue_links).class_name('Vulnerabilities::IssueLink').inverse_of(:vulnerability) }
it { is_expected.to have_many(:created_issue_links).class_name('Vulnerabilities::IssueLink').inverse_of(:vulnerability).conditions(link_type: Vulnerabilities::IssueLink.link_types['created']) }
it { is_expected.to have_many(:related_issues).through(:issue_links).source(:issue) }
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Vulnerabilities::ExternalIssueLink do
describe 'associations and fields' do
it { is_expected.to belong_to(:vulnerability) }
it { is_expected.to define_enum_for(:link_type).with_values(created: 1) }
it 'provides the "created" as default link_type' do
expect(create(:vulnerabilities_external_issue_link).link_type).to eq 'created'
end
end
describe 'validations' do
it { is_expected.to validate_presence_of(:vulnerability) }
it { is_expected.to validate_presence_of(:external_issue_key) }
it { is_expected.to validate_presence_of(:external_project_key) }
it { is_expected.to validate_presence_of(:external_type) }
describe 'uniqueness' do
before do
create(:vulnerabilities_external_issue_link)
end
it do
is_expected.to(
validate_uniqueness_of(:external_issue_key)
.scoped_to([:vulnerability_id, :external_type, :external_project_key])
.with_message('has already been linked to another vulnerability'))
end
end
describe 'only one "created" link allowed per vulnerability' do
let!(:existing_link) { create(:vulnerabilities_external_issue_link, :created) }
subject(:issue_link) do
build(:vulnerabilities_external_issue_link, :created, vulnerability: existing_link.vulnerability)
end
it do
is_expected.to(
validate_uniqueness_of(:vulnerability_id)
.with_message('already has a "created" issue link'))
end
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