Commit 593e986b authored by Craig Smith's avatar Craig Smith Committed by Mark Chao

Add delete mutation for DAST scanner profile

To allow DAST on-demand scan users to manage
their scanner profiles, the MR adds a delete
mutation so they can remove profiles that are no
longer required.
parent ce32fa31
...@@ -3061,6 +3061,41 @@ type DastScannerProfileCreatePayload { ...@@ -3061,6 +3061,41 @@ type DastScannerProfileCreatePayload {
id: ID @deprecated(reason: "Use `global_id`. Deprecated in 13.4") id: ID @deprecated(reason: "Use `global_id`. Deprecated in 13.4")
} }
"""
Autogenerated input type of DastScannerProfileDelete
"""
input DastScannerProfileDeleteInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Full path for the project the scanner profile belongs to.
"""
fullPath: ID!
"""
ID of the scanner profile to be deleted.
"""
id: DastScannerProfileID!
}
"""
Autogenerated return type of DastScannerProfileDelete
"""
type DastScannerProfileDeletePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
}
""" """
An edge in a connection. An edge in a connection.
""" """
...@@ -10081,6 +10116,7 @@ type Mutation { ...@@ -10081,6 +10116,7 @@ type Mutation {
createSnippet(input: CreateSnippetInput!): CreateSnippetPayload createSnippet(input: CreateSnippetInput!): CreateSnippetPayload
dastOnDemandScanCreate(input: DastOnDemandScanCreateInput!): DastOnDemandScanCreatePayload dastOnDemandScanCreate(input: DastOnDemandScanCreateInput!): DastOnDemandScanCreatePayload
dastScannerProfileCreate(input: DastScannerProfileCreateInput!): DastScannerProfileCreatePayload dastScannerProfileCreate(input: DastScannerProfileCreateInput!): DastScannerProfileCreatePayload
dastScannerProfileDelete(input: DastScannerProfileDeleteInput!): DastScannerProfileDeletePayload
dastScannerProfileUpdate(input: DastScannerProfileUpdateInput!): DastScannerProfileUpdatePayload dastScannerProfileUpdate(input: DastScannerProfileUpdateInput!): DastScannerProfileUpdatePayload
dastSiteProfileCreate(input: DastSiteProfileCreateInput!): DastSiteProfileCreatePayload dastSiteProfileCreate(input: DastSiteProfileCreateInput!): DastSiteProfileCreatePayload
dastSiteProfileDelete(input: DastSiteProfileDeleteInput!): DastSiteProfileDeletePayload dastSiteProfileDelete(input: DastSiteProfileDeleteInput!): DastSiteProfileDeletePayload
......
...@@ -8306,6 +8306,108 @@ ...@@ -8306,6 +8306,108 @@
"enumValues": null, "enumValues": null,
"possibleTypes": null "possibleTypes": null
}, },
{
"kind": "INPUT_OBJECT",
"name": "DastScannerProfileDeleteInput",
"description": "Autogenerated input type of DastScannerProfileDelete",
"fields": null,
"inputFields": [
{
"name": "fullPath",
"description": "Full path for the project the scanner profile belongs to.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "id",
"description": "ID of the scanner profile to be deleted.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DastScannerProfileID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"defaultValue": null
}
],
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DastScannerProfileDeletePayload",
"description": "Autogenerated return type of DastScannerProfileDelete",
"fields": [
{
"name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "String",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "errors",
"description": "Errors encountered during execution of the mutation.",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
}
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "DastScannerProfileEdge", "name": "DastScannerProfileEdge",
...@@ -28873,6 +28975,33 @@ ...@@ -28873,6 +28975,33 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "dastScannerProfileDelete",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DastScannerProfileDeleteInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DastScannerProfileDeletePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "dastScannerProfileUpdate", "name": "dastScannerProfileUpdate",
"description": null, "description": null,
...@@ -532,6 +532,15 @@ Autogenerated return type of DastScannerProfileCreate ...@@ -532,6 +532,15 @@ Autogenerated return type of DastScannerProfileCreate
| `globalId` | DastScannerProfileID | ID of the scanner profile. | | `globalId` | DastScannerProfileID | ID of the scanner profile. |
| `id` **{warning-solid}** | ID | **Deprecated:** Use `global_id`. Deprecated in 13.4 | | `id` **{warning-solid}** | ID | **Deprecated:** Use `global_id`. Deprecated in 13.4 |
## DastScannerProfileDeletePayload
Autogenerated return type of DastScannerProfileDelete
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
## DastScannerProfileUpdatePayload ## DastScannerProfileUpdatePayload
Autogenerated return type of DastScannerProfileUpdate Autogenerated return type of DastScannerProfileUpdate
......
...@@ -34,6 +34,7 @@ module EE ...@@ -34,6 +34,7 @@ module EE
mount_mutation ::Mutations::DastSiteProfiles::Delete mount_mutation ::Mutations::DastSiteProfiles::Delete
mount_mutation ::Mutations::DastScannerProfiles::Create mount_mutation ::Mutations::DastScannerProfiles::Create
mount_mutation ::Mutations::DastScannerProfiles::Update mount_mutation ::Mutations::DastScannerProfiles::Update
mount_mutation ::Mutations::DastScannerProfiles::Delete
mount_mutation ::Mutations::Security::CiConfiguration::ConfigureSast mount_mutation ::Mutations::Security::CiConfiguration::ConfigureSast
mount_mutation ::Mutations::Namespaces::IncreaseStorageTemporarily mount_mutation ::Mutations::Namespaces::IncreaseStorageTemporarily
end end
......
# frozen_string_literal: true
module Mutations
module DastScannerProfiles
class Delete < BaseMutation
include AuthorizesProject
graphql_name 'DastScannerProfileDelete'
argument :full_path, GraphQL::ID_TYPE,
required: true,
description: 'Full path for the project the scanner profile belongs to.'
argument :id, ::Types::GlobalIDType[::DastScannerProfile],
required: true,
description: 'ID of the scanner profile to be deleted.'
authorize :create_on_demand_dast_scan
def resolve(full_path:, id:)
project = authorized_find_project!(full_path: full_path)
service = ::DastScannerProfiles::DestroyService.new(project, current_user)
result = service.execute({ id: id.model_id })
if result.success?
{ errors: [] }
else
{ errors: result.errors }
end
end
end
end
end
# frozen_string_literal: true
module DastScannerProfiles
class DestroyService < BaseService
include Gitlab::Allowable
def execute(id:)
return unauthorized unless can_delete_scanner_profile?
dast_scanner_profile = find_dast_scanner_profile(id)
return ServiceResponse.error(message: "Scanner profile not found for given parameters") unless dast_scanner_profile
if dast_scanner_profile.destroy
ServiceResponse.success(payload: dast_scanner_profile)
else
ServiceResponse.error(message: 'Scanner profile failed to delete')
end
end
private
def unauthorized
::ServiceResponse.error(message: _('You are not authorized to update this scanner profile'), http_status: 403)
end
def can_delete_scanner_profile?
can?(current_user, :create_on_demand_dast_scan, project)
end
def find_dast_scanner_profile(id)
project.dast_scanner_profiles.id_in(id).first
end
end
end
---
title: Add delete mutation for DAST scanner profile
merge_request: 40805
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::DastScannerProfiles::Delete do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:full_path) { project.full_path }
let_it_be(:dast_scanner_profile) { create(:dast_scanner_profile, project: project) }
let(:dast_scanner_profile_id) { dast_scanner_profile.to_global_id }
subject(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
before do
stub_licensed_features(security_on_demand_scans: true)
end
describe '#resolve' do
subject do
mutation.resolve(
full_path: full_path,
id: dast_scanner_profile_id
)
end
context 'when the project does not exist' do
let(:full_path) { SecureRandom.hex }
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is not associated with the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can run a DAST scan' do
before do
project.add_developer(user)
end
it 'deletes the DAST scanner profile' do
expect { subject }.to change { DastScannerProfile.count }.by(-1)
end
context 'when the dast_scanner_profile does not exist' do
let(:dast_scanner_profile_id) { Gitlab::GlobalId.build(nil, model_name: 'DastScannerProfile', id: 'does_not_exist') }
it 'returns an error' do
expect(subject[:errors]).to include('Scanner profile not found for given parameters')
end
end
context 'when security_on_demand_scans_feature_flag is disabled' do
it 'raises an exception' do
stub_feature_flags(security_on_demand_scans_feature_flag: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when on demand scan licensed feature is not available' do
it 'raises an exception' do
stub_licensed_features(security_on_demand_scans: false)
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when deletion fails' do
it 'returns an error' do
allow_next_instance_of(::DastScannerProfiles::DestroyService) do |service|
allow(service).to receive(:execute).and_return(
ServiceResponse.error(message: 'Scanner profile failed to delete')
)
end
expect(subject[:errors]).to include('Scanner profile failed to delete')
end
end
end
end
end
...@@ -41,14 +41,6 @@ RSpec.describe Mutations::DastScannerProfiles::Update do ...@@ -41,14 +41,6 @@ RSpec.describe Mutations::DastScannerProfiles::Update do
end end
end end
context 'when dast scanner profile does not exist' do
let(:scanner_profile_id) { Gitlab::GlobalId.build(nil, model_name: 'DastScannerProfile', id: 'does_not_exist') }
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user is not associated with the project' do context 'when the user is not associated with the project' do
it 'raises an exception' do it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable) expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
...@@ -78,6 +70,14 @@ RSpec.describe Mutations::DastScannerProfiles::Update do ...@@ -78,6 +70,14 @@ RSpec.describe Mutations::DastScannerProfiles::Update do
end end
end end
context 'when dast scanner profile does not exist' do
let(:scanner_profile_id) { Gitlab::GlobalId.build(nil, model_name: 'DastScannerProfile', id: 'does_not_exist') }
it 'raises an exception' do
expect(subject[:errors]).to include('Scanner profile not found for given parameters')
end
end
context 'when on demand scan feature is not enabled' do context 'when on demand scan feature is not enabled' do
it 'raises an exception' do it 'raises an exception' do
stub_feature_flags(security_on_demand_scans_feature_flag: false) stub_feature_flags(security_on_demand_scans_feature_flag: false)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Delete a DAST Scanner Profile' do
include GraphqlHelpers
let!(:dast_scanner_profile) { create(:dast_scanner_profile, project: project, target_timeout: 200, spider_timeout: 5000) }
let(:dast_scanner_profile_id) { dast_scanner_profile.to_global_id.to_s }
let(:mutation_name) { :dast_scanner_profile_delete }
let(:mutation) do
graphql_mutation(
mutation_name,
full_path: full_path,
id: dast_scanner_profile_id
)
end
it_behaves_like 'an on-demand scan mutation when user cannot run an on-demand scan'
it_behaves_like 'an on-demand scan mutation when user can run an on-demand scan' do
it 'deletes the dast_scanner_profile' do
expect { subject }.to change { DastScannerProfile.count }.by(-1)
end
context 'when the dast_scanner_profile belongs to another project' do
let(:project_1) { create(:project, :repository, creator: current_user) }
let(:full_path) { project_1.full_path }
it_behaves_like 'a mutation that returns a top-level access error'
end
context 'when the dast_scanner_profile does not exist' do
let(:dast_scanner_profile_id) { Gitlab::GlobalId.build(nil, model_name: 'DastScannerProfile', id: 'does_not_exist') }
it_behaves_like 'a mutation that returns errors in the response', errors: ['Scanner profile not found for given parameters']
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DastScannerProfiles::DestroyService do
let_it_be(:user) { create(:user) }
let_it_be(:dast_scanner_profile, reload: true) { create(:dast_scanner_profile, target_timeout: 200, spider_timeout: 5000) }
let(:project) { dast_scanner_profile.project }
before do
stub_licensed_features(security_on_demand_scans: true)
end
describe '#execute' do
subject do
described_class.new(project, user).execute(
id: dast_scanner_profile_id
)
end
let(:dast_scanner_profile_id) { dast_scanner_profile.id }
let(:status) { subject.status }
let(:message) { subject.message }
let(:payload) { subject.payload }
context 'when a user does not have access to the project' do
it 'returns an error status' do
expect(status).to eq(:error)
end
it 'populates message' do
expect(message).to eq('You are not authorized to update this scanner profile')
end
end
context 'when the user can run a DAST scan' do
before do
project.add_developer(user)
end
it 'returns a success status' do
expect(status).to eq(:success)
end
it 'deletes the dast_scanner_profile' do
expect { subject }.to change { DastScannerProfile.count }.by(-1)
end
it 'returns a dast_scanner_profile payload' do
expect(payload).to be_a(DastScannerProfile)
end
context 'when the dast_scanner_profile doesn\'t exist' do
let(:dast_scanner_profile_id) do
Gitlab::GlobalId.build(nil, model_name: 'DastScannerProfile', id: 'does_not_exist')
end
it 'returns an error status' do
expect(status).to eq(:error)
end
it 'populates message' do
expect(message).to eq('Scanner profile not found for given parameters')
end
end
context 'when on demand scan feature is disabled' do
before do
stub_feature_flags(security_on_demand_scans_feature_flag: false)
end
it 'returns an error status' do
expect(status).to eq(:error)
end
it 'populates message' do
expect(message).to eq('You are not authorized to update this scanner profile')
end
end
context 'when on demand scan licensed feature is not available' do
before do
stub_licensed_features(security_on_demand_scans: false)
end
it 'returns an error status' do
expect(status).to eq(:error)
end
it 'populates message' do
expect(message).to eq('You are not authorized to update this scanner profile')
end
end
end
end
end
...@@ -28,7 +28,6 @@ RSpec.describe DastScannerProfiles::UpdateService do ...@@ -28,7 +28,6 @@ RSpec.describe DastScannerProfiles::UpdateService do
let(:dast_scanner_profile_id) { dast_scanner_profile.id } let(:dast_scanner_profile_id) { dast_scanner_profile.id }
let(:status) { subject.status } let(:status) { subject.status }
let(:message) { subject.message } let(:message) { subject.message }
let(:errors) { subject.errors }
let(:payload) { subject.payload } let(:payload) { subject.payload }
context 'when a user does not have access to the project' do context 'when a user does not have access to the project' do
......
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