Commit d02bb2f2 authored by Jackie Fraser's avatar Jackie Fraser Committed by Nick Thomas

Add import project members API endpoint

parent 923d0a26
# frozen_string_literal: true
module Members
class ImportProjectTeamService < BaseService
attr_reader :params, :current_user
def target_project_id
@target_project_id ||= params[:id].presence
end
def source_project_id
@source_project_id ||= params[:project_id].presence
end
def target_project
@target_project ||= Project.find_by_id(target_project_id)
end
def source_project
@source_project ||= Project.find_by_id(source_project_id)
end
def execute
import_project_team
end
private
def import_project_team
return false unless target_project.present? && source_project.present? && current_user.present?
return false unless can?(current_user, :read_project_member, source_project)
return false unless can?(current_user, :admin_project_member, target_project)
target_project.team.import(source_project, current_user)
end
end
end
...@@ -2220,6 +2220,29 @@ DELETE /projects/:id/share/:group_id ...@@ -2220,6 +2220,29 @@ DELETE /projects/:id/share/:group_id
curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/share/17" curl --request DELETE --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/share/17"
``` ```
## Import project members
Import members from another project.
```plaintext
POST /projects/:id/import_project_members/:project_id
```
| Attribute | Type | Required | Description |
|--------------|-------------------|------------------------|-------------|
| `id` | integer or string | **{check-circle}** Yes | The ID or [URL-encoded path](index.md#namespaced-path-encoding) of the target project to receive the members. |
| `project_id` | integer or string | **{check-circle}** Yes | The ID or [URL-encoded path](index.md#namespaced-path-encoding) of the source project to import the members from. |
```shell
curl --request POST --header "PRIVATE-TOKEN: <your_access_token>" "https://gitlab.example.com/api/v4/projects/5/import_project_members/32"
```
Returns:
- `200 OK` on success.
- `404 Project Not Found` if the target or source project does not exist or cannot be accessed by the requester.
- `422 Unprocessable Entity` if the import of project members does not complete successfully.
## Hooks ## Hooks
Also called Project Hooks and Webhooks. These are different for [System Hooks](system_hooks.md) Also called Project Hooks and Webhooks. These are different for [System Hooks](system_hooks.md)
......
...@@ -587,6 +587,27 @@ module API ...@@ -587,6 +587,27 @@ module API
end end
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
desc 'Import members from another project' do
detail 'This feature was introduced in GitLab 14.2'
end
params do
requires :project_id, type: Integer, desc: 'The ID of the source project to import the members from.'
end
post ":id/import_project_members/:project_id", feature_category: :experimentation_expansion do
authorize! :admin_project, user_project
source_project = Project.find_by_id(params[:project_id])
not_found!('Project') unless source_project && can?(current_user, :read_project, source_project)
result = ::Members::ImportProjectTeamService.new(current_user, params).execute
if result
{ status: result, message: 'Successfully imported' }
else
render_api_error!('Import failed', :unprocessable_entity)
end
end
desc 'Workhorse authorize the file upload' do desc 'Workhorse authorize the file upload' do
detail 'This feature was introduced in GitLab 13.11' detail 'This feature was introduced in GitLab 13.11'
end end
......
...@@ -3037,6 +3037,59 @@ RSpec.describe API::Projects do ...@@ -3037,6 +3037,59 @@ RSpec.describe API::Projects do
end end
end end
describe 'POST /projects/:id/import_project_members/:project_id' do
let_it_be(:project2) { create(:project) }
let_it_be(:project2_user) { create(:user) }
before_all do
project.add_maintainer(user)
project2.add_maintainer(user)
project2.add_developer(project2_user)
end
it 'returns 200 when it successfully imports members from another project' do
expect do
post api("/projects/#{project.id}/import_project_members/#{project2.id}", user)
end.to change { project.members.count }.by(2)
expect(response).to have_gitlab_http_status(:created)
expect(json_response['message']).to eq('Successfully imported')
end
it 'returns 404 if the source project does not exist' do
expect do
post api("/projects/#{project.id}/import_project_members/#{non_existing_record_id}", user)
end.not_to change { project.members.count }
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'returns 404 if the target project members cannot be administered by the requester' do
private_project = create(:project, :private)
expect do
post api("/projects/#{private_project.id}/import_project_members/#{project2.id}", user)
end.not_to change { project.members.count }
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Project Not Found')
end
it 'returns 422 if the import failed for valid projects' do
allow_next_instance_of(::ProjectTeam) do |project_team|
allow(project_team).to receive(:import).and_return(false)
end
expect do
post api("/projects/#{project.id}/import_project_members/#{project2.id}", user)
end.not_to change { project.members.count }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
expect(json_response['message']).to eq('Import failed')
end
end
describe 'PUT /projects/:id' do describe 'PUT /projects/:id' do
before do before do
expect(project).to be_persisted expect(project).to be_persisted
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Members::ImportProjectTeamService do
describe '#execute' do
let_it_be(:source_project) { create(:project) }
let_it_be(:target_project) { create(:project) }
let_it_be(:user) { create(:user) }
subject { described_class.new(user, { id: target_project_id, project_id: source_project_id }) }
before_all do
source_project.add_guest(user)
target_project.add_maintainer(user)
end
context 'when project team members are imported successfully' do
let(:source_project_id) { source_project.id }
let(:target_project_id) { target_project.id }
it 'returns true' do
expect(subject.execute).to be(true)
end
end
context 'when the project team import fails' do
context 'when the target project cannot be found' do
let(:source_project_id) { source_project.id }
let(:target_project_id) { non_existing_record_id }
it 'returns false' do
expect(subject.execute).to be(false)
end
end
context 'when the source project cannot be found' do
let(:source_project_id) { non_existing_record_id }
let(:target_project_id) { target_project.id }
it 'returns false' do
expect(subject.execute).to be(false)
end
end
context 'when the user doing the import does not exist' do
let(:user) { nil }
let(:source_project_id) { source_project.id }
let(:target_project_id) { target_project.id }
it 'returns false' do
expect(subject.execute).to be(false)
end
end
context 'when the user does not have permission to read the source project members' do
let(:user) { create(:user) }
let(:source_project_id) { create(:project, :private).id }
let(:target_project_id) { target_project.id }
it 'returns false' do
expect(subject.execute).to be(false)
end
end
context 'when the user does not have permission to admin the target project' do
let(:source_project_id) { source_project.id }
let(:target_project_id) { create(:project).id }
it 'returns false' do
expect(subject.execute).to be(false)
end
end
context 'when the source and target project are valid but the ProjectTeam#import command fails' do
let(:source_project_id) { source_project.id }
let(:target_project_id) { target_project.id }
before do
allow_next_instance_of(ProjectTeam) do |project_team|
allow(project_team).to receive(:import).and_return(false)
end
end
it 'returns false' do
expect(subject.execute).to be(false)
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