Commit 460d7af3 authored by Alan (Maciej) Paruszewski's avatar Alan (Maciej) Paruszewski Committed by Dylan Griffith

Modify mutation to add Project to Instance Security Dashboard

This change introduces small improvement to AddProject mutation to not
accept multiple project ids, but only one and let GraphQL manage
multiple mutations to add multiple projects to Security Dashboard.
parent 500f70a5
...@@ -39,48 +39,38 @@ type AddAwardEmojiPayload { ...@@ -39,48 +39,38 @@ type AddAwardEmojiPayload {
} }
""" """
Autogenerated input type of AddProjectsToSecurityDashboard Autogenerated input type of AddProjectToSecurityDashboard
""" """
input AddProjectsToSecurityDashboardInput { input AddProjectToSecurityDashboardInput {
""" """
A unique identifier for the client performing the mutation. A unique identifier for the client performing the mutation.
""" """
clientMutationId: String clientMutationId: String
""" """
IDs of projects to be added to Instance Security Dashboard ID of the project to be added to Instance Security Dashboard
""" """
projectIds: [ID!]! id: ID!
} }
""" """
Autogenerated return type of AddProjectsToSecurityDashboard Autogenerated return type of AddProjectToSecurityDashboard
""" """
type AddProjectsToSecurityDashboardPayload { type AddProjectToSecurityDashboardPayload {
"""
IDs of projects that were added to the Instance Security Dashboard
"""
addedProjectIds: [ID!]
""" """
A unique identifier for the client performing the mutation. A unique identifier for the client performing the mutation.
""" """
clientMutationId: String clientMutationId: String
"""
IDs of projects that are already added to the Instance Security Dashboard
"""
duplicatedProjectIds: [ID!]
""" """
Reasons why the mutation failed. Reasons why the mutation failed.
""" """
errors: [String!]! errors: [String!]!
""" """
IDs of projects that were not added to the Instance Security Dashboard Project that was added to the Instance Security Dashboard
""" """
invalidProjectIds: [ID!] project: Project
} }
""" """
...@@ -5972,7 +5962,7 @@ enum MoveType { ...@@ -5972,7 +5962,7 @@ enum MoveType {
type Mutation { type Mutation {
addAwardEmoji(input: AddAwardEmojiInput!): AddAwardEmojiPayload addAwardEmoji(input: AddAwardEmojiInput!): AddAwardEmojiPayload
addProjectsToSecurityDashboard(input: AddProjectsToSecurityDashboardInput!): AddProjectsToSecurityDashboardPayload addProjectToSecurityDashboard(input: AddProjectToSecurityDashboardInput!): AddProjectToSecurityDashboardPayload
adminSidekiqQueuesDeleteJobs(input: AdminSidekiqQueuesDeleteJobsInput!): AdminSidekiqQueuesDeleteJobsPayload adminSidekiqQueuesDeleteJobs(input: AdminSidekiqQueuesDeleteJobsInput!): AdminSidekiqQueuesDeleteJobsPayload
boardListUpdateLimitMetrics(input: BoardListUpdateLimitMetricsInput!): BoardListUpdateLimitMetricsPayload boardListUpdateLimitMetrics(input: BoardListUpdateLimitMetricsInput!): BoardListUpdateLimitMetricsPayload
createDiffNote(input: CreateDiffNoteInput!): CreateDiffNotePayload createDiffNote(input: CreateDiffNoteInput!): CreateDiffNotePayload
......
...@@ -127,20 +127,14 @@ ...@@ -127,20 +127,14 @@
}, },
{ {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "AddProjectsToSecurityDashboardInput", "name": "AddProjectToSecurityDashboardInput",
"description": "Autogenerated input type of AddProjectsToSecurityDashboard", "description": "Autogenerated input type of AddProjectToSecurityDashboard",
"fields": null, "fields": null,
"inputFields": [ "inputFields": [
{ {
"name": "projectIds", "name": "id",
"description": "IDs of projects to be added to Instance Security Dashboard", "description": "ID of the project to be added to Instance Security Dashboard",
"type": { "type": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL", "kind": "NON_NULL",
"name": null, "name": null,
"ofType": { "ofType": {
...@@ -148,8 +142,6 @@ ...@@ -148,8 +142,6 @@
"name": "ID", "name": "ID",
"ofType": null "ofType": null
} }
}
}
}, },
"defaultValue": null "defaultValue": null
}, },
...@@ -170,31 +162,9 @@ ...@@ -170,31 +162,9 @@
}, },
{ {
"kind": "OBJECT", "kind": "OBJECT",
"name": "AddProjectsToSecurityDashboardPayload", "name": "AddProjectToSecurityDashboardPayload",
"description": "Autogenerated return type of AddProjectsToSecurityDashboard", "description": "Autogenerated return type of AddProjectToSecurityDashboard",
"fields": [ "fields": [
{
"name": "addedProjectIds",
"description": "IDs of projects that were added to the Instance Security Dashboard",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "clientMutationId", "name": "clientMutationId",
"description": "A unique identifier for the client performing the mutation.", "description": "A unique identifier for the client performing the mutation.",
...@@ -209,28 +179,6 @@ ...@@ -209,28 +179,6 @@
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
}, },
{
"name": "duplicatedProjectIds",
"description": "IDs of projects that are already added to the Instance Security Dashboard",
"args": [
],
"type": {
"kind": "LIST",
"name": null,
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null
}
}
},
"isDeprecated": false,
"deprecationReason": null
},
{ {
"name": "errors", "name": "errors",
"description": "Reasons why the mutation failed.", "description": "Reasons why the mutation failed.",
...@@ -258,23 +206,15 @@ ...@@ -258,23 +206,15 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "invalidProjectIds", "name": "project",
"description": "IDs of projects that were not added to the Instance Security Dashboard", "description": "Project that was added to the Instance Security Dashboard",
"args": [ "args": [
], ],
"type": { "type": {
"kind": "LIST", "kind": "OBJECT",
"name": null, "name": "Project",
"ofType": {
"kind": "NON_NULL",
"name": null,
"ofType": {
"kind": "SCALAR",
"name": "ID",
"ofType": null "ofType": null
}
}
}, },
"isDeprecated": false, "isDeprecated": false,
"deprecationReason": null "deprecationReason": null
...@@ -17054,7 +16994,7 @@ ...@@ -17054,7 +16994,7 @@
"deprecationReason": null "deprecationReason": null
}, },
{ {
"name": "addProjectsToSecurityDashboard", "name": "addProjectToSecurityDashboard",
"description": null, "description": null,
"args": [ "args": [
{ {
...@@ -17065,7 +17005,7 @@ ...@@ -17065,7 +17005,7 @@
"name": null, "name": null,
"ofType": { "ofType": {
"kind": "INPUT_OBJECT", "kind": "INPUT_OBJECT",
"name": "AddProjectsToSecurityDashboardInput", "name": "AddProjectToSecurityDashboardInput",
"ofType": null "ofType": null
} }
}, },
...@@ -17074,7 +17014,7 @@ ...@@ -17074,7 +17014,7 @@
], ],
"type": { "type": {
"kind": "OBJECT", "kind": "OBJECT",
"name": "AddProjectsToSecurityDashboardPayload", "name": "AddProjectToSecurityDashboardPayload",
"ofType": null "ofType": null
}, },
"isDeprecated": false, "isDeprecated": false,
......
...@@ -26,17 +26,15 @@ Autogenerated return type of AddAwardEmoji ...@@ -26,17 +26,15 @@ Autogenerated return type of AddAwardEmoji
| `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `errors` | String! => Array | Reasons why the mutation failed. | | `errors` | String! => Array | Reasons why the mutation failed. |
## AddProjectsToSecurityDashboardPayload ## AddProjectToSecurityDashboardPayload
Autogenerated return type of AddProjectsToSecurityDashboard Autogenerated return type of AddProjectToSecurityDashboard
| Name | Type | Description | | Name | Type | Description |
| --- | ---- | ---------- | | --- | ---- | ---------- |
| `addedProjectIds` | ID! => Array | IDs of projects that were added to the Instance Security Dashboard |
| `clientMutationId` | String | A unique identifier for the client performing the mutation. | | `clientMutationId` | String | A unique identifier for the client performing the mutation. |
| `duplicatedProjectIds` | ID! => Array | IDs of projects that are already added to the Instance Security Dashboard |
| `errors` | String! => Array | Reasons why the mutation failed. | | `errors` | String! => Array | Reasons why the mutation failed. |
| `invalidProjectIds` | ID! => Array | IDs of projects that were not added to the Instance Security Dashboard | | `project` | Project | Project that was added to the Instance Security Dashboard |
## AdminSidekiqQueuesDeleteJobsPayload ## AdminSidekiqQueuesDeleteJobsPayload
......
...@@ -18,7 +18,7 @@ module EE ...@@ -18,7 +18,7 @@ module EE
mount_mutation ::Mutations::Requirements::Update mount_mutation ::Mutations::Requirements::Update
mount_mutation ::Mutations::Vulnerabilities::Dismiss mount_mutation ::Mutations::Vulnerabilities::Dismiss
mount_mutation ::Mutations::Boards::Lists::UpdateLimitMetrics mount_mutation ::Mutations::Boards::Lists::UpdateLimitMetrics
mount_mutation ::Mutations::SecurityDashboard::AddProjects mount_mutation ::Mutations::SecurityDashboard::AddProject
end end
end end
end end
......
# frozen_string_literal: true
module Mutations
module SecurityDashboard
class AddProject < BaseMutation
graphql_name 'AddProjectToSecurityDashboard'
authorize :read_vulnerability
field :project, Types::ProjectType,
null: true,
description: 'Project that was added to the Instance Security Dashboard'
argument :id, GraphQL::ID_TYPE,
required: true,
description: 'ID of the project to be added to Instance Security Dashboard'
def resolve(id:)
project = authorized_find!(id: id)
result = add_project(project)
{
project: result ? project : nil,
errors: result ? [] : ['The project already belongs to your dashboard or you don\'t have permission to perform this action']
}
end
private
def find_object(id:)
GitlabSchema.object_from_id(id)
end
def add_project(project)
Dashboard::Projects::CreateService
.new(current_user, current_user.security_dashboard_projects, feature: :security_dashboard)
.execute([project.id])
.then { |result| result.added_project_ids.include?(project.id) }
end
end
end
end
# frozen_string_literal: true
module Mutations
module SecurityDashboard
class AddProjects < BaseMutation
graphql_name 'AddProjectsToSecurityDashboard'
authorize :read_instance_security_dashboard
field :invalid_project_ids, [GraphQL::ID_TYPE],
null: true,
description: 'IDs of projects that were not added to the Instance Security Dashboard'
field :added_project_ids, [GraphQL::ID_TYPE],
null: true,
description: 'IDs of projects that were added to the Instance Security Dashboard'
field :duplicated_project_ids, [GraphQL::ID_TYPE],
null: true,
description: 'IDs of projects that are already added to the Instance Security Dashboard'
argument :project_ids, [GraphQL::ID_TYPE],
required: true,
description: 'IDs of projects to be added to Instance Security Dashboard'
def resolve(project_ids:)
dashboard = authorized_find!
raise_resource_not_available_error! unless dashboard.feature_available?(:security_dashboard)
result = add_projects(project_ids.map(&method(:extract_project_id)))
{
invalid_project_ids: result.invalid_project_ids.map(&method(:to_global_id)),
added_project_ids: result.added_project_ids.map(&method(:to_global_id)),
duplicated_project_ids: result.duplicate_project_ids.map(&method(:to_global_id)),
errors: []
}
end
private
def find_object(*args)
InstanceSecurityDashboard.new(current_user)
end
def extract_project_id(global_id)
return unless global_id.present?
GitlabSchema.parse_gid(global_id, expected_type: ::Project).model_id
end
def add_projects(project_ids)
Dashboard::Projects::CreateService.new(
current_user,
current_user.security_dashboard_projects,
feature: :security_dashboard
).execute(project_ids.compact)
end
def to_global_id(project_id)
GitlabSchema.id_from_object(Project.new(id: project_id))
end
end
end
end
---
title: Modify GraphQL mutation for adding projects to Instance Security Dashboard
to support only single project id
merge_request: 30865
author:
type: changed
# frozen_string_literal: true # frozen_string_literal: true
require 'spec_helper' require 'spec_helper'
describe Mutations::SecurityDashboard::AddProjects do describe Mutations::SecurityDashboard::AddProject do
let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) } let(:mutation) { described_class.new(object: nil, context: { current_user: current_user }, field: nil) }
describe '#resolve' do describe '#resolve' do
...@@ -11,14 +11,14 @@ describe Mutations::SecurityDashboard::AddProjects do ...@@ -11,14 +11,14 @@ describe Mutations::SecurityDashboard::AddProjects do
let_it_be(:user) { create(:user, security_dashboard_projects: [already_added_project]) } let_it_be(:user) { create(:user, security_dashboard_projects: [already_added_project]) }
let(:project_ids) { [project, my_project, already_added_project].map(&GitlabSchema.method(:id_from_object)).map(&:to_s) } let(:selected_project) { project }
before do before do
my_project.add_developer(user) my_project.add_developer(user)
already_added_project.add_developer(user) already_added_project.add_developer(user)
end end
subject { mutation.resolve(project_ids: project_ids) } subject { mutation.resolve(id: GitlabSchema.id_from_object(selected_project)) }
context 'when user is not logged_in' do context 'when user is not logged_in' do
let(:current_user) { nil } let(:current_user) { nil }
...@@ -42,25 +42,31 @@ describe Mutations::SecurityDashboard::AddProjects do ...@@ -42,25 +42,31 @@ describe Mutations::SecurityDashboard::AddProjects do
stub_licensed_features(security_dashboard: true) stub_licensed_features(security_dashboard: true)
end end
context 'when project_ids is empty' do context 'when project is available to the user and can be added to the security dashboard' do
let(:project_ids) { [] } let(:selected_project) { my_project }
it { is_expected.to eq(added_project_ids: [], duplicated_project_ids: [], invalid_project_ids: [], errors: []) } it 'adds project to the security dashboard', :aggregate_failures do
end expect(subject[:project]).to eq(my_project)
expect(subject[:errors]).to be_empty
context 'when project_ids contains ids' do
it 'adds project that is available to the user to the security dashboard', :aggregate_failures do
expect(subject[:added_project_ids]).to eq([GitlabSchema.id_from_object(my_project)])
expect(user.security_dashboard_projects).to include(my_project) expect(user.security_dashboard_projects).to include(my_project)
end end
end
it 'does not add project that already exist in the security dashboard', :aggregate_failures do context 'when project is not available to the user' do
expect(subject[:duplicated_project_ids]).to eq([GitlabSchema.id_from_object(already_added_project)]) let(:selected_project) { project }
expect(user.security_dashboard_projects).to include(already_added_project)
it 'raises Gitlab::Graphql::Errors::ResourceNotAvailable error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end end
it 'does not add project that is not available for the user' do context 'when project is already added to the security dashboard' do
expect(subject[:invalid_project_ids]).to eq([GitlabSchema.id_from_object(project)]) let(:selected_project) { already_added_project }
it 'does not add project to the security dashboard', :aggregate_failures do
expect(subject[:project]).to be_nil
expect(subject[:errors]).to eq(['The project already belongs to your dashboard or you don\'t have permission to perform this action'])
expect(user.security_dashboard_projects).to include(already_added_project)
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