Commit 2dae8561 authored by Robert Speicher's avatar Robert Speicher

Merge branch '213598-add-mutation-to-dismiss-vulnerability' into 'master'

Add mutation to Dismiss Vulnerability GraphQL API

See merge request gitlab-org/gitlab!29150
parents 5cb12ceb dbffe56e
......@@ -1832,6 +1832,46 @@ type DiscussionEdge {
node: Discussion
}
"""
Autogenerated input type of DismissVulnerability
"""
input DismissVulnerabilityInput {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reason why vulnerability should be dismissed
"""
comment: String
"""
ID of the vulnerability to be dismissed
"""
id: ID!
}
"""
Autogenerated return type of DismissVulnerability
"""
type DismissVulnerabilityPayload {
"""
A unique identifier for the client performing the mutation.
"""
clientMutationId: String
"""
Reasons why the mutation failed.
"""
errors: [String!]!
"""
The vulnerability after dismissal
"""
vulnerability: Vulnerability
}
interface Entry {
"""
Flat path of the entry
......@@ -5413,6 +5453,7 @@ type Mutation {
designManagementUpload(input: DesignManagementUploadInput!): DesignManagementUploadPayload
destroyNote(input: DestroyNoteInput!): DestroyNotePayload
destroySnippet(input: DestroySnippetInput!): DestroySnippetPayload
dismissVulnerability(input: DismissVulnerabilityInput!): DismissVulnerabilityPayload
epicAddIssue(input: EpicAddIssueInput!): EpicAddIssuePayload
epicSetSubscription(input: EpicSetSubscriptionInput!): EpicSetSubscriptionPayload
epicTreeReorder(input: EpicTreeReorderInput!): EpicTreeReorderPayload
......@@ -9534,6 +9575,11 @@ type Vulnerability {
"""
title: String
"""
Permissions for the current user on the resource
"""
userPermissions: VulnerabilityPermissions!
"""
URL to the vulnerability's details page
"""
......@@ -9575,6 +9621,51 @@ type VulnerabilityEdge {
node: Vulnerability
}
"""
Check permissions for the current user on a vulnerability
"""
type VulnerabilityPermissions {
"""
Indicates the user can perform `admin_vulnerability` on this resource
"""
adminVulnerability: Boolean!
"""
Indicates the user can perform `admin_vulnerability_issue_link` on this resource
"""
adminVulnerabilityIssueLink: Boolean!
"""
Indicates the user can perform `create_vulnerability` on this resource
"""
createVulnerability: Boolean!
"""
Indicates the user can perform `create_vulnerability_export` on this resource
"""
createVulnerabilityExport: Boolean!
"""
Indicates the user can perform `create_vulnerability_feedback` on this resource
"""
createVulnerabilityFeedback: Boolean!
"""
Indicates the user can perform `destroy_vulnerability_feedback` on this resource
"""
destroyVulnerabilityFeedback: Boolean!
"""
Indicates the user can perform `read_vulnerability_feedback` on this resource
"""
readVulnerabilityFeedback: Boolean!
"""
Indicates the user can perform `update_vulnerability_feedback` on this resource
"""
updateVulnerabilityFeedback: Boolean!
}
"""
The type of the security scan that found the vulnerability.
"""
......
......@@ -5393,6 +5393,118 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INPUT_OBJECT",
"name": "DismissVulnerabilityInput",
"description": "Autogenerated input type of DismissVulnerability",
"fields": null,
"inputFields": [
{
"name": "id",
"description": "ID of the vulnerability to be dismissed",
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
},
"defaultValue": null
},
{
"name": "comment",
"description": "Reason why vulnerability should be dismissed",
"type": {
"kind": "SCALAR",
"name": "String",
"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": "DismissVulnerabilityPayload",
"description": "Autogenerated return type of DismissVulnerability",
"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": "Reasons why the mutation failed.",
"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": "vulnerability",
"description": "The vulnerability after dismissal",
"args": [
],
"type": {
"kind": "OBJECT",
"name": "Vulnerability",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "INTERFACE",
"name": "Entry",
......@@ -15821,6 +15933,33 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "dismissVulnerability",
"description": null,
"args": [
{
"name": "input",
"description": null,
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "INPUT_OBJECT",
"name": "DismissVulnerabilityInput",
"ofType": null
}
},
"defaultValue": null
}
],
"type": {
"kind": "OBJECT",
"name": "DismissVulnerabilityPayload",
"ofType": null
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "epicAddIssue",
"description": null,
......@@ -28762,6 +28901,24 @@
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "userPermissions",
"description": "Permissions for the current user on the resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "OBJECT",
"name": "VulnerabilityPermissions",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "vulnerabilityPath",
"description": "URL to the vulnerability's details page",
......@@ -28896,6 +29053,163 @@
"enumValues": null,
"possibleTypes": null
},
{
"kind": "OBJECT",
"name": "VulnerabilityPermissions",
"description": "Check permissions for the current user on a vulnerability",
"fields": [
{
"name": "adminVulnerability",
"description": "Indicates the user can perform `admin_vulnerability` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "adminVulnerabilityIssueLink",
"description": "Indicates the user can perform `admin_vulnerability_issue_link` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createVulnerability",
"description": "Indicates the user can perform `create_vulnerability` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createVulnerabilityExport",
"description": "Indicates the user can perform `create_vulnerability_export` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "createVulnerabilityFeedback",
"description": "Indicates the user can perform `create_vulnerability_feedback` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "destroyVulnerabilityFeedback",
"description": "Indicates the user can perform `destroy_vulnerability_feedback` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "readVulnerabilityFeedback",
"description": "Indicates the user can perform `read_vulnerability_feedback` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
},
{
"name": "updateVulnerabilityFeedback",
"description": "Indicates the user can perform `update_vulnerability_feedback` on this resource",
"args": [
],
"type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "Boolean",
"ofType": null
}
},
"isDeprecated": false,
"deprecationReason": null
}
],
"inputFields": null,
"interfaces": [
],
"enumValues": null,
"possibleTypes": null
},
{
"kind": "ENUM",
"name": "VulnerabilityReportType",
......
......@@ -317,6 +317,16 @@ Autogenerated return type of DestroySnippet
| `id` | ID! | ID of this discussion |
| `replyId` | ID! | ID used to reply to this discussion |
## DismissVulnerabilityPayload
Autogenerated return type of DismissVulnerability
| Name | Type | Description |
| --- | ---- | ---------- |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. |
| `vulnerability` | Vulnerability | The vulnerability after dismissal |
## Environment
Describes where code is deployed for a project
......@@ -1495,8 +1505,24 @@ Represents a vulnerability.
| `severity` | VulnerabilitySeverity | Severity of the vulnerability (INFO, UNKNOWN, LOW, MEDIUM, HIGH, CRITICAL) |
| `state` | VulnerabilityState | State of the vulnerability (DETECTED, DISMISSED, RESOLVED, CONFIRMED) |
| `title` | String | Title of the vulnerability |
| `userPermissions` | VulnerabilityPermissions! | Permissions for the current user on the resource |
| `vulnerabilityPath` | String | URL to the vulnerability's details page |
## VulnerabilityPermissions
Check permissions for the current user on a vulnerability
| Name | Type | Description |
| --- | ---- | ---------- |
| `adminVulnerability` | Boolean! | Indicates the user can perform `admin_vulnerability` on this resource |
| `adminVulnerabilityIssueLink` | Boolean! | Indicates the user can perform `admin_vulnerability_issue_link` on this resource |
| `createVulnerability` | Boolean! | Indicates the user can perform `create_vulnerability` on this resource |
| `createVulnerabilityExport` | Boolean! | Indicates the user can perform `create_vulnerability_export` on this resource |
| `createVulnerabilityFeedback` | Boolean! | Indicates the user can perform `create_vulnerability_feedback` on this resource |
| `destroyVulnerabilityFeedback` | Boolean! | Indicates the user can perform `destroy_vulnerability_feedback` on this resource |
| `readVulnerabilityFeedback` | Boolean! | Indicates the user can perform `read_vulnerability_feedback` on this resource |
| `updateVulnerabilityFeedback` | Boolean! | Indicates the user can perform `update_vulnerability_feedback` on this resource |
## VulnerabilitySeveritiesCount
Represents vulnerability counts by severity
......
......@@ -16,6 +16,7 @@ module EE
mount_mutation ::Mutations::Epics::AddIssue
mount_mutation ::Mutations::Requirements::Create
mount_mutation ::Mutations::Requirements::Update
mount_mutation ::Mutations::Vulnerabilities::Dismiss
end
end
end
......
# frozen_string_literal: true
module Mutations
module Vulnerabilities
class Dismiss < BaseMutation
graphql_name 'DismissVulnerability'
authorize :admin_vulnerability
field :vulnerability, Types::VulnerabilityType,
null: true,
description: 'The vulnerability after dismissal'
argument :id,
GraphQL::ID_TYPE,
required: true,
description: 'ID of the vulnerability to be dismissed'
argument :comment,
GraphQL::STRING_TYPE,
required: false,
description: 'Reason why vulnerability should be dismissed'
def resolve(id:, comment: nil)
vulnerability = authorized_find!(id: id)
result = dismiss_vulnerability(vulnerability, comment)
{
vulnerability: result,
errors: result.errors.full_messages || []
}
end
private
def dismiss_vulnerability(vulnerability, comment)
::Vulnerabilities::DismissService.new(current_user, vulnerability, comment).execute
end
def find_object(id:)
GitlabSchema.object_from_id(id)
end
end
end
end
# frozen_string_literal: true
module Types
module PermissionTypes
class Vulnerability < BasePermissionType
graphql_name 'VulnerabilityPermissions'
description 'Check permissions for the current user on a vulnerability'
abilities :read_vulnerability_feedback, :create_vulnerability_feedback, :destroy_vulnerability_feedback,
:update_vulnerability_feedback, :create_vulnerability, :create_vulnerability_export,
:admin_vulnerability, :admin_vulnerability_issue_link
end
end
end
......@@ -7,6 +7,8 @@ module Types
authorize :read_vulnerability
expose_permissions Types::PermissionTypes::Vulnerability
field :id, GraphQL::ID_TYPE, null: false,
description: 'GraphQL ID of the vulnerability'
......
......@@ -6,6 +6,11 @@ module Vulnerabilities
FindingsDismissResult = Struct.new(:ok?, :finding, :message)
def initialize(current_user, vulnerability, comment = nil)
super(current_user, vulnerability)
@comment = comment
end
def execute
raise Gitlab::Access::AccessDeniedError unless authorized?
......@@ -33,7 +38,8 @@ module Vulnerabilities
{
category: finding.report_type,
feedback_type: 'dismissal',
project_fingerprint: finding.project_fingerprint
project_fingerprint: finding.project_fingerprint,
comment: @comment
}
end
......
---
title: Add mutation to Dismiss Vulnerability GraphQL API
merge_request: 29150
author:
type: added
# frozen_string_literal: true
require 'spec_helper'
describe Mutations::Vulnerabilities::Dismiss do
let(:mutation) { described_class.new(object: nil, context: { current_user: user }, field: nil) }
describe '#resolve' do
let_it_be(:vulnerability) { create(:vulnerability, :with_findings) }
let_it_be(:user) { create(:user) }
let(:comment) { 'Dismissal Feedbacl' }
let(:mutated_vulnerability) { subject[:vulnerability] }
subject { mutation.resolve(id: GitlabSchema.id_from_object(vulnerability).to_s, comment: comment) }
context 'when the user can dismiss the vulnerability' do
before do
stub_licensed_features(security_dashboard: true)
end
context 'when user doe not have access to the project' do
it 'raises an error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when user has access to the project' do
before do
vulnerability.project.add_developer(user)
end
it 'returns the dismissed vulnerability' do
expect(mutated_vulnerability).to eq(vulnerability)
expect(mutated_vulnerability).to be_dismissed
expect(subject[:errors]).to be_empty
end
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Types::PermissionTypes::Vulnerability do
it do
expected_permissions = %i[read_vulnerability_feedback create_vulnerability_feedback destroy_vulnerability_feedback
update_vulnerability_feedback create_vulnerability create_vulnerability_export
admin_vulnerability admin_vulnerability_issue_link]
expected_permissions.each do |permission|
expect(described_class).to have_graphql_field(permission)
end
end
end
......@@ -8,7 +8,7 @@ describe GitlabSchema.types['Vulnerability'] do
let_it_be(:vulnerability) { create(:vulnerability, project: project) }
let(:fields) do
%i[id title description state severity report_type vulnerability_path location]
%i[userPermissions id title description state severity report_type vulnerability_path location]
end
before do
......
......@@ -31,6 +31,25 @@ describe Vulnerabilities::DismissService do
end
end
context 'when comment is added' do
let(:comment) { 'Dismissal Comment' }
let(:service) { described_class.new(user, vulnerability, comment) }
it 'dismisses a vulnerability and its associated findings with comment', :aggregate_failures do
Timecop.freeze do
dismiss_vulnerability
aggregate_failures do
expect(vulnerability.reload).to(
have_attributes(state: 'dismissed', dismissed_by: user, dismissed_at: be_like_time(Time.current)))
expect(vulnerability.findings).to all have_vulnerability_dismissal_feedback
expect(vulnerability.findings.map(&:dismissal_feedback)).to(
all(have_attributes(comment: comment, comment_author: user, comment_timestamp: be_like_time(Time.current))))
end
end
end
end
it 'creates note' do
expect(SystemNoteService).to receive(:change_vulnerability_state).with(vulnerability, user)
......
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