gitlab-org--gitlab-foss/spec/requests/git_http_spec.rb
Jacob Vosmaer b8f754dd0a 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.
2016-08-03 14:54:12 +02:00

398 lines
13 KiB
Ruby

require "spec_helper"
describe 'Git HTTP requests', lib: true do
let(:user) { create(:user) }
let(:project) { create(:project, path: 'project.git-project') }
it "gives WWW-Authenticate hints" do
clone_get('doesnt/exist.git')
expect(response.header['WWW-Authenticate']).to start_with('Basic ')
end
context "when the project doesn't exist" do
context "when no authentication is provided" do
it "responds with status 401 (no project existence information leak)" do
download('doesnt/exist.git') do |response|
expect(response).to have_http_status(401)
end
end
end
context "when username and password are provided" do
context "when authentication fails" do
it "responds with status 401" do
download('doesnt/exist.git', user: user.username, password: "nope") do |response|
expect(response).to have_http_status(401)
end
end
end
context "when authentication succeeds" do
it "responds with status 404" do
download('/doesnt/exist.git', user: user.username, password: user.password) do |response|
expect(response).to have_http_status(404)
end
end
end
end
end
context "when the Wiki for a project exists" do
it "responds with the right project" do
wiki = ProjectWiki.new(project)
project.update_attribute(:visibility_level, Project::PUBLIC)
download("/#{wiki.repository.path_with_namespace}.git") do |response|
json_body = ActiveSupport::JSON.decode(response.body)
expect(response).to have_http_status(200)
expect(json_body['RepoPath']).to include(wiki.repository.path_with_namespace)
end
end
end
context "when the project exists" do
let(:path) { "#{project.path_with_namespace}.git" }
context "when the project is public" do
before do
project.update_attribute(:visibility_level, Project::PUBLIC)
end
it "downloads get status 200" do
download(path, {}) do |response|
expect(response).to have_http_status(200)
end
end
it "uploads get status 401" do
upload(path, {}) do |response|
expect(response).to have_http_status(401)
end
end
context "with correct credentials" do
let(:env) { { user: user.username, password: user.password } }
it "uploads get status 403" do
upload(path, env) do |response|
expect(response).to have_http_status(403)
end
end
context 'but git-receive-pack is disabled' do
it "responds with status 404" do
allow(Gitlab.config.gitlab_shell).to receive(:receive_pack).and_return(false)
upload(path, env) do |response|
expect(response).to have_http_status(403)
end
end
end
end
context 'but git-upload-pack is disabled' do
it "responds with status 404" do
allow(Gitlab.config.gitlab_shell).to receive(:upload_pack).and_return(false)
download(path, {}) do |response|
expect(response).to have_http_status(404)
end
end
end
end
context "when the project is private" do
before do
project.update_attribute(:visibility_level, Project::PRIVATE)
end
context "when no authentication is provided" do
it "responds with status 401 to downloads" do
download(path, {}) do |response|
expect(response).to have_http_status(401)
end
end
it "responds with status 401 to uploads" do
upload(path, {}) do |response|
expect(response).to have_http_status(401)
end
end
end
context "when username and password are provided" do
let(:env) { { user: user.username, password: 'nope' } }
context "when authentication fails" do
it "responds with status 401" do
download(path, env) do |response|
expect(response).to have_http_status(401)
end
end
context "when the user is IP banned" do
it "responds with status 401" do
expect(Rack::Attack::Allow2Ban).to receive(:filter).and_return(true)
allow_any_instance_of(Rack::Request).to receive(:ip).and_return('1.2.3.4')
clone_get(path, env)
expect(response).to have_http_status(401)
end
end
end
context "when authentication succeeds" do
let(:env) { { user: user.username, password: user.password } }
context "when the user has access to the project" do
before do
project.team << [user, :master]
end
context "when the user is blocked" do
it "responds with status 404" do
user.block
project.team << [user, :master]
download(path, env) do |response|
expect(response).to have_http_status(404)
end
end
end
context "when the user isn't blocked" do
it "downloads get status 200" do
expect(Rack::Attack::Allow2Ban).to receive(:reset)
clone_get(path, env)
expect(response).to have_http_status(200)
end
it "uploads get status 200" do
upload(path, env) do |response|
expect(response).to have_http_status(200)
end
end
end
context "when an oauth token is provided" do
before do
application = Doorkeeper::Application.create!(name: "MyApp", redirect_uri: "https://app.com", owner: user)
@token = Doorkeeper::AccessToken.create!(application_id: application.id, resource_owner_id: user.id)
end
it "downloads get status 200" do
clone_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
expect(response).to have_http_status(200)
end
it "uploads get status 401 (no project existence information leak)" do
push_get "#{project.path_with_namespace}.git", user: 'oauth2', password: @token.token
expect(response).to have_http_status(401)
end
end
context "when blank password attempts follow a valid login" do
def attempt_login(include_password)
password = include_password ? user.password : ""
clone_get path, user: user.username, password: password
response.status
end
it "repeated attempts followed by successful attempt" do
options = Gitlab.config.rack_attack.git_basic_auth
maxretry = options[:maxretry] - 1
ip = '1.2.3.4'
allow_any_instance_of(Rack::Request).to receive(:ip).and_return(ip)
Rack::Attack::Allow2Ban.reset(ip, options)
maxretry.times.each do
expect(attempt_login(false)).to eq(401)
end
expect(attempt_login(true)).to eq(200)
expect(Rack::Attack::Allow2Ban.banned?(ip)).to be_falsey
maxretry.times.each do
expect(attempt_login(false)).to eq(401)
end
Rack::Attack::Allow2Ban.reset(ip, options)
end
end
end
context "when the user doesn't have access to the project" do
it "downloads get status 404" do
download(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(404)
end
end
it "uploads get status 404" do
upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_http_status(404)
end
end
end
end
end
context "when a gitlab ci token is provided" do
let(:token) { 123 }
let(:project) { FactoryGirl.create :empty_project }
before do
project.update_attributes(runners_token: token, builds_enabled: true)
end
it "downloads get status 200" do
clone_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
expect(response).to have_http_status(200)
end
it "uploads get status 401 (no project existence information leak)" do
push_get "#{project.path_with_namespace}.git", user: 'gitlab-ci-token', password: token
expect(response).to have_http_status(401)
end
end
end
end
context "when the project path doesn't end in .git" do
context "GET info/refs" do
let(:path) { "/#{project.path_with_namespace}/info/refs" }
context "when no params are added" do
before { get path }
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs")
end
end
context "when the upload-pack service is requested" do
let(:params) { { service: 'git-upload-pack' } }
before { get path, params }
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
end
end
context "when the receive-pack service is requested" do
let(:params) { { service: 'git-receive-pack' } }
before { get path, params }
it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project.path_with_namespace}.git/info/refs?service=#{params[:service]}")
end
end
context "when the params are anything else" do
let(:params) { { service: 'git-implode-pack' } }
before { get path, params }
it "redirects to the sign-in page" do
expect(response).to redirect_to(new_user_session_path)
end
end
end
context "POST git-upload-pack" do
it "fails to find a route" do
expect { clone_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
end
end
context "POST git-receive-pack" do
it "failes to find a route" do
expect { push_post(project.path_with_namespace) }.to raise_error(ActionController::RoutingError)
end
end
end
context "retrieving an info/refs file" do
before { project.update_attribute(:visibility_level, Project::PUBLIC) }
context "when the file exists" do
before do
# Provide a dummy file in its place
allow_any_instance_of(Repository).to receive(:blob_at).and_call_original
allow_any_instance_of(Repository).to receive(:blob_at).with('5937ac0a7beb003549fc5fd26fc247adbce4a52e', 'info/refs') do
Gitlab::Git::Blob.find(project.repository, 'master', '.gitignore')
end
get "/#{project.path_with_namespace}/blob/master/info/refs"
end
it "returns the file" do
expect(response).to have_http_status(200)
end
end
context "when the file does not exist" do
before { get "/#{project.path_with_namespace}/blob/master/info/refs" }
it "returns not found" do
expect(response).to have_http_status(404)
end
end
end
def clone_get(project, options={})
get "/#{project}/info/refs", { service: 'git-upload-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
def clone_post(project, options={})
post "/#{project}/git-upload-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
def push_get(project, options={})
get "/#{project}/info/refs", { service: 'git-receive-pack' }, auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
def push_post(project, options={})
post "/#{project}/git-receive-pack", {}, auth_env(*options.values_at(:user, :password, :spnego_request_token))
end
def download(project, user: nil, password: nil, spnego_request_token: nil)
args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
clone_get(*args)
yield response
clone_post(*args)
yield response
end
def upload(project, user: nil, password: nil, spnego_request_token: nil)
args = [project, { user: user, password: password, spnego_request_token: spnego_request_token }]
push_get(*args)
yield response
push_post(*args)
yield response
end
def auth_env(user, password, spnego_request_token)
env = {}
if user && password
env['HTTP_AUTHORIZATION'] = ActionController::HttpAuthentication::Basic.encode_credentials(user, password)
elsif spnego_request_token
env['HTTP_AUTHORIZATION'] = "Negotiate #{::Base64.strict_encode64('opaque_request_token')}"
end
env
end
end