require 'spec_helper' describe Gitlab::Lfs::Router, lib: true do let(:project) { create(:project) } let(:public_project) { create(:project, :public) } let(:forked_project) { fork_project(public_project, user) } let(:user) { create(:user) } let(:user_two) { create(:user) } let!(:lfs_object) { create(:lfs_object, :with_file) } let(:request) { Rack::Request.new(env) } let(:env) do { 'rack.input' => '', 'REQUEST_METHOD' => 'GET', } end let(:lfs_router_auth) { new_lfs_router(project, user: user) } let(:lfs_router_ci_auth) { new_lfs_router(project, ci: true) } let(:lfs_router_noauth) { new_lfs_router(project) } let(:lfs_router_public_auth) { new_lfs_router(public_project, user: user) } let(:lfs_router_public_ci_auth) { new_lfs_router(public_project, ci: true) } let(:lfs_router_public_noauth) { new_lfs_router(public_project) } let(:lfs_router_forked_noauth) { new_lfs_router(forked_project) } let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user: user_two) } let(:lfs_router_forked_ci_auth) { new_lfs_router(forked_project, ci: true) } let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" } let(:sample_size) { 499013 } let(:respond_with_deprecated) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} let(:respond_with_disabled) {[ 501, { "Content-Type" => "application/json; charset=utf-8" }, ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]]} describe 'when lfs is disabled' do before do allow(Gitlab.config.lfs).to receive(:enabled).and_return(false) env['REQUEST_METHOD'] = 'POST' body = { 'objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', 'size' => 1575078 }, { 'oid' => sample_oid, 'size' => sample_size } ], 'operation' => 'upload' }.to_json env['rack.input'] = StringIO.new(body) env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch" end it 'responds with 501' do expect(lfs_router_auth.try_call).to match_array(respond_with_disabled) end end describe 'when fetching lfs object using deprecated API' do before do enable_lfs env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}" end it 'responds with 501' do expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated) end end describe 'when fetching lfs object' do before do enable_lfs env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8" env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}" end describe 'and request comes from gitlab-workhorse' do context 'without user being authorized' do it "responds with status 401" do expect(lfs_router_noauth.try_call.first).to eq(401) end end context 'with required headers' do before do project.lfs_objects << lfs_object env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile" end context 'when user does not have project access' do it "responds with status 403" do expect(lfs_router_auth.try_call.first).to eq(403) end end context 'when user has project access' do before do project.team << [user, :master] end it "responds with status 200" do expect(lfs_router_auth.try_call.first).to eq(200) end it "responds with the file location" do expect(lfs_router_auth.try_call[1]['Content-Type']).to eq("application/octet-stream") expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path) end end context 'when CI is authorized' do it "responds with status 200" do expect(lfs_router_ci_auth.try_call.first).to eq(200) end it "responds with the file location" do expect(lfs_router_ci_auth.try_call[1]['Content-Type']).to eq("application/octet-stream") expect(lfs_router_ci_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path) end end end context 'without required headers' do it "responds with status 403" do expect(lfs_router_auth.try_call.first).to eq(403) end end end end describe 'when handling lfs request using deprecated API' do before do enable_lfs env['REQUEST_METHOD'] = 'POST' env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects" end it 'responds with 501' do expect(lfs_router_auth.try_call).to match_array(respond_with_deprecated) end end describe 'when handling lfs batch request' do before do enable_lfs env['REQUEST_METHOD'] = 'POST' env['PATH_INFO'] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch" end describe 'download' do before do body = { 'operation' => 'download', 'objects' => [ { 'oid' => sample_oid, 'size' => sample_size }] }.to_json env['rack.input'] = StringIO.new(body) end shared_examples 'an authorized requests' do context 'when downloading an lfs object that is assigned to our project' do before do project.lfs_objects << lfs_object end it 'responds with status 200 and href to download' do response = router.try_call expect(response.first).to eq(200) response_body = ActiveSupport::JSON.decode(response.last.first) expect(response_body).to eq('objects' => [ { 'oid' => sample_oid, 'size' => sample_size, 'actions' => { 'download' => { 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", 'header' => { 'Authorization' => auth } } } }]) end end context 'when downloading an lfs object that is assigned to other project' do before do public_project.lfs_objects << lfs_object end it 'responds with status 200 and error message' do response = router.try_call expect(response.first).to eq(200) response_body = ActiveSupport::JSON.decode(response.last.first) expect(response_body).to eq('objects' => [ { 'oid' => sample_oid, 'size' => sample_size, 'error' => { 'code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it", } }]) end end context 'when downloading a lfs object that does not exist' do before do body = { 'operation' => 'download', 'objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', 'size' => 1575078 }] }.to_json env['rack.input'] = StringIO.new(body) end it "responds with status 200 and error message" do response = router.try_call expect(response.first).to eq(200) response_body = ActiveSupport::JSON.decode(response.last.first) expect(response_body).to eq('objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', 'size' => 1575078, 'error' => { 'code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it", } }]) end end context 'when downloading one new and one existing lfs object' do before do body = { 'operation' => 'download', 'objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', 'size' => 1575078 }, { 'oid' => sample_oid, 'size' => sample_size } ] }.to_json env['rack.input'] = StringIO.new(body) project.lfs_objects << lfs_object end it "responds with status 200 with upload hypermedia link for the new object" do response = router.try_call expect(response.first).to eq(200) response_body = ActiveSupport::JSON.decode(response.last.first) expect(response_body).to eq('objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', 'size' => 1575078, 'error' => { 'code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it", } }, { 'oid' => sample_oid, 'size' => sample_size, 'actions' => { 'download' => { 'href' => "#{project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", 'header' => { 'Authorization' => auth } } } }]) end end end context 'when user is authenticated' do let(:auth) { authorize(user) } before do env["HTTP_AUTHORIZATION"] = auth project.team << [user, role] end it_behaves_like 'an authorized requests' do let(:role) { :reporter } let(:router) { lfs_router_auth } end context 'when user does is not member of the project' do let(:role) { :guest } it 'responds with 403' do expect(lfs_router_auth.try_call.first).to eq(403) end end context 'when user does not have download access' do let(:role) { :guest } it 'responds with 403' do expect(lfs_router_auth.try_call.first).to eq(403) end end end context 'when CI is authorized' do let(:auth) { 'gitlab-ci-token:password' } before do env["HTTP_AUTHORIZATION"] = auth end it_behaves_like 'an authorized requests' do let(:router) { lfs_router_ci_auth } end end context 'when user is not authenticated' do describe 'is accessing public project' do before do public_project.lfs_objects << lfs_object end it 'responds with status 200 and href to download' do response = lfs_router_public_noauth.try_call expect(response.first).to eq(200) response_body = ActiveSupport::JSON.decode(response.last.first) expect(response_body).to eq('objects' => [ { 'oid' => sample_oid, 'size' => sample_size, 'actions' => { 'download' => { 'href' => "#{public_project.http_url_to_repo}/gitlab-lfs/objects/#{sample_oid}", 'header' => {} } } }]) end end describe 'is accessing non-public project' do before do project.lfs_objects << lfs_object end it 'responds with authorization required' do expect(lfs_router_noauth.try_call.first).to eq(401) end end end end describe 'upload' do before do body = { 'operation' => 'upload', 'objects' => [ { 'oid' => sample_oid, 'size' => sample_size }] }.to_json env['rack.input'] = StringIO.new(body) end describe 'when request is authenticated' do describe 'when user has project push access' do before do @auth = authorize(user) env["HTTP_AUTHORIZATION"] = @auth project.team << [user, :developer] end context 'when pushing an lfs object that already exists' do before do public_project.lfs_objects << lfs_object end it "responds with status 200 and links the object to the project" do response_body = lfs_router_auth.try_call.last response = ActiveSupport::JSON.decode(response_body.first) expect(response['objects']).to be_kind_of(Array) expect(response['objects'].first['oid']).to eq(sample_oid) expect(response['objects'].first['size']).to eq(sample_size) expect(lfs_object.projects.pluck(:id)).not_to include(project.id) expect(lfs_object.projects.pluck(:id)).to include(public_project.id) expect(response['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}") expect(response['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth) end end context 'when pushing a lfs object that does not exist' do before do body = { 'operation' => 'upload', 'objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', 'size' => 1575078 }] }.to_json env['rack.input'] = StringIO.new(body) end it "responds with status 200 and upload hypermedia link" do response = lfs_router_auth.try_call expect(response.first).to eq(200) response_body = ActiveSupport::JSON.decode(response.last.first) expect(response_body['objects']).to be_kind_of(Array) expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") expect(response_body['objects'].first['size']).to eq(1575078) expect(lfs_object.projects.pluck(:id)).not_to include(project.id) expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") expect(response_body['objects'].first['actions']['upload']['header']).to eq('Authorization' => @auth) end end context 'when pushing one new and one existing lfs object' do before do body = { 'operation' => 'upload', 'objects' => [ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897', 'size' => 1575078 }, { 'oid' => sample_oid, 'size' => sample_size } ] }.to_json env['rack.input'] = StringIO.new(body) project.lfs_objects << lfs_object end it "responds with status 200 with upload hypermedia link for the new object" do response = lfs_router_auth.try_call expect(response.first).to eq(200) response_body = ActiveSupport::JSON.decode(response.last.first) expect(response_body['objects']).to be_kind_of(Array) expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897") expect(response_body['objects'].first['size']).to eq(1575078) expect(response_body['objects'].first['actions']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078") expect(response_body['objects'].first['actions']['upload']['header']).to eq("Authorization" => @auth) expect(response_body['objects'].last['oid']).to eq(sample_oid) expect(response_body['objects'].last['size']).to eq(sample_size) expect(response_body['objects'].last).not_to have_key('actions') end end end context 'when user does not have push access' do it 'responds with 403' do expect(lfs_router_auth.try_call.first).to eq(403) end end context 'when CI is authorized' do it 'responds with 401' do expect(lfs_router_ci_auth.try_call.first).to eq(401) end end end context 'when user is not authenticated' do context 'when user has push access' do before do project.team << [user, :master] end it "responds with status 401" do expect(lfs_router_public_noauth.try_call.first).to eq(401) end end context 'when user does not have push access' do it "responds with status 401" do expect(lfs_router_public_noauth.try_call.first).to eq(401) end end end context 'when CI is authorized' do let(:auth) { 'gitlab-ci-token:password' } before do env["HTTP_AUTHORIZATION"] = auth end it "responds with status 403" do expect(lfs_router_public_ci_auth.try_call.first).to eq(401) end end end describe 'unsupported' do before do body = { 'operation' => 'other', 'objects' => [ { 'oid' => sample_oid, 'size' => sample_size }] }.to_json env['rack.input'] = StringIO.new(body) end it 'responds with status 404' do expect(lfs_router_public_noauth.try_call.first).to eq(404) end end end describe 'when pushing a lfs object' do before do enable_lfs env['REQUEST_METHOD'] = 'PUT' end shared_examples 'unauthorized' do context 'and request is sent by gitlab-workhorse to authorize the request' do before do header_for_upload_authorize(router.project) end it 'responds with status 401' do expect(router.try_call.first).to eq(401) end end context 'and request is sent by gitlab-workhorse to finalize the upload' do before do headers_for_upload_finalize(router.project) end it 'responds with status 401' do expect(router.try_call.first).to eq(401) end end context 'and request is sent with a malformed headers' do before do env["PATH_INFO"] = "#{router.project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}" env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd" end it 'does not recognize it as a valid lfs command' do expect(router.try_call).to eq(nil) end end end shared_examples 'forbidden' do context 'and request is sent by gitlab-workhorse to authorize the request' do before do header_for_upload_authorize(router.project) end it 'responds with 403' do expect(router.try_call.first).to eq(403) end end context 'and request is sent by gitlab-workhorse to finalize the upload' do before do headers_for_upload_finalize(router.project) end it 'responds with 403' do expect(router.try_call.first).to eq(403) end end end describe 'to one project' do describe 'when user is authenticated' do describe 'when user has push access to the project' do before do project.team << [user, :developer] end context 'and request is sent by gitlab-workhorse to authorize the request' do before do header_for_upload_authorize(project) end it 'responds with status 200, location of lfs store and object details' do json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first) expect(lfs_router_auth.try_call.first).to eq(200) expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload") expect(json_response['LfsOid']).to eq(sample_oid) expect(json_response['LfsSize']).to eq(sample_size) end end context 'and request is sent by gitlab-workhorse to finalize the upload' do before do headers_for_upload_finalize(project) end it 'responds with status 200 and lfs object is linked to the project' do expect(lfs_router_auth.try_call.first).to eq(200) expect(lfs_object.projects.pluck(:id)).to include(project.id) end end end describe 'and user does not have push access' do let(:router) { lfs_router_auth } it_behaves_like 'forbidden' end end context 'when CI is authenticated' do let(:router) { lfs_router_ci_auth } it_behaves_like 'unauthorized' end context 'for unauthenticated' do let(:router) { new_lfs_router(project) } it_behaves_like 'unauthorized' end end describe 'to a forked project' do let(:forked_project) { fork_project(public_project, user) } describe 'when user is authenticated' do describe 'when user has push access to the project' do before do forked_project.team << [user_two, :developer] end context 'and request is sent by gitlab-workhorse to authorize the request' do before do header_for_upload_authorize(forked_project) end it 'responds with status 200, location of lfs store and object details' do json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first) expect(lfs_router_forked_auth.try_call.first).to eq(200) expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload") expect(json_response['LfsOid']).to eq(sample_oid) expect(json_response['LfsSize']).to eq(sample_size) end end context 'and request is sent by gitlab-workhorse to finalize the upload' do before do headers_for_upload_finalize(forked_project) end it 'responds with status 200 and lfs object is linked to the source project' do expect(lfs_router_forked_auth.try_call.first).to eq(200) expect(lfs_object.projects.pluck(:id)).to include(public_project.id) end end end describe 'and user does not have push access' do let(:router) { lfs_router_forked_auth } it_behaves_like 'forbidden' end end context 'when CI is authenticated' do let(:router) { lfs_router_forked_ci_auth } it_behaves_like 'unauthorized' end context 'for unauthenticated' do let(:router) { lfs_router_forked_noauth } it_behaves_like 'unauthorized' end describe 'and second project not related to fork or a source project' do let(:second_project) { create(:project) } let(:lfs_router_second_project) { new_lfs_router(second_project, user: user) } before do public_project.lfs_objects << lfs_object headers_for_upload_finalize(second_project) end context 'when pushing the same lfs object to the second project' do before do second_project.team << [user, :master] end it 'responds with 200 and links the lfs object to the project' do expect(lfs_router_second_project.try_call.first).to eq(200) expect(lfs_object.projects.pluck(:id)).to include(second_project.id, public_project.id) end end end end end def enable_lfs allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) end def authorize(user) ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password) end def new_lfs_router(project, user: nil, ci: false) Gitlab::Lfs::Router.new(project, user, ci, request) end def header_for_upload_authorize(project) env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize" end def headers_for_upload_finalize(project) env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}" env["HTTP_X_GITLAB_LFS_TMP"] = "#{sample_oid}6e561c9d4" end def fork_project(project, user, object = nil) allow(RepositoryForkWorker).to receive(:perform_async).and_return(true) Projects::ForkService.new(project, user, {}).execute end end