Commit 0f3a817c authored by Sean McGivern's avatar Sean McGivern

Merge branch '276897-add-external-issue-links' into 'master'

Add Vulnerabilities External Link model

See merge request gitlab-org/gitlab!48465
parents c6da895b 037ee7d2
---
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
...@@ -17254,6 +17254,29 @@ CREATE SEQUENCE vulnerability_exports_id_seq ...@@ -17254,6 +17254,29 @@ CREATE SEQUENCE vulnerability_exports_id_seq
ALTER SEQUENCE vulnerability_exports_id_seq OWNED BY vulnerability_exports.id; 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 ( CREATE TABLE vulnerability_feedback (
id integer NOT NULL, id integer NOT NULL,
created_at timestamp with time zone NOT NULL, created_at timestamp with time zone NOT NULL,
...@@ -18454,6 +18477,8 @@ ALTER TABLE ONLY vulnerabilities ALTER COLUMN id SET DEFAULT nextval('vulnerabil ...@@ -18454,6 +18477,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_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_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); ALTER TABLE ONLY vulnerability_finding_links ALTER COLUMN id SET DEFAULT nextval('vulnerability_finding_links_id_seq'::regclass);
...@@ -19926,6 +19951,9 @@ ALTER TABLE ONLY vulnerabilities ...@@ -19926,6 +19951,9 @@ ALTER TABLE ONLY vulnerabilities
ALTER TABLE ONLY vulnerability_exports ALTER TABLE ONLY vulnerability_exports
ADD CONSTRAINT vulnerability_exports_pkey PRIMARY KEY (id); 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 ALTER TABLE ONLY vulnerability_feedback
ADD CONSTRAINT vulnerability_feedback_pkey PRIMARY KEY (id); ADD CONSTRAINT vulnerability_feedback_pkey PRIMARY KEY (id);
...@@ -20258,6 +20286,10 @@ CREATE INDEX idx_security_scans_on_scan_type ON security_scans USING btree (scan ...@@ -20258,6 +20286,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_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_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); 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);
...@@ -22428,6 +22460,10 @@ CREATE INDEX index_vulnerability_exports_on_group_id_not_null ON vulnerability_e ...@@ -22428,6 +22460,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_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_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); CREATE INDEX index_vulnerability_feedback_on_comment_author_id ON vulnerability_feedback USING btree (comment_author_id);
...@@ -23458,6 +23494,9 @@ ALTER TABLE ONLY emails ...@@ -23458,6 +23494,9 @@ ALTER TABLE ONLY emails
ALTER TABLE ONLY clusters ALTER TABLE ONLY clusters
ADD CONSTRAINT fk_f05c5e5a42 FOREIGN KEY (management_project_id) REFERENCES projects(id) ON DELETE SET NULL; 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 ALTER TABLE ONLY epics
ADD CONSTRAINT fk_f081aa4489 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE; ADD CONSTRAINT fk_f081aa4489 FOREIGN KEY (group_id) REFERENCES namespaces(id) ON DELETE CASCADE;
...@@ -24679,6 +24718,9 @@ ALTER TABLE ONLY vulnerability_occurrence_identifiers ...@@ -24679,6 +24718,9 @@ ALTER TABLE ONLY vulnerability_occurrence_identifiers
ALTER TABLE ONLY serverless_domain_cluster 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; 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 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; 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 ...@@ -43,6 +43,7 @@ module EE
has_many :findings, class_name: '::Vulnerabilities::Finding', inverse_of: :vulnerability has_many :findings, class_name: '::Vulnerabilities::Finding', inverse_of: :vulnerability
has_many :dismissed_findings, -> { dismissed }, 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 :issue_links, class_name: '::Vulnerabilities::IssueLink', inverse_of: :vulnerability
has_many :created_issue_links, -> { created }, 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 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 ...@@ -35,6 +35,7 @@ RSpec.describe Vulnerability do
it { is_expected.to belong_to(:epic) } 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(: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(: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(: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(: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) } 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