Skip to content
Snippets Groups Projects
Commit df90f315 authored by Philip Cunningham's avatar Philip Cunningham Committed by Luke Duncalfe
Browse files

Add DastProfileDelete GraphQL mutation

Allow users to delete existing Dast::Profiles.
parent f6e06d4d
No related branches found
No related tags found
No related merge requests found
Loading
Loading
@@ -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.
"""
Loading
Loading
@@ -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
Loading
Loading
Loading
Loading
@@ -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",
Loading
Loading
@@ -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,
Loading
Loading
@@ -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.
Loading
Loading
Loading
Loading
@@ -43,6 +43,7 @@ module MutationType
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
Loading
Loading
# 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
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment