Commit 626f3d03 authored by Sean McGivern's avatar Sean McGivern

Merge branch 'dm-git-access-any-ce' into 'master'

[CE] Don't run checks for changed refs when specific changes are unknown

See merge request gitlab-org/gitlab-ce!23990
parents d56124b5 e893f560
...@@ -80,9 +80,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -80,9 +80,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end end
def access_check def access_check
# Use the magic string '_any' to indicate we do not know what the access.check(git_command, Gitlab::GitAccess::ANY)
# changes are. This is also what gitlab-shell does.
access.check(git_command, '_any')
@project ||= access.project @project ||= access.project
end end
......
...@@ -1928,23 +1928,15 @@ class Project < ActiveRecord::Base ...@@ -1928,23 +1928,15 @@ class Project < ActiveRecord::Base
.where('project_authorizations.project_id = merge_requests.target_project_id') .where('project_authorizations.project_id = merge_requests.target_project_id')
.limit(1) .limit(1)
.select(1) .select(1)
source_of_merge_requests.opened merge_requests_allowing_collaboration.where('EXISTS (?)', developer_access_exists)
.where(allow_collaboration: true)
.where('EXISTS (?)', developer_access_exists)
end end
def branch_allows_collaboration?(user, branch_name) def any_branch_allows_collaboration?(user)
return false unless user fetch_branch_allows_collaboration(user)
end
cache_key = "user:#{user.id}:#{branch_name}:branch_allows_push"
memoized_results = strong_memoize(:branch_allows_collaboration) do
Hash.new do |result, cache_key|
result[cache_key] = fetch_branch_allows_collaboration?(user, branch_name)
end
end
memoized_results[cache_key] def branch_allows_collaboration?(user, branch_name)
fetch_branch_allows_collaboration(user, branch_name)
end end
def licensed_features def licensed_features
...@@ -2018,6 +2010,12 @@ class Project < ActiveRecord::Base ...@@ -2018,6 +2010,12 @@ class Project < ActiveRecord::Base
private private
def merge_requests_allowing_collaboration(source_branch = nil)
relation = source_of_merge_requests.opened.where(allow_collaboration: true)
relation = relation.where(source_branch: source_branch) if source_branch
relation
end
def create_new_pool_repository def create_new_pool_repository
pool = begin pool = begin
create_pool_repository!(shard: Shard.by_name(repository_storage), source_project: self) create_pool_repository!(shard: Shard.by_name(repository_storage), source_project: self)
...@@ -2142,26 +2140,19 @@ class Project < ActiveRecord::Base ...@@ -2142,26 +2140,19 @@ class Project < ActiveRecord::Base
raise ex raise ex
end end
def fetch_branch_allows_collaboration?(user, branch_name) def fetch_branch_allows_collaboration(user, branch_name = nil)
check_access = -> do return false unless user
next false if empty_repo?
merge_requests = source_of_merge_requests.opened Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
.where(allow_collaboration: true) next false if empty_repo?
# Issue for N+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/49322 # Issue for N+1: https://gitlab.com/gitlab-org/gitlab-ce/issues/49322
Gitlab::GitalyClient.allow_n_plus_1_calls do Gitlab::GitalyClient.allow_n_plus_1_calls do
if branch_name merge_requests_allowing_collaboration(branch_name).any? do |merge_request|
merge_requests.find_by(source_branch: branch_name)&.can_be_merged_by?(user) merge_request.can_be_merged_by?(user)
else
merge_requests.any? { |merge_request| merge_request.can_be_merged_by?(user) }
end end
end end
end end
Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
check_access.call
end
end end
def services_templates def services_templates
......
...@@ -18,12 +18,16 @@ module Gitlab ...@@ -18,12 +18,16 @@ module Gitlab
private private
def creation?
Gitlab::Git.blank_ref?(oldrev)
end
def deletion? def deletion?
Gitlab::Git.blank_ref?(newrev) Gitlab::Git.blank_ref?(newrev)
end end
def update? def update?
!Gitlab::Git.blank_ref?(oldrev) && !deletion? !creation? && !deletion?
end end
def updated_from_web? def updated_from_web?
......
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
attr_reader(*ATTRIBUTES) attr_reader(*ATTRIBUTES)
def initialize( def initialize(
change, user_access:, project:, skip_authorization: false, change, user_access:, project:,
skip_lfs_integrity_check: false, protocol:, logger: skip_lfs_integrity_check: false, protocol:, logger:
) )
@oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref) @oldrev, @newrev, @ref = change.values_at(:oldrev, :newrev, :ref)
...@@ -18,7 +18,6 @@ module Gitlab ...@@ -18,7 +18,6 @@ module Gitlab
@tag_name = Gitlab::Git.tag_name(@ref) @tag_name = Gitlab::Git.tag_name(@ref)
@user_access = user_access @user_access = user_access
@project = project @project = project
@skip_authorization = skip_authorization
@skip_lfs_integrity_check = skip_lfs_integrity_check @skip_lfs_integrity_check = skip_lfs_integrity_check
@protocol = protocol @protocol = protocol
...@@ -27,8 +26,6 @@ module Gitlab ...@@ -27,8 +26,6 @@ module Gitlab
end end
def exec def exec
return true if skip_authorization
ref_level_checks ref_level_checks
# Check of commits should happen as the last step # Check of commits should happen as the last step
# given they're expensive in terms of performance # given they're expensive in terms of performance
......
...@@ -11,7 +11,7 @@ module Gitlab ...@@ -11,7 +11,7 @@ module Gitlab
}.freeze }.freeze
def validate! def validate!
return if deletion? || newrev.nil? return if deletion?
return unless should_run_diff_validations? return unless should_run_diff_validations?
return if commits.empty? return if commits.empty?
......
...@@ -6,7 +6,7 @@ module Gitlab ...@@ -6,7 +6,7 @@ module Gitlab
def validate! def validate!
logger.log_timed("Checking if you are allowed to push...") do logger.log_timed("Checking if you are allowed to push...") do
unless can_push? unless can_push?
raise GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.' raise GitAccess::UnauthorizedError, GitAccess::ERROR_MESSAGES[:push_code]
end end
end end
end end
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
def can_push? def can_push?
user_access.can_do_action?(:push_code) || user_access.can_do_action?(:push_code) ||
user_access.can_push_to_branch?(branch_name) project.branch_allows_collaboration?(user_access.user, branch_name)
end end
end end
end end
......
...@@ -12,6 +12,10 @@ module Gitlab ...@@ -12,6 +12,10 @@ module Gitlab
TimeoutError = Class.new(StandardError) TimeoutError = Class.new(StandardError)
ProjectMovedError = Class.new(NotFoundError) ProjectMovedError = Class.new(NotFoundError)
# Use the magic string '_any' to indicate we do not know what the
# changes are. This is also what gitlab-shell does.
ANY = '_any'
ERROR_MESSAGES = { ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for this project.', upload: 'You are not allowed to upload code for this project.',
download: 'You are not allowed to download code from this project.', download: 'You are not allowed to download code from this project.',
...@@ -24,7 +28,8 @@ module Gitlab ...@@ -24,7 +28,8 @@ module Gitlab
upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.', upload_pack_disabled_over_http: 'Pulling over HTTP is not allowed.',
receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.', receive_pack_disabled_over_http: 'Pushing over HTTP is not allowed.',
read_only: 'The repository is temporarily read-only. Please try again later.', read_only: 'The repository is temporarily read-only. Please try again later.',
cannot_push_to_read_only: "You can't push code to a read-only GitLab instance." cannot_push_to_read_only: "You can't push code to a read-only GitLab instance.",
push_code: 'You are not allowed to push code to this project.'
}.freeze }.freeze
INTERNAL_TIMEOUT = 50.seconds.freeze INTERNAL_TIMEOUT = 50.seconds.freeze
...@@ -199,7 +204,7 @@ module Gitlab ...@@ -199,7 +204,7 @@ module Gitlab
def ensure_project_on_push!(cmd, changes) def ensure_project_on_push!(cmd, changes)
return if project || deploy_key? return if project || deploy_key?
return unless receive_pack?(cmd) && changes == '_any' && authentication_abilities.include?(:push_code) return unless receive_pack?(cmd) && changes == ANY && authentication_abilities.include?(:push_code)
namespace = Namespace.find_by_full_path(namespace_path) namespace = Namespace.find_by_full_path(namespace_path)
...@@ -256,24 +261,34 @@ module Gitlab ...@@ -256,24 +261,34 @@ module Gitlab
raise UnauthorizedError, ERROR_MESSAGES[:upload] raise UnauthorizedError, ERROR_MESSAGES[:upload]
end end
return if changes.blank? # Allow access this is needed for EE.
check_change_access! check_change_access!
end end
def check_change_access! def check_change_access!
# If there are worktrees with a HEAD pointing to a non-existent object, # Deploy keys with write access can push anything
# calls to `git rev-list --all` will fail in git 2.15+. This should also return if deploy_key?
# clear stale lock files.
project.repository.clean_stale_repository_files if changes == ANY
can_push = user_access.can_do_action?(:push_code) ||
# Iterate over all changes to find if user allowed all of them to be applied project.any_branch_allows_collaboration?(user_access.user)
changes_list.each.with_index do |change, index|
first_change = index == 0 unless can_push
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
# If user does not have access to make at least one change, cancel all end
# push by allowing the exception to bubble up else
check_single_change_access(change, skip_lfs_integrity_check: !first_change) # If there are worktrees with a HEAD pointing to a non-existent object,
# calls to `git rev-list --all` will fail in git 2.15+. This should also
# clear stale lock files.
project.repository.clean_stale_repository_files
# Iterate over all changes to find if user allowed all of them to be applied
changes_list.each.with_index do |change, index|
first_change = index == 0
# If user does not have access to make at least one change, cancel all
# push by allowing the exception to bubble up
check_single_change_access(change, skip_lfs_integrity_check: !first_change)
end
end end
end end
...@@ -282,7 +297,6 @@ module Gitlab ...@@ -282,7 +297,6 @@ module Gitlab
change, change,
user_access: user_access, user_access: user_access,
project: project, project: project,
skip_authorization: deploy_key?,
skip_lfs_integrity_check: skip_lfs_integrity_check, skip_lfs_integrity_check: skip_lfs_integrity_check,
protocol: protocol, protocol: protocol,
logger: logger logger: logger
...@@ -348,7 +362,7 @@ module Gitlab ...@@ -348,7 +362,7 @@ module Gitlab
protected protected
def changes_list def changes_list
@changes_list ||= Gitlab::ChangesList.new(changes) @changes_list ||= Gitlab::ChangesList.new(changes == ANY ? [] : changes)
end end
def user def user
......
...@@ -15,7 +15,7 @@ module Gitlab ...@@ -15,7 +15,7 @@ module Gitlab
authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code) authentication_abilities.include?(:download_code) && user_access.can_do_action?(:download_wiki_code)
end end
def check_single_change_access(change, _options = {}) def check_change_access!
unless user_access.can_do_action?(:create_wiki) unless user_access.can_do_action?(:create_wiki)
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki] raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end end
......
...@@ -13,7 +13,7 @@ describe Gitlab::Checks::PushCheck do ...@@ -13,7 +13,7 @@ describe Gitlab::Checks::PushCheck do
context 'when the user is not allowed to push to the repo' do context 'when the user is not allowed to push to the repo' do
it 'raises an error' do it 'raises an error' do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false) expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
expect(user_access).to receive(:can_push_to_branch?).with('master').and_return(false) expect(project).to receive(:branch_allows_collaboration?).with(user_access.user, 'master').and_return(false)
expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.') expect { subject.validate! }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
end end
......
...@@ -14,7 +14,7 @@ describe Gitlab::GitAccess do ...@@ -14,7 +14,7 @@ describe Gitlab::GitAccess do
let(:authentication_abilities) { %i[read_project download_code push_code] } let(:authentication_abilities) { %i[read_project download_code push_code] }
let(:redirected_path) { nil } let(:redirected_path) { nil }
let(:auth_result_type) { nil } let(:auth_result_type) { nil }
let(:changes) { '_any' } let(:changes) { Gitlab::GitAccess::ANY }
let(:push_access_check) { access.check('git-receive-pack', changes) } let(:push_access_check) { access.check('git-receive-pack', changes) }
let(:pull_access_check) { access.check('git-upload-pack', changes) } let(:pull_access_check) { access.check('git-upload-pack', changes) }
...@@ -437,7 +437,7 @@ describe Gitlab::GitAccess do ...@@ -437,7 +437,7 @@ describe Gitlab::GitAccess do
let(:project) { nil } let(:project) { nil }
context 'when changes is _any' do context 'when changes is _any' do
let(:changes) { '_any' } let(:changes) { Gitlab::GitAccess::ANY }
context 'when authentication abilities include push code' do context 'when authentication abilities include push code' do
let(:authentication_abilities) { [:push_code] } let(:authentication_abilities) { [:push_code] }
...@@ -483,7 +483,7 @@ describe Gitlab::GitAccess do ...@@ -483,7 +483,7 @@ describe Gitlab::GitAccess do
end end
context 'when project exists' do context 'when project exists' do
let(:changes) { '_any' } let(:changes) { Gitlab::GitAccess::ANY }
let!(:project) { create(:project) } let!(:project) { create(:project) }
it 'does not create a new project' do it 'does not create a new project' do
...@@ -497,7 +497,7 @@ describe Gitlab::GitAccess do ...@@ -497,7 +497,7 @@ describe Gitlab::GitAccess do
let(:project_path) { "nonexistent" } let(:project_path) { "nonexistent" }
let(:project) { nil } let(:project) { nil }
let(:namespace_path) { user.namespace.path } let(:namespace_path) { user.namespace.path }
let(:changes) { '_any' } let(:changes) { Gitlab::GitAccess::ANY }
it 'does not create a new project' do it 'does not create a new project' do
expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count } expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
...@@ -507,7 +507,7 @@ describe Gitlab::GitAccess do ...@@ -507,7 +507,7 @@ describe Gitlab::GitAccess do
context 'when pull' do context 'when pull' do
let(:cmd) { 'git-upload-pack' } let(:cmd) { 'git-upload-pack' }
let(:changes) { '_any' } let(:changes) { Gitlab::GitAccess::ANY }
context 'when project does not exist' do context 'when project does not exist' do
let(:project_path) { "new-project" } let(:project_path) { "new-project" }
...@@ -736,7 +736,8 @@ describe Gitlab::GitAccess do ...@@ -736,7 +736,8 @@ describe Gitlab::GitAccess do
end end
let(:changes) do let(:changes) do
{ push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow", { any: Gitlab::GitAccess::ANY,
push_new_branch: "#{Gitlab::Git::BLANK_SHA} 570e7b2ab refs/heads/wow",
push_master: '6f6d7e7ed 570e7b2ab refs/heads/master', push_master: '6f6d7e7ed 570e7b2ab refs/heads/master',
push_protected_branch: '6f6d7e7ed 570e7b2ab refs/heads/feature', push_protected_branch: '6f6d7e7ed 570e7b2ab refs/heads/feature',
push_remove_protected_branch: "570e7b2ab #{Gitlab::Git::BLANK_SHA} "\ push_remove_protected_branch: "570e7b2ab #{Gitlab::Git::BLANK_SHA} "\
...@@ -798,6 +799,7 @@ describe Gitlab::GitAccess do ...@@ -798,6 +799,7 @@ describe Gitlab::GitAccess do
permissions_matrix = { permissions_matrix = {
admin: { admin: {
any: true,
push_new_branch: true, push_new_branch: true,
push_master: true, push_master: true,
push_protected_branch: true, push_protected_branch: true,
...@@ -809,6 +811,7 @@ describe Gitlab::GitAccess do ...@@ -809,6 +811,7 @@ describe Gitlab::GitAccess do
}, },
maintainer: { maintainer: {
any: true,
push_new_branch: true, push_new_branch: true,
push_master: true, push_master: true,
push_protected_branch: true, push_protected_branch: true,
...@@ -820,6 +823,7 @@ describe Gitlab::GitAccess do ...@@ -820,6 +823,7 @@ describe Gitlab::GitAccess do
}, },
developer: { developer: {
any: true,
push_new_branch: true, push_new_branch: true,
push_master: true, push_master: true,
push_protected_branch: false, push_protected_branch: false,
...@@ -831,6 +835,7 @@ describe Gitlab::GitAccess do ...@@ -831,6 +835,7 @@ describe Gitlab::GitAccess do
}, },
reporter: { reporter: {
any: false,
push_new_branch: false, push_new_branch: false,
push_master: false, push_master: false,
push_protected_branch: false, push_protected_branch: false,
...@@ -842,6 +847,7 @@ describe Gitlab::GitAccess do ...@@ -842,6 +847,7 @@ describe Gitlab::GitAccess do
}, },
guest: { guest: {
any: false,
push_new_branch: false, push_new_branch: false,
push_master: false, push_master: false,
push_protected_branch: false, push_protected_branch: false,
......
...@@ -38,7 +38,7 @@ describe Gitlab::GitAccessWiki do ...@@ -38,7 +38,7 @@ describe Gitlab::GitAccessWiki do
end end
describe '#access_check_download!' do describe '#access_check_download!' do
subject { access.check('git-upload-pack', '_any') } subject { access.check('git-upload-pack', Gitlab::GitAccess::ANY) }
before do before do
project.add_developer(user) project.add_developer(user)
......
...@@ -3831,6 +3831,16 @@ describe Project do ...@@ -3831,6 +3831,16 @@ describe Project do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:target_project) { create(:project, :repository) } let(:target_project) { create(:project, :repository) }
let(:project) { fork_project(target_project, nil, repository: true) } let(:project) { fork_project(target_project, nil, repository: true) }
let!(:local_merge_request) do
create(
:merge_request,
target_project: project,
target_branch: 'target-branch',
source_project: project,
source_branch: 'awesome-feature-1',
allow_collaboration: true
)
end
let!(:merge_request) do let!(:merge_request) do
create( create(
:merge_request, :merge_request,
...@@ -3875,14 +3885,23 @@ describe Project do ...@@ -3875,14 +3885,23 @@ describe Project do
end end
end end
describe '#branch_allows_collaboration_push?' do describe '#any_branch_allows_collaboration?' do
it 'allows access if the user can merge the merge request' do it 'allows access when there are merge requests open allowing collaboration' do
expect(project.branch_allows_collaboration?(user, 'awesome-feature-1')) expect(project.any_branch_allows_collaboration?(user))
.to be_truthy .to be_truthy
end end
it 'allows access when there are merge requests open but no branch name is given' do it 'does not allow access when there are no merge requests open allowing collaboration' do
expect(project.branch_allows_collaboration?(user, nil)) merge_request.close!
expect(project.any_branch_allows_collaboration?(user))
.to be_falsey
end
end
describe '#branch_allows_collaboration?' do
it 'allows access if the user can merge the merge request' do
expect(project.branch_allows_collaboration?(user, 'awesome-feature-1'))
.to be_truthy .to be_truthy
end end
...@@ -3913,13 +3932,6 @@ describe Project do ...@@ -3913,13 +3932,6 @@ describe Project do
.to be_falsy .to be_falsy
end end
it 'caches the result' do
control = ActiveRecord::QueryRecorder.new { project.branch_allows_collaboration?(user, 'awesome-feature-1') }
expect { 3.times { project.branch_allows_collaboration?(user, 'awesome-feature-1') } }
.not_to exceed_query_limit(control)
end
context 'when the requeststore is active', :request_store do context 'when the requeststore is active', :request_store do
it 'only queries per project across instances' do it 'only queries per project across instances' do
control = ActiveRecord::QueryRecorder.new { project.branch_allows_collaboration?(user, 'awesome-feature-1') } control = ActiveRecord::QueryRecorder.new { project.branch_allows_collaboration?(user, 'awesome-feature-1') }
......
...@@ -144,7 +144,7 @@ describe PipelineSerializer do ...@@ -144,7 +144,7 @@ describe PipelineSerializer do
# pipeline. With the same ref this check is cached but if refs are # pipeline. With the same ref this check is cached but if refs are
# different then there is an extra query per ref # different then there is an extra query per ref
# https://gitlab.com/gitlab-org/gitlab-ce/issues/46368 # https://gitlab.com/gitlab-org/gitlab-ce/issues/46368
expect(recorded.count).to be_within(2).of(34) expect(recorded.count).to be_within(2).of(38)
expect(recorded.cached_count).to eq(0) expect(recorded.cached_count).to eq(0)
end end
end end
......
...@@ -285,7 +285,7 @@ describe Ci::RetryPipelineService, '#execute' do ...@@ -285,7 +285,7 @@ describe Ci::RetryPipelineService, '#execute' do
end end
it 'allows to retry failed pipeline' do it 'allows to retry failed pipeline' do
allow_any_instance_of(Project).to receive(:fetch_branch_allows_collaboration?).and_return(true) allow_any_instance_of(Project).to receive(:branch_allows_collaboration?).and_return(true)
allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false) allow_any_instance_of(Project).to receive(:empty_repo?).and_return(false)
service.execute(pipeline) service.execute(pipeline)
......
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