gitlab-org--gitlab-foss/spec/requests/lfs_http_spec.rb

1213 lines
43 KiB
Ruby

# frozen_string_literal: true
require 'spec_helper'
RSpec.describe 'Git LFS API and storage' do
include LfsHttpHelpers
include ProjectForksHelper
include WorkhorseHelpers
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
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
# 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
context 'when feature flag is disabled' do
before do
stub_feature_flags(lfs_auto_link_fork_source: false)
end
it_behaves_like 'batch upload with existing LFS object'
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' 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(lfs_tmp_file, verified: false) }.to raise_error(JWT::DecodeError)
end
end
context 'and workhorse requests upload finalize for a new LFS object' do
before do
lfs_object.destroy!
end
context 'with object storage disabled' do
it "doesn't attempt to migrate file to object storage" do
expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
put_finalize(with_tempfile: true)
end
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: 'content'
)
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
context 'and background upload enabled' do
before do
stub_lfs_object_storage(background_upload: true)
end
it 'schedules migration of file to object storage' do
expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('LfsObjectUploader', 'LfsObject', :file, kind_of(Numeric))
put_finalize(with_tempfile: true)
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(: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 '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
finalize_headers = headers
.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file)
.merge(workhorse_internal_api_request_header)
put objects_url(second_project, sample_oid, sample_size),
params: {},
headers: finalize_headers
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
def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, remote_object: nil, args: {})
uploaded_file = nil
if with_tempfile
upload_path = LfsObjectUploader.workhorse_local_upload_path
file_path = upload_path + '/' + lfs_tmp if lfs_tmp
FileUtils.mkdir_p(upload_path)
FileUtils.touch(file_path)
uploaded_file = UploadedFile.new(file_path, filename: File.basename(file_path))
elsif remote_object
uploaded_file = fog_to_uploaded_file(remote_object)
end
finalize_headers = headers
finalize_headers.merge!(workhorse_internal_api_request_header) if verified
workhorse_finalize(
objects_url(project, sample_oid, sample_size),
method: :put,
file_key: :file,
params: args.merge(file: uploaded_file),
headers: finalize_headers,
send_rewritten_field: include_workhorse_jwt_header
)
end
def lfs_tmp_file
"#{sample_oid}012345678"
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