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)
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 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,25 +2140,18 @@ class Project < ActiveRecord::Base ...@@ -2142,25 +2140,18 @@ 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
Gitlab::SafeRequestStore.fetch("project-#{id}:branch-#{branch_name}:user-#{user.id}:branch_allows_collaboration") do
check_access.call
end end
end end
......
...@@ -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,12 +261,21 @@ module Gitlab ...@@ -256,12 +261,21 @@ 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!
# Deploy keys with write access can push anything
return if deploy_key?
if changes == ANY
can_push = user_access.can_do_action?(:push_code) ||
project.any_branch_allows_collaboration?(user_access.user)
unless can_push
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
end
else
# If there are worktrees with a HEAD pointing to a non-existent object, # 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 # calls to `git rev-list --all` will fail in git 2.15+. This should also
# clear stale lock files. # clear stale lock files.
...@@ -276,13 +290,13 @@ module Gitlab ...@@ -276,13 +290,13 @@ module Gitlab
check_single_change_access(change, skip_lfs_integrity_check: !first_change) check_single_change_access(change, skip_lfs_integrity_check: !first_change)
end end
end end
end
def check_single_change_access(change, skip_lfs_integrity_check: false) def check_single_change_access(change, skip_lfs_integrity_check: false)
change_access = Checks::ChangeAccess.new( change_access = Checks::ChangeAccess.new(
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