# frozen_string_literal: true require 'spec_helper' RSpec.describe 'Git LFS API and storage', feature_category: :source_code_management do using RSpec::Parameterized::TableSyntax include LfsHttpHelpers include ProjectForksHelper include WorkhorseHelpers include WorkhorseLfsHelpers let_it_be(:project, reload: true) { create(:project, :empty_repo) } let_it_be(:user) { create(:user) } context 'with projects' do it_behaves_like 'LFS http requests' do let_it_be(:other_project, reload: true) { create(:project, :empty_repo) } let(:container) { project } let(:authorize_guest) { project.add_guest(user) } let(:authorize_download) { project.add_reporter(user) } let(:authorize_upload) { project.add_developer(user) } context 'project specific LFS settings' do let(:body) { upload_body(sample_object) } before do authorize_upload project.update_attribute(:lfs_enabled, project_lfs_enabled) subject end describe 'LFS disabled in project' do let(:project_lfs_enabled) { false } context 'when uploading' do subject(:request) { post_lfs_json(batch_url(project), body, headers) } it_behaves_like 'LFS http 404 response' end context 'when downloading' do subject(:request) { get(objects_url(project, sample_oid), params: {}, headers: headers) } it_behaves_like 'LFS http 404 response' end end describe 'LFS enabled in project' do let(:project_lfs_enabled) { true } context 'when uploading' do subject(:request) { post_lfs_json(batch_url(project), body, headers) } it_behaves_like 'LFS http 200 response' end context 'when downloading' do subject(:request) { get(objects_url(project, sample_oid), params: {}, headers: headers) } it_behaves_like 'LFS http 200 blob response' end end end describe 'when fetching LFS object' do subject(:request) { get objects_url(project, sample_oid), params: {}, headers: headers } let(:response) { request && super() } before do project.lfs_objects << lfs_object end context 'when LFS uses object storage' do before do authorize_download end context 'when proxy download is enabled' do before do stub_lfs_object_storage(proxy_download: true) lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE) end it 'responds with the workhorse send-url' do expect(response).to have_gitlab_http_status(:ok) expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:") end end context 'when proxy download is disabled' do before do stub_lfs_object_storage(proxy_download: false) lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE) end it 'responds with redirect' do expect(response).to have_gitlab_http_status(:found) end it 'responds with the file location' do expect(response.location).to include(lfs_object.reload.file.path) end end end context 'when deploy key is authorized' do let_it_be(:key) { create(:deploy_key) } let(:authorization) { authorize_deploy_key } before do project.deploy_keys << key end it_behaves_like 'LFS http 200 blob response' end context 'when using a user key (LFSToken)' do let(:authorization) { authorize_user_key } context 'when user allowed' do before do authorize_download end it_behaves_like 'LFS http 200 blob response' context 'when user password is expired' do let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago) } it_behaves_like 'LFS http 401 response' end context 'when user is blocked' do let_it_be(:user) { create(:user, :blocked) } it_behaves_like 'LFS http 401 response' end end context 'when user not allowed' do it_behaves_like 'LFS http 404 response' end end context 'when build is authorized as' do let(:authorization) { authorize_ci_project } let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } shared_examples 'can download LFS only from own projects' do context 'for owned project' do let_it_be(:project) { create(:project, namespace: user.namespace) } it_behaves_like 'LFS http 200 blob response' end context 'for member of project' do before do authorize_download end it_behaves_like 'LFS http 200 blob response' end context 'for other project' do let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } it 'rejects downloading code' do expect(response).to have_gitlab_http_status(:not_found) end end end context 'administrator', :enable_admin_mode do let_it_be(:user) { create(:admin) } it_behaves_like 'can download LFS only from own projects' end context 'regular user' do it_behaves_like 'can download LFS only from own projects' end context 'does not have user' do let(:build) { create(:ci_build, :running, pipeline: pipeline) } it_behaves_like 'can download LFS only from own projects' end end end describe 'when handling LFS batch request' do subject(:request) { post_lfs_json batch_url(project), body, headers } let(:response) { request && super() } before do project.lfs_objects << lfs_object end shared_examples 'process authorization header' do |renew_authorization:| let(:response_authorization) do authorization_in_action(lfs_actions.first) end if renew_authorization context 'when the authorization comes from a user' do it 'returns a new valid LFS token authorization' do expect(response_authorization).not_to eq(authorization) end it 'returns a valid token' do username, token = ::Base64.decode64(response_authorization.split(' ', 2).last).split(':', 2) expect(username).to eq(user.username) expect(Gitlab::LfsToken.new(user).token_valid?(token)).to be_truthy end it 'generates only one new token per each request' do authorizations = lfs_actions.map do |action| authorization_in_action(action) end.compact expect(authorizations.uniq.count).to eq 1 end end else context 'when the authorization comes from a token' do it 'returns the same authorization header' do expect(response_authorization).to eq(authorization) end end end def lfs_actions json_response['objects'].map { |a| a['actions'] }.compact end def authorization_in_action(action) (action['upload'] || action['download']).dig('header', 'Authorization') end end describe 'download' do let(:body) { download_body(sample_object) } shared_examples 'an authorized request' do |renew_authorization:| context 'when downloading an LFS object that is assigned to our project' do it_behaves_like 'LFS http 200 response' it 'with href to download' do expect(json_response['objects'].first).to include(sample_object) expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid)) end it_behaves_like 'process authorization header', renew_authorization: renew_authorization end context 'when downloading an LFS object that is assigned to other project' do before do lfs_object.update!(projects: [other_project]) end it_behaves_like 'LFS http 200 response' it 'with an 404 for specific object' do expect(json_response['objects'].first).to include(sample_object) expect(json_response['objects'].first['error']).to include('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 let(:body) { download_body(non_existing_object) } it_behaves_like 'LFS http 200 response' it 'with an 404 for specific object' do expect(json_response['objects'].first).to include(non_existing_object) expect(json_response['objects'].first['error']).to include('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 existing and one missing LFS object' do let(:body) { download_body(multiple_objects) } it_behaves_like 'LFS http 200 response' it 'responds with download hypermedia link for the existing object' do expect(json_response['objects'].first).to include(sample_object) expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid)) expect(json_response['objects'].last).to eq({ 'oid' => non_existing_object_oid, 'size' => non_existing_object_size, 'error' => { 'code' => 404, 'message' => "Object does not exist on the server or you don't have permissions to access it" } }) end it_behaves_like 'process authorization header', renew_authorization: renew_authorization end context 'when downloading two existing LFS objects' do let(:body) { download_body(multiple_objects) } let(:other_object) { create(:lfs_object, :with_file, oid: non_existing_object_oid, size: non_existing_object_size) } before do project.lfs_objects << other_object end it 'responds with the download hypermedia link for each object' do expect(json_response['objects'].first).to include(sample_object) expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid)) expect(json_response['objects'].last).to include(non_existing_object) expect(json_response['objects'].last['actions']['download']).to include('href' => objects_url(project, non_existing_object_oid)) end it_behaves_like 'process authorization header', renew_authorization: renew_authorization end context 'when downloading an LFS object that is stored on object storage' do before do stub_lfs_object_storage lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE) end context 'when lfs.object_store.proxy_download=true' do before do stub_lfs_object_storage(proxy_download: true) end it_behaves_like 'LFS http 200 response' it 'does return proxied address URL' do expect(json_response['objects'].first).to include(sample_object) expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid)) end end context 'when "lfs.object_store.proxy_download" is "false"' do before do stub_lfs_object_storage(proxy_download: false) end it_behaves_like 'LFS http 200 response' it 'does return direct object storage URL' do expect(json_response['objects'].first).to include(sample_object) expect(json_response['objects'].first['actions']['download']['href']).to start_with("https://lfs-objects.s3.amazonaws.com/") expect(json_response['objects'].first['actions']['download']['href']).to include("X-Amz-Expires=3600&") end context 'when feature flag "lfs_batch_direct_downloads" is "false"' do before do stub_feature_flags(lfs_batch_direct_downloads: false) end it_behaves_like 'LFS http 200 response' it 'does return proxied address URL' do expect(json_response['objects'].first).to include(sample_object) expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid)) end end end end context 'when sending objects=[]' do let(:body) { download_body([]) } it_behaves_like 'LFS http expected response code and message' do let(:response_code) { 404 } let(:message) { 'Not found.' } end end end context 'when user is authenticated' do before do project.add_role(user, role) if role end it_behaves_like 'an authorized request', renew_authorization: true do let(:role) { :reporter } end context 'when user is not a member of the project' do let(:role) { nil } it_behaves_like 'LFS http 404 response' end context 'when user does not have download access' do let(:role) { :guest } it_behaves_like 'LFS http 404 response' end context 'when user password is expired' do let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago) } let(:role) { :reporter } it_behaves_like 'LFS http 401 response' end context 'when user is blocked' do let_it_be(:user) { create(:user, :blocked) } let(:role) { :reporter } it_behaves_like 'LFS http 401 response' end end context 'when using Deploy Tokens' do let(:authorization) { authorize_deploy_token } context 'when Deploy Token is not valid' do let(:deploy_token) { create(:deploy_token, projects: [project], read_repository: false) } it_behaves_like 'LFS http 401 response' end context 'when Deploy Token is not related to the project' do let(:deploy_token) { create(:deploy_token, projects: [other_project]) } it_behaves_like 'LFS http 401 response' end context 'when deploy token is from an unrelated group to the project' do let(:group) { create(:group) } let(:deploy_token) { create(:deploy_token, :group, groups: [group]) } it_behaves_like 'LFS http 401 response' end context 'when deploy token is from a parent group of the project and valid' do let(:group) { create(:group) } let(:project) { create(:project, group: group) } let(:deploy_token) { create(:deploy_token, :group, groups: [group]) } it_behaves_like 'an authorized request', renew_authorization: false end # TODO: We should fix this test case that causes flakyness by alternating the result of the above test cases. context 'when Deploy Token is valid' do let(:deploy_token) { create(:deploy_token, projects: [project]) } it_behaves_like 'an authorized request', renew_authorization: false end end context 'when build is authorized as' do let(:authorization) { authorize_ci_project } shared_examples 'can download LFS only from own projects' do |renew_authorization:| context 'for own project' do let(:pipeline) { create(:ci_empty_pipeline, project: project) } before do authorize_download end it_behaves_like 'an authorized request', renew_authorization: renew_authorization end context 'for other project' do let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } it 'rejects downloading code' do expect(response).to have_gitlab_http_status(:not_found) end end end context 'administrator', :enable_admin_mode do let_it_be(:user) { create(:admin) } let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } it_behaves_like 'can download LFS only from own projects', renew_authorization: true end context 'regular user' do let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } it_behaves_like 'can download LFS only from own projects', renew_authorization: true end context 'does not have user' do let(:build) { create(:ci_build, :running, pipeline: pipeline) } it_behaves_like 'can download LFS only from own projects', renew_authorization: false end end context 'when user is not authenticated' do let(:authorization) { nil } describe 'is accessing public project' do let_it_be(:project) { create(:project, :public) } it_behaves_like 'LFS http 200 response' it 'returns href to download' do expect(json_response).to eq({ 'objects' => [ { 'oid' => sample_oid, 'size' => sample_size, 'authenticated' => true, 'actions' => { 'download' => { 'href' => objects_url(project, sample_oid), 'header' => {} } } } ] }) end end describe 'is accessing non-public project' do it_behaves_like 'LFS http 401 response' end end end describe 'upload' do let_it_be(:project) { create(:project, :public) } let(:body) { upload_body(sample_object) } shared_examples 'pushes new LFS objects' do |renew_authorization:| let(:sample_size) { 150.megabytes } let(:sample_oid) { non_existing_object_oid } it_behaves_like 'LFS http 200 response' it 'responds with upload hypermedia link' do expect(json_response['objects']).to be_kind_of(Array) expect(json_response['objects'].first).to include(sample_object) expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size)) headers = json_response['objects'].first['actions']['upload']['header'] expect(headers['Content-Type']).to eq('application/octet-stream') expect(headers['Transfer-Encoding']).to eq('chunked') end it_behaves_like 'process authorization header', renew_authorization: renew_authorization end describe 'when request is authenticated' do describe 'when user has project push access' do before do authorize_upload end context 'when pushing an LFS object that already exists' do shared_examples_for 'batch upload with existing LFS object' do it_behaves_like 'LFS http 200 response' it 'responds with links to the object in the project' do expect(json_response['objects']).to be_kind_of(Array) expect(json_response['objects'].first).to include(sample_object) expect(lfs_object.projects.pluck(:id)).not_to include(project.id) expect(lfs_object.projects.pluck(:id)).to include(other_project.id) expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size)) headers = json_response['objects'].first['actions']['upload']['header'] expect(headers['Content-Type']).to eq('application/octet-stream') expect(headers['Transfer-Encoding']).to eq('chunked') end it_behaves_like 'process authorization header', renew_authorization: true end context 'in another project' do before do lfs_object.update!(projects: [other_project]) end it_behaves_like 'batch upload with existing LFS object' end context 'in source of fork project' do let(:other_project) { create(:project, :empty_repo) } let(:project) { fork_project(other_project) } before do lfs_object.update!(projects: [other_project]) end context 'when user has access to both the parent and fork' do before do project.add_developer(user) other_project.add_developer(user) end it 'links existing LFS objects to other project' do expect(Gitlab::AppJsonLogger).to receive(:info).with( message: "LFS object auto-linked to forked project", lfs_object_oid: lfs_object.oid, lfs_object_size: lfs_object.size, source_project_id: other_project.id, source_project_path: other_project.full_path, target_project_id: project.id, target_project_path: project.full_path).and_call_original expect(json_response['objects']).to be_kind_of(Array) expect(json_response['objects'].first).to include(sample_object) expect(json_response['objects'].first).not_to have_key('actions') expect(lfs_object.reload.projects.pluck(:id)).to match_array([other_project.id, project.id]) end end context 'when user does not have access to parent' do before do project.add_developer(user) end it_behaves_like 'batch upload with existing LFS object' end end end context 'when pushing a LFS object that does not exist' do it_behaves_like 'pushes new LFS objects', renew_authorization: true end context 'when pushing one new and one existing LFS object' do let(:body) { upload_body(multiple_objects) } it_behaves_like 'LFS http 200 response' it 'responds with upload hypermedia link for the new object' do expect(json_response['objects']).to be_kind_of(Array) expect(json_response['objects'].first).to include(sample_object) expect(json_response['objects'].first).not_to have_key('actions') expect(json_response['objects'].last).to include(non_existing_object) expect(json_response['objects'].last['actions']['upload']['href']).to eq(objects_url(project, non_existing_object_oid, non_existing_object_size)) headers = json_response['objects'].last['actions']['upload']['header'] expect(headers['Content-Type']).to eq('application/octet-stream') expect(headers['Transfer-Encoding']).to eq('chunked') end it_behaves_like 'process authorization header', renew_authorization: true end end context 'when user does not have push access' do it_behaves_like 'LFS http 403 response' end context 'when build is authorized' do let(:authorization) { authorize_ci_project } let(:pipeline) { create(:ci_empty_pipeline, project: project) } context 'build has an user' do let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } context 'tries to push to own project' do it_behaves_like 'LFS http 403 response' end context 'tries to push to other project' do let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } # I'm not sure what this tests that is different from the previous test it_behaves_like 'LFS http 403 response' end end context 'does not have user' do let(:build) { create(:ci_build, :running, pipeline: pipeline) } it_behaves_like 'LFS http 403 response' end end context 'when deploy key has project push access' do let(:key) { create(:deploy_key) } let(:authorization) { authorize_deploy_key } before do project.deploy_keys_projects.create!(deploy_key: key, can_push: true) end it_behaves_like 'pushes new LFS objects', renew_authorization: false end end context 'when user is not authenticated' do let(:authorization) { nil } context 'when user has push access' do before do authorize_upload end it_behaves_like 'LFS http 401 response' end context 'when user does not have push access' do it_behaves_like 'LFS http 401 response' end end end describe 'unsupported' do let(:body) { request_body('other', sample_object) } it_behaves_like 'LFS http 404 response' end end describe 'when handling LFS batch request on a read-only GitLab instance' do subject { post_lfs_json(batch_url(project), body, headers) } before do allow(Gitlab::Database).to receive(:read_only?) { true } project.add_maintainer(user) subject end context 'when downloading' do let(:body) { download_body(sample_object) } it_behaves_like 'LFS http 200 response' end context 'when uploading' do let(:body) { upload_body(sample_object) } it_behaves_like 'LFS http expected response code and message' do let(:response_code) { 403 } let(:message) { 'You cannot write to this read-only GitLab instance.' } end end end describe 'when pushing a LFS object' do let(:include_workhorse_jwt_header) { true } shared_examples 'unauthorized' do context 'and request is sent by gitlab-workhorse to authorize the request' do before do put_authorize end it_behaves_like 'LFS http 401 response' end context 'and request is sent by gitlab-workhorse to finalize the upload' do before do put_finalize end it_behaves_like 'LFS http 401 response' end context 'and request is sent with a malformed headers' do before do put_finalize('/etc/passwd') end it_behaves_like 'LFS http 401 response' end end shared_examples 'forbidden' do context 'and request is sent by gitlab-workhorse to authorize the request' do before do put_authorize end it_behaves_like 'LFS http 403 response' end context 'and request is sent by gitlab-workhorse to finalize the upload' do before do put_finalize end it_behaves_like 'LFS http 403 response' end context 'and request is sent with a malformed headers' do before do put_finalize('/etc/passwd') end it_behaves_like 'LFS http 403 response' end end describe 'to one project', quarantine: 'https://gitlab.com/gitlab-org/gitlab/-/issues/418757' do describe 'when user is authenticated' do describe 'when user has push access to the project' do before do project.add_developer(user) end context 'and the request bypassed workhorse' do it 'raises an exception' do expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError end end context 'and request is sent by gitlab-workhorse to authorize the request' do shared_examples 'a valid response' do before do put_authorize end it_behaves_like 'LFS http 200 workhorse response' end shared_examples 'a local file' do it_behaves_like 'a valid response' do it 'responds with status 200, location of LFS store and object details' do expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path) expect(json_response['RemoteObject']).to be_nil expect(json_response['LfsOid']).to eq(sample_oid) expect(json_response['LfsSize']).to eq(sample_size) end end end context 'when using local storage' do it_behaves_like 'a local file' end context 'when using remote storage' do context 'when direct upload is enabled' do before do stub_lfs_object_storage(enabled: true, direct_upload: true) end it_behaves_like 'a valid response' do it 'responds with status 200, location of LFS remote store and object details' do expect(json_response).not_to have_key('TempPath') expect(json_response['RemoteObject']).to have_key('ID') expect(json_response['RemoteObject']).to have_key('GetURL') expect(json_response['RemoteObject']).to have_key('StoreURL') expect(json_response['RemoteObject']).to have_key('DeleteURL') expect(json_response['RemoteObject']).to have_key('MultipartUpload') expect(json_response['LfsOid']).to eq(sample_oid) expect(json_response['LfsSize']).to eq(sample_size) end end end context 'when direct upload is disabled' do before do stub_lfs_object_storage(enabled: true, direct_upload: false) end it_behaves_like 'a local file' end end end context 'and request is sent by gitlab-workhorse to finalize the upload' do before do put_finalize end it_behaves_like 'LFS http 200 response' it 'LFS object is linked to the project' do expect(lfs_object.projects.pluck(:id)).to include(project.id) end end context 'and request to finalize the upload is not sent by gitlab-workhorse' do it 'fails with a JWT decode error' do expect { put_finalize(verified: false) }.to raise_error(JWT::DecodeError) end end context 'and the uploaded file is invalid' do where(:size, :sha256, :status) do nil | nil | :ok # Test setup sanity check 0 | nil | :bad_request nil | 'a' * 64 | :bad_request end with_them do it 'validates the upload size and SHA256' do put_finalize(size: size, sha256: sha256) expect(response).to have_gitlab_http_status(status) end end end context 'and workhorse requests upload finalize for a new LFS object' do before do lfs_object.destroy! end context 'with object storage enabled' do context 'and direct upload enabled' do let!(:fog_connection) do stub_lfs_object_storage(direct_upload: true) end let(:tmp_object) do fog_connection.directories.new(key: 'lfs-objects').files.create( # rubocop: disable Rails/SaveBang key: 'tmp/uploads/12312300', body: 'x' * sample_size ) end ['123123', '../../123123'].each do |remote_id| context "with invalid remote_id: #{remote_id}" do subject do put_finalize(remote_object: tmp_object, args: { 'file.remote_id' => remote_id }) end it 'responds with status 403' do subject expect(response).to have_gitlab_http_status(:forbidden) end end end context 'with valid remote_id' do subject do put_finalize(remote_object: tmp_object, args: { 'file.remote_id' => '12312300', 'file.name' => 'name' }) end it 'responds with status 200' do subject expect(response).to have_gitlab_http_status(:ok) object = LfsObject.find_by_oid(sample_oid) expect(object).to be_present expect(object.file.read).to eq(tmp_object.body) end it 'schedules migration of file to object storage' do subject expect(LfsObject.last.projects).to include(project) end it 'have valid file' do subject expect(LfsObject.last.file_store).to eq(ObjectStorage::Store::REMOTE) expect(LfsObject.last.file).to be_exists end end end end end context 'without the lfs object' do before do lfs_object.destroy! end it 'rejects slashes in the tempfile name (path traversal)' do put_finalize('../bar', with_tempfile: true) expect(response).to have_gitlab_http_status(:bad_request) end context 'not sending the workhorse jwt header' do let(:include_workhorse_jwt_header) { false } it 'rejects the request' do put_finalize(with_tempfile: true) expect(response).to have_gitlab_http_status(:unprocessable_entity) end end end end describe 'and user does not have push access' do before do project.add_reporter(user) end it_behaves_like 'forbidden' end end context 'when build is authorized' do let(:authorization) { authorize_ci_project } let(:pipeline) { create(:ci_empty_pipeline, project: project) } context 'build has an user' do let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } context 'tries to push to own project' do before do project.add_developer(user) put_authorize end it_behaves_like 'LFS http 403 response' end context 'tries to push to other project' do let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } before do put_authorize end it_behaves_like 'LFS http 404 response' end end context 'does not have user' do let(:build) { create(:ci_build, :running, pipeline: pipeline) } before do put_authorize end it_behaves_like 'LFS http 403 response' end end describe 'when using a user key (LFSToken)' do let(:authorization) { authorize_user_key } context 'when user allowed' do before do project.add_developer(user) put_authorize end it_behaves_like 'LFS http 200 workhorse response' context 'when user password is expired' do let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago) } it_behaves_like 'LFS http 401 response' end context 'when user is blocked' do let_it_be(:user) { create(:user, :blocked) } it_behaves_like 'LFS http 401 response' end end context 'when user not allowed' do before do put_authorize end it_behaves_like 'LFS http 404 response' end end context 'for unauthenticated' do let(:authorization) { nil } it_behaves_like 'unauthorized' end end describe 'to a forked project' do let_it_be_with_reload(:upstream_project) { create(:project, :public) } let_it_be(:project_owner) { create(:user) } let(:project) { fork_project(upstream_project, project_owner) } describe 'when user is authenticated' do describe 'when user has push access to the project' do before do project.add_developer(user) end context 'and request is sent by gitlab-workhorse to authorize the request' do before do put_authorize end it_behaves_like 'LFS http 200 workhorse response' it 'with location of LFS store and object details' do expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path) 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 put_finalize end it_behaves_like 'LFS http 200 response' it 'LFS object is linked to the forked project' do expect(lfs_object.projects.pluck(:id)).to include(project.id) end end end describe 'when user has push access to upstream project' do before do upstream_project.add_maintainer(user) end context 'an MR exists on target forked project' do let(:allow_collaboration) { true } let(:merge_request) do create(:merge_request, target_project: upstream_project, source_project: project, allow_collaboration: allow_collaboration) end before do merge_request end context 'with allow_collaboration option set to true' do context 'and request is sent by gitlab-workhorse to authorize the request' do before do put_authorize end it_behaves_like 'LFS http 200 workhorse response' end context 'and request is sent by gitlab-workhorse to finalize the upload' do before do put_finalize end it_behaves_like 'LFS http 200 response' end end context 'with allow_collaboration option set to false' do context 'request is sent by gitlab-workhorse to authorize the request' do let(:allow_collaboration) { false } before do put_authorize end it_behaves_like 'forbidden' end end end end describe 'and user does not have push access' do it_behaves_like 'forbidden' end end context 'when build is authorized' do let(:authorization) { authorize_ci_project } let(:pipeline) { create(:ci_empty_pipeline, project: project) } before do put_authorize end context 'build has an user' do let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } context 'tries to push to own project' do it_behaves_like 'LFS http 403 response' end context 'tries to push to other project' do let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } # I'm not sure what this tests that is different from the previous test it_behaves_like 'LFS http 403 response' end end context 'does not have user' do let(:build) { create(:ci_build, :running, pipeline: pipeline) } it_behaves_like 'LFS http 403 response' end end context 'for unauthenticated' do let(:authorization) { nil } it_behaves_like 'unauthorized' end describe 'and second project not related to fork or a source project' do let_it_be(:second_project) { create(:project) } before do second_project.add_maintainer(user) upstream_project.lfs_objects << lfs_object end context 'when pushing the same LFS object to the second project' do before do put_finalize(with_tempfile: true, to_project: second_project) end it_behaves_like 'LFS http 200 response' it 'links the LFS object to the project' do expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id) end end end end def put_authorize(verified: true) authorize_headers = headers authorize_headers.merge!(workhorse_internal_api_request_header) if verified put authorize_url(project, sample_oid, sample_size), params: {}, headers: authorize_headers end end end end context 'with project wikis' do it_behaves_like 'LFS http requests' do let(:container) { create(:project_wiki, :empty_repo, project: project) } let(:authorize_guest) { project.add_guest(user) } let(:authorize_download) { project.add_reporter(user) } let(:authorize_upload) { project.add_developer(user) } end end context 'with snippets' do # LFS is not supported on snippets, so we override the shared examples # to expect 404 responses instead. [ 'LFS http 200 response', 'LFS http 200 blob response', 'LFS http 403 response' ].each do |examples| shared_examples_for(examples) { it_behaves_like 'LFS http 404 response' } end context 'with project snippets' do it_behaves_like 'LFS http requests' do let(:container) { create(:project_snippet, :empty_repo, project: project) } let(:authorize_guest) { project.add_guest(user) } let(:authorize_download) { project.add_reporter(user) } let(:authorize_upload) { project.add_developer(user) } end end context 'with personal snippets' do it_behaves_like 'LFS http requests' do let(:container) { create(:personal_snippet, :empty_repo) } let(:authorize_upload) { container.update!(author: user) } end end end end