debian-mirror-gitlab/spec/requests/lfs_http_spec.rb
2023-07-07 10:43:13 +05:30

1288 lines
46 KiB
Ruby

# 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' 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