GitLab wird am Montag, den 31. Januar, zwischen 08:00 und 12:00 Uhr wegen wichtigen Wartungsarbeiten nicht zur Verfügung stehen.

Commit 5578506e authored by Douwe Maan's avatar Douwe Maan
Browse files

Merge branch 'mk-fix-git-over-http-rejections' into 'master'

Fix Git-over-HTTP rejections

See merge request !11398
parents 059b9627 ef4e9135
......@@ -106,4 +106,8 @@ def storage_project
def objects
@objects ||= (params[:objects] || []).to_a
end
def has_authentication_ability?(capability)
(authentication_abilities || []).include?(capability)
end
end
......@@ -128,32 +128,10 @@ def handle_basic_authentication(login, password)
@authentication_result = Gitlab::Auth.find_for_git_client(
login, password, project: project, ip: request.ip)
return false unless @authentication_result.success?
if download_request?
authentication_has_download_access?
else
authentication_has_upload_access?
end
@authentication_result.success?
end
def ci?
authentication_result.ci?(project)
end
def authentication_has_download_access?
has_authentication_ability?(:download_code) || has_authentication_ability?(:build_download_code)
end
def authentication_has_upload_access?
has_authentication_ability?(:push_code)
end
def has_authentication_ability?(capability)
(authentication_abilities || []).include?(capability)
end
def authentication_project
authentication_result.project
end
end
class Projects::GitHttpController < Projects::GitHttpClientController
include WorkhorseRequest
before_action :access_check
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs
if upload_pack? && upload_pack_allowed?
log_user_activity
render_ok
elsif receive_pack? && receive_pack_allowed?
render_ok
elsif http_blocked?
render_http_not_allowed
else
render_denied
end
log_user_activity if upload_pack?
render_ok
end
# POST /foo/bar.git/git-upload-pack (git pull)
def git_upload_pack
if upload_pack? && upload_pack_allowed?
render_ok
else
render_denied
end
render_ok
end
# POST /foo/bar.git/git-receive-pack" (git push)
def git_receive_pack
if receive_pack? && receive_pack_allowed?
render_ok
else
render_denied
end
render_ok
end
private
......@@ -45,10 +34,6 @@ def upload_pack?
git_command == 'git-upload-pack'
end
def receive_pack?
git_command == 'git-receive-pack'
end
def git_command
if action_name == 'info_refs'
params[:service]
......@@ -62,47 +47,27 @@ def render_ok
render json: Gitlab::Workhorse.git_http_ok(repository, wiki?, user, action_name)
end
def render_http_not_allowed
render plain: access_check.message, status: :forbidden
def render_403(exception)
render plain: exception.message, status: :forbidden
end
def render_denied
if user && can?(user, :read_project, project)
render plain: access_denied_message, status: :forbidden
else
# Do not leak information about project existence
render_not_found
end
end
def access_denied_message
'Access denied'
def render_404(exception)
render plain: exception.message, status: :not_found
end
def upload_pack_allowed?
return false unless Gitlab.config.gitlab_shell.upload_pack
access_check.allowed? || ci?
def access
@access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities)
end
def access
@access ||= access_klass.new(user, project, 'http', authentication_abilities: authentication_abilities)
def access_actor
return user if user
return :ci if ci?
end
def access_check
# Use the magic string '_any' to indicate we do not know what the
# changes are. This is also what gitlab-shell does.
@access_check ||= access.check(git_command, '_any')
end
def http_blocked?
!access.protocol_allowed?
end
def receive_pack_allowed?
return false unless Gitlab.config.gitlab_shell.receive_pack
access_check.allowed?
access.check(git_command, '_any')
end
def access_klass
......
---
title: Fix Git-over-HTTP error statuses and improve error messages
merge_request: 11398
author:
......@@ -42,6 +42,22 @@ def set_project
@project, @wiki = Gitlab::RepoPath.parse(params[:project])
end
end
# Project id to pass between components that don't share/don't have
# access to the same filesystem mounts
def gl_repository
Gitlab::GlRepository.gl_repository(project, wiki?)
end
# Return the repository full path so that gitlab-shell has it when
# handling ssh commands
def repository_path
if wiki?
project.wiki.repository.path_to_repo
else
project.repository.path_to_repo
end
end
end
end
end
......@@ -32,31 +32,23 @@ class Internal < Grape::API
actor.update_last_used_at if actor.is_a?(Key)
access_checker = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
access_status = access_checker
access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
access_checker = access_checker_klass
.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities)
.check(params[:action], params[:changes])
response = { status: access_status.status, message: access_status.message }
if access_status.status
log_user_activity(actor)
# Project id to pass between components that don't share/don't have
# access to the same filesystem mounts
response[:gl_repository] = Gitlab::GlRepository.gl_repository(project, wiki?)
# Return the repository full path so that gitlab-shell has it when
# handling ssh commands
response[:repository_path] =
if wiki?
project.wiki.repository.path_to_repo
else
project.repository.path_to_repo
end
begin
access_checker.check(params[:action], params[:changes])
rescue Gitlab::GitAccess::UnauthorizedError, Gitlab::GitAccess::NotFoundError => e
return { status: false, message: e.message }
end
response
log_user_activity(actor)
{
status: true,
gl_repository: gl_repository,
repository_path: repository_path
}
end
post "/lfs_authenticate" do
......
module Gitlab
module Checks
class ChangeAccess
ERROR_MESSAGES = {
push_code: 'You are not allowed to push code to this project.',
delete_default_branch: 'The default branch of a project cannot be deleted.',
force_push_protected_branch: 'You are not allowed to force push code to a protected branch on this project.',
non_master_delete_protected_branch: 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.',
non_web_delete_protected_branch: 'You can only delete protected branches using the web interface.',
merge_protected_branch: 'You are not allowed to merge code into protected branches on this project.',
push_protected_branch: 'You are not allowed to push code to protected branches on this project.',
change_existing_tags: 'You are not allowed to change existing tags on this project.',
update_protected_tag: 'Protected tags cannot be updated.',
delete_protected_tag: 'Protected tags cannot be deleted.',
create_protected_tag: 'You are not allowed to create this tag as it is protected.'
}.freeze
attr_reader :user_access, :project, :skip_authorization, :protocol
def initialize(
......@@ -17,22 +31,20 @@ def initialize(
end
def exec
return GitAccessStatus.new(true) if skip_authorization
return true if skip_authorization
error = push_checks || branch_checks || tag_checks
push_checks
branch_checks
tag_checks
if error
GitAccessStatus.new(false, error)
else
GitAccessStatus.new(true)
end
true
end
protected
def push_checks
if user_access.cannot_do_action?(:push_code)
"You are not allowed to push code to this project."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_code]
end
end
......@@ -40,7 +52,7 @@ def branch_checks
return unless @branch_name
if deletion? && @branch_name == project.default_branch
return "The default branch of a project cannot be deleted."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_default_branch]
end
protected_branch_checks
......@@ -50,7 +62,7 @@ def protected_branch_checks
return unless ProtectedBranch.protected?(project, @branch_name)
if forced_push?
return "You are not allowed to force push code to a protected branch on this project."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:force_push_protected_branch]
end
if deletion?
......@@ -62,22 +74,22 @@ def protected_branch_checks
def protected_branch_deletion_checks
unless user_access.can_delete_branch?(@branch_name)
return 'You are not allowed to delete protected branches from this project. Only a project master or owner can delete a protected branch.'
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_master_delete_protected_branch]
end
unless protocol == 'web'
'You can only delete protected branches using the web interface.'
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:non_web_delete_protected_branch]
end
end
def protected_branch_push_checks
if matching_merge_request?
unless user_access.can_merge_to_branch?(@branch_name) || user_access.can_push_to_branch?(@branch_name)
"You are not allowed to merge code into protected branches on this project."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:merge_protected_branch]
end
else
unless user_access.can_push_to_branch?(@branch_name)
"You are not allowed to push code to protected branches on this project."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:push_protected_branch]
end
end
end
......@@ -86,7 +98,7 @@ def tag_checks
return unless @tag_name
if tag_exists? && user_access.cannot_do_action?(:admin_project)
return "You are not allowed to change existing tags on this project."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:change_existing_tags]
end
protected_tag_checks
......@@ -95,11 +107,11 @@ def tag_checks
def protected_tag_checks
return unless ProtectedTag.protected?(project, @tag_name)
return "Protected tags cannot be updated." if update?
return "Protected tags cannot be deleted." if deletion?
raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:update_protected_tag]) if update?
raise(GitAccess::UnauthorizedError, ERROR_MESSAGES[:delete_protected_tag]) if deletion?
unless user_access.can_create_tag?(@tag_name)
return "You are not allowed to create this tag as it is protected."
raise GitAccess::UnauthorizedError, ERROR_MESSAGES[:create_protected_tag]
end
end
......
module Gitlab
# For backwards compatibility, generic CI (which is a build without a user) is
# allowed to :build_download_code without any other checks.
class CiAccess
def can_do_action?(action)
action == :build_download_code
end
end
end
......@@ -3,33 +3,39 @@
module Gitlab
class GitAccess
UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError)
ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for this project.',
download: 'You are not allowed to download code from this project.',
deploy_key_upload:
'This deploy key does not have write access to this project.',
no_repo: 'A repository for this project does not exist yet.'
no_repo: 'A repository for this project does not exist yet.',
project_not_found: 'The project you were looking for could not be found.',
account_blocked: 'Your account has been blocked.',
command_not_allowed: "The command you're trying to execute 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.'
}.freeze
DOWNLOAD_COMMANDS = %w{ git-upload-pack git-upload-archive }.freeze
PUSH_COMMANDS = %w{ git-receive-pack }.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
attr_reader :actor, :project, :protocol, :user_access, :authentication_abilities
attr_reader :actor, :project, :protocol, :authentication_abilities
def initialize(actor, project, protocol, authentication_abilities:)
@actor = actor
@project = project
@protocol = protocol
@authentication_abilities = authentication_abilities
@user_access = UserAccess.new(user, project: project)
end
def check(cmd, changes)
check_protocol!
check_active_user!
check_project_accessibility!
check_command_disabled!(cmd)
check_command_existence!(cmd)
check_repository_existence!
......@@ -40,9 +46,7 @@ def check(cmd, changes)
check_push_access!(changes)
end
build_status_object(true)
rescue UnauthorizedError => ex
build_status_object(false, ex.message)
true
end
def guest_can_download_code?
......@@ -73,19 +77,39 @@ def check_active_user!
return if deploy_key?
if user && !user_access.allowed?
raise UnauthorizedError, "Your account has been blocked."
raise UnauthorizedError, ERROR_MESSAGES[:account_blocked]
end
end
def check_project_accessibility!
if project.blank? || !can_read_project?
raise UnauthorizedError, 'The project you were looking for could not be found.'
raise NotFoundError, ERROR_MESSAGES[:project_not_found]
end
end
def check_command_disabled!(cmd)
if upload_pack?(cmd)
check_upload_pack_disabled!
elsif receive_pack?(cmd)
check_receive_pack_disabled!
end
end
def check_upload_pack_disabled!
if http? && upload_pack_disabled_over_http?
raise UnauthorizedError, ERROR_MESSAGES[:upload_pack_disabled_over_http]
end
end
def check_receive_pack_disabled!
if http? && receive_pack_disabled_over_http?
raise UnauthorizedError, ERROR_MESSAGES[:receive_pack_disabled_over_http]
end
end
def check_command_existence!(cmd)
unless ALL_COMMANDS.include?(cmd)
raise UnauthorizedError, "The command you're trying to execute is not allowed."
raise UnauthorizedError, ERROR_MESSAGES[:command_not_allowed]
end
end
......@@ -138,11 +162,9 @@ def check_change_access!(changes)
# Iterate over all changes to find if user allowed all of them to be applied
changes_list.each do |change|
status = check_single_change_access(change)
unless status.allowed?
# If user does not have access to make at least one change - cancel all push
raise UnauthorizedError, status.message
end
# 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)
end
end
......@@ -168,14 +190,40 @@ def deploy_key?
actor.is_a?(DeployKey)
end
def ci?
actor == :ci
end
def can_read_project?
if deploy_key
if deploy_key?
deploy_key.has_access_to?(project)
elsif user
user.can?(:read_project, project)
elsif ci?
true # allow CI (build without a user) for backwards compatibility
end || Guest.can?(:read_project, project)
end
def http?
protocol == 'http'
end
def upload_pack?(command)
command == 'git-upload-pack'
end
def receive_pack?(command)
command == 'git-receive-pack'
end
def upload_pack_disabled_over_http?
!Gitlab.config.gitlab_shell.upload_pack
end
def receive_pack_disabled_over_http?
!Gitlab.config.gitlab_shell.receive_pack
end
protected
def user
......@@ -185,15 +233,19 @@ def user
case actor
when User
actor
when DeployKey
nil
when Key
actor.user
actor.user unless actor.is_a?(DeployKey)
when :ci
nil
end
end
def build_status_object(status, message = '')
Gitlab::GitAccessStatus.new(status, message)
def user_access
@user_access ||= if ci?
CiAccess.new
else
UserAccess.new(user, project: project)
end
end
end
end
module Gitlab
class GitAccessStatus
attr_accessor :status, :message
alias_method :allowed?, :status
def initialize(status, message = '')
@status = status
@message = message
end
def to_json(opts = nil)
{ status: @status, message: @message }.to_json(opts)
end
end
end
module Gitlab
class GitAccessWiki < GitAccess
ERROR_MESSAGES = {
write_to_wiki: "You are not allowed to write to this project's wiki."
}.freeze
def guest_can_download_code?
Guest.can?(:download_wiki_code, project)
end
......@@ -9,11 +13,11 @@ def user_can_download_code?
end
def check_single_change_access(change)
if user_access.can_do_action?(:create_wiki)
build_status_object(true)
else
build_status_object(false, "You are not allowed to write to this project's wiki.")
unless user_access.can_do_action?(:create_wiki)
raise UnauthorizedError, ERROR_MESSAGES[:write_to_wiki]
end
true
end
end
end
......@@ -23,29 +23,27 @@
before { project.add_developer(user) }
context 'without failed checks' do
it "doesn't return any error" do
expect(subject.status).to be(true)
it "doesn't raise an error" do
expect { subject }.not_to raise_error
end
end
context 'when the user is not allowed to push code' do
it 'returns an error' do
it 'raises an error' do
expect(user_access).to receive(:can_do_action?).with(:push_code).and_return(false)
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to push code to this project.')
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to push code to this project.')
end
end
context 'tags check' do
let(:ref) { 'refs/tags/v1.0.0' }
it 'returns an error if the user is not allowed to update tags' do
it 'raises an error if the user is not allowed to update tags' do
allow(user_access).to receive(:can_do_action?).with(:push_code).and_return(true)
expect(user_access).to receive(:can_do_action?).with(:admin_project).and_return(false)
expect(subject.status).to be(false)
expect(subject.message).to eq('You are not allowed to change existing tags on this project.')
expect { subject }.to raise_error(Gitlab::GitAccess::UnauthorizedError, 'You are not allowed to change existing tags on this project.')
end
context 'with protected tag' do
......@@ -59,8 +57,7 @@
let(:newrev) { '0000000000000000000000000000000000000000' }
it 'is prevented' do
expect(subject.status).to be(false)