Commit 75232036 authored by Etienne Baqué's avatar Etienne Baqué

Merge branch '346480_provide_graphql_interface_for_security_training_providers' into 'master'

Provide GraphQL interface to query Security Training Vendors

See merge request gitlab-org/gitlab!78195
parents 10470973 3060698a
# frozen_string_literal: true
class CreateSecurityTrainingProviders < Gitlab::Database::Migration[1.0]
def change
create_table :security_training_providers do |t|
t.text :name, limit: 256, null: false
t.text :description, limit: 512
t.text :url, limit: 512, null: false
t.text :logo_url, limit: 512
t.timestamps_with_timezone null: false
end
end
end
# frozen_string_literal: true
class CreateSecurityTrainings < Gitlab::Database::Migration[1.0]
enable_lock_retries!
def change
create_table :security_trainings do |t|
t.references :project, null: false, foreign_key: { on_delete: :cascade }
t.references :provider, null: false, foreign_key: { to_table: :security_training_providers, on_delete: :cascade }
t.boolean :is_primary, default: false, null: false
t.timestamps_with_timezone null: false
# Guarantee that there will be only one primary per project
t.index :project_id, name: 'index_security_trainings_on_unique_project_id', unique: true, where: 'is_primary IS TRUE'
end
end
end
65d9a1d63e90dfc336d0c69503c0899259fda773bc68a330782c206ac0fc48fd
\ No newline at end of file
afe57b6b1b8b10e0e26d7f499b25adc5aef9f7c52af644d6a260f0ed3aab16d5
\ No newline at end of file
......@@ -19446,6 +19446,47 @@ CREATE SEQUENCE security_scans_id_seq
ALTER SEQUENCE security_scans_id_seq OWNED BY security_scans.id;
CREATE TABLE security_training_providers (
id bigint NOT NULL,
name text NOT NULL,
description text,
url text NOT NULL,
logo_url text,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL,
CONSTRAINT check_544b3dc935 CHECK ((char_length(url) <= 512)),
CONSTRAINT check_6fe222f071 CHECK ((char_length(logo_url) <= 512)),
CONSTRAINT check_a8ff21ced5 CHECK ((char_length(description) <= 512)),
CONSTRAINT check_dae433eed6 CHECK ((char_length(name) <= 256))
);
CREATE SEQUENCE security_training_providers_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE security_training_providers_id_seq OWNED BY security_training_providers.id;
CREATE TABLE security_trainings (
id bigint NOT NULL,
project_id bigint NOT NULL,
provider_id bigint NOT NULL,
is_primary boolean DEFAULT false NOT NULL,
created_at timestamp with time zone NOT NULL,
updated_at timestamp with time zone NOT NULL
);
CREATE SEQUENCE security_trainings_id_seq
START WITH 1
INCREMENT BY 1
NO MINVALUE
NO MAXVALUE
CACHE 1;
ALTER SEQUENCE security_trainings_id_seq OWNED BY security_trainings.id;
CREATE TABLE self_managed_prometheus_alert_events (
id bigint NOT NULL,
project_id bigint NOT NULL,
......@@ -22074,6 +22115,10 @@ ALTER TABLE ONLY security_orchestration_policy_rule_schedules ALTER COLUMN id SE
ALTER TABLE ONLY security_scans ALTER COLUMN id SET DEFAULT nextval('security_scans_id_seq'::regclass);
ALTER TABLE ONLY security_training_providers ALTER COLUMN id SET DEFAULT nextval('security_training_providers_id_seq'::regclass);
ALTER TABLE ONLY security_trainings ALTER COLUMN id SET DEFAULT nextval('security_trainings_id_seq'::regclass);
ALTER TABLE ONLY self_managed_prometheus_alert_events ALTER COLUMN id SET DEFAULT nextval('self_managed_prometheus_alert_events_id_seq'::regclass);
ALTER TABLE ONLY sent_notifications ALTER COLUMN id SET DEFAULT nextval('sent_notifications_id_seq'::regclass);
......@@ -24015,6 +24060,12 @@ ALTER TABLE ONLY security_orchestration_policy_rule_schedules
ALTER TABLE ONLY security_scans
ADD CONSTRAINT security_scans_pkey PRIMARY KEY (id);
ALTER TABLE ONLY security_training_providers
ADD CONSTRAINT security_training_providers_pkey PRIMARY KEY (id);
ALTER TABLE ONLY security_trainings
ADD CONSTRAINT security_trainings_pkey PRIMARY KEY (id);
ALTER TABLE ONLY self_managed_prometheus_alert_events
ADD CONSTRAINT self_managed_prometheus_alert_events_pkey PRIMARY KEY (id);
......@@ -27486,6 +27537,12 @@ CREATE INDEX index_security_scans_on_pipeline_id ON security_scans USING btree (
CREATE INDEX index_security_scans_on_project_id ON security_scans USING btree (project_id);
CREATE INDEX index_security_trainings_on_project_id ON security_trainings USING btree (project_id);
CREATE INDEX index_security_trainings_on_provider_id ON security_trainings USING btree (provider_id);
CREATE UNIQUE INDEX index_security_trainings_on_unique_project_id ON security_trainings USING btree (project_id) WHERE (is_primary IS TRUE);
CREATE INDEX index_self_managed_prometheus_alert_events_on_environment_id ON self_managed_prometheus_alert_events USING btree (environment_id);
CREATE INDEX index_sent_notifications_on_noteable_type_noteable_id ON sent_notifications USING btree (noteable_id) WHERE ((noteable_type)::text = 'Issue'::text);
......@@ -30753,6 +30810,9 @@ ALTER TABLE ONLY required_code_owners_sections
ALTER TABLE ONLY dast_site_profiles
ADD CONSTRAINT fk_rails_83e309d69e FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY security_trainings
ADD CONSTRAINT fk_rails_84c7951d72 FOREIGN KEY (provider_id) REFERENCES security_training_providers(id) ON DELETE CASCADE;
ALTER TABLE ONLY zentao_tracker_data
ADD CONSTRAINT fk_rails_84efda7be0 FOREIGN KEY (integration_id) REFERENCES integrations(id) ON DELETE CASCADE;
......@@ -31458,6 +31518,9 @@ ALTER TABLE ONLY internal_ids
ALTER TABLE ONLY issues_self_managed_prometheus_alert_events
ADD CONSTRAINT fk_rails_f7db2d72eb FOREIGN KEY (self_managed_prometheus_alert_event_id) REFERENCES self_managed_prometheus_alert_events(id) ON DELETE CASCADE;
ALTER TABLE ONLY security_trainings
ADD CONSTRAINT fk_rails_f80240fae0 FOREIGN KEY (project_id) REFERENCES projects(id) ON DELETE CASCADE;
ALTER TABLE ONLY merge_requests_closing_issues
ADD CONSTRAINT fk_rails_f8540692be FOREIGN KEY (issue_id) REFERENCES issues(id) ON DELETE CASCADE;
......@@ -14024,6 +14024,18 @@ four standard [pagination arguments](#connection-pagination-arguments):
| ---- | ---- | ----------- |
| <a id="projectscanexecutionpoliciesactionscantypes"></a>`actionScanTypes` | [`[SecurityReportTypeEnum!]`](#securityreporttypeenum) | Filters policies by the action scan type. Only these scan types are supported: `dast`, `secret_detection`, `cluster_image_scanning`, `container_scanning`, `sast`. |
##### `Project.securityTrainingProviders`
List of security training providers for the project.
Returns [`[ProjectSecurityTraining!]`](#projectsecuritytraining).
###### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectsecuritytrainingprovidersonlyenabled"></a>`onlyEnabled` | [`Boolean`](#boolean) | Filter the list by only enabled security trainings. |
##### `Project.sentryDetailedError`
Detailed version of a Sentry error on the project.
......@@ -14247,6 +14259,20 @@ Represents a Project Membership.
| <a id="projectpermissionsupdatewiki"></a>`updateWiki` | [`Boolean!`](#boolean) | Indicates the user can perform `update_wiki` on this resource. |
| <a id="projectpermissionsuploadfile"></a>`uploadFile` | [`Boolean!`](#boolean) | Indicates the user can perform `upload_file` on this resource. |
### `ProjectSecurityTraining`
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="projectsecuritytrainingdescription"></a>`description` | [`String`](#string) | Description of the training provider. |
| <a id="projectsecuritytrainingid"></a>`id` | [`GlobalID!`](#globalid) | ID of the training provider. |
| <a id="projectsecuritytrainingisenabled"></a>`isEnabled` | [`Boolean!`](#boolean) | Represents whether the provider is enabled or not. |
| <a id="projectsecuritytrainingisprimary"></a>`isPrimary` | [`Boolean!`](#boolean) | Represents whether the provider is set as primary or not. |
| <a id="projectsecuritytraininglogourl"></a>`logoUrl` | [`String`](#string) | Logo URL of the provider. |
| <a id="projectsecuritytrainingname"></a>`name` | [`String!`](#string) | Name of the training provider. |
| <a id="projectsecuritytrainingurl"></a>`url` | [`String!`](#string) | URL of the provider. |
### `ProjectStatistics`
#### Fields
......@@ -10,6 +10,10 @@ module EE
description: 'Information about security analyzers used in the project.',
method: :itself
field :security_training_providers, [::Types::Security::TrainingType], null: true,
description: 'List of security training providers for the project',
resolver: ::Resolvers::SecurityTrainingProvidersResolver
field :vulnerabilities,
::Types::VulnerabilityType.connection_type,
null: true,
......
......@@ -26,8 +26,8 @@ module Resolvers
# This finder class has been deprecated and will be removed by
# https://gitlab.com/gitlab-org/gitlab/-/issues/334488.
# We can remove the rescue block while addressing that issue.
Security::PipelineVulnerabilitiesFinder.new(pipeline: pipeline, params: args).execute.findings
rescue Security::PipelineVulnerabilitiesFinder::ParseError
::Security::PipelineVulnerabilitiesFinder.new(pipeline: pipeline, params: args).execute.findings
rescue ::Security::PipelineVulnerabilitiesFinder::ParseError
[]
end
end
......
......@@ -9,7 +9,7 @@ module Resolvers
argument :action_scan_types, [::Types::Security::ReportTypeEnum],
description: "Filters policies by the action scan type. "\
"Only these scan types are supported: #{Security::ScanExecutionPolicy::SCAN_TYPES.map { |type| "`#{type}`" }.join(', ')}.",
"Only these scan types are supported: #{::Security::ScanExecutionPolicy::SCAN_TYPES.map { |type| "`#{type}`" }.join(', ')}.",
required: false
def resolve(**args)
......
# frozen_string_literal: true
module Resolvers
class SecurityTrainingProvidersResolver < BaseResolver
include Gitlab::Graphql::Authorize::AuthorizeResource
type Types::Security::TrainingType, null: false
authorize :access_security_and_compliance
authorizes_object!
argument :only_enabled, GraphQL::Types::Boolean,
required: false,
description: "Filter the list by only enabled security trainings."
def resolve(only_enabled: false)
::Security::TrainingProvider.for_project(object, only_enabled: only_enabled)
end
end
end
......@@ -85,7 +85,7 @@ module Resolvers
private
def vulnerabilities(params)
apply_lookahead(Security::VulnerabilitiesFinder.new(vulnerable, params).execute)
apply_lookahead(::Security::VulnerabilitiesFinder.new(vulnerable, params).execute)
end
end
end
# frozen_string_literal: true
module Types
module Security
class TrainingType < BaseObject # rubocop:disable Graphql/AuthorizeTypes(Authorization is done in resolver layer)
graphql_name 'ProjectSecurityTraining'
field :id,
type: ::Types::GlobalIDType,
null: false,
description: 'ID of the training provider.'
field :name, GraphQL::Types::String, null: false,
description: 'Name of the training provider.'
field :description, GraphQL::Types::String, null: true,
description: 'Description of the training provider.'
field :url, GraphQL::Types::String, null: false,
description: 'URL of the provider.'
field :logo_url, GraphQL::Types::String, null: true,
description: 'Logo URL of the provider.'
field :is_enabled, GraphQL::Types::Boolean, null: false,
description: 'Represents whether the provider is enabled or not.'
field :is_primary, GraphQL::Types::Boolean, null: false,
description: 'Represents whether the provider is set as primary or not.'
end
end
end
# frozen_string_literal: true
module Security
class Training < ApplicationRecord
self.table_name = 'security_trainings'
belongs_to :project, optional: false
belongs_to :provider, optional: false, inverse_of: :trainings, class_name: 'Security::TrainingProvider'
validates :project_id, uniqueness: true, if: :is_primary?
end
end
# frozen_string_literal: true
module Security
class TrainingProvider < ApplicationRecord
self.table_name = 'security_training_providers'
has_many :trainings, inverse_of: :provider, class_name: 'Security::Training'
# These are the virtual attributes
# generated by the `for_project` scope.
attribute :is_enabled, :boolean
attribute :is_primary, :boolean
validates :name, presence: true, length: { maximum: 256 }
validates :description, length: { maximum: 512 }
validates :url, presence: true, length: { maximum: 512 }
validates :logo_url, length: { maximum: 512 }
scope :for_project, -> (project, only_enabled: false) do
joins("LEFT OUTER JOIN security_trainings st ON st.provider_id = security_training_providers.id AND st.project_id = #{project.id}")
.select(default_select_columns)
.select('CASE WHEN st.id IS NULL THEN false ELSE true END AS is_enabled')
.select('COALESCE(st.is_primary, FALSE) AS is_primary')
.tap { |relation| relation.where!('st.id IS NOT NULL') if only_enabled }
end
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :security_training_provider, class: 'Security::TrainingProvider' do
name { 'Acme' }
url { 'example.com' }
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :security_training, class: 'Security::Training' do
project
provider factory: :security_training_provider
trait :primary do
is_primary { true }
end
end
end
......@@ -17,7 +17,7 @@ RSpec.describe GitlabSchema.types['Project'] do
it 'includes the ee specific fields' do
expected_fields = %w[
vulnerabilities vulnerability_scanners requirement_states_count
security_training_providers vulnerabilities vulnerability_scanners requirement_states_count
vulnerability_severities_count packages compliance_frameworks vulnerabilities_count_by_day
security_dashboard_path iterations iteration_cadences repository_size_excess actual_repository_size_limit
code_coverage_summary api_fuzzing_ci_configuration corpuses path_locks incident_management_escalation_policies
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe GitlabSchema.types['ProjectSecurityTraining'] do
let(:fields) { %i[id name description url logo_url is_enabled is_primary] }
it { expect(described_class).to have_graphql_fields(fields) }
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Security::TrainingProvider do
describe 'associations' do
it { is_expected.to have_many(:trainings) }
end
describe 'validations' do
it { is_expected.to validate_presence_of(:name) }
it { is_expected.to validate_length_of(:name).is_at_most(256) }
it { is_expected.to validate_length_of(:description).is_at_most(512) }
it { is_expected.to validate_presence_of(:url) }
it { is_expected.to validate_length_of(:url).is_at_most(512) }
it { is_expected.to validate_length_of(:logo_url).is_at_most(512) }
end
describe '.for_project' do
let_it_be(:project) { create(:project) }
let_it_be(:security_training_provider_1) { create(:security_training_provider) }
let_it_be(:security_training_provider_2) { create(:security_training_provider) }
let_it_be(:security_training_provider_3) { create(:security_training_provider) }
subject { described_class.for_project(project, only_enabled: only_enabled) }
before_all do
create(:security_training, :primary, project: project, provider: security_training_provider_1)
create(:security_training, project: project, provider: security_training_provider_2)
end
context 'when the `only_enabled` flag is provided as `false`' do
let(:only_enabled) { false }
it { is_expected.to match_array([security_training_provider_1, security_training_provider_2, security_training_provider_3]) }
end
context 'when the `only_enabled` flag is provided as `true`' do
let(:only_enabled) { true }
it { is_expected.to match_array([security_training_provider_1, security_training_provider_2]) }
end
describe 'virtual attributes' do
let(:only_enabled) { false }
it 'sets the virtual attributes correctly' do
is_expected.to match_array([
an_object_having_attributes(is_primary: true, is_enabled: true),
an_object_having_attributes(is_primary: false, is_enabled: true),
an_object_having_attributes(is_primary: false, is_enabled: false)
])
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Security::Training do
describe 'associations' do
it { is_expected.to belong_to(:project).required }
it { is_expected.to belong_to(:provider).required }
end
describe 'validations' do
describe 'one primary per project' do
context 'when the training is primary' do
subject { create(:security_training, :primary) }
it { is_expected.to validate_uniqueness_of(:project_id) }
end
context 'when the training is not primary' do
subject { create(:security_training) }
it { is_expected.not_to validate_uniqueness_of(:project_id) }
end
end
end
end
......@@ -462,6 +462,8 @@ security_findings: :gitlab_main
security_orchestration_policy_configurations: :gitlab_main
security_orchestration_policy_rule_schedules: :gitlab_main
security_scans: :gitlab_main
security_training_providers: :gitlab_main
security_trainings: :gitlab_main
self_managed_prometheus_alert_events: :gitlab_main
sent_notifications: :gitlab_main
sentry_issues: :gitlab_main
......
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