Commit 8fa3522e authored by Douglas Barbosa Alexandre's avatar Douglas Barbosa Alexandre

Merge branch '36788-feature-proposal-api-for-import-from-bitbucket-server' into 'master'

Resolve "Feature proposal: API for import from BitBucket Server"

Closes #36788

See merge request gitlab-org/gitlab!33097
parents d20e41ca 2dc03ca2
......@@ -34,26 +34,18 @@ class Import::BitbucketServerController < Import::BaseController
return render json: { errors: _("Project %{project_repo} could not be found") % { project_repo: "#{@project_key}/#{@repo_slug}" } }, status: :unprocessable_entity
end
project_name = params[:new_name].presence || repo.name
namespace_path = params[:new_namespace].presence || current_user.username
target_namespace = find_or_create_namespace(namespace_path, current_user)
result = Import::BitbucketServerService.new(client, current_user, params).execute(credentials)
if current_user.can?(:create_projects, target_namespace)
project = Gitlab::BitbucketServerImport::ProjectCreator.new(@project_key, @repo_slug, repo, project_name, target_namespace, current_user, credentials).execute
if project.persisted?
render json: ProjectSerializer.new.represent(project, serializer: :import)
else
render json: { errors: project_save_error(project) }, status: :unprocessable_entity
end
if result[:status] == :success
render json: ProjectSerializer.new.represent(result[:project], serializer: :import)
else
render json: { errors: _('This namespace has already been taken! Please choose another one.') }, status: :unprocessable_entity
render json: { errors: result[:message] }, status: result[:http_status]
end
end
def configure
session[personal_access_token_key] = params[:personal_access_token]
session[bitbucket_server_username_key] = params[:bitbucket_username]
session[bitbucket_server_username_key] = params[:bitbucket_server_username]
session[bitbucket_server_url_key] = params[:bitbucket_server_url]
redirect_to status_import_bitbucket_server_path
......@@ -127,8 +119,8 @@ class Import::BitbucketServerController < Import::BaseController
end
def validate_import_params
@project_key = params[:project]
@repo_slug = params[:repository]
@project_key = params[:bitbucketServerProject]
@repo_slug = params[:bitbucketServerRepo]
return render_validation_error('Missing project key') unless @project_key.present? && @repo_slug.present?
return render_validation_error('Missing repository slug') unless @repo_slug.present?
......
# frozen_string_literal: true
module Import
class BitbucketServerService < Import::BaseService
attr_reader :client, :params, :current_user
def execute(credentials)
if blocked_url?
return log_and_return_error("Invalid URL: #{url}", :bad_request)
end
unless authorized?
return log_and_return_error("You don't have permissions to create this project", :unauthorized)
end
unless repo
return log_and_return_error("Project %{project_repo} could not be found" % { project_repo: "#{project_key}/#{repo_slug}" }, :unprocessable_entity)
end
project = create_project(credentials)
if project.persisted?
success(project)
else
log_and_return_error(project_save_error(project), :unprocessable_entity)
end
rescue BitbucketServer::Connection::ConnectionError => e
log_and_return_error("Import failed due to a BitBucket Server error: #{e}", :bad_request)
end
private
def create_project(credentials)
Gitlab::BitbucketServerImport::ProjectCreator.new(
project_key,
repo_slug,
repo,
project_name,
target_namespace,
current_user,
credentials
).execute
end
def repo
@repo ||= client.repo(project_key, repo_slug)
end
def project_name
@project_name ||= params[:new_name].presence || repo.name
end
def namespace_path
@namespace_path ||= params[:new_namespace].presence || current_user.namespace_path
end
def target_namespace
@target_namespace ||= find_or_create_namespace(namespace_path, current_user.namespace_path)
end
def repo_slug
@repo_slug ||= params[:bitbucket_server_repo] || params[:bitbucketServerRepo]
end
def project_key
@project_key ||= params[:bitbucket_server_project] || params[:bitbucketServerProject]
end
def url
@url ||= params[:bitbucket_server_url]
end
def authorized?
can?(current_user, :create_projects, target_namespace)
end
def allow_local_requests?
Gitlab::CurrentSettings.allow_local_requests_from_web_hooks_and_services?
end
def blocked_url?
Gitlab::UrlBlocker.blocked_url?(
url,
{
allow_localhost: allow_local_requests?,
allow_local_network: allow_local_requests?,
schemes: %w(http https)
}
)
end
def log_and_return_error(message, error_type)
log_error(message)
error(_(message), error_type)
end
def log_error(message)
Gitlab::Import::Logger.error(
message: 'Import failed due to a BitBucket Server error',
error: message
)
end
end
end
......@@ -17,7 +17,7 @@
.form-group.row
= label_tag :bitbucket_server_url, 'Username', class: 'col-form-label col-md-2'
.col-md-4
= text_field_tag :bitbucket_username, '', class: 'form-control gl-mr-3', placeholder: _('username'), size: 40
= text_field_tag :bitbucket_server_username, '', class: 'form-control gl-mr-3', placeholder: _('username'), size: 40
.form-group.row
= label_tag :personal_access_token, 'Password/Personal Access Token', class: 'col-form-label col-md-2'
.col-md-4
......
......@@ -55,7 +55,7 @@
= project.human_import_status_name
- @repos.each do |repo|
%tr{ id: "repo_#{repo.project_key}___#{repo.slug}", data: { project: repo.project_key, repository: repo.slug } }
%tr{ id: "repo_#{repo.project_key}___#{repo.slug}", data: { bitbucket_server_project: repo.project_key, bitbucket_server_repo: repo.slug } }
%td
= sanitize(link_to(repo.browse_url, repo.browse_url, target: '_blank', rel: 'noopener noreferrer'), attributes: %w(href target rel))
%td.import-target
......
---
title: 'Resolve Feature proposal: API for import from BitBucket Server'
merge_request: 33097
author:
type: added
......@@ -29,3 +29,41 @@ Example response:
"full_name": "Administrator / my-repo"
}
```
## Import repository from Bitbucket Server
Import your projects from Bitbucket Server to GitLab via the API.
NOTE: **Note:**
The Bitbucket Project Key is only used for finding the repository in Bitbucket.
You must specify a `target_namespace` if you want to import the repository to a GitLab group.
If you do not specify `target_namespace`, the project will import to your personal user namespace.
```plaintext
POST /import/bitbucket_server
```
| Attribute | Type | Required | Description |
|------------|---------|----------|---------------------|
| `bitbucket_server_url` | string | yes | Bitbucket Server URL |
| `bitbucket_server_username` | string | yes | Bitbucket Server Username |
| `personal_access_token` | string | yes | Bitbucket Server personal access token/password |
| `bitbucket_server_project` | string | yes | Bitbucket Project Key |
| `bitbucket_server_repo` | string | yes | Bitbucket Repository Name |
| `new_name` | string | no | New repo name |
| `target_namespace` | string | no | Namespace to import repo into |
```shell
curl --request POST \
--url https://gitlab.example.com/api/v4/import/bitbucket/server \
--header "content-type: application/json" \
--header "PRIVATE-TOKEN: 9koXpg98eAheJpvBs5tK" \
--data '{
"bitbucket_server_url": "http://bitbucket.example.com",
"bitbucket_server_username": "root",
"personal_access_token": "Nzk4MDcxODY4MDAyOiP8y410zF3tGAyLnHRv/E0+3xYs",
"bitbucket_server_project": "NEW",
"bitbucket_server_repo": "my-repo"
}'
```
......@@ -156,6 +156,7 @@ module API
mount ::API::Groups
mount ::API::GroupContainerRepositories
mount ::API::GroupVariables
mount ::API::ImportBitbucketServer
mount ::API::ImportGithub
mount ::API::Issues
mount ::API::JobArtifacts
......
# frozen_string_literal: true
module API
class ImportBitbucketServer < Grape::API::Instance
helpers do
def client
@client ||= BitbucketServer::Client.new(credentials)
end
def credentials
@credentials ||= {
base_uri: params[:bitbucket_server_url],
user: params[:bitbucket_server_username],
password: params[:personal_access_token]
}
end
end
desc 'Import a BitBucket Server repository' do
detail 'This feature was introduced in GitLab 13.2.'
success ::ProjectEntity
end
params do
requires :bitbucket_server_url, type: String, desc: 'Bitbucket Server URL'
requires :bitbucket_server_username, type: String, desc: 'BitBucket Server Username'
requires :personal_access_token, type: String, desc: 'BitBucket Server personal access token/password'
requires :bitbucket_server_project, type: String, desc: 'BitBucket Server Project Key'
requires :bitbucket_server_repo, type: String, desc: 'BitBucket Server Repository Name'
optional :new_name, type: String, desc: 'New repo name'
optional :new_namespace, type: String, desc: 'Namespace to import repo into'
end
post 'import/bitbucket_server' do
result = Import::BitbucketServerService.new(client, current_user, params).execute(credentials)
if result[:status] == :success
present ProjectSerializer.new.represent(result[:project], serializer: :import)
else
render_api_error!({ error: result[:message] }, result[:http_status])
end
end
end
end
......@@ -46,7 +46,7 @@ RSpec.describe Import::BitbucketServerController do
.to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
.and_return(double(execute: project))
post :create, params: { project: project_key, repository: repo_slug }, format: :json
post :create, params: { bitbucketServerProject: project_key, bitbucketServerRepo: repo_slug }, format: :json
expect(response).to have_gitlab_http_status(:ok)
end
......@@ -59,20 +59,20 @@ RSpec.describe Import::BitbucketServerController do
.to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
.and_return(double(execute: project))
post :create, params: { project: project_key, repository: repo_slug, format: :json }
post :create, params: { bitbucketServerProject: project_key, bitbucketServerRepo: repo_slug, format: :json }
expect(response).to have_gitlab_http_status(:ok)
end
end
it 'returns an error when an invalid project key is used' do
post :create, params: { project: 'some&project' }
post :create, params: { bitbucket_server_project: 'some&project' }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
it 'returns an error when an invalid repository slug is used' do
post :create, params: { project: 'some-project', repository: 'try*this' }
post :create, params: { bitbucket_server_project: 'some-project', bitbucket_server_repo: 'try*this' }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
......@@ -80,7 +80,7 @@ RSpec.describe Import::BitbucketServerController do
it 'returns an error when the project cannot be found' do
allow(client).to receive(:repo).with(project_key, repo_slug).and_return(nil)
post :create, params: { project: project_key, repository: repo_slug }, format: :json
post :create, params: { bitbucket_server_project: project_key, bitbucket_server_repo: repo_slug }, format: :json
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
......@@ -90,15 +90,15 @@ RSpec.describe Import::BitbucketServerController do
.to receive(:new).with(project_key, repo_slug, anything, project_name, user.namespace, user, anything)
.and_return(double(execute: build(:project)))
post :create, params: { project: project_key, repository: repo_slug }, format: :json
post :create, params: { bitbucket_server_project: project_key, bitbucket_server_repo: repo_slug }, format: :json
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
it "returns an error when the server can't be contacted" do
expect(client).to receive(:repo).with(project_key, repo_slug).and_raise(::BitbucketServer::Connection::ConnectionError)
allow(client).to receive(:repo).with(project_key, repo_slug).and_return([nil, nil])
post :create, params: { project: project_key, repository: repo_slug }, format: :json
post :create, params: { bitbucket_server_project: project_key, bitbucket_server_repo: repo_slug }, format: :json
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
......@@ -123,7 +123,9 @@ RSpec.describe Import::BitbucketServerController do
end
it 'sets the session variables' do
post :configure, params: { personal_access_token: token, bitbucket_username: username, bitbucket_server_url: url }
allow(controller).to receive(:allow_local_requests?).and_return(true)
post :configure, params: { personal_access_token: token, bitbucket_server_username: username, bitbucket_server_url: url }
expect(session[:bitbucket_server_url]).to eq(url)
expect(session[:bitbucket_server_username]).to eq(username)
......
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe API::ImportBitbucketServer do
let(:base_uri) { "https://test:7990" }
let(:user) { create(:user) }
let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" }
let(:project_key) { 'TES' }
let(:repo_slug) { 'vim' }
let(:repo) { { name: 'vim' } }
describe "POST /import/bitbucket_server" do
context 'with no optional parameters' do
let_it_be(:project) { create(:project) }
let(:client) { double(BitbucketServer::Client) }
before do
Grape::Endpoint.before_each do |endpoint|
allow(endpoint).to receive(:client).and_return(client.as_null_object)
allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(name: repo_slug))
end
end
after do
Grape::Endpoint.before_each nil
end
it 'returns 201 response when the project is imported successfully' do
allow(Gitlab::BitbucketServerImport::ProjectCreator)
.to receive(:new).with(project_key, repo_slug, anything, repo_slug, user.namespace, user, anything)
.and_return(double(execute: project))
post api("/import/bitbucket_server", user), params: {
bitbucket_server_url: base_uri,
bitbucket_server_username: user,
personal_access_token: token,
bitbucket_server_project: project_key,
bitbucket_server_repo: repo_slug
}
expect(response).to have_gitlab_http_status(:created)
expect(json_response).to be_a Hash
expect(json_response['name']).to eq(project.name)
end
end
context 'with a new project name' do
let_it_be(:project) { create(:project, name: 'new-name') }
let(:client) { instance_double(BitbucketServer::Client) }
before do
Grape::Endpoint.before_each do |endpoint|
allow(endpoint).to receive(:client).and_return(client)
allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(name: repo_slug))
end
end
after do
Grape::Endpoint.before_each nil
end
it 'returns 201 response when the project is imported successfully with a new project name' do
allow(Gitlab::BitbucketServerImport::ProjectCreator)
.to receive(:new).with(project_key, repo_slug, anything, project.name, user.namespace, user, anything)
.and_return(double(execute: project))
post api("/import/bitbucket_server", user), params: {
bitbucket_server_url: base_uri,
bitbucket_server_username: user,
personal_access_token: token,
bitbucket_server_project: project_key,
bitbucket_server_repo: repo_slug,
new_name: 'new-name'
}
expect(response).to have_gitlab_http_status(:created)
expect(json_response).to be_a Hash
expect(json_response['name']).to eq('new-name')
end
end
context 'with an invalid URL' do
let_it_be(:project) { create(:project, name: 'new-name') }
let(:client) { instance_double(BitbucketServer::Client) }
before do
Grape::Endpoint.before_each do |endpoint|
allow(endpoint).to receive(:client).and_return(client)
allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(name: repo_slug))
end
end
after do
Grape::Endpoint.before_each nil
end
it 'returns 400 response due to a blcoked URL' do
allow(Gitlab::BitbucketServerImport::ProjectCreator)
.to receive(:new).with(project_key, repo_slug, anything, project.name, user.namespace, user, anything)
.and_return(double(execute: project))
allow(Gitlab::UrlBlocker)
.to receive(:blocked_url?)
.and_return(true)
post api("/import/bitbucket_server", user), params: {
bitbucket_server_url: base_uri,
bitbucket_server_username: user,
personal_access_token: token,
bitbucket_server_project: project_key,
bitbucket_server_repo: repo_slug,
new_name: 'new-name'
}
expect(response).to have_gitlab_http_status(:bad_request)
end
end
context 'with a new namespace' do
let(:bitbucket_client) { instance_double(BitbucketServer::Client) }
before do
Grape::Endpoint.before_each do |endpoint|
allow(endpoint).to receive(:client).and_return(bitbucket_client)
repo = double(name: repo_slug, full_path: "/other-namespace/#{repo_slug}")
allow(bitbucket_client).to receive(:repo).with(project_key, repo_slug).and_return(repo)
end
end
after do
Grape::Endpoint.before_each nil
end
it 'returns 201 response when the project is imported successfully to a new namespace' do
allow(Gitlab::BitbucketServerImport::ProjectCreator)
.to receive(:new).with(project_key, repo_slug, anything, repo_slug, an_instance_of(Group), user, anything)
.and_return(double(execute: create(:project, name: repo_slug)))
post api("/import/bitbucket_server", user), params: {
bitbucket_server_url: base_uri,
bitbucket_server_username: user,
personal_access_token: token,
bitbucket_server_project: project_key,
bitbucket_server_repo: repo_slug,
new_namespace: 'new-namespace'
}
expect(response).to have_gitlab_http_status(:created)
expect(json_response).to be_a Hash
expect(json_response['full_path']).not_to eq("/#{user.namespace}/#{repo_slug}")
end
end
context 'with a private inaccessible namespace' do
let(:bitbucket_client) { instance_double(BitbucketServer::Client) }
let(:project) { create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim', namespace: 'private-group/vim') }
before do
Grape::Endpoint.before_each do |endpoint|
allow(endpoint).to receive(:client).and_return(bitbucket_client)
repo = double(name: repo_slug, full_path: "/private-group/#{repo_slug}")
allow(bitbucket_client).to receive(:repo).with(project_key, repo_slug).and_return(repo)
end
end
after do
Grape::Endpoint.before_each nil
end
it 'returns 401 response when user can not create projects in the chosen namespace' do
allow(Gitlab::BitbucketServerImport::ProjectCreator)
.to receive(:new).with(project_key, repo_slug, anything, repo_slug, an_instance_of(Group), user, anything)
.and_return(double(execute: build(:project)))
other_namespace = create(:group, :private, name: 'private-group')
post api("/import/bitbucket_server", user), params: {
bitbucket_server_url: base_uri,
bitbucket_server_username: user,
personal_access_token: token,
bitbucket_server_project: project_key,
bitbucket_server_repo: repo_slug,
new_namespace: other_namespace.name
}
expect(response).to have_gitlab_http_status(:unauthorized)
end
end
context 'with an inaccessible bitbucket server instance' do
let(:bitbucket_client) { instance_double(BitbucketServer::Client) }
let(:project) { create(:project, import_type: 'bitbucket', creator_id: user.id, import_source: 'asd/vim', namespace: 'private-group/vim') }
before do
Grape::Endpoint.before_each do |endpoint|
allow(endpoint).to receive(:client).and_return(bitbucket_client)
allow(bitbucket_client).to receive(:repo).with(project_key, repo_slug).and_raise(::BitbucketServer::Connection::ConnectionError)
end
end
it 'raises a connection error' do
post api("/import/bitbucket_server", user), params: {
bitbucket_server_url: base_uri,
bitbucket_server_username: user,
personal_access_token: token,
bitbucket_server_project: project_key,
bitbucket_server_repo: repo_slug,
new_namespace: 'new-namespace'
}
expect(response).to have_gitlab_http_status(:bad_request)
end
end
end
end
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe Import::BitbucketServerService do
let_it_be(:user) { create(:user) }
let(:base_uri) { "https://test:7990" }
let(:token) { "asdasd12345" }
let(:secret) { "sekrettt" }
let(:project_key) { 'TES' }
let(:repo_slug) { 'vim' }
let(:repo) do
{
name: 'vim',
description: 'test',
visibility_level: Gitlab::VisibilityLevel::PUBLIC,
browse_url: 'http://repo.com/repo/repo',
clone_url: 'http://repo.com/repo/repo.git'
}
end
let(:client) { double(BitbucketServer::Client) }
let(:credentials) { { base_uri: base_uri, user: user, password: token } }
let(:params) { { bitbucket_server_url: base_uri, bitbucket_server_username: user, personal_access_token: token, bitbucket_server_project: project_key, bitbucket_server_repo: repo_slug } }
subject { described_class.new(client, user, params) }
before do
allow(subject).to receive(:authorized?).and_return(true)
end
context 'when no repo is found' do
before do
allow(subject).to receive(:authorized?).and_return(true)
allow(client).to receive(:repo).and_return(nil)
end
it 'returns an error' do
result = subject.execute(credentials)
expect(result).to include(
message: "Project #{project_key}/#{repo_slug} could not be found",
status: :error,
http_status: :unprocessable_entity
)
end
end
context 'when user is unauthorized' do
before do
allow(subject).to receive(:authorized?).and_return(false)
end
it 'returns an error' do
result = subject.execute(credentials)
expect(result).to include(
message: "You don't have permissions to create this project",
status: :error,
http_status: :unauthorized
)
end
end
context 'verify url' do
shared_examples 'denies local request' do
before do
allow(client).to receive(:repo).with(project_key, repo_slug).and_return(double(repo))
end
it 'does not allow requests' do
result = subject.execute(credentials)
expect(result[:status]).to eq(:error)
expect(result[:message]).to include("Invalid URL:")
end
end
context 'when host is localhost' do
before do
allow(subject).to receive(:url).and_return('https://localhost:3000')
end
include_examples 'denies local request'
end
context 'when host is on local network' do
before do
allow(subject).to receive(:url).and_return('https://192.168.0.191')
end
include_examples 'denies local request'
end
context 'when host is ftp protocol' do
before do
allow(subject).to receive(:url).and_return('ftp://testing')
end
include_examples 'denies local request'
end
end
it 'raises an exception for unknown error causes' do
exception = StandardError.new('Not Implemented')
allow(client).to receive(:repo).and_raise(exception)
expect(Gitlab::Import::Logger).not_to receive(:error)
expect { subject.execute(credentials) }.to raise_error(exception)
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