Merge branch 'git-http-push-check' into 'master'

Stop 'git push' over HTTP early

Before this change we always let users push Git data over HTTP before
deciding whether to accept to push. This was different from pushing
over SSH where we terminate a 'git push' early if we already know the
user is not allowed to push.

This change let Git over HTTP follow the same behavior as Git over
SSH. We also distinguish between HTTP 404 and 403 responses when
denying Git requests, depending on whether the user is allowed to know
the project exists.


See merge request !5639
This commit is contained in:
Robert Speicher 2016-08-08 19:23:31 +00:00
commit 86c081f71f
4 changed files with 34 additions and 29 deletions

View file

@ -20,9 +20,9 @@ class Projects::GitHttpController < Projects::ApplicationController
elsif receive_pack? && receive_pack_allowed? elsif receive_pack? && receive_pack_allowed?
render_ok render_ok
elsif http_blocked? elsif http_blocked?
render_not_allowed render_http_not_allowed
else else
render_not_found render_denied
end end
end end
@ -31,7 +31,7 @@ class Projects::GitHttpController < Projects::ApplicationController
if upload_pack? && upload_pack_allowed? if upload_pack? && upload_pack_allowed?
render_ok render_ok
else else
render_not_found render_denied
end end
end end
@ -40,7 +40,7 @@ class Projects::GitHttpController < Projects::ApplicationController
if receive_pack? && receive_pack_allowed? if receive_pack? && receive_pack_allowed?
render_ok render_ok
else else
render_not_found render_denied
end end
end end
@ -156,8 +156,17 @@ class Projects::GitHttpController < Projects::ApplicationController
render plain: 'Not Found', status: :not_found render plain: 'Not Found', status: :not_found
end end
def render_not_allowed def render_http_not_allowed
render plain: download_access.message, status: :forbidden render plain: access_check.message, status: :forbidden
end
def render_denied
if user && user.can?(:read_project, project)
render plain: 'Access denied', status: :forbidden
else
# Do not leak information about project existence
render_not_found
end
end end
def ci? def ci?
@ -168,22 +177,20 @@ class Projects::GitHttpController < Projects::ApplicationController
return false unless Gitlab.config.gitlab_shell.upload_pack return false unless Gitlab.config.gitlab_shell.upload_pack
if user if user
download_access.allowed? access_check.allowed?
else else
ci? || project.public? ci? || project.public?
end end
end end
def access def access
return @access if defined?(@access) @access ||= Gitlab::GitAccess.new(user, project, 'http')
@access = Gitlab::GitAccess.new(user, project, 'http')
end end
def download_access def access_check
return @download_access if defined?(@download_access) # Use the magic string '_any' to indicate we do not know what the
# changes are. This is also what gitlab-shell does.
@download_access = access.check('git-upload-pack') @access_check ||= access.check(git_command, '_any')
end end
def http_blocked? def http_blocked?
@ -193,8 +200,6 @@ class Projects::GitHttpController < Projects::ApplicationController
def receive_pack_allowed? def receive_pack_allowed?
return false unless Gitlab.config.gitlab_shell.receive_pack return false unless Gitlab.config.gitlab_shell.receive_pack
# Skip user authorization on upload request. access_check.allowed?
# It will be done by the pre-receive hook in the repository.
user.present?
end end
end end

View file

@ -14,7 +14,7 @@ module Gitlab
@user_access = UserAccess.new(user, project: project) @user_access = UserAccess.new(user, project: project)
end end
def check(cmd, changes = nil) def check(cmd, changes)
return build_status_object(false, "Git access over #{protocol.upcase} is not allowed") unless protocol_allowed? return build_status_object(false, "Git access over #{protocol.upcase} is not allowed") unless protocol_allowed?
unless actor unless actor

View file

@ -19,11 +19,11 @@ describe Gitlab::GitAccess, lib: true do
end end
it 'blocks ssh git push' do it 'blocks ssh git push' do
expect(@acc.check('git-receive-pack').allowed?).to be_falsey expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey
end end
it 'blocks ssh git pull' do it 'blocks ssh git pull' do
expect(@acc.check('git-upload-pack').allowed?).to be_falsey expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey
end end
end end
@ -34,17 +34,17 @@ describe Gitlab::GitAccess, lib: true do
end end
it 'blocks http push' do it 'blocks http push' do
expect(@acc.check('git-receive-pack').allowed?).to be_falsey expect(@acc.check('git-receive-pack', '_any').allowed?).to be_falsey
end end
it 'blocks http git pull' do it 'blocks http git pull' do
expect(@acc.check('git-upload-pack').allowed?).to be_falsey expect(@acc.check('git-upload-pack', '_any').allowed?).to be_falsey
end end
end end
end end
describe 'download_access_check' do describe 'download_access_check' do
subject { access.check('git-upload-pack') } subject { access.check('git-upload-pack', '_any') }
describe 'master permissions' do describe 'master permissions' do
before { project.team << [user, :master] } before { project.team << [user, :master] }
@ -288,7 +288,7 @@ describe Gitlab::GitAccess, lib: true do
let(:actor) { key } let(:actor) { key }
context 'push code' do context 'push code' do
subject { access.check('git-receive-pack') } subject { access.check('git-receive-pack', '_any') }
context 'when project is authorized' do context 'when project is authorized' do
before { key.projects << project } before { key.projects << project }

View file

@ -75,9 +75,9 @@ describe 'Git HTTP requests', lib: true do
context "with correct credentials" do context "with correct credentials" do
let(:env) { { user: user.username, password: user.password } } let(:env) { { user: user.username, password: user.password } }
it "uploads get status 200 (because Git hooks do the real check)" do it "uploads get status 403" do
upload(path, env) do |response| upload(path, env) do |response|
expect(response).to have_http_status(200) expect(response).to have_http_status(403)
end end
end end
@ -86,7 +86,7 @@ describe 'Git HTTP requests', lib: true do
allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false) allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
upload(path, env) do |response| upload(path, env) do |response|
expect(response).to have_http_status(404) expect(response).to have_http_status(403)
end end
end end
end end
@ -236,9 +236,9 @@ describe 'Git HTTP requests', lib: true do
end end
end end
it "uploads get status 200 (because Git hooks do the real check)" do it "uploads get status 404" do
upload(path, user: user.username, password: user.password) do |response| upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(200) expect(response).to have_http_status(404)
end end
end end
end end