Commit df90f315 authored by Philip Cunningham's avatar Philip Cunningham Committed by Luke Duncalfe

Add DastProfileDelete GraphQL mutation

Allow users to delete existing Dast::Profiles.
parent f6e06d4d
......@@ -5852,6 +5852,36 @@ type DastProfileCreatePayload {
pipelineUrl: String
}
"""
Autogenerated input type of DastProfileDelete
"""
input DastProfileDeleteInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
ID of the profile to be deleted.
"""
id: DastProfileID!
}
"""
Autogenerated return type of DastProfileDelete
"""
type DastProfileDeletePayload {
"""
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.
"""
......@@ -16368,6 +16398,7 @@ type Mutation {
createTestCase(input: CreateTestCaseInput!): CreateTestCasePayload
dastOnDemandScanCreate(input: DastOnDemandScanCreateInput!): DastOnDemandScanCreatePayload
dastProfileCreate(input: DastProfileCreateInput!): DastProfileCreatePayload
dastProfileDelete(input: DastProfileDeleteInput!): DastProfileDeletePayload
dastScannerProfileCreate(input: DastScannerProfileCreateInput!): DastScannerProfileCreatePayload
dastScannerProfileDelete(input: DastScannerProfileDeleteInput!): DastScannerProfileDeletePayload
dastScannerProfileUpdate(input: DastScannerProfileUpdateInput!): DastScannerProfileUpdatePayload
......
......@@ -15916,6 +15916,94 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DastProfileDeleteInput",
"description": "Autogenerated input type of DastProfileDelete",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "ID of the profile to be deleted.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DastProfileID",
"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": "DastProfileDeletePayload",
"description": "Autogenerated return type of DastProfileDelete",
"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",
"name": "DastProfileEdge",
......@@ -45740,6 +45828,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dastProfileDelete",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DastProfileDeleteInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DastProfileDeletePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dastScannerProfileCreate",
"description": null,
......@@ -941,6 +941,15 @@ Autogenerated return type of DastProfileCreate.
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `pipelineUrl` | String | The URL of the pipeline that was created. Requires `runAfterCreate` to be set to `true`. |
### DastProfileDeletePayload
Autogenerated return type of DastProfileDelete.
| Field | Type | Description |
| ----- | ---- | ----------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
### DastScannerProfile
Represents a DAST scanner profile.
......
......@@ -43,6 +43,7 @@ module EE
mount_mutation ::Mutations::InstanceSecurityDashboard::RemoveProject
mount_mutation ::Mutations::DastOnDemandScans::Create
mount_mutation ::Mutations::Dast::Profiles::Create
mount_mutation ::Mutations::Dast::Profiles::Delete
mount_mutation ::Mutations::DastSiteProfiles::Create
mount_mutation ::Mutations::DastSiteProfiles::Update
mount_mutation ::Mutations::DastSiteProfiles::Delete
......
# frozen_string_literal: true
module Mutations
module Dast
module Profiles
class Delete < BaseMutation
graphql_name 'DastProfileDelete'
ProfileID = ::Types::GlobalIDType[::Dast::Profile]
argument :id, ProfileID,
required: true,
description: 'ID of the profile to be deleted.'
authorize :create_on_demand_dast_scan
def resolve(id:)
dast_profile = authorized_find!(id)
raise Gitlab::Graphql::Errors::ResourceNotAvailable, 'Feature disabled' unless enabled?(dast_profile.project)
response = ::Dast::Profiles::DestroyService.new(
container: dast_profile.project,
current_user: current_user,
params: { dast_profile: dast_profile }
).execute
{ errors: response.errors }
end
private
def enabled?(project)
Feature.enabled?(:dast_saved_scans, project, default_enabled: :yaml)
end
def find_object(id)
# TODO: remove this line when the compatibility layer is removed
# See: https://gitlab.com/gitlab-org/gitlab/-/issues/257883
id = ProfileID.coerce_isolated_input(id)
GitlabSchema.find_by_gid(id)
end
end
end
end
end
# frozen_string_literal: true
module Dast
module Profiles
class DestroyService < BaseContainerService
def execute
return unauthorized unless allowed?
return ServiceResponse.error(message: 'Profile parameter missing') unless dast_profile
return ServiceResponse.error(message: 'Profile failed to delete') unless dast_profile.destroy
ServiceResponse.success(payload: dast_profile)
end
private
def allowed?
Feature.enabled?(:dast_saved_scans, container, default_enabled: :yaml) &&
can?(current_user, :create_on_demand_dast_scan, container)
end
def unauthorized
ServiceResponse.error(
message: 'You are not authorized to update this profile',
http_status: 403
)
end
def dast_profile
params[:dast_profile]
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Dast::Profiles::Delete do
let_it_be(:project) { create(:project) }
let_it_be(:user) { create(:user) }
let_it_be(:dast_profile) { create(:dast_profile, project: project) }
let(:dast_profile_gid) { dast_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
specify { expect(described_class).to require_graphql_authorizations(:create_on_demand_dast_scan) }
describe '#resolve' do
subject { mutation.resolve(id: dast_profile_gid) }
context 'when the user cannot read the project' do
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when the user can destroy a DAST profile' do
before do
project.add_developer(user)
end
it 'deletes the profile' do
expect { subject }.to change { Dast::Profile.count }.by(-1)
end
context 'when the dast_profile does not exist' do
let(:dast_profile_gid) { Gitlab::GlobalId.build(nil, model_name: 'Dast::Profile', id: 'does_not_exist') }
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when DAST profile belongs to a project the user does not have access to' do
let_it_be(:dast_profile) { create(:dast_profile) }
it 'raises an exception' do
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(::Dast::Profiles::DestroyService) do |service|
allow(service).to receive(:execute).and_return(
ServiceResponse.error(message: 'Profile failed to delete')
)
end
expect(subject[:errors]).to include('Profile failed to delete')
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Deleting a DAST Profile' do
include GraphqlHelpers
let!(:dast_profile) { create(:dast_profile, project: project) }
let(:mutation_name) { :dast_profile_delete }
let(:mutation) { graphql_mutation(mutation_name, id: global_id_of(dast_profile)) }
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_profile' do
expect { subject }.to change { Dast::Profile.count }.by(-1)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Dast::Profiles::DestroyService do
include GraphqlHelpers
let_it_be(:user) { create(:user) }
let_it_be(:project) { create(:project) }
let_it_be(:dast_profile, reload: true) { create(:dast_profile, project: project) }
subject do
described_class.new(
container: project,
current_user: user,
params: { dast_profile: dast_profile }
).execute
end
describe '#execute' do
before do
project.clear_memoization(:licensed_feature_available)
end
context 'when the feature flag dast_saved_scans is disabled' do
it 'communicates failure' do
stub_licensed_features(security_on_demand_scans: true)
stub_feature_flags(dast_saved_scans: false)
expect(subject).to have_attributes(
status: :error,
message: 'You are not authorized to update this profile'
)
end
end
context 'when on demand scan licensed feature is not available' do
it 'communicates failure' do
stub_licensed_features(security_on_demand_scans: false)
stub_feature_flags(dast_saved_scans: true)
expect(subject).to have_attributes(
status: :error,
message: 'You are not authorized to update this profile'
)
end
end
context 'when the feature is enabled' do
before do
stub_licensed_features(security_on_demand_scans: true)
stub_feature_flags(dast_saved_scans: true)
end
context 'when the user cannot destroy a DAST profile' do
it 'communicates failure' do
expect(subject).to have_attributes(
status: :error,
message: 'You are not authorized to update this profile'
)
end
end
context 'when the user can destroy a DAST profile' do
before do
project.add_developer(user)
end
it 'returns a success status' do
expect(subject.status).to eq(:success)
end
it 'deletes the dast_profile' do
expect { subject }.to change { Dast::Profile.count }.by(-1)
end
it 'returns a dast_profile payload' do
expect(subject.payload).to be_a(Dast::Profile)
end
context 'when the dast_profile fails to destroy' do
it 'communicates failure' do
allow(dast_profile).to receive(:destroy).and_return(false)
expect(subject).to have_attributes(
status: :error,
message: 'Profile failed to delete'
)
end
end
context 'when the dast_profile parameter is missing' do
let(:dast_profile) { nil }
it 'communicates failure' do
expect(subject).to have_attributes(
status: :error,
message: 'Profile parameter missing'
)
end
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