Commit 2f7c980d authored by Imre Farkas's avatar Imre Farkas

Add setting to disable project forking

It also adds a new ProjectSetting table to store this setting and can be
used in the future as the Project table already has too many columns.
parent 9cd82c24
......@@ -104,6 +104,7 @@ export default {
visibilityLevel: visibilityOptions.PUBLIC,
issuesAccessLevel: 20,
repositoryAccessLevel: 20,
forkingEnabled: true,
mergeRequestsAccessLevel: 20,
buildsAccessLevel: 20,
wikiAccessLevel: 20,
......@@ -300,6 +301,18 @@ export default {
name="project[project_feature_attributes][merge_requests_access_level]"
/>
</project-setting-row>
<project-setting-row
:label="s__('ProjectSettings|Forks')"
:help-text="
s__('ProjectSettings|Allow users to make copies of your repository to a new project')
"
>
<project-feature-toggle
v-model="forkingEnabled"
:disabled-input="!repositoryEnabled"
name="project[project_setting_attributes][forking_enabled]"
/>
</project-setting-row>
<project-setting-row
:label="s__('ProjectSettings|Pipelines')"
:help-text="s__('ProjectSettings|Build, test, and deploy your changes')"
......
......@@ -12,11 +12,13 @@ export default {
this.buildsAccessLevel = Math.min(this.buildsAccessLevel, value);
if (value === 0) {
this.forkingEnabled = false;
this.containerRegistryEnabled = false;
this.lfsEnabled = false;
}
} else if (oldValue === 0) {
this.mergeRequestsAccessLevel = value;
this.forkingEnabled = true;
this.buildsAccessLevel = value;
this.containerRegistryEnabled = true;
this.lfsEnabled = true;
......
......@@ -9,6 +9,7 @@ class Projects::ForksController < Projects::ApplicationController
before_action :require_non_empty_project
before_action :authorize_download_code!
before_action :authenticate_user!, only: [:new, :create]
before_action :check_forking_availability, only: [:new, :create]
# rubocop: disable CodeReuse/ActiveRecord
def index
......@@ -61,7 +62,13 @@ class Projects::ForksController < Projects::ApplicationController
end
# rubocop: enable CodeReuse/ActiveRecord
private
def whitelist_query_limiting
Gitlab::QueryLimiting.whitelist('https://gitlab.com/gitlab-org/gitlab-foss/issues/42335')
end
def check_forking_availability
access_denied!(_('Forking is disabled for this project.')) unless @project.forking_enabled
end
end
......@@ -396,6 +396,10 @@ class ProjectsController < Projects::ApplicationController
snippets_access_level
wiki_access_level
pages_access_level
],
project_setting_attributes: %i[
forking_enabled
]
]
end
......
......@@ -563,6 +563,7 @@ module ProjectsHelper
requestAccessEnabled: !!project.request_access_enabled,
issuesAccessLevel: feature.issues_access_level,
repositoryAccessLevel: feature.repository_access_level,
forkingEnabled: project.forking_enabled,
mergeRequestsAccessLevel: feature.merge_requests_access_level,
buildsAccessLevel: feature.builds_access_level,
wikiAccessLevel: feature.wiki_access_level,
......
......@@ -235,6 +235,7 @@ class Project < ApplicationRecord
has_one :import_data, class_name: 'ProjectImportData', inverse_of: :project, autosave: true
has_one :project_feature, inverse_of: :project
has_one :project_setting
has_one :statistics, class_name: 'ProjectStatistics'
has_one :cluster_project, class_name: 'Clusters::Project'
......@@ -304,6 +305,7 @@ class Project < ApplicationRecord
accepts_nested_attributes_for :variables, allow_destroy: true
accepts_nested_attributes_for :project_feature, update_only: true
accepts_nested_attributes_for :project_setting, update_only: true
accepts_nested_attributes_for :import_data
accepts_nested_attributes_for :auto_devops, update_only: true
accepts_nested_attributes_for :ci_cd_settings, update_only: true
......@@ -334,6 +336,7 @@ class Project < ApplicationRecord
delegate :group_runners_enabled, :group_runners_enabled=, :group_runners_enabled?, to: :ci_cd_settings
delegate :root_ancestor, to: :namespace, allow_nil: true
delegate :last_pipeline, to: :commit, allow_nil: true
delegate :forking_enabled, to: :project_setting, allow_nil: true
delegate :external_dashboard_url, to: :metrics_setting, allow_nil: true, prefix: true
delegate :default_git_depth, :default_git_depth=, to: :ci_cd_settings, prefix: :ci
......@@ -664,6 +667,10 @@ class Project < ApplicationRecord
super
end
def project_setting
super || build_project_setting
end
def all_pipelines
if builds_enabled?
super
......
# frozen_string_literal: true
class ProjectSetting < ApplicationRecord
self.primary_key = :project_id
belongs_to :project
validates :forking_enabled, inclusion: { in: [true, false] }
end
......@@ -83,6 +83,9 @@ class ProjectPolicy < BasePolicy
project.merge_requests_allowing_push_to_user(user).any?
end
desc "Project allows forking"
condition(:forking_allowed) { @subject.forking_enabled }
with_scope :global
condition(:mirror_available, score: 0) do
::Gitlab::CurrentSettings.current_application_settings.mirror_available
......@@ -203,7 +206,6 @@ class ProjectPolicy < BasePolicy
enable :download_code
enable :read_statistics
enable :download_wiki_code
enable :fork_project
enable :create_project_snippet
enable :update_issue
enable :reopen_issue
......@@ -232,12 +234,15 @@ class ProjectPolicy < BasePolicy
enable :public_access
enable :guest_access
enable :fork_project
enable :build_download_code
enable :build_read_container_image
enable :request_access
end
rule { (can?(:public_user_access) | can?(:reporter_access)) & forking_allowed }.policy do
enable :fork_project
end
rule { owner | admin | guest | group_member }.prevent :request_access
rule { ~request_access_enabled }.prevent :request_access
......
---
title: Add an option to configure forking restriction
merge_request: 17988
author:
type: added
# frozen_string_literal: true
class CreateProjectSettings < ActiveRecord::Migration[5.0]
DOWNTIME = false
def change
create_table(:project_settings, id: false) do |t|
t.references :project,
primary_key: true,
null: false,
index: { unique: true },
foreign_key: { on_delete: :cascade }
t.boolean :forking_enabled,
default: true,
null: false
t.datetime_with_timezone :updated_at, null: false
end
end
end
......@@ -3253,6 +3253,12 @@ ActiveRecord::Schema.define(version: 2020_01_14_204949) do
t.index ["project_id"], name: "index_project_repository_states_on_project_id", unique: true
end
create_table "project_settings", primary_key: "project_id", id: :serial, force: :cascade do |t|
t.boolean "forking_enabled", default: true, null: false
t.datetime_with_timezone "updated_at", null: false
t.index ["project_id"], name: "index_project_settings_on_project_id", unique: true
end
create_table "project_statistics", id: :serial, force: :cascade do |t|
t.integer "project_id", null: false
t.integer "namespace_id", null: false
......@@ -4776,6 +4782,7 @@ ActiveRecord::Schema.define(version: 2020_01_14_204949) do
add_foreign_key "project_repositories", "projects", on_delete: :cascade
add_foreign_key "project_repositories", "shards", on_delete: :restrict
add_foreign_key "project_repository_states", "projects", on_delete: :cascade
add_foreign_key "project_settings", "projects", on_delete: :cascade
add_foreign_key "project_statistics", "projects", on_delete: :cascade
add_foreign_key "project_tracing_settings", "projects", on_delete: :cascade
add_foreign_key "projects", "pool_repositories", name: "fk_6e5c14658a", on_delete: :nullify
......
......@@ -90,6 +90,7 @@ tree:
- protected_tags:
- :create_access_levels
- :project_feature
- :project_setting
- :custom_attributes
- :prometheus_metrics
- :project_badges
......
......@@ -8285,6 +8285,9 @@ msgstr ""
msgid "Forking in progress"
msgstr ""
msgid "Forking is disabled for this project."
msgstr ""
msgid "Forking repository"
msgstr ""
......@@ -14346,6 +14349,9 @@ msgstr ""
msgid "ProjectSettings|All discussions must be resolved"
msgstr ""
msgid "ProjectSettings|Allow users to make copies of your repository to a new project"
msgstr ""
msgid "ProjectSettings|Allow users to request access"
msgstr ""
......@@ -14406,6 +14412,9 @@ msgstr ""
msgid "ProjectSettings|Fast-forward merges only"
msgstr ""
msgid "ProjectSettings|Forks"
msgstr ""
msgid "ProjectSettings|Git Large File Storage"
msgstr ""
......
......@@ -12,6 +12,22 @@ describe Projects::ForksController do
group.add_owner(user)
end
shared_examples 'forking disabled' do
let(:project) { create(:project, :private, :repository) }
before do
create(:project_setting, { project: project, forking_enabled: false })
project.add_developer(user)
sign_in(user)
end
it 'returns with 403' do
subject
expect(response).to have_gitlab_http_status(403)
end
end
describe 'GET index' do
def get_forks(search: nil)
get :index,
......@@ -138,19 +154,19 @@ describe Projects::ForksController do
end
describe 'GET new' do
def get_new
subject do
get :new,
params: {
namespace_id: project.namespace,
project_id: project
}
params: {
namespace_id: project.namespace,
project_id: project
}
end
context 'when user is signed in' do
it 'responds with status 200' do
sign_in(user)
get_new
subject
expect(response).to have_gitlab_http_status(200)
end
......@@ -160,21 +176,26 @@ describe Projects::ForksController do
it 'redirects to the sign-in page' do
sign_out(user)
get_new
subject
expect(response).to redirect_to(new_user_session_path)
end
end
it_behaves_like 'forking disabled'
end
describe 'POST create' do
def post_create(params = {})
post :create,
params: {
let(:params) do
{
namespace_id: project.namespace,
project_id: project,
namespace_key: user.namespace.id
}.merge(params)
}
end
subject do
post :create, params: params
end
context 'when user is signed in' do
......@@ -183,18 +204,34 @@ describe Projects::ForksController do
end
it 'responds with status 302' do
post_create
subject
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(namespace_project_import_path(user.namespace, project))
end
it 'passes continue params to the redirect' do
continue_params = { to: '/-/ide/project/path', notice: 'message' }
post_create continue: continue_params
context 'continue params' do
let(:params) do
{
namespace_id: project.namespace,
project_id: project,
namespace_key: user.namespace.id,
continue: continue_params
}
end
let(:continue_params) do
{
to: '/-/ide/project/path',
notice: 'message'
}
end
it 'passes continue params to the redirect' do
subject
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(namespace_project_import_path(user.namespace, project, continue: continue_params))
expect(response).to have_gitlab_http_status(302)
expect(response).to redirect_to(namespace_project_import_path(user.namespace, project, continue: continue_params))
end
end
end
......@@ -202,10 +239,12 @@ describe Projects::ForksController do
it 'redirects to the sign-in page' do
sign_out(user)
post_create
subject
expect(response).to redirect_to(new_user_session_path)
end
end
it_behaves_like 'forking disabled'
end
end
# frozen_string_literal: true
FactoryBot.define do
factory :project_setting do
project
end
end
......@@ -186,7 +186,7 @@ describe 'Edit Project Settings' do
click_button "Save changes"
end
expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.is-disabled", count: 2)
expect(find(".sharing-permissions")).to have_selector(".project-feature-toggle.is-disabled", count: 3)
end
it "shows empty features project homepage" do
......
......@@ -27,6 +27,53 @@ describe 'Project fork' do
expect(page).to have_css('a.disabled', text: 'Fork')
end
context 'forking enabled / disabled in project settings' do
let(:project) { create(:project, :private, :repository) }
before do
project.add_developer(user)
create(:project_setting,
{ project: project,
forking_enabled: forking_enabled })
end
context 'forking is enabled' do
let(:forking_enabled) { true }
it 'enables fork button' do
visit project_path(project)
expect(page).to have_css('a', text: 'Fork')
expect(page).not_to have_css('a.disabled', text: 'Fork')
end
it 'renders new project fork page' do
visit new_project_fork_path(project)
expect(page.status_code).to eq(200)
expect(page).to have_text(' Select a namespace to fork the project ')
end
end
context 'forking is disabled' do
let(:forking_enabled) { false }
it 'does not render fork button' do
visit project_path(project)
expect(page).not_to have_css('a', text: 'Fork')
end
it 'does not render new project fork page' do
visit new_project_fork_path(project)
expect(page.status_code).to eq(403)
expect(page).to have_text('Forking is disabled for this project')
end
end
end
it 'forks the project', :sidekiq_might_not_need_inline do
visit project_path(project)
......
......@@ -34,6 +34,26 @@ describe 'Projects settings' do
expect_toggle_state(:expanded)
end
context 'forking enabled', :js do
it 'toggles forking enabled / disabled' do
visit edit_project_path(project)
forking_enabled_input = find('input[name="project[project_setting_attributes][forking_enabled]"]', visible: :hidden)
forking_enabled_button = find('input[name="project[project_setting_attributes][forking_enabled]"] + button')
expect(forking_enabled_input.value).to eq('true')
# disable by clicking toggle
forking_enabled_button.click
page.within('.sharing-permissions') do
find('input[value="Save changes"]').click
end
wait_for_requests
expect(forking_enabled_input.value).to eq('false')
end
end
def expect_toggle_state(state)
is_collapsed = state == :collapsed
......
......@@ -373,6 +373,7 @@ project:
- environments_for_dashboard
- deployments
- project_feature
- project_setting
- auto_devops
- pages_domains
- authorized_users
......
......@@ -552,6 +552,12 @@ ProjectFeature:
- pages_access_level
- created_at
- updated_at
ProjectSetting:
- id
- project_id
- forking_enabled
- created_at
- updated_at
ProtectedBranch::MergeAccessLevel:
- id
- protected_branch_id
......
# frozen_string_literal: true
require 'spec_helper'
describe ProjectSetting do
describe 'relations' do
it { is_expected.to belong_to(:project) }
end
describe 'validations' do
it { is_expected.to validate_inclusion_of(:forking_enabled).in_array([true, false]) }
end
end
......@@ -62,6 +62,7 @@ describe Project do
it { is_expected.to have_one(:external_wiki_service) }
it { is_expected.to have_one(:project_feature) }
it { is_expected.to have_one(:project_repository) }
it { is_expected.to have_one(:project_setting) }
it { is_expected.to have_one(:container_expiration_policy) }
it { is_expected.to have_one(:statistics).class_name('ProjectStatistics') }
it { is_expected.to have_one(:import_data).class_name('ProjectImportData') }
......
......@@ -2858,6 +2858,20 @@ describe API::Projects do
expect(json_response['message']).to eq('401 Unauthorized')
end
end
context 'forking disabled' do
before do
create(:project_setting,
{ project: project, forking_enabled: false })
end
it 'denies project to be forked' do
post api("/projects/#{project.id}/fork", admin)
expect(response).to have_gitlab_http_status(409)
expect(json_response['message']['forked_from_project_id']).to eq(['is forbidden'])
end
end
end
describe 'POST /projects/:id/housekeeping' do
......
......@@ -224,6 +224,19 @@ describe Projects::ForkService do
end
end
end
context 'when forking is disabled' do
before do
create(:project_setting,
{ project: @from_project, forking_enabled: false })
end
it 'fails' do
to_project = fork_project(@from_project, @to_user, namespace: @to_user.namespace)
expect(to_project.errors[:forked_from_project_id]).to eq(['is forbidden'])
end
end
end
describe 'fork to namespace' do
......
......@@ -8,8 +8,11 @@ describe Projects::UpdateService do
let(:user) { create(:user) }
let(:project) do
create(:project, creator: user, namespace: user.namespace)
create(:project, { creator: user,
namespace: user.namespace,
visibility_level: project_visibility_level })
end
let(:project_visibility_level) { Gitlab::VisibilityLevel::PRIVATE }
describe '#execute' do
let(:gitlab_shell) { Gitlab::Shell.new }
......@@ -154,6 +157,40 @@ describe Projects::UpdateService do
end
end
context 'when changing forking enabled' do
subject do
update_project(project, user,
project_setting_attributes: { forking_enabled: forking_enabled } )
end
shared_examples 'valid forking_enabled change' do
it 'succeeds' do
expect(subject).to eq({ status: :success })
end
it 'updates the project' do
expect { subject }.to change(project, :forking_enabled).to(forking_enabled)
end
end
context 'enables forking' do
let(:forking_enabled) { true }
before do
create(:project_setting,
{ project: project, forking_enabled: false })
end
it_behaves_like 'valid forking_enabled change'
end
context 'disables forking' do
let(:forking_enabled) { false }
it_behaves_like 'valid forking_enabled change'
end
end
describe 'when updating project that has forks' do
let(:project) { create(:project, :internal) }
let(:forked_project) { fork_project(project) }
......
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