Commit 2dc03ca2 authored by Michael Leopard's avatar Michael Leopard

Creating Bitbucket Server import API

Adding integration tests for the new endpoint
Adding CHANGELOG
Creating new Bitbucket server service to handle endpoint logic
parent 368ba899
......@@ -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