Commit 55c7d244 authored by Craig Smith's avatar Craig Smith

Add endpoint to update dast scanner profile

To allow dast users to edit their scanner profiles
this commit implements the update DastScannerProfiles
mutation
parent 47ae2430
......@@ -2959,6 +2959,66 @@ type DastScannerProfileEdge {
node: DastScannerProfile
}
"""
Identifier of DastScannerProfile
"""
scalar DastScannerProfileID
"""
Autogenerated input type of DastScannerProfileUpdate
"""
input DastScannerProfileUpdateInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
The project the scanner profile belongs to.
"""
fullPath: ID!
"""
ID of the scanner profile to be updated.
"""
id: DastScannerProfileID!
"""
The name of the scanner profile.
"""
profileName: String!
"""
The maximum number of seconds allowed for the spider to traverse the site.
"""
spiderTimeout: Int!
"""
The maximum number of seconds allowed for the site under test to respond to a request.
"""
targetTimeout: Int!
}
"""
Autogenerated return type of DastScannerProfileUpdate
"""
type DastScannerProfileUpdatePayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Errors encountered during execution of the mutation.
"""
errors: [String!]!
"""
ID of the scanner profile.
"""
id: DastScannerProfileID
}
"""
Represents a DAST Site Profile.
"""
......@@ -9757,6 +9817,7 @@ type Mutation {
createSnippet(input: CreateSnippetInput!): CreateSnippetPayload
dastOnDemandScanCreate(input: DastOnDemandScanCreateInput!): DastOnDemandScanCreatePayload
dastScannerProfileCreate(input: DastScannerProfileCreateInput!): DastScannerProfileCreatePayload
dastScannerProfileUpdate(input: DastScannerProfileUpdateInput!): DastScannerProfileUpdatePayload
dastSiteProfileCreate(input: DastSiteProfileCreateInput!): DastSiteProfileCreatePayload
dastSiteProfileDelete(input: DastSiteProfileDeleteInput!): DastSiteProfileDeletePayload
dastSiteProfileUpdate(input: DastSiteProfileUpdateInput!): DastSiteProfileUpdatePayload
......
......@@ -8016,6 +8016,174 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "SCALAR",
"name": "DastScannerProfileID",
"description": "Identifier of DastScannerProfile",
"fields": null,
"inputFields": null,
"interfaces": null,
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DastScannerProfileUpdateInput",
"description": "Autogenerated input type of DastScannerProfileUpdate",
"fields": null,
"inputFields": [
{
"name": "fullPath",
"description": "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 updated.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "DastScannerProfileID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "profileName",
"description": "The name of the scanner profile.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "String",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "spiderTimeout",
"description": "The maximum number of seconds allowed for the spider to traverse the site.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "targetTimeout",
"description": "The maximum number of seconds allowed for the site under test to respond to a request.",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Int",
"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": "DastScannerProfileUpdatePayload",
"description": "Autogenerated return type of DastScannerProfileUpdate",
"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
},
{
"name": "id",
"description": "ID of the scanner profile.",
"args": [
],
"type": {
"kind": "SCALAR",
"name": "DastScannerProfileID",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "DastSiteProfile",
......@@ -27986,6 +28154,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dastScannerProfileUpdate",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DastScannerProfileUpdateInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DastScannerProfileUpdatePayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dastSiteProfileCreate",
"description": null,
......@@ -502,6 +502,16 @@ Autogenerated return type of DastScannerProfileCreate
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `id` | ID | ID of the scanner profile. |
## DastScannerProfileUpdatePayload
Autogenerated return type of DastScannerProfileUpdate
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Errors encountered during execution of the mutation. |
| `id` | DastScannerProfileID | ID of the scanner profile. |
## DastSiteProfile
Represents a DAST Site Profile.
......
......@@ -31,6 +31,7 @@ module EE
mount_mutation ::Mutations::DastSiteProfiles::Update
mount_mutation ::Mutations::DastSiteProfiles::Delete
mount_mutation ::Mutations::DastScannerProfiles::Create
mount_mutation ::Mutations::DastScannerProfiles::Update
mount_mutation ::Mutations::Security::CiConfiguration::ConfigureSast
mount_mutation ::Mutations::Namespaces::IncreaseStorageTemporarily
end
......
# frozen_string_literal: true
module Mutations
module DastScannerProfiles
class Update < BaseMutation
include ResolvesProject
graphql_name 'DastScannerProfileUpdate'
field :id, ::Types::GlobalIDType[::DastScannerProfile],
null: true,
description: 'ID of the scanner profile.'
argument :full_path, GraphQL::ID_TYPE,
required: true,
description: 'The project the scanner profile belongs to.'
argument :id, ::Types::GlobalIDType[::DastScannerProfile],
required: true,
description: 'ID of the scanner profile to be updated.'
argument :profile_name, GraphQL::STRING_TYPE,
required: true,
description: 'The name of the scanner profile.'
argument :spider_timeout, GraphQL::INT_TYPE,
required: true,
description: 'The maximum number of seconds allowed for the spider to traverse the site.'
argument :target_timeout, GraphQL::INT_TYPE,
required: true,
description: 'The maximum number of seconds allowed for the site under test to respond to a request.'
authorize :create_on_demand_dast_scan
def resolve(full_path:, **service_args)
project = authorized_find!(full_path: full_path)
service = ::DastScannerProfiles::UpdateService.new(project, current_user)
result = service.execute({ **service_args, id: service_args[:id].model_id })
if result.success?
{ id: result.payload.to_global_id, errors: [] }
else
{ errors: result.errors }
end
end
private
def find_object(full_path:)
resolve_project(full_path: full_path)
end
end
end
end
# frozen_string_literal: true
module DastScannerProfiles
class UpdateService < BaseService
include Gitlab::Allowable
def execute(id:, profile_name:, target_timeout:, spider_timeout:)
return unauthorized unless can_update_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.update(name: profile_name, target_timeout: target_timeout, spider_timeout: spider_timeout)
ServiceResponse.success(payload: dast_scanner_profile)
else
ServiceResponse.error(message: dast_scanner_profile.errors.full_messages)
end
end
private
def unauthorized
::ServiceResponse.error(message: _('You are not authorized to update this scanner profile'), http_status: 403)
end
def can_update_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 endpoint to update Dast Scanner Profile
merge_request: 40208
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::DastScannerProfiles::Update do
let_it_be(:group) { create(:group) }
let_it_be(:project) { create(:project, group: group) }
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, target_timeout: 200, spider_timeout: 5000) }
let_it_be(:new_profile_name) { SecureRandom.hex }
let_it_be(:new_target_timeout) { dast_scanner_profile.target_timeout + 1 }
let_it_be(:new_spider_timeout) { dast_scanner_profile.spider_timeout + 1 }
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: scanner_profile_id,
profile_name: new_profile_name,
target_timeout: new_target_timeout,
spider_timeout: new_spider_timeout
)
end
let(:scanner_profile_id) { dast_scanner_profile.to_global_id }
context 'when on demand scan feature is enabled' do
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 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
it 'raises an exception' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when user can not run a DAST scan' do
it 'raises an exception' do
project.add_guest(user)
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 'updates the dast_scanner_profile' do
dast_scanner_profile = subject[:id].find
aggregate_failures do
expect(dast_scanner_profile.name).to eq(new_profile_name)
expect(dast_scanner_profile.target_timeout).to eq(new_target_timeout)
expect(dast_scanner_profile.spider_timeout).to eq(new_spider_timeout)
end
end
context 'when on demand scan feature is not enabled' 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
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Update 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_1) { create(:dast_scanner_profile, project: project) }
let_it_be(:new_profile_name) { SecureRandom.hex }
let(:new_target_timeout) { dast_scanner_profile.target_timeout + 1 }
let(:new_spider_timeout) { dast_scanner_profile.spider_timeout + 1 }
let(:mutation_name) { :dast_scanner_profile_update }
let(:mutation) do
graphql_mutation(
mutation_name,
full_path: full_path,
id: dast_scanner_profile.to_global_id.to_s,
profile_name: new_profile_name,
target_timeout: new_target_timeout,
spider_timeout: new_spider_timeout
)
end
def mutation_response
graphql_mutation_response(:dast_scanner_profile_update)
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 'updates the dast_scanner_profile' do
subject
dast_scanner_profile = GlobalID.parse(mutation_response['id']).find
aggregate_failures do
expect(dast_scanner_profile.name).to eq(new_profile_name)
expect(dast_scanner_profile.target_timeout).to eq(new_target_timeout)
expect(dast_scanner_profile.spider_timeout).to eq(new_spider_timeout)
end
end
context 'when there is an issue updating the dast_scanner_profile' do
let(:new_profile_name) { dast_scanner_profile_1.name }
it_behaves_like 'a mutation that returns errors in the response', errors: ['Name has already been taken']
end
context 'when the dast_scanner_profile does not exist' do
before do
dast_scanner_profile.destroy!
end
it_behaves_like 'a mutation that returns errors in the response', errors: ['Scanner profile not found for given parameters']
end
context 'when the dast_scanner_profile belongs to a different project' do
let(:mutation) do
graphql_mutation(
mutation_name,
full_path: create(:project).full_path,
id: dast_scanner_profile.to_global_id.to_s,
profile_name: new_profile_name,
target_timeout: new_target_timeout,
spider_timeout: new_spider_timeout
)
end
it_behaves_like 'a mutation that returns a top-level access error'
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe DastScannerProfiles::UpdateService 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 }
let_it_be(:new_profile_name) { SecureRandom.hex }
let_it_be(:new_target_timeout) { dast_scanner_profile.target_timeout + 1 }
let_it_be(:new_spider_timeout) { dast_scanner_profile.spider_timeout + 1 }
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,
profile_name: new_profile_name,
target_timeout: new_target_timeout,
spider_timeout: new_spider_timeout
)
end
let(:dast_scanner_profile_id) { dast_scanner_profile.id }
let(:status) { subject.status }
let(:message) { subject.message }
let(:errors) { subject.errors }
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 'updates the dast_scanner_profile' do
updated_dast_scanner_profile = payload.reload
aggregate_failures do
expect(updated_dast_scanner_profile.name).to eq(new_profile_name)
expect(updated_dast_scanner_profile.target_timeout).to eq(new_target_timeout)
expect(updated_dast_scanner_profile.spider_timeout).to eq(new_spider_timeout)
end
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
......@@ -28080,6 +28080,9 @@ msgstr ""
msgid "You are not authorized to perform this action"
msgstr ""
msgid "You are not authorized to update this scanner profile"
msgstr ""
msgid "You are now impersonating %{username}"
msgstr ""
......
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