Commit 13967e02 authored by Stan Hu's avatar Stan Hu

Merge branch '34648-restrict-forking-outside-of-gma-part-2' into 'master'

Reduce number of fork targets for group-managed accounts

Closes #34648

See merge request gitlab-org/gitlab!24698
parents 7fcb86f5 60689fc3
...@@ -3,6 +3,7 @@ ...@@ -3,6 +3,7 @@
class Projects::ForksController < Projects::ApplicationController class Projects::ForksController < Projects::ApplicationController
include ContinueParams include ContinueParams
include RendersMemberAccess include RendersMemberAccess
include Gitlab::Utils::StrongMemoize
# Authorize # Authorize
before_action :whitelist_query_limiting, only: [:create] before_action :whitelist_query_limiting, only: [:create]
...@@ -10,6 +11,7 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -10,6 +11,7 @@ class Projects::ForksController < Projects::ApplicationController
before_action :authorize_download_code! before_action :authorize_download_code!
before_action :authenticate_user!, only: [:new, :create] before_action :authenticate_user!, only: [:new, :create]
before_action :authorize_fork_project!, only: [:new, :create] before_action :authorize_fork_project!, only: [:new, :create]
before_action :authorize_fork_namespace!, only: [:create]
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def index def index
...@@ -37,18 +39,15 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -37,18 +39,15 @@ class Projects::ForksController < Projects::ApplicationController
# rubocop: enable CodeReuse/ActiveRecord # rubocop: enable CodeReuse/ActiveRecord
def new def new
@namespaces = current_user.manageable_namespaces @namespaces = fork_service.valid_fork_targets
@namespaces.delete(@project.namespace)
end end
# rubocop: disable CodeReuse/ActiveRecord # rubocop: disable CodeReuse/ActiveRecord
def create def create
namespace = Namespace.find(params[:namespace_key]) @forked_project = fork_namespace.projects.find_by(path: project.path)
@forked_project = namespace.projects.find_by(path: project.path)
@forked_project = nil unless @forked_project && @forked_project.forked_from_project == project @forked_project = nil unless @forked_project && @forked_project.forked_from_project == project
@forked_project ||= ::Projects::ForkService.new(project, current_user, namespace: namespace).execute @forked_project ||= fork_service.execute
if !@forked_project.saved? || !@forked_project.forked? if !@forked_project.saved? || !@forked_project.forked?
render :error render :error
...@@ -64,6 +63,22 @@ class Projects::ForksController < Projects::ApplicationController ...@@ -64,6 +63,22 @@ class Projects::ForksController < Projects::ApplicationController
private private
def fork_service
strong_memoize(:fork_service) do
::Projects::ForkService.new(project, current_user, namespace: fork_namespace)
end
end
def fork_namespace
strong_memoize(:fork_namespace) do
Namespace.find(params[:namespace_key]) if params[:namespace_key].present?
end
end
def authorize_fork_namespace!
access_denied! unless fork_namespace && fork_service.valid_fork_target?
end
def whitelist_query_limiting def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42335') Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42335')
end end
......
# frozen_string_literal: true
class ForkTargetsFinder
def initialize(project, user)
@project = project
@user = user
end
# rubocop: disable CodeReuse/ActiveRecord
def execute
::Namespace.where(id: user.manageable_namespaces).where.not(id: project.namespace).sort_by_type
end
# rubocop: enable CodeReuse/ActiveRecord
private
attr_reader :project, :user
end
ForkTargetsFinder.prepend_if_ee('EE::ForkTargetsFinder')
...@@ -68,6 +68,7 @@ class Namespace < ApplicationRecord ...@@ -68,6 +68,7 @@ class Namespace < ApplicationRecord
after_destroy :rm_dir after_destroy :rm_dir
scope :for_user, -> { where('type IS NULL') } scope :for_user, -> { where('type IS NULL') }
scope :sort_by_type, -> { order(Gitlab::Database.nulls_first_order(:type)) }
scope :with_statistics, -> do scope :with_statistics, -> do
joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id') joins('LEFT JOIN project_statistics ps ON ps.namespace_id = namespaces.id')
......
...@@ -3,24 +3,25 @@ ...@@ -3,24 +3,25 @@
module Projects module Projects
class ForkService < BaseService class ForkService < BaseService
def execute(fork_to_project = nil) def execute(fork_to_project = nil)
forked_project = forked_project = fork_to_project ? link_existing_project(fork_to_project) : fork_new_project
if fork_to_project
link_existing_project(fork_to_project)
else
fork_new_project
end
refresh_forks_count if forked_project&.saved? refresh_forks_count if forked_project&.saved?
forked_project forked_project
end end
private def valid_fork_targets
@valid_fork_targets ||= ForkTargetsFinder.new(@project, current_user).execute
end
def allowed_fork? def valid_fork_target?
current_user.can?(:fork_project, @project) return true if current_user.admin?
valid_fork_targets.include?(target_namespace)
end end
private
def link_existing_project(fork_to_project) def link_existing_project(fork_to_project)
return if fork_to_project.forked? return if fork_to_project.forked?
...@@ -30,6 +31,21 @@ module Projects ...@@ -30,6 +31,21 @@ module Projects
end end
def fork_new_project def fork_new_project
new_project = CreateService.new(current_user, new_fork_params).execute
return new_project unless new_project.persisted?
# Set the forked_from_project relation after saving to avoid having to
# reload the project to reset the association information and cause an
# extra query.
new_project.forked_from_project = @project
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update(builds_access_level: builds_access_level)
new_project
end
def new_fork_params
new_params = { new_params = {
visibility_level: allowed_visibility_level, visibility_level: allowed_visibility_level,
description: @project.description, description: @project.description,
...@@ -57,18 +73,11 @@ module Projects ...@@ -57,18 +73,11 @@ module Projects
new_params.merge!(@project.object_pool_params) new_params.merge!(@project.object_pool_params)
new_project = CreateService.new(current_user, new_params).execute new_params
return new_project unless new_project.persisted? end
# Set the forked_from_project relation after saving to avoid having to
# reload the project to reset the association information and cause an
# extra query.
new_project.forked_from_project = @project
builds_access_level = @project.project_feature.builds_access_level
new_project.project_feature.update(builds_access_level: builds_access_level)
new_project def allowed_fork?
current_user.can?(:fork_project, @project)
end end
def fork_network def fork_network
......
# frozen_string_literal: true
class ChangeSamlProviderOuterForksDefault < ActiveRecord::Migration[6.0]
DOWNTIME = false
def up
change_column_null :saml_providers, :prohibited_outer_forks, false
change_column_default :saml_providers, :prohibited_outer_forks, true
end
def down
change_column_default :saml_providers, :prohibited_outer_forks, false
change_column_null :saml_providers, :prohibited_outer_forks, true
end
end
...@@ -3770,7 +3770,7 @@ ActiveRecord::Schema.define(version: 2020_02_13_220211) do ...@@ -3770,7 +3770,7 @@ ActiveRecord::Schema.define(version: 2020_02_13_220211) do
t.string "sso_url", null: false t.string "sso_url", null: false
t.boolean "enforced_sso", default: false, null: false t.boolean "enforced_sso", default: false, null: false
t.boolean "enforced_group_managed_accounts", default: false, null: false t.boolean "enforced_group_managed_accounts", default: false, null: false
t.boolean "prohibited_outer_forks", default: false t.boolean "prohibited_outer_forks", default: true, null: false
t.index ["group_id"], name: "index_saml_providers_on_group_id" t.index ["group_id"], name: "index_saml_providers_on_group_id"
end end
......
...@@ -64,7 +64,7 @@ We intend to add a similar SSO requirement for [Git and API activity](https://gi ...@@ -64,7 +64,7 @@ We intend to add a similar SSO requirement for [Git and API activity](https://gi
#### Group-managed accounts #### Group-managed accounts
[Introduced in GitLab 12.1](https://gitlab.com/groups/gitlab-org/-/epics/709). > [Introduced in GitLab 12.1](https://gitlab.com/groups/gitlab-org/-/epics/709).
When SSO is being enforced, groups can enable an additional level of protection by enforcing the creation of dedicated user accounts to access the group. When SSO is being enforced, groups can enable an additional level of protection by enforcing the creation of dedicated user accounts to access the group.
...@@ -95,6 +95,14 @@ To access the Credentials inventory of a group, navigate to **{shield}** **Secur ...@@ -95,6 +95,14 @@ To access the Credentials inventory of a group, navigate to **{shield}** **Secur
This feature is similar to the [Credentials inventory for self-managed instances](../../admin_area/credentials_inventory.md). This feature is similar to the [Credentials inventory for self-managed instances](../../admin_area/credentials_inventory.md).
##### Outer forks restriction for Group-managed accounts
> [Introduced in GitLab 12.9](https://gitlab.com/gitlab-org/gitlab/issues/34648)
Groups with enabled group-managed accounts can allow or disallow forking of projects outside of root group
by using separate toggle. If forking is disallowed any project of given root group or its subgroups can be forked to
a subgroup of the same root group only.
#### Assertions #### Assertions
When using group-managed accounts, the following user details need to be passed to GitLab as SAML When using group-managed accounts, the following user details need to be passed to GitLab as SAML
......
# frozen_string_literal: true
module EE
module ForkTargetsFinder
extend ActiveSupport::Concern
extend ::Gitlab::Utils::Override
override :execute
# rubocop: disable CodeReuse/ActiveRecord
def execute
targets = super
root_group = project.group&.root_ancestor
return targets unless root_group&.saml_provider
if root_group.saml_provider.prohibited_outer_forks?
targets = targets.where(id: root_group.self_and_descendants)
end
targets
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
---
title: Add restrict outer forks functionality for group SAML
merge_request: 24698
author:
type: added
...@@ -75,7 +75,7 @@ describe 'SAML provider settings' do ...@@ -75,7 +75,7 @@ describe 'SAML provider settings' do
end end
context 'with existing SAML provider' do context 'with existing SAML provider' do
let!(:saml_provider) { create(:saml_provider, group: group) } let!(:saml_provider) { create(:saml_provider, group: group, prohibited_outer_forks: false) }
it 'allows provider to be disabled', :js do it 'allows provider to be disabled', :js do
visit group_saml_providers_path(group) visit group_saml_providers_path(group)
......
# frozen_string_literal: true
require 'spec_helper'
describe ForkTargetsFinder do
subject(:finder) { described_class.new(project, user) }
let(:project) { create :project, namespace: project_group }
describe '#execute' do
subject(:fork_targets) { finder.execute }
let(:user) { create :user, :group_managed, managing_group: project_group }
let(:outer_group) { create :group }
let(:inner_subgroup) { create(:group, :nested, parent: project_group) }
before do
project_group.add_reporter(user)
outer_group.add_owner(user)
inner_subgroup.add_owner(user)
stub_licensed_features(group_saml: true)
end
context 'when project root group prohibits outer forks' do
let(:project_group) do
create(:saml_provider, :enforced_group_managed_accounts, prohibited_outer_forks: true).group
end
it 'returns namespaces with the same root group as project one only' do
expect(fork_targets).to be_a(ActiveRecord::Relation)
expect(fork_targets).to match_array([inner_subgroup])
end
end
context 'when project root does not prohibit outer forks' do
let(:project_group) do
create(:saml_provider, :enforced_group_managed_accounts, prohibited_outer_forks: false).group
end
it 'returns outer namespaces as well as inner' do
expect(fork_targets).to be_a(ActiveRecord::Relation)
expect(fork_targets).to match_array([outer_group, inner_subgroup, user.namespace])
end
end
end
end
...@@ -759,4 +759,53 @@ describe API::Projects do ...@@ -759,4 +759,53 @@ describe API::Projects do
end end
end end
end end
describe 'POST /projects/:id/fork' do
subject(:fork_call) { post api("/projects/#{group_project.id}/fork", user), params: { namespace: target_namespace.id } }
let!(:target_namespace) do
create(:group).tap { |g| g.add_owner(user) }
end
let!(:group_project) { create(:project, namespace: group)}
let(:group) { create(:group) }
before do
group.add_reporter(user)
end
context 'when project namespace has prohibit_outer_forks enabled' do
let(:group) do
create(:saml_provider, :enforced_group_managed_accounts, prohibited_outer_forks: true).group
end
let(:user) do
create(:user, managing_group: group).tap do |u|
create(:group_saml_identity, user: u, saml_provider: group.saml_provider)
end
end
before do
stub_feature_flags(enforced_sso_requires_session: false)
stub_licensed_features(group_saml: true)
end
context 'and target namespace is outer' do
it 'renders 404' do
expect { fork_call }.not_to change { ::Project.count }
expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq "404 Target Namespace Not Found"
end
end
context 'and target namespace is inner to project namespace' do
let!(:target_namespace) { create(:group, parent: group) }
it 'forks the project' do
target_namespace.add_owner(user)
expect { fork_call }.to change { ::Project.count }.by(1)
end
end
end
end
end end
...@@ -267,18 +267,16 @@ module API ...@@ -267,18 +267,16 @@ module API
post ':id/fork' do post ':id/fork' do
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42284') Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42284')
not_found! unless can?(current_user, :fork_project, user_project)
fork_params = declared_params(include_missing: false) fork_params = declared_params(include_missing: false)
namespace_id = fork_params[:namespace] fork_params[:namespace] = find_namespace!(fork_params[:namespace]) if fork_params[:namespace].present?
if namespace_id.present? service = ::Projects::ForkService.new(user_project, current_user, fork_params)
fork_params[:namespace] = find_namespace(namespace_id)
unless fork_params[:namespace] && can?(current_user, :create_projects, fork_params[:namespace]) not_found!('Target Namespace') unless service.valid_fork_target?
not_found!('Target Namespace')
end
end
forked_project = ::Projects::ForkService.new(user_project, current_user, fork_params).execute forked_project = service.execute
if forked_project.errors.any? if forked_project.errors.any?
conflict!(forked_project.errors.messages) conflict!(forked_project.errors.messages)
......
...@@ -51,7 +51,7 @@ module Gitlab ...@@ -51,7 +51,7 @@ module Gitlab
# and all their ancestors (recursively). # and all their ancestors (recursively).
# #
# Passing an `upto` will stop the recursion once the specified parent_id is # Passing an `upto` will stop the recursion once the specified parent_id is
# reached. So all ancestors *lower* than the specified acestor will be # reached. So all ancestors *lower* than the specified ancestor will be
# included. # included.
# #
# Passing a `hierarchy_order` with either `:asc` or `:desc` will cause the # Passing a `hierarchy_order` with either `:asc` or `:desc` will cause the
......
...@@ -209,6 +209,17 @@ describe Projects::ForksController do ...@@ -209,6 +209,17 @@ describe Projects::ForksController do
expect(response).to redirect_to(namespace_project_import_path(user.namespace, project)) expect(response).to redirect_to(namespace_project_import_path(user.namespace, project))
end end
context 'when target namespace is not valid for forking' do
let(:params) { super().merge(namespace_key: another_group.id) }
let(:another_group) { create :group }
it 'responds with :not_found' do
subject
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'continue params' do context 'continue params' do
let(:params) do let(:params) do
{ {
......
# frozen_string_literal: true
require 'spec_helper'
describe ForkTargetsFinder do
subject(:finder) { described_class.new(project, user) }
let(:project) { create(:project, namespace: create(:group)) }
let(:user) { create(:user) }
let!(:maintained_group) do
create(:group).tap { |g| g.add_maintainer(user) }
end
let!(:owned_group) do
create(:group).tap { |g| g.add_owner(user) }
end
let!(:developer_group) do
create(:group).tap { |g| g.add_developer(user) }
end
let!(:reporter_group) do
create(:group).tap { |g| g.add_reporter(user) }
end
let!(:guest_group) do
create(:group).tap { |g| g.add_guest(user) }
end
before do
project.namespace.add_owner(user)
end
describe '#execute' do
it 'returns all user manageable namespaces except project namespace' do
expect(finder.execute).to match_array([user.namespace, maintained_group, owned_group])
end
end
end
...@@ -2698,20 +2698,14 @@ describe API::Projects do ...@@ -2698,20 +2698,14 @@ describe API::Projects do
create(:project, :repository, creator: user, namespace: user.namespace) create(:project, :repository, creator: user, namespace: user.namespace)
end end
let(:group) { create(:group) } let(:group) { create(:group, :public) }
let(:group2) do let(:group2) { create(:group, name: 'group2_name') }
group = create(:group, name: 'group2_name') let(:group3) { create(:group, name: 'group3_name', parent: group2) }
group.add_maintainer(user2)
group
end
let(:group3) do
group = create(:group, name: 'group3_name', parent: group2)
group.add_owner(user2)
group
end
before do before do
group.add_guest(user2)
group2.add_maintainer(user2)
group3.add_owner(user2)
project.add_reporter(user2) project.add_reporter(user2)
project2.add_reporter(user2) project2.add_reporter(user2)
end end
...@@ -2720,7 +2714,7 @@ describe API::Projects do ...@@ -2720,7 +2714,7 @@ describe API::Projects do
it 'forks if user has sufficient access to project' do it 'forks if user has sufficient access to project' do
post api("/projects/#{project.id}/fork", user2) post api("/projects/#{project.id}/fork", user2)
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(project.name) expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq(project.path) expect(json_response['path']).to eq(project.path)
expect(json_response['owner']['id']).to eq(user2.id) expect(json_response['owner']['id']).to eq(user2.id)
...@@ -2733,7 +2727,7 @@ describe API::Projects do ...@@ -2733,7 +2727,7 @@ describe API::Projects do
it 'forks if user is admin' do it 'forks if user is admin' do
post api("/projects/#{project.id}/fork", admin) post api("/projects/#{project.id}/fork", admin)
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(project.name) expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq(project.path) expect(json_response['path']).to eq(project.path)
expect(json_response['owner']['id']).to eq(admin.id) expect(json_response['owner']['id']).to eq(admin.id)
...@@ -2747,14 +2741,17 @@ describe API::Projects do ...@@ -2747,14 +2741,17 @@ describe API::Projects do
new_user = create(:user) new_user = create(:user)
post api("/projects/#{project.id}/fork", new_user) post api("/projects/#{project.id}/fork", new_user)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Project Not Found') expect(json_response['message']).to eq('404 Project Not Found')
end end
it 'fails if forked project exists in the user namespace' do it 'fails if forked project exists in the user namespace' do
post api("/projects/#{project.id}/fork", user) new_project = create(:project, name: project.name, path: project.path)
new_project.add_reporter(user)
expect(response).to have_gitlab_http_status(409) post api("/projects/#{new_project.id}/fork", user)
expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']['name']).to eq(['has already been taken']) expect(json_response['message']['name']).to eq(['has already been taken'])
expect(json_response['message']['path']).to eq(['has already been taken']) expect(json_response['message']['path']).to eq(['has already been taken'])
end end
...@@ -2762,48 +2759,48 @@ describe API::Projects do ...@@ -2762,48 +2759,48 @@ describe API::Projects do
it 'fails if project to fork from does not exist' do it 'fails if project to fork from does not exist' do
post api('/projects/424242/fork', user) post api('/projects/424242/fork', user)
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Project Not Found') expect(json_response['message']).to eq('404 Project Not Found')
end end
it 'forks with explicit own user namespace id' do it 'forks with explicit own user namespace id' do
post api("/projects/#{project.id}/fork", user2), params: { namespace: user2.namespace.id } post api("/projects/#{project.id}/fork", user2), params: { namespace: user2.namespace.id }
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(json_response['owner']['id']).to eq(user2.id) expect(json_response['owner']['id']).to eq(user2.id)
end end
it 'forks with explicit own user name as namespace' do it 'forks with explicit own user name as namespace' do
post api("/projects/#{project.id}/fork", user2), params: { namespace: user2.username } post api("/projects/#{project.id}/fork", user2), params: { namespace: user2.username }
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(json_response['owner']['id']).to eq(user2.id) expect(json_response['owner']['id']).to eq(user2.id)
end end
it 'forks to another user when admin' do it 'forks to another user when admin' do
post api("/projects/#{project.id}/fork", admin), params: { namespace: user2.username } post api("/projects/#{project.id}/fork", admin), params: { namespace: user2.username }
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(json_response['owner']['id']).to eq(user2.id) expect(json_response['owner']['id']).to eq(user2.id)
end end
it 'fails if trying to fork to another user when not admin' do it 'fails if trying to fork to another user when not admin' do
post api("/projects/#{project.id}/fork", user2), params: { namespace: admin.namespace.id } post api("/projects/#{project.id}/fork", user2), params: { namespace: admin.namespace.id }
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
end end
it 'fails if trying to fork to non-existent namespace' do it 'fails if trying to fork to non-existent namespace' do
post api("/projects/#{project.id}/fork", user2), params: { namespace: 42424242 } post api("/projects/#{project.id}/fork", user2), params: { namespace: 42424242 }
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq('404 Target Namespace Not Found') expect(json_response['message']).to eq('404 Namespace Not Found')
end end
it 'forks to owned group' do it 'forks to owned group' do
post api("/projects/#{project.id}/fork", user2), params: { namespace: group2.name } post api("/projects/#{project.id}/fork", user2), params: { namespace: group2.name }
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(json_response['namespace']['name']).to eq(group2.name) expect(json_response['namespace']['name']).to eq(group2.name)
end end
...@@ -2811,7 +2808,7 @@ describe API::Projects do ...@@ -2811,7 +2808,7 @@ describe API::Projects do
full_path = "#{group2.path}/#{group3.path}" full_path = "#{group2.path}/#{group3.path}"
post api("/projects/#{project.id}/fork", user2), params: { namespace: full_path } post api("/projects/#{project.id}/fork", user2), params: { namespace: full_path }
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(json_response['namespace']['name']).to eq(group3.name) expect(json_response['namespace']['name']).to eq(group3.name)
expect(json_response['namespace']['full_path']).to eq(full_path) expect(json_response['namespace']['full_path']).to eq(full_path)
end end
...@@ -2819,20 +2816,21 @@ describe API::Projects do ...@@ -2819,20 +2816,21 @@ describe API::Projects do
it 'fails to fork to not owned group' do it 'fails to fork to not owned group' do
post api("/projects/#{project.id}/fork", user2), params: { namespace: group.name } post api("/projects/#{project.id}/fork", user2), params: { namespace: group.name }
expect(response).to have_gitlab_http_status(404) expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']).to eq("404 Target Namespace Not Found")
end end
it 'forks to not owned group when admin' do it 'forks to not owned group when admin' do
post api("/projects/#{project.id}/fork", admin), params: { namespace: group.name } post api("/projects/#{project.id}/fork", admin), params: { namespace: group.name }
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(json_response['namespace']['name']).to eq(group.name) expect(json_response['namespace']['name']).to eq(group.name)
end end
it 'accepts a path for the target project' do it 'accepts a path for the target project' do
post api("/projects/#{project.id}/fork", user2), params: { path: 'foobar' } post api("/projects/#{project.id}/fork", user2), params: { path: 'foobar' }
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq(project.name) expect(json_response['name']).to eq(project.name)
expect(json_response['path']).to eq('foobar') expect(json_response['path']).to eq('foobar')
expect(json_response['owner']['id']).to eq(user2.id) expect(json_response['owner']['id']).to eq(user2.id)
...@@ -2846,14 +2844,14 @@ describe API::Projects do ...@@ -2846,14 +2844,14 @@ describe API::Projects do
post api("/projects/#{project.id}/fork", user2), params: { path: 'foobar' } post api("/projects/#{project.id}/fork", user2), params: { path: 'foobar' }
post api("/projects/#{project2.id}/fork", user2), params: { path: 'foobar' } post api("/projects/#{project2.id}/fork", user2), params: { path: 'foobar' }
expect(response).to have_gitlab_http_status(409) expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']['path']).to eq(['has already been taken']) expect(json_response['message']['path']).to eq(['has already been taken'])
end end
it 'accepts a name for the target project' do it 'accepts a name for the target project' do
post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project' } post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project' }
expect(response).to have_gitlab_http_status(201) expect(response).to have_gitlab_http_status(:created)
expect(json_response['name']).to eq('My Random Project') expect(json_response['name']).to eq('My Random Project')
expect(json_response['path']).to eq(project.path) expect(json_response['path']).to eq(project.path)
expect(json_response['owner']['id']).to eq(user2.id) expect(json_response['owner']['id']).to eq(user2.id)
...@@ -2867,7 +2865,7 @@ describe API::Projects do ...@@ -2867,7 +2865,7 @@ describe API::Projects do
post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project' } post api("/projects/#{project.id}/fork", user2), params: { name: 'My Random Project' }
post api("/projects/#{project2.id}/fork", user2), params: { name: 'My Random Project' } post api("/projects/#{project2.id}/fork", user2), params: { name: 'My Random Project' }
expect(response).to have_gitlab_http_status(409) expect(response).to have_gitlab_http_status(:conflict)
expect(json_response['message']['name']).to eq(['has already been taken']) expect(json_response['message']['name']).to eq(['has already been taken'])
end end
end end
...@@ -2876,7 +2874,7 @@ describe API::Projects do ...@@ -2876,7 +2874,7 @@ describe API::Projects do
it 'returns authentication error' do it 'returns authentication error' do
post api("/projects/#{project.id}/fork") post api("/projects/#{project.id}/fork")
expect(response).to have_gitlab_http_status(401) expect(response).to have_gitlab_http_status(:unauthorized)
expect(json_response['message']).to eq('401 Unauthorized') expect(json_response['message']).to eq('401 Unauthorized')
end end
end end
...@@ -2890,8 +2888,7 @@ describe API::Projects do ...@@ -2890,8 +2888,7 @@ describe API::Projects do
it 'denies project to be forked' do it 'denies project to be forked' do
post api("/projects/#{project.id}/fork", admin) post api("/projects/#{project.id}/fork", admin)
expect(response).to have_gitlab_http_status(409) expect(response).to have_gitlab_http_status(:not_found)
expect(json_response['message']['forked_from_project_id']).to eq(['is forbidden'])
end end
end end
end end
......
...@@ -275,6 +275,7 @@ describe Projects::ForkService do ...@@ -275,6 +275,7 @@ describe Projects::ForkService do
context 'fork project for group when user not owner' do context 'fork project for group when user not owner' do
it 'group developer fails to fork project into the group' do it 'group developer fails to fork project into the group' do
to_project = fork_project(@project, @developer, @opts) to_project = fork_project(@project, @developer, @opts)
expect(to_project.errors[:namespace]).to eq(['is not valid']) expect(to_project.errors[:namespace]).to eq(['is not valid'])
end end
end end
...@@ -336,7 +337,9 @@ describe Projects::ForkService do ...@@ -336,7 +337,9 @@ describe Projects::ForkService do
context 'when linking fork to an existing project' do context 'when linking fork to an existing project' do
let(:fork_from_project) { create(:project, :public) } let(:fork_from_project) { create(:project, :public) }
let(:fork_to_project) { create(:project, :public) } let(:fork_to_project) { create(:project, :public) }
let(:user) { create(:user) } let(:user) do
create(:user).tap { |u| fork_to_project.add_maintainer(u) }
end
subject { described_class.new(fork_from_project, user) } subject { described_class.new(fork_from_project, user) }
...@@ -387,4 +390,54 @@ describe Projects::ForkService do ...@@ -387,4 +390,54 @@ describe Projects::ForkService do
end end
end end
end end
describe '#valid_fork_targets' do
let(:finder_mock) { instance_double('ForkTargetsFinder', execute: ['finder_return_value']) }
let(:current_user) { instance_double('User') }
let(:project) { instance_double('Project') }
before do
allow(ForkTargetsFinder).to receive(:new).with(project, current_user).and_return(finder_mock)
end
it 'returns whatever finder returns' do
expect(described_class.new(project, current_user).valid_fork_targets).to eq ['finder_return_value']
end
end
describe '#valid_fork_target?' do
subject { described_class.new(project, user, params).valid_fork_target? }
let(:project) { Project.new }
let(:params) { {} }
context 'when current user is an admin' do
let(:user) { build(:user, :admin) }
it { is_expected.to be_truthy }
end
context 'when current_user is not an admin' do
let(:user) { create(:user) }
let(:finder_mock) { instance_double('ForkTargetsFinder', execute: [user.namespace]) }
let(:project) { create(:project) }
before do
allow(ForkTargetsFinder).to receive(:new).with(project, user).and_return(finder_mock)
end
context 'when target namespace is in valid fork targets' do
let(:params) { { namespace: user.namespace } }
it { is_expected.to be_truthy }
end
context 'when target namespace is not in valid fork targets' do
let(:params) { { namespace: create(:group) } }
it { is_expected.to be_falsey }
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