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
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
Also called Project Hooks and Webhooks. These are different for [System Hooks](system_hooks.md)
......
......@@ -587,6 +587,27 @@ module API
end
# 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
detail 'This feature was introduced in GitLab 13.11'
end
......
......@@ -3037,6 +3037,59 @@ RSpec.describe API::Projects do
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
before do
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