Commit c0090034 authored by Stan Hu's avatar Stan Hu

Merge branch '341485-corpus-create-mutation' into 'master'

Add corpus create mutation and service

See merge request gitlab-org/gitlab!71992
parents e7c1d5e7 07e6c2eb
......@@ -1034,6 +1034,27 @@ Input type: `ConfigureSecretDetectionInput`
| <a id="mutationconfiguresecretdetectionerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
| <a id="mutationconfiguresecretdetectionsuccesspath"></a>`successPath` | [`String`](#string) | Redirect path to use when the response is successful. |
### `Mutation.corpusCreate`
Available only when feature flag `corpus_management` is enabled. This flag is disabled by default, because the feature is experimental and is subject to change without notice.
Input type: `CorpusCreateInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationcorpuscreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationcorpuscreatefullpath"></a>`fullPath` | [`ID!`](#id) | Project the corpus belongs to. |
| <a id="mutationcorpuscreatepackageid"></a>`packageId` | [`PackagesPackageID!`](#packagespackageid) | ID of the corpus package. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationcorpuscreateclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationcorpuscreateerrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.createAlertIssue`
Input type: `CreateAlertIssueInput`
......
......@@ -78,6 +78,7 @@ module EE
mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Update
mount_mutation ::Mutations::IncidentManagement::EscalationPolicy::Destroy
mount_mutation ::Mutations::AppSec::Fuzzing::API::CiConfiguration::Create
mount_mutation ::Mutations::AppSec::Fuzzing::Coverage::Corpus::Create, feature_flag: :corpus_management
mount_mutation ::Mutations::Projects::SetComplianceFramework
mount_mutation ::Mutations::SecurityPolicy::CommitScanExecutionPolicy
mount_mutation ::Mutations::SecurityPolicy::AssignSecurityPolicyProject
......
# frozen_string_literal: true
module Mutations
module AppSec::Fuzzing::Coverage
module Corpus
class Create < BaseMutation
include FindsProject
graphql_name 'CorpusCreate'
authorize :create_coverage_fuzzing_corpus
argument :package_id, Types::GlobalIDType[::Packages::Package],
required: true,
description: 'ID of the corpus package.'
argument :full_path, GraphQL::Types::ID,
required: true,
description: 'Project the corpus belongs to.'
def resolve(full_path:, package_id:)
project = authorized_find!(full_path)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless allowed?(project)
response = ::AppSec::Fuzzing::Coverage::Corpuses::CreateService.new(
project: project,
current_user: current_user,
params: {
package_id: Gitlab::Graphql::Lazy.force(GitlabSchema.find_by_gid(package_id))&.id
}
).execute
return { errors: response.errors } if response.error?
build_response(response.payload)
end
private
def allowed?(project)
Feature.enabled?(:corpus_management, project, default_enabled: :yaml)
end
def build_response(payload)
{
errors: [],
corpus: payload.fetch(:corpus)
}
end
end
end
end
end
......@@ -9,6 +9,20 @@ module AppSec
belongs_to :package, class_name: 'Packages::Package'
belongs_to :user, optional: true
belongs_to :project
validate :project_same_as_package_project
def audit_details
user&.name
end
private
def project_same_as_package_project
if package && package.project_id != project_id
errors.add(:package_id, 'should belong to the associated project')
end
end
end
end
end
......
......@@ -210,6 +210,7 @@ module EE
rule { coverage_fuzzing_enabled & can?(:developer_access) }.policy do
enable :read_coverage_fuzzing
enable :create_coverage_fuzzing_corpus
end
rule { on_demand_scans_enabled & can?(:developer_access) }.policy do
......
# frozen_string_literal: true
module AppSec
module Fuzzing
module Coverage
module Corpuses
class CreateService < BaseProjectService
def execute
return ServiceResponse.error(message: 'Insufficient permissions') unless allowed?
corpus = AppSec::Fuzzing::Coverage::Corpus.new(
project: project,
user: current_user,
package_id: params.fetch(:package_id)
)
if corpus.save
create_audit_event(corpus)
return ServiceResponse.success(
payload: {
corpus: corpus
}
)
end
ServiceResponse.error(message: corpus.errors.full_messages)
rescue KeyError => err
ServiceResponse.error(message: err.message.capitalize)
end
private
def allowed?
project.licensed_feature_available?(:coverage_fuzzing)
end
def create_audit_event(corpus)
::Gitlab::Audit::Auditor.audit(
name: 'coverage_fuzzing_corpus_create',
author: current_user,
scope: project,
target: corpus,
message: 'Added Coverage Fuzzing Corpus'
)
end
end
end
end
end
end
......@@ -3,7 +3,7 @@
FactoryBot.define do
factory :corpus, class: 'AppSec::Fuzzing::Coverage::Corpus' do
user
package
project
package { association :package, project: project }
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::AppSec::Fuzzing::Coverage::Corpus::Create do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user, developer_projects: [project] ) }
let_it_be(:package) { create(:package, project: project, creator: developer) }
let(:corpus) { AppSec::Fuzzing::Coverage::Corpus.find_by(user: developer, project: project) }
let(:mutation) { described_class.new(object: nil, context: { current_user: developer }, field: nil) }
before do
stub_licensed_features(coverage_fuzzing: true)
end
specify { expect(described_class).to require_graphql_authorizations(:create_coverage_fuzzing_corpus) }
describe '#resolve' do
subject(:resolve) do
mutation.resolve(
full_path: project.full_path,
package_id: package.to_global_id
)
end
context 'when the feature is licensed' do
context 'when the user can create a corpus' do
context 'when corpus_management feature is enabled' do
before do
stub_feature_flags(corpus_management: true)
end
it 'returns the corpus' do
expect(resolve[:corpus]).to eq(corpus)
end
end
context 'when corpus_management feature is disabled' do
before do
stub_feature_flags(corpus_management: false)
end
it 'raises the resource not available error' do
expect { resolve }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
end
end
end
end
......@@ -12,4 +12,16 @@ RSpec.describe AppSec::Fuzzing::Coverage::Corpus, type: :model do
it { is_expected.to belong_to(:user).optional }
it { is_expected.to belong_to(:project) }
end
describe 'validate' do
describe 'project_same_as_package_project' do
let(:package_2) { create(:package) }
subject(:corpus) { build(:corpus, package: package_2) }
it 'raises the error on adding the package of a different project' do
expect { corpus.save! }.to raise_error(ActiveRecord::RecordInvalid, 'Validation failed: Package should belong to the associated project')
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe AppSec::Fuzzing::Coverage::Corpuses::CreateService do
let_it_be(:project) { create(:project) }
let_it_be(:developer) { create(:user, developer_projects: [project] ) }
let_it_be(:package) { create(:package, project: project, creator: developer) }
let_it_be(:default_params) do
{
package_id: package.id
}
end
let(:params) { default_params }
subject(:service_result) { described_class.new(project: project, current_user: developer, params: params).execute }
describe 'execute' do
before do
stub_licensed_features(coverage_fuzzing: coverage_fuzzing_enabled?)
end
context 'when the feature coverage_fuzzing is not available' do
let(:coverage_fuzzing_enabled?) { false }
it 'communicates failure', :aggregate_failures do
expect(service_result.status).to eq(:error)
expect(service_result.message).to eq('Insufficient permissions')
end
end
context 'when the feature coverage_fuzzing is enabled' do
let(:coverage_fuzzing_enabled?) { true }
it 'communicates success' do
expect(service_result.status).to eq(:success)
end
it 'creates a corpus' do
expect { service_result }.to change { AppSec::Fuzzing::Coverage::Corpus.count }.by(1)
end
it 'audits the creation', :aggregate_failures do
corpus = service_result.payload[:corpus]
audit_event = AuditEvent.find_by(target_id: corpus.id)
expect(audit_event.author).to eq(developer)
expect(audit_event.entity).to eq(project)
expect(audit_event.target_id).to eq(corpus.id)
expect(audit_event.target_type).to eq('AppSec::Fuzzing::Coverage::Corpus')
expect(audit_event.target_details).to eq(developer.name)
expect(audit_event.details).to eq({
author_name: developer.name,
custom_message: 'Added Coverage Fuzzing Corpus',
target_id: corpus.id,
target_type: 'AppSec::Fuzzing::Coverage::Corpus',
target_details: developer.name
})
end
context 'when a param is missing' do
let(:params) { default_params.except(:package_id) }
it 'communicates failure', :aggregate_failures do
expect(service_result.status).to eq(:error)
expect(service_result.message).to eq('Key not found: :package_id')
end
end
context 'when a param is incorrect' do
let(:package_2) { create(:package) }
let(:params) { { package_id: package_2.id } }
it 'communicates failure', :aggregate_failures do
allow_next_instance_of(AppSec::Fuzzing::Coverage::Corpus) do |service|
allow(service).to receive(:save).and_return(false)
allow(service).to receive_message_chain(:errors, :full_messages)
.and_return(['error message'])
end
expect(service_result.status).to eq(:error)
expect(service_result.message).to match_array(['error message'])
end
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