Commit c20de7a9 authored by Fabio Pitino - OOO until July 6's avatar Fabio Pitino - OOO until July 6 Committed by Alex Kalderimis

Remove project from Job Token Scope via GraphQL

- Introduce service object to remove project from scope
- Use new service object in new GraphQL mutation

Changelog: added
parent f0c1975b
# frozen_string_literal: true
module Mutations
module Ci
module JobTokenScope
class RemoveProject < BaseMutation
include FindsProject
graphql_name 'CiJobTokenScopeRemoveProject'
authorize :admin_project
argument :project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project that the CI job token scope belongs to.'
argument :target_project_path, GraphQL::ID_TYPE,
required: true,
description: 'The project to be removed from the CI job token scope.'
field :ci_job_token_scope,
Types::Ci::JobTokenScopeType,
null: true,
description: "The CI job token's scope of access."
def resolve(project_path:, target_project_path:)
project = authorized_find!(project_path)
target_project = Project.find_by_full_path(target_project_path)
result = ::Ci::JobTokenScope::RemoveProjectService
.new(project, current_user)
.execute(target_project)
if result.success?
{
ci_job_token_scope: ::Ci::JobToken::Scope.new(project),
errors: []
}
else
{
ci_job_token_scope: nil,
errors: [result.message]
}
end
end
end
end
end
end
...@@ -100,6 +100,7 @@ module Types ...@@ -100,6 +100,7 @@ module Types
mount_mutation Mutations::Ci::Job::Play mount_mutation Mutations::Ci::Job::Play
mount_mutation Mutations::Ci::Job::Retry mount_mutation Mutations::Ci::Job::Retry
mount_mutation Mutations::Ci::JobTokenScope::AddProject mount_mutation Mutations::Ci::JobTokenScope::AddProject
mount_mutation Mutations::Ci::JobTokenScope::RemoveProject
mount_mutation Mutations::Ci::Runner::Update, feature_flag: :runner_graphql_query mount_mutation Mutations::Ci::Runner::Update, feature_flag: :runner_graphql_query
mount_mutation Mutations::Ci::Runner::Delete, feature_flag: :runner_graphql_query mount_mutation Mutations::Ci::Runner::Delete, feature_flag: :runner_graphql_query
mount_mutation Mutations::Ci::RunnersRegistrationToken::Reset, feature_flag: :runner_graphql_query mount_mutation Mutations::Ci::RunnersRegistrationToken::Reset, feature_flag: :runner_graphql_query
......
...@@ -19,6 +19,10 @@ module Ci ...@@ -19,6 +19,10 @@ module Ci
validates :target_project, presence: true validates :target_project, presence: true
validate :not_self_referential_link validate :not_self_referential_link
def self.for_source_and_target(source_project, target_project)
self.find_by(source_project: source_project, target_project: target_project)
end
private private
def not_self_referential_link def not_self_referential_link
......
...@@ -3,13 +3,10 @@ ...@@ -3,13 +3,10 @@
module Ci module Ci
module JobTokenScope module JobTokenScope
class AddProjectService < ::BaseService class AddProjectService < ::BaseService
TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND = "The target_project that you are attempting to access does " \ include EditScopeValidations
"not exist or you don't have permission to perform this action"
def execute(target_project) def execute(target_project)
if error_response = validation_error(target_project) validate_edit!(project, target_project, current_user)
return error_response
end
link = add_project!(target_project) link = add_project!(target_project)
ServiceResponse.success(payload: { project_link: link }) ServiceResponse.success(payload: { project_link: link })
...@@ -18,28 +15,8 @@ module Ci ...@@ -18,28 +15,8 @@ module Ci
ServiceResponse.error(message: "Target project is already in the job token scope") ServiceResponse.error(message: "Target project is already in the job token scope")
rescue ActiveRecord::RecordInvalid => e rescue ActiveRecord::RecordInvalid => e
ServiceResponse.error(message: e.message) ServiceResponse.error(message: e.message)
end rescue EditScopeValidations::ValidationError => e
ServiceResponse.error(message: e.message)
private
def validation_error(target_project)
unless project.ci_job_token_scope_enabled?
return ServiceResponse.error(message: "Job token scope is disabled for this project")
end
unless can?(current_user, :admin_project, project)
return ServiceResponse.error(message: "Insufficient permissions to modify the job token scope")
end
unless target_project
return ServiceResponse.error(message: TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND)
end
unless can?(current_user, :read_project, target_project)
return ServiceResponse.error(message: TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND)
end
nil
end end
def add_project!(target_project) def add_project!(target_project)
......
# frozen_string_literal: true
module Ci
module JobTokenScope
class RemoveProjectService < ::BaseService
include EditScopeValidations
def execute(target_project)
validate_edit!(project, target_project, current_user)
if project == target_project
return ServiceResponse.error(message: "Source project cannot be removed from the job token scope")
end
link = ::Ci::JobToken::ProjectScopeLink.for_source_and_target(project, target_project)
unless link
return ServiceResponse.error(message: "Target project is not in the job token scope")
end
if link.destroy
ServiceResponse.success
else
ServiceResponse.error(message: link.errors.full_messages.to_sentence, payload: { project_link: link })
end
rescue EditScopeValidations::ValidationError => e
ServiceResponse.error(message: e.message)
end
end
end
end
# frozen_string_literal: true
module Ci
module JobTokenScope
module EditScopeValidations
ValidationError = Class.new(StandardError)
TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND = "The target_project that you are attempting to access does " \
"not exist or you don't have permission to perform this action"
def validate_edit!(source_project, target_project, current_user)
unless source_project.ci_job_token_scope_enabled?
raise ValidationError, "Job token scope is disabled for this project"
end
unless can?(current_user, :admin_project, source_project)
raise ValidationError, "Insufficient permissions to modify the job token scope"
end
unless can?(current_user, :read_project, target_project)
raise ValidationError, TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND
end
end
end
end
end
...@@ -800,6 +800,26 @@ Input type: `CiJobTokenScopeAddProjectInput` ...@@ -800,6 +800,26 @@ Input type: `CiJobTokenScopeAddProjectInput`
| <a id="mutationcijobtokenscopeaddprojectclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. | | <a id="mutationcijobtokenscopeaddprojectclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationcijobtokenscopeaddprojecterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. | | <a id="mutationcijobtokenscopeaddprojecterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.ciJobTokenScopeRemoveProject`
Input type: `CiJobTokenScopeRemoveProjectInput`
#### Arguments
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationcijobtokenscoperemoveprojectclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationcijobtokenscoperemoveprojectprojectpath"></a>`projectPath` | [`ID!`](#id) | The project that the CI job token scope belongs to. |
| <a id="mutationcijobtokenscoperemoveprojecttargetprojectpath"></a>`targetProjectPath` | [`ID!`](#id) | The project to be removed from the CI job token scope. |
#### Fields
| Name | Type | Description |
| ---- | ---- | ----------- |
| <a id="mutationcijobtokenscoperemoveprojectcijobtokenscope"></a>`ciJobTokenScope` | [`CiJobTokenScopeType`](#cijobtokenscopetype) | The CI job token's scope of access. |
| <a id="mutationcijobtokenscoperemoveprojectclientmutationid"></a>`clientMutationId` | [`String`](#string) | A unique identifier for the client performing the mutation. |
| <a id="mutationcijobtokenscoperemoveprojecterrors"></a>`errors` | [`[String!]!`](#string) | Errors encountered during execution of the mutation. |
### `Mutation.clusterAgentDelete` ### `Mutation.clusterAgentDelete`
Input type: `ClusterAgentDeleteInput` Input type: `ClusterAgentDeleteInput`
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Mutations::Ci::JobTokenScope::RemoveProject do
let(:mutation) do
described_class.new(object: nil, context: { current_user: current_user }, field: nil)
end
describe '#resolve' do
let_it_be(:project) { create(:project) }
let_it_be(:target_project) { create(:project) }
let_it_be(:link) do
create(:ci_job_token_project_scope_link,
source_project: project,
target_project: target_project)
end
let(:target_project_path) { target_project.full_path }
subject do
mutation.resolve(project_path: project.full_path, target_project_path: target_project_path)
end
context 'when user is not logged in' do
let(:current_user) { nil }
it 'raises error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when user is logged in' do
let(:current_user) { create(:user) }
context 'when user does not have permissions to admin project' do
it 'raises error' do
expect { subject }.to raise_error(Gitlab::Graphql::Errors::ResourceNotAvailable)
end
end
context 'when user has permissions to admin project and read target project' do
before do
project.add_maintainer(current_user)
target_project.add_guest(current_user)
end
it 'removes target project from the job token scope' do
expect do
expect(subject).to include(ci_job_token_scope: be_present, errors: be_empty)
end.to change { Ci::JobToken::ProjectScopeLink.count }.by(-1)
end
context 'when the service returns an error' do
let(:service) { double(:service) }
it 'returns an error response' do
expect(::Ci::JobTokenScope::RemoveProjectService).to receive(:new).with(project, current_user).and_return(service)
expect(service).to receive(:execute).with(target_project).and_return(ServiceResponse.error(message: 'The error message'))
expect(subject.fetch(:ci_job_token_scope)).to be_nil
expect(subject.fetch(:errors)).to include("The error message")
end
end
end
end
end
end
...@@ -65,4 +65,22 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do ...@@ -65,4 +65,22 @@ RSpec.describe Ci::JobToken::ProjectScopeLink do
expect(subject).to contain_exactly(target_link) expect(subject).to contain_exactly(target_link)
end end
end end
describe '.for_source_and_target' do
let_it_be(:link) { create(:ci_job_token_project_scope_link, source_project: project) }
subject { described_class.for_source_and_target(project, target_project) }
context 'when link is found' do
let(:target_project) { link.target_project }
it { is_expected.to eq(link) }
end
context 'when link is not found' do
let(:target_project) { create(:project) }
it { is_expected.to be_nil }
end
end
end end
...@@ -71,7 +71,7 @@ RSpec.describe 'CiJobTokenScopeAddProject' do ...@@ -71,7 +71,7 @@ RSpec.describe 'CiJobTokenScopeAddProject' do
it 'has mutation errors' do it 'has mutation errors' do
post_graphql_mutation(mutation, current_user: current_user) post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to contain_exactly(Ci::JobTokenScope::AddProjectService::TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND) expect(mutation_response['errors']).to contain_exactly(Ci::JobTokenScope::EditScopeValidations::TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND)
end end
end end
end end
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'CiJobTokenScopeRemoveProject' do
include GraphqlHelpers
let_it_be(:project) { create(:project) }
let_it_be(:target_project) { create(:project) }
let_it_be(:link) do
create(:ci_job_token_project_scope_link,
source_project: project,
target_project: target_project)
end
let(:variables) do
{
project_path: project.full_path,
target_project_path: target_project.full_path
}
end
let(:mutation) do
graphql_mutation(:ci_job_token_scope_remove_project, variables) do
<<~QL
errors
ciJobTokenScope {
projects {
nodes {
path
}
}
}
QL
end
end
let(:mutation_response) { graphql_mutation_response(:ci_job_token_scope_remove_project) }
context 'when unauthorized' do
let(:current_user) { create(:user) }
context 'when not a maintainer' do
before do
project.add_developer(current_user)
end
it 'has graphql errors' do
post_graphql_mutation(mutation, current_user: current_user)
expect(graphql_errors).not_to be_empty
end
end
end
context 'when authorized' do
let_it_be(:current_user) { project.owner }
before do
target_project.add_guest(current_user)
end
it 'removes the target project from the job token scope' do
expect do
post_graphql_mutation(mutation, current_user: current_user)
expect(response).to have_gitlab_http_status(:success)
expect(mutation_response.dig('ciJobTokenScope', 'projects', 'nodes')).not_to be_empty
end.to change { Ci::JobToken::Scope.new(project).includes?(target_project) }.from(true).to(false)
end
context 'when invalid target project is provided' do
before do
variables[:target_project_path] = 'unknown/project'
end
it 'has mutation errors' do
post_graphql_mutation(mutation, current_user: current_user)
expect(mutation_response['errors']).to contain_exactly(Ci::JobTokenScope::EditScopeValidations::TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND)
end
end
end
end
...@@ -11,44 +11,11 @@ RSpec.describe Ci::JobTokenScope::AddProjectService do ...@@ -11,44 +11,11 @@ RSpec.describe Ci::JobTokenScope::AddProjectService do
describe '#execute' do describe '#execute' do
subject(:result) { service.execute(target_project) } subject(:result) { service.execute(target_project) }
shared_examples 'returns error' do |error| it_behaves_like 'editable job token scope' do
it 'returns an error response', :aggregate_failures do context 'when user has permissions on source and target projects' do
expect(result).to be_error
expect(result.message).to eq(error)
end
end
context 'when job token scope is disabled for the given project' do
before do
allow(project).to receive(:ci_job_token_scope_enabled?).and_return(false)
end
it_behaves_like 'returns error', 'Job token scope is disabled for this project'
end
context 'when user does not have permissions to edit the job token scope' do
it_behaves_like 'returns error', 'Insufficient permissions to modify the job token scope'
end
context 'when user has permissions to edit the job token scope' do
before do before do
project.add_maintainer(current_user) project.add_maintainer(current_user)
end target_project.add_developer(current_user)
context 'when target project is not provided' do
let(:target_project) { nil }
it_behaves_like 'returns error', Ci::JobTokenScope::AddProjectService::TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND
end
context 'when target project is provided' do
context 'when user does not have permissions to read the target project' do
it_behaves_like 'returns error', Ci::JobTokenScope::AddProjectService::TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND
end
context 'when user has permissions to read the target project' do
before do
target_project.add_guest(current_user)
end end
it 'adds the project to the scope' do it 'adds the project to the scope' do
...@@ -56,24 +23,17 @@ RSpec.describe Ci::JobTokenScope::AddProjectService do ...@@ -56,24 +23,17 @@ RSpec.describe Ci::JobTokenScope::AddProjectService do
expect(result).to be_success expect(result).to be_success
end.to change { Ci::JobToken::ProjectScopeLink.count }.by(1) end.to change { Ci::JobToken::ProjectScopeLink.count }.by(1)
end end
context 'when target project is already in scope' do
before do
create(:ci_job_token_project_scope_link,
source_project: project,
target_project: target_project)
end end
it_behaves_like 'returns error', "Target project is already in the job token scope" context 'when target project is same as the source project' do
end before do
project.add_maintainer(current_user)
end end
context 'when target project is same as the source project' do
let(:target_project) { project } let(:target_project) { project }
it_behaves_like 'returns error', "Validation failed: Target project can't be the same as the source project" it_behaves_like 'returns error', "Validation failed: Target project can't be the same as the source project"
end end
end end
end end
end
end end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Ci::JobTokenScope::RemoveProjectService do
let(:service) { described_class.new(project, current_user) }
let_it_be(:project) { create(:project) }
let_it_be(:target_project) { create(:project) }
let_it_be(:current_user) { create(:user) }
let_it_be(:link) do
create(:ci_job_token_project_scope_link,
source_project: project,
target_project: target_project)
end
describe '#execute' do
subject(:result) { service.execute(target_project) }
it_behaves_like 'editable job token scope' do
context 'when user has permissions on source and target project' do
before do
project.add_maintainer(current_user)
target_project.add_developer(current_user)
end
it 'removes the project from the scope' do
expect do
expect(result).to be_success
end.to change { Ci::JobToken::ProjectScopeLink.count }.by(-1)
end
end
context 'when target project is same as the source project' do
before do
project.add_maintainer(current_user)
end
let(:target_project) { project }
it_behaves_like 'returns error', "Source project cannot be removed from the job token scope"
end
end
end
end
# frozen_string_literal: true
RSpec.shared_examples 'editable job token scope' do
shared_examples 'returns error' do |error|
it 'returns an error response', :aggregate_failures do
expect(result).to be_error
expect(result.message).to eq(error)
end
end
context 'when job token scope is disabled for the given project' do
before do
allow(project).to receive(:ci_job_token_scope_enabled?).and_return(false)
end
it_behaves_like 'returns error', 'Job token scope is disabled for this project'
end
context 'when user does not have permissions to edit the job token scope' do
it_behaves_like 'returns error', 'Insufficient permissions to modify the job token scope'
end
context 'when user has permissions to edit the job token scope' do
before do
project.add_maintainer(current_user)
end
context 'when target project is not provided' do
let(:target_project) { nil }
it_behaves_like 'returns error', Ci::JobTokenScope::EditScopeValidations::TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND
end
context 'when target project is provided' do
context 'when user does not have permissions to read the target project' do
it_behaves_like 'returns error', Ci::JobTokenScope::EditScopeValidations::TARGET_PROJECT_UNAUTHORIZED_OR_UNFOUND
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