2019-09-27 20:06:20 -04:00
# frozen_string_literal: true
2016-07-15 08:01:34 -04:00
require 'spec_helper'
2020-06-24 02:09:01 -04:00
RSpec . describe 'Git LFS API and storage' do
2019-09-27 20:06:20 -04:00
include LfsHttpHelpers
2017-09-29 04:04:50 -04:00
include ProjectForksHelper
2019-10-18 07:11:44 -04:00
include WorkhorseHelpers
2016-08-19 13:10:41 -04:00
2020-03-26 14:08:03 -04:00
let_it_be ( :project , reload : true ) { create ( :project , :repository ) }
let_it_be ( :other_project ) { create ( :project , :repository ) }
let_it_be ( :user ) { create ( :user ) }
2016-07-15 08:01:34 -04:00
let! ( :lfs_object ) { create ( :lfs_object , :with_file ) }
let ( :headers ) do
{
'Authorization' = > authorization ,
'X-Sendfile-Type' = > sendfile
} . compact
end
let ( :authorization ) { }
let ( :sendfile ) { }
2016-09-15 10:36:39 -04:00
let ( :pipeline ) { create ( :ci_empty_pipeline , project : project ) }
2016-07-15 08:01:34 -04:00
let ( :sample_oid ) { lfs_object . oid }
let ( :sample_size ) { lfs_object . size }
2019-09-27 20:06:20 -04:00
let ( :sample_object ) { { 'oid' = > sample_oid , 'size' = > sample_size } }
let ( :non_existing_object_oid ) { '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897' }
let ( :non_existing_object_size ) { 1575078 }
let ( :non_existing_object ) { { 'oid' = > non_existing_object_oid , 'size' = > non_existing_object_size } }
let ( :multiple_objects ) { [ sample_object , non_existing_object ] }
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
let ( :lfs_enabled ) { true }
before do
stub_lfs_setting ( enabled : lfs_enabled )
end
describe 'when LFS is disabled' do
let ( :lfs_enabled ) { false }
let ( :body ) { upload_body ( multiple_objects ) }
2016-07-20 12:41:26 -04:00
let ( :authorization ) { authorize_user }
2016-07-15 08:01:34 -04:00
before do
2019-09-27 20:06:20 -04:00
post_lfs_json batch_url ( project ) , body , headers
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 501 response'
2016-07-15 08:01:34 -04:00
end
2016-08-24 18:08:23 -04:00
context 'project specific LFS settings' do
2019-09-27 20:06:20 -04:00
let ( :body ) { upload_body ( sample_object ) }
2016-08-24 18:08:23 -04:00
let ( :authorization ) { authorize_user }
2019-09-27 20:06:20 -04:00
before do
project . add_maintainer ( user )
project . update_attribute ( :lfs_enabled , project_lfs_enabled )
subject
end
2016-08-24 18:08:23 -04:00
context 'with LFS disabled globally' do
2019-09-27 20:06:20 -04:00
let ( :lfs_enabled ) { false }
2016-08-24 18:08:23 -04:00
describe 'LFS disabled in project' do
2019-09-27 20:06:20 -04:00
let ( :project_lfs_enabled ) { false }
2016-08-24 18:08:23 -04:00
2019-09-27 20:06:20 -04:00
context 'when uploading' do
subject { post_lfs_json ( batch_url ( project ) , body , headers ) }
2016-08-24 18:08:23 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 501 response'
2016-08-24 18:08:23 -04:00
end
2019-09-27 20:06:20 -04:00
context 'when downloading' do
subject { get ( objects_url ( project , sample_oid ) , params : { } , headers : headers ) }
2016-08-24 18:08:23 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 501 response'
2016-08-24 18:08:23 -04:00
end
end
describe 'LFS enabled in project' do
2019-09-27 20:06:20 -04:00
let ( :project_lfs_enabled ) { true }
2016-08-24 18:08:23 -04:00
2019-09-27 20:06:20 -04:00
context 'when uploading' do
subject { post_lfs_json ( batch_url ( project ) , body , headers ) }
2016-08-24 18:08:23 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 501 response'
2016-08-24 18:08:23 -04:00
end
2019-09-27 20:06:20 -04:00
context 'when downloading' do
subject { get ( objects_url ( project , sample_oid ) , params : { } , headers : headers ) }
2016-08-24 18:08:23 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 501 response'
2016-08-24 18:08:23 -04:00
end
end
end
context 'with LFS enabled globally' do
describe 'LFS disabled in project' do
2019-09-27 20:06:20 -04:00
let ( :project_lfs_enabled ) { false }
2016-08-24 18:08:23 -04:00
2019-09-27 20:06:20 -04:00
context 'when uploading' do
subject { post_lfs_json ( batch_url ( project ) , body , headers ) }
2016-08-24 18:08:23 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-08-24 18:08:23 -04:00
end
2019-09-27 20:06:20 -04:00
context 'when downloading' do
subject { get ( objects_url ( project , sample_oid ) , params : { } , headers : headers ) }
2016-08-24 18:08:23 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-08-24 18:08:23 -04:00
end
end
describe 'LFS enabled in project' do
2019-09-27 20:06:20 -04:00
let ( :project_lfs_enabled ) { true }
2016-08-24 18:08:23 -04:00
2019-09-27 20:06:20 -04:00
context 'when uploading' do
subject { post_lfs_json ( batch_url ( project ) , body , headers ) }
2016-08-24 18:08:23 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2016-08-24 18:08:23 -04:00
end
2019-09-27 20:06:20 -04:00
context 'when downloading' do
subject { get ( objects_url ( project , sample_oid ) , params : { } , headers : headers ) }
2016-08-24 18:08:23 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2016-08-24 18:08:23 -04:00
end
end
end
end
2016-07-15 08:01:34 -04:00
describe 'deprecated API' do
2019-09-27 20:06:20 -04:00
let ( :authorization ) { authorize_user }
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
shared_examples 'deprecated request' do
before do
subject
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http expected response code and message' do
let ( :response_code ) { 501 }
let ( :message ) { 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.' }
2016-07-15 08:01:34 -04:00
end
end
2019-09-27 20:06:20 -04:00
context 'when fetching LFS object using deprecated API' do
subject { get ( deprecated_objects_url ( project , sample_oid ) , params : { } , headers : headers ) }
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'deprecated request'
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
context 'when handling LFS request using deprecated API' do
subject { post_lfs_json ( deprecated_objects_url ( project ) , nil , headers ) }
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'deprecated request'
end
def deprecated_objects_url ( project , oid = nil )
File . join ( [ " #{ project . http_url_to_repo } /info/lfs/objects/ " , oid ] . compact )
2016-07-15 08:01:34 -04:00
end
end
2019-09-27 20:06:20 -04:00
describe 'when fetching LFS object' do
2016-07-15 08:01:34 -04:00
let ( :update_permissions ) { }
2017-09-07 17:27:04 -04:00
let ( :before_get ) { }
2016-07-15 08:01:34 -04:00
before do
update_permissions
2017-09-07 17:27:04 -04:00
before_get
2019-09-27 20:06:20 -04:00
get objects_url ( project , sample_oid ) , params : { } , headers : headers
2016-07-15 08:01:34 -04:00
end
context 'and request comes from gitlab-workhorse' do
context 'without user being authorized' do
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 401 response'
2016-07-15 08:01:34 -04:00
end
context 'with required headers' do
shared_examples 'responds with a file' do
2016-07-15 08:23:26 -04:00
let ( :sendfile ) { 'X-Sendfile' }
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2016-07-15 08:01:34 -04:00
it 'responds with the file location' do
expect ( response . headers [ 'Content-Type' ] ) . to eq ( 'application/octet-stream' )
expect ( response . headers [ 'X-Sendfile' ] ) . to eq ( lfs_object . file . path )
end
end
context 'with user is authorized' do
let ( :authorization ) { authorize_user }
context 'and does not have project access' do
let ( :update_permissions ) do
project . lfs_objects << lfs_object
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 404 response'
2016-07-15 08:01:34 -04:00
end
context 'and does have project access' do
let ( :update_permissions ) do
2018-07-11 10:36:08 -04:00
project . add_maintainer ( user )
2016-07-15 08:01:34 -04:00
project . lfs_objects << lfs_object
end
it_behaves_like 'responds with a file'
2017-09-07 17:27:04 -04:00
context 'when LFS uses object storage' do
2018-03-23 06:07:22 -04:00
context 'when proxy download is enabled' do
let ( :before_get ) do
stub_lfs_object_storage ( proxy_download : true )
lfs_object . file . migrate! ( LfsObjectUploader :: Store :: REMOTE )
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2018-03-23 06:07:22 -04:00
it 'responds with the workhorse send-url' do
expect ( response . headers [ Gitlab :: Workhorse :: SEND_DATA_HEADER ] ) . to start_with ( " send-url: " )
end
2017-09-07 17:27:04 -04:00
end
2018-03-23 06:07:22 -04:00
context 'when proxy download is disabled' do
let ( :before_get ) do
stub_lfs_object_storage ( proxy_download : false )
lfs_object . file . migrate! ( LfsObjectUploader :: Store :: REMOTE )
end
it 'responds with redirect' do
2020-01-27 10:08:51 -05:00
expect ( response ) . to have_gitlab_http_status ( :found )
2018-03-23 06:07:22 -04:00
end
2017-09-07 17:27:04 -04:00
2018-03-23 06:07:22 -04:00
it 'responds with the file location' do
expect ( response . location ) . to include ( lfs_object . reload . file . path )
end
2017-09-07 17:27:04 -04:00
end
end
2016-07-15 08:01:34 -04:00
end
end
2016-09-19 10:34:32 -04:00
context 'when deploy key is authorized' do
let ( :key ) { create ( :deploy_key ) }
let ( :authorization ) { authorize_deploy_key }
let ( :update_permissions ) do
project . deploy_keys << key
project . lfs_objects << lfs_object
end
it_behaves_like 'responds with a file'
end
2019-09-27 20:06:20 -04:00
describe 'when using a user key (LFSToken)' do
2016-09-27 14:23:51 -04:00
let ( :authorization ) { authorize_user_key }
context 'when user allowed' do
let ( :update_permissions ) do
2018-07-11 10:36:08 -04:00
project . add_maintainer ( user )
2016-09-27 14:23:51 -04:00
project . lfs_objects << lfs_object
end
it_behaves_like 'responds with a file'
2019-09-27 20:06:20 -04:00
context 'when user password is expired' do
let ( :user ) { create ( :user , password_expires_at : 1 . minute . ago ) }
it_behaves_like 'LFS http 401 response'
end
context 'when user is blocked' do
let ( :user ) { create ( :user , :blocked ) }
it_behaves_like 'LFS http 401 response'
end
2016-09-27 14:23:51 -04:00
end
context 'when user not allowed' do
let ( :update_permissions ) do
project . lfs_objects << lfs_object
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 404 response'
2016-09-27 14:23:51 -04:00
end
end
2016-09-16 05:06:57 -04:00
context 'when build is authorized as' do
2016-07-15 08:01:34 -04:00
let ( :authorization ) { authorize_ci_project }
2016-09-16 05:06:57 -04:00
shared_examples 'can download LFS only from own projects' do
2016-10-17 11:23:51 -04:00
context 'for owned project' do
2017-08-02 15:55:11 -04:00
let ( :project ) { create ( :project , namespace : user . namespace ) }
2016-10-17 11:23:51 -04:00
let ( :update_permissions ) do
project . lfs_objects << lfs_object
end
it_behaves_like 'responds with a file'
end
context 'for member of project' do
2016-09-16 05:06:57 -04:00
let ( :pipeline ) { create ( :ci_empty_pipeline , project : project ) }
let ( :update_permissions ) do
2017-12-22 03:18:28 -05:00
project . add_reporter ( user )
2016-09-16 05:06:57 -04:00
project . lfs_objects << lfs_object
end
it_behaves_like 'responds with a file'
end
context 'for other project' do
let ( :pipeline ) { create ( :ci_empty_pipeline , project : other_project ) }
let ( :update_permissions ) do
project . lfs_objects << lfs_object
end
it 'rejects downloading code' do
2017-10-19 14:28:19 -04:00
expect ( response ) . to have_gitlab_http_status ( other_project_status )
2016-09-16 05:06:57 -04:00
end
end
2016-07-15 08:23:26 -04:00
end
2016-09-16 05:06:57 -04:00
context 'administrator' do
let ( :user ) { create ( :admin ) }
let ( :build ) { create ( :ci_build , :running , pipeline : pipeline , user : user ) }
it_behaves_like 'can download LFS only from own projects' do
# We render 403, because administrator does have normally access
let ( :other_project_status ) { 403 }
end
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' do
# We render 404, to prevent data leakage about existence of the project
let ( :other_project_status ) { 404 }
end
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' do
2016-09-16 07:34:05 -04:00
# We render 404, to prevent data leakage about existence of the project
let ( :other_project_status ) { 404 }
2016-09-16 05:06:57 -04:00
end
end
2016-07-15 08:01:34 -04:00
end
end
context 'without required headers' do
let ( :authorization ) { authorize_user }
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 404 response'
2016-07-15 08:01:34 -04:00
end
end
end
2019-09-27 20:06:20 -04:00
describe 'when handling LFS batch request' do
2016-07-15 08:01:34 -04:00
let ( :update_lfs_permissions ) { }
let ( :update_user_permissions ) { }
before do
update_lfs_permissions
update_user_permissions
2019-09-27 20:06:20 -04:00
post_lfs_json batch_url ( project ) , body , headers
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
shared_examples 'process authorization header' do | renew_authorization : |
let ( :response_authorization ) do
authorization_in_action ( lfs_actions . first )
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
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 )
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
it 'returns a 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
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
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 )
2016-07-15 08:01:34 -04:00
end
end
2019-09-27 20:06:20 -04:00
end
def lfs_actions
json_response [ 'objects' ] . map { | a | a [ 'actions' ] } . compact
end
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
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
2016-07-15 08:01:34 -04:00
let ( :update_lfs_permissions ) do
2019-09-27 20:06:20 -04:00
project . lfs_objects << lfs_object
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2016-07-15 08:01:34 -04:00
it 'with href to download' do
2019-09-27 20:06:20 -04:00
expect ( json_response [ 'objects' ] . first ) . to include ( sample_object )
expect ( json_response [ 'objects' ] . first [ 'actions' ] [ 'download' ] [ 'href' ] ) . to eq ( objects_url ( project , sample_oid ) )
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'process authorization header' , renew_authorization : renew_authorization
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
context 'when downloading an LFS object that is assigned to other project' do
let ( :update_lfs_permissions ) do
other_project . lfs_objects << lfs_object
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2016-07-15 08:01:34 -04:00
it 'with an 404 for specific object' do
2019-09-27 20:06:20 -04:00
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 " )
2016-07-15 08:01:34 -04:00
end
end
2019-09-27 20:06:20 -04:00
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 " )
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
end
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
context 'when downloading one new and one existing LFS object' do
let ( :body ) { download_body ( multiple_objects ) }
2016-07-15 08:01:34 -04:00
let ( :update_lfs_permissions ) do
project . lfs_objects << lfs_object
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
it 'responds with download hypermedia link for the new 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 "
}
2017-02-22 12:35:20 -05:00
} )
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
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 ) }
let ( :update_lfs_permissions ) do
project . lfs_objects << [ lfs_object , 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
2016-07-15 08:01:34 -04:00
end
end
context 'when user is authenticated' do
let ( :authorization ) { authorize_user }
let ( :update_user_permissions ) do
2017-12-22 03:18:28 -05:00
project . add_role ( user , role )
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'an authorized request' , renew_authorization : true do
2016-07-15 08:01:34 -04:00
let ( :role ) { :reporter }
end
context 'when user does is not member of the project' do
2016-07-20 12:41:26 -04:00
let ( :update_user_permissions ) { nil }
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 404 response'
2016-07-15 08:01:34 -04:00
end
context 'when user does not have download access' do
let ( :role ) { :guest }
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
end
context 'when user password is expired' do
let ( :role ) { :reporter }
let ( :user ) { create ( :user , password_expires_at : 1 . minute . ago ) }
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 " )
2016-07-15 08:01:34 -04:00
end
end
2019-09-27 20:06:20 -04:00
context 'when user is blocked' do
let ( :role ) { :reporter }
let ( :user ) { create ( :user , :blocked ) }
it_behaves_like 'LFS http 401 response'
end
2016-07-15 08:01:34 -04:00
end
2018-07-23 05:23:08 -04:00
context 'when using Deploy Tokens' do
let ( :authorization ) { authorize_deploy_token }
let ( :update_user_permissions ) { nil }
let ( :role ) { nil }
let ( :update_lfs_permissions ) do
project . lfs_objects << lfs_object
end
context 'when Deploy Token is valid' do
let ( :deploy_token ) { create ( :deploy_token , projects : [ project ] ) }
2019-09-27 20:06:20 -04:00
it_behaves_like 'an authorized request' , renew_authorization : false
2018-07-23 05:23:08 -04:00
end
context 'when Deploy Token is not valid' do
let ( :deploy_token ) { create ( :deploy_token , projects : [ project ] , read_repository : false ) }
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 401 response'
2018-07-23 05:23:08 -04:00
end
context 'when Deploy Token is not related to the project' do
2019-09-27 20:06:20 -04:00
let ( :deploy_token ) { create ( :deploy_token , projects : [ other_project ] ) }
2018-07-23 05:23:08 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 404 response'
2018-07-23 05:23:08 -04:00
end
end
2016-09-16 05:06:57 -04:00
context 'when build is authorized as' do
2016-07-15 08:01:34 -04:00
let ( :authorization ) { authorize_ci_project }
2016-09-16 05:06:57 -04:00
let ( :update_lfs_permissions ) do
project . lfs_objects << lfs_object
end
2019-09-27 20:06:20 -04:00
shared_examples 'can download LFS only from own projects' do | renew_authorization : |
2016-09-16 05:06:57 -04:00
context 'for own project' do
let ( :pipeline ) { create ( :ci_empty_pipeline , project : project ) }
let ( :update_user_permissions ) do
2017-12-22 03:18:28 -05:00
project . add_reporter ( user )
2016-09-16 05:06:57 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'an authorized request' , renew_authorization : renew_authorization
2016-09-16 05:06:57 -04:00
end
context 'for other project' do
let ( :pipeline ) { create ( :ci_empty_pipeline , project : other_project ) }
it 'rejects downloading code' do
2017-10-19 14:28:19 -04:00
expect ( response ) . to have_gitlab_http_status ( other_project_status )
2016-09-16 05:06:57 -04:00
end
end
end
context 'administrator' do
let ( :user ) { create ( :admin ) }
let ( :build ) { create ( :ci_build , :running , pipeline : pipeline , user : user ) }
2019-09-27 20:06:20 -04:00
it_behaves_like 'can download LFS only from own projects' , renew_authorization : true do
2016-09-16 05:06:57 -04:00
# We render 403, because administrator does have normally access
let ( :other_project_status ) { 403 }
end
end
context 'regular user' do
let ( :build ) { create ( :ci_build , :running , pipeline : pipeline , user : user ) }
2019-09-27 20:06:20 -04:00
it_behaves_like 'can download LFS only from own projects' , renew_authorization : true do
2016-09-16 05:06:57 -04:00
# We render 404, to prevent data leakage about existence of the project
let ( :other_project_status ) { 404 }
end
end
context 'does not have user' do
let ( :build ) { create ( :ci_build , :running , pipeline : pipeline ) }
2019-09-27 20:06:20 -04:00
it_behaves_like 'can download LFS only from own projects' , renew_authorization : false do
2016-09-16 07:34:05 -04:00
# We render 404, to prevent data leakage about existence of the project
let ( :other_project_status ) { 404 }
2016-09-16 05:06:57 -04:00
end
end
2016-07-15 08:01:34 -04:00
end
context 'when user is not authenticated' do
describe 'is accessing public project' do
2017-08-02 15:55:11 -04:00
let ( :project ) { create ( :project , :public ) }
2016-07-15 08:01:34 -04:00
let ( :update_lfs_permissions ) do
project . lfs_objects << lfs_object
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
it 'returns href to download' do
2017-02-22 12:35:20 -05:00
expect ( json_response ) . to eq ( {
'objects' = > [
2017-02-22 12:44:44 -05:00
{
'oid' = > sample_oid ,
2017-02-22 12:35:20 -05:00
'size' = > sample_size ,
'authenticated' = > true ,
'actions' = > {
'download' = > {
2019-09-27 20:06:20 -04:00
'href' = > objects_url ( project , sample_oid ) ,
2017-02-22 12:35:20 -05:00
'header' = > { }
}
2016-07-15 08:01:34 -04:00
}
}
2017-02-22 12:35:20 -05:00
]
} )
2016-07-15 08:01:34 -04:00
end
end
describe 'is accessing non-public project' do
let ( :update_lfs_permissions ) do
project . lfs_objects << lfs_object
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 401 response'
2016-07-15 08:01:34 -04:00
end
end
end
describe 'upload' do
2017-08-02 15:55:11 -04:00
let ( :project ) { create ( :project , :public ) }
2019-09-27 20:06:20 -04:00
let ( :body ) { upload_body ( sample_object ) }
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
shared_examples 'pushes new LFS objects' do | renew_authorization : |
2017-11-08 11:21:39 -05:00
let ( :sample_size ) { 150 . megabytes }
2019-09-27 20:06:20 -04:00
let ( :sample_oid ) { non_existing_object_oid }
it_behaves_like 'LFS http 200 response'
2017-11-08 11:21:39 -05:00
it 'responds with upload hypermedia link' do
expect ( json_response [ 'objects' ] ) . to be_kind_of ( Array )
2019-09-27 20:06:20 -04:00
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 ) )
expect ( json_response [ 'objects' ] . first [ 'actions' ] [ 'upload' ] [ 'header' ] ) . to include ( 'Content-Type' = > 'application/octet-stream' )
2017-11-08 11:21:39 -05:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'process authorization header' , renew_authorization : renew_authorization
2017-11-08 11:21:39 -05:00
end
2016-07-15 08:01:34 -04:00
describe 'when request is authenticated' do
describe 'when user has project push access' do
let ( :authorization ) { authorize_user }
let ( :update_user_permissions ) do
2017-12-22 03:18:28 -05:00
project . add_developer ( user )
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
context 'when pushing an LFS object that already exists' do
2020-03-04 22:07:52 -05:00
shared_examples_for 'batch upload with existing LFS object' do
it_behaves_like 'LFS http 200 response'
it 'responds with links the object to 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 ) )
expect ( json_response [ 'objects' ] . first [ 'actions' ] [ 'upload' ] [ 'header' ] ) . to include ( 'Content-Type' = > 'application/octet-stream' )
end
it_behaves_like 'process authorization header' , renew_authorization : true
end
2016-07-15 08:01:34 -04:00
let ( :update_lfs_permissions ) do
other_project . lfs_objects << lfs_object
end
2020-03-04 22:07:52 -05:00
context 'in another project' do
it_behaves_like 'batch upload with existing LFS object'
2016-07-15 08:01:34 -04:00
end
2020-03-04 22:07:52 -05:00
context 'in source of fork project' do
let ( :project ) { fork_project ( other_project ) }
it_behaves_like 'batch upload with existing LFS object'
end
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
context 'when pushing a LFS object that does not exist' do
it_behaves_like 'pushes new LFS objects' , renew_authorization : true
end
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
context 'when pushing one new and one existing LFS object' do
let ( :body ) { upload_body ( multiple_objects ) }
2016-07-15 08:01:34 -04:00
let ( :update_lfs_permissions ) do
project . lfs_objects << lfs_object
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2016-07-15 08:01:34 -04:00
it 'responds with upload hypermedia link for the new object' do
expect ( json_response [ 'objects' ] ) . to be_kind_of ( Array )
2019-09-27 20:06:20 -04:00
expect ( json_response [ 'objects' ] . first ) . to include ( sample_object )
expect ( json_response [ 'objects' ] . first ) . not_to have_key ( 'actions' )
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
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 ) )
expect ( json_response [ 'objects' ] . last [ 'actions' ] [ 'upload' ] [ 'header' ] ) . to include ( 'Content-Type' = > 'application/octet-stream' )
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'process authorization header' , renew_authorization : true
2016-07-15 08:01:34 -04:00
end
end
context 'when user does not have push access' do
let ( :authorization ) { authorize_user }
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-07-15 08:01:34 -04:00
end
2016-09-16 05:06:57 -04:00
context 'when build is authorized' do
2016-07-15 08:01:34 -04:00
let ( :authorization ) { authorize_ci_project }
2016-09-16 05:06:57 -04:00
context 'build has an user' do
2019-09-27 20:06:20 -04:00
let ( :build ) { create ( :ci_build , :running , pipeline : pipeline , user : user ) }
2016-09-16 05:06:57 -04:00
context 'tries to push to own project' do
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-09-16 05:06:57 -04:00
end
context 'tries to push to other project' do
let ( :pipeline ) { create ( :ci_empty_pipeline , project : other_project ) }
2017-05-17 14:00:36 -04:00
# I'm not sure what this tests that is different from the previous test
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-09-16 05:06:57 -04:00
end
end
context 'does not have user' do
let ( :build ) { create ( :ci_build , :running , pipeline : pipeline ) }
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-07-15 08:01:34 -04:00
end
end
2017-11-08 11:21:39 -05:00
context 'when deploy key has project push access' do
2018-01-05 10:23:44 -05:00
let ( :key ) { create ( :deploy_key ) }
2017-11-08 11:21:39 -05:00
let ( :authorization ) { authorize_deploy_key }
let ( :update_user_permissions ) do
2018-01-05 10:23:44 -05:00
project . deploy_keys_projects . create ( deploy_key : key , can_push : true )
2017-11-08 11:21:39 -05:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'pushes new LFS objects' , renew_authorization : false
2017-11-08 11:21:39 -05:00
end
2016-07-15 08:01:34 -04:00
end
context 'when user is not authenticated' do
context 'when user has push access' do
let ( :update_user_permissions ) do
2018-07-11 10:36:08 -04:00
project . add_maintainer ( user )
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 401 response'
2016-07-15 08:01:34 -04:00
end
context 'when user does not have push access' do
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 401 response'
2016-07-15 08:01:34 -04:00
end
end
end
describe 'unsupported' do
2016-07-20 12:41:26 -04:00
let ( :authorization ) { authorize_user }
2019-09-27 20:06:20 -04:00
let ( :body ) { request_body ( 'other' , sample_object ) }
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 404 response'
2016-07-15 08:01:34 -04:00
end
end
2019-09-27 20:06:20 -04:00
describe 'when handling LFS batch request on a read-only GitLab instance' do
2017-09-19 03:44:58 -04:00
let ( :authorization ) { authorize_user }
2019-09-27 20:06:20 -04:00
subject { post_lfs_json ( batch_url ( project ) , body , headers ) }
2017-09-19 03:44:58 -04:00
before do
allow ( Gitlab :: Database ) . to receive ( :read_only? ) { true }
2019-09-27 20:06:20 -04:00
2018-07-11 10:36:08 -04:00
project . add_maintainer ( user )
2019-09-27 20:06:20 -04:00
subject
2017-09-19 03:44:58 -04:00
end
2019-09-27 20:06:20 -04:00
context 'when downloading' do
let ( :body ) { download_body ( sample_object ) }
2017-09-19 03:44:58 -04:00
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2017-09-19 03:44:58 -04:00
end
2019-09-27 20:06:20 -04:00
context 'when uploading' do
let ( :body ) { upload_body ( sample_object ) }
2017-09-19 03:44:58 -04:00
2019-09-27 20:06:20 -04:00
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
2017-09-19 03:44:58 -04:00
end
end
2019-09-27 20:06:20 -04:00
describe 'when pushing a LFS object' do
2016-07-15 08:01:34 -04:00
shared_examples 'unauthorized' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
put_authorize
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 401 response'
2016-07-15 08:01:34 -04:00
end
context 'and request is sent by gitlab-workhorse to finalize the upload' do
before do
put_finalize
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 401 response'
2016-07-15 08:01:34 -04:00
end
context 'and request is sent with a malformed headers' do
before do
2016-08-10 11:40:20 -04:00
put_finalize ( '/etc/passwd' )
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 401 response'
2016-07-15 08:01:34 -04:00
end
end
shared_examples 'forbidden' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
put_authorize
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-07-15 08:01:34 -04:00
end
context 'and request is sent by gitlab-workhorse to finalize the upload' do
before do
put_finalize
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-07-15 08:01:34 -04:00
end
2016-07-20 12:41:26 -04:00
context 'and request is sent with a malformed headers' do
before do
2016-08-10 11:40:20 -04:00
put_finalize ( '/etc/passwd' )
2016-07-20 12:41:26 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-07-20 12:41:26 -04:00
end
2016-07-15 08:01:34 -04:00
end
describe 'to one project' do
describe 'when user is authenticated' do
let ( :authorization ) { authorize_user }
describe 'when user has push access to the project' do
before do
2017-12-22 03:18:28 -05:00
project . add_developer ( user )
2016-07-15 08:01:34 -04:00
end
2016-08-19 13:10:41 -04:00
context 'and the request bypassed workhorse' do
it 'raises an exception' do
expect { put_authorize ( verified : false ) } . to raise_error JWT :: DecodeError
end
end
2016-07-15 08:01:34 -04:00
context 'and request is sent by gitlab-workhorse to authorize the request' do
2018-03-23 06:07:22 -04:00
shared_examples 'a valid response' do
before do
put_authorize
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2018-03-23 06:07:22 -04:00
it 'uses the gitlab-workhorse content type' do
2020-02-10 10:08:54 -05:00
expect ( response . media_type ) . to eq ( Gitlab :: Workhorse :: INTERNAL_API_CONTENT_TYPE )
2018-03-23 06:07:22 -04:00
end
2016-07-15 08:01:34 -04:00
end
2018-03-23 06:07:22 -04:00
shared_examples 'a local file' do
it_behaves_like 'a valid response' do
2019-09-27 20:06:20 -04:00
it 'responds with status 200, location of LFS store and object details' do
2018-03-23 06:07:22 -04:00
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
2016-07-15 08:01:34 -04:00
end
2018-03-23 06:07:22 -04:00
context 'when using local storage' do
it_behaves_like 'a local file'
2016-08-19 13:10:41 -04:00
end
2018-03-23 06:07:22 -04:00
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
2019-09-27 20:06:20 -04:00
it 'responds with status 200, location of LFS remote store and object details' do
2019-10-18 07:11:44 -04:00
expect ( json_response ) . not_to have_key ( 'TempPath' )
2018-03-23 06:07:22 -04:00
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' )
2018-05-09 11:27:38 -04:00
expect ( json_response [ 'RemoteObject' ] ) . not_to have_key ( 'MultipartUpload' )
2018-03-23 06:07:22 -04:00
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
2016-07-15 08:01:34 -04:00
end
end
context 'and request is sent by gitlab-workhorse to finalize the upload' do
before do
put_finalize
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
it 'LFS object is linked to the project' do
2016-07-15 08:01:34 -04:00
expect ( lfs_object . projects . pluck ( :id ) ) . to include ( project . id )
end
2016-08-11 09:05:49 -04:00
end
2016-08-10 11:40:20 -04:00
Verify that LFS upload requests are genuine
LFS uploads are handled in concert by workhorse and rails. In normal
use, workhorse:
* Authorizes the request with rails (upload_authorize)
* Handles the upload of the file to a tempfile - disk or object storage
* Validates the file size and contents
* Hands off to rails to complete the upload (upload_finalize)
In `upload_finalize`, the LFS object is linked to the project. As LFS
objects are deduplicated across all projects, it may already exist. If
not, the temporary file is copied to the correct place, and will be
used by all future LFS objects with the same OID.
Workhorse uses the Content-Type of the request to decide to follow this
routine, as the URLs are ambiguous. If the Content-Type is anything but
"application/octet-stream", the request is proxied directly to rails,
on the assumption that this is a normal file edit request. If it's an
actual LFS request with a different content-type, however, it is routed
to the Rails `upload_finalize` action, which treats it as an LFS upload
just as it would a workhorse-modified request.
The outcome is that users can upload LFS objects that don't match the
declared size or OID. They can also create links to LFS objects they
don't really own, allowing them to read the contents of files if they
know just the size or OID.
We can close this hole by requiring requests to `upload_finalize` to be
sourced from Workhorse. The mechanism to do this already exists.
2019-01-08 12:57:58 -05:00
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
2019-09-27 20:06:20 -04:00
context 'and workhorse requests upload finalize for a new LFS object' do
2017-09-07 17:27:04 -04:00
before do
2018-02-28 10:44:34 -05:00
lfs_object . destroy
2017-09-07 17:27:04 -04:00
end
context 'with object storage disabled' do
it " doesn't attempt to migrate file to object storage " do
2018-02-21 11:43:21 -05:00
expect ( ObjectStorage :: BackgroundMoveWorker ) . not_to receive ( :perform_async )
2017-09-07 17:27:04 -04:00
put_finalize ( with_tempfile : true )
end
end
context 'with object storage enabled' do
2018-03-23 06:07:22 -04:00
context 'and direct upload enabled' do
let! ( :fog_connection ) do
stub_lfs_object_storage ( direct_upload : true )
end
2019-10-18 07:11:44 -04:00
let ( :tmp_object ) do
fog_connection . directories . new ( key : 'lfs-objects' ) . files . create (
key : 'tmp/uploads/12312300' ,
body : 'content'
)
end
2018-03-23 06:07:22 -04:00
[ '123123' , '../../123123' ] . each do | remote_id |
context " with invalid remote_id: #{ remote_id } " do
subject do
2019-10-18 07:11:44 -04:00
put_finalize ( remote_object : tmp_object , args : {
2018-04-03 12:47:33 -04:00
'file.remote_id' = > remote_id
} )
2018-03-23 06:07:22 -04:00
end
it 'responds with status 403' do
subject
2020-01-27 10:08:51 -05:00
expect ( response ) . to have_gitlab_http_status ( :forbidden )
2018-03-23 06:07:22 -04:00
end
end
end
context 'with valid remote_id' do
subject do
2019-10-18 07:11:44 -04:00
put_finalize ( remote_object : tmp_object , args : {
2018-03-23 06:07:22 -04:00
'file.remote_id' = > '12312300' ,
2018-04-03 12:47:33 -04:00
'file.name' = > 'name'
} )
2018-03-23 06:07:22 -04:00
end
it 'responds with status 200' do
subject
2020-01-27 10:08:51 -05:00
expect ( response ) . to have_gitlab_http_status ( :ok )
2019-10-18 07:11:44 -04:00
object = LfsObject . find_by_oid ( sample_oid )
expect ( object ) . to be_present
expect ( object . file . read ) . to eq ( tmp_object . body )
2018-03-23 06:07:22 -04:00
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
2017-09-07 17:27:04 -04:00
end
2018-03-23 06:07:22 -04:00
context 'and background upload enabled' do
before do
stub_lfs_object_storage ( background_upload : true )
end
2017-09-07 17:27:04 -04:00
2018-03-23 06:07:22 -04:00
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
2017-09-07 17:27:04 -04:00
end
end
end
2016-08-10 11:40:20 -04:00
context 'invalid tempfiles' do
2018-03-23 06:07:22 -04:00
before do
lfs_object . destroy
2016-08-10 11:40:20 -04:00
end
2018-03-23 06:07:22 -04:00
it 'rejects slashes in the tempfile name (path traversal)' do
put_finalize ( '../bar' , with_tempfile : true )
2020-01-27 10:08:51 -05:00
expect ( response ) . to have_gitlab_http_status ( :forbidden )
2016-08-10 11:40:20 -04:00
end
2016-07-15 08:01:34 -04:00
end
end
describe 'and user does not have push access' do
2016-07-20 12:41:26 -04:00
before do
2017-12-22 03:18:28 -05:00
project . add_reporter ( user )
2016-07-20 12:41:26 -04:00
end
2016-07-15 08:01:34 -04:00
it_behaves_like 'forbidden'
end
end
2016-09-16 05:06:57 -04:00
context 'when build is authorized' do
2016-07-15 08:01:34 -04:00
let ( :authorization ) { authorize_ci_project }
2016-09-16 05:06:57 -04:00
context 'build has an user' do
2019-09-27 20:06:20 -04:00
let ( :build ) { create ( :ci_build , :running , pipeline : pipeline , user : user ) }
2016-09-16 05:06:57 -04:00
context 'tries to push to own project' do
before do
2017-12-22 03:18:28 -05:00
project . add_developer ( user )
2016-09-16 05:06:57 -04:00
put_authorize
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-09-16 05:06:57 -04:00
end
context 'tries to push to other project' do
let ( :pipeline ) { create ( :ci_empty_pipeline , project : other_project ) }
before do
put_authorize
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 404 response'
2016-09-16 05:06:57 -04:00
end
end
context 'does not have user' do
let ( :build ) { create ( :ci_build , :running , pipeline : pipeline ) }
before do
put_authorize
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 404 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 response'
context 'when user password is expired' do
let ( :user ) { create ( :user , password_expires_at : 1 . minute . ago ) }
it_behaves_like 'LFS http 401 response'
end
context 'when user is blocked' do
let ( :user ) { create ( :user , :blocked ) }
it_behaves_like 'LFS http 401 response'
end
end
context 'when user not allowed' do
before do
put_authorize
2016-09-16 05:06:57 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 404 response'
2016-09-16 05:06:57 -04:00
end
2016-07-15 08:01:34 -04:00
end
context 'for unauthenticated' do
it_behaves_like 'unauthorized'
end
end
describe 'to a forked project' do
2017-08-02 15:55:11 -04:00
let ( :upstream_project ) { create ( :project , :public ) }
2016-07-15 08:01:34 -04:00
let ( :project_owner ) { create ( :user ) }
let ( :project ) { fork_project ( upstream_project , project_owner ) }
describe 'when user is authenticated' do
let ( :authorization ) { authorize_user }
describe 'when user has push access to the project' do
before do
2017-12-22 03:18:28 -05:00
project . add_developer ( user )
2016-07-15 08:01:34 -04:00
end
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
put_authorize
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
it 'with location of LFS store and object details' do
2018-03-23 06:07:22 -04:00
expect ( json_response [ 'TempPath' ] ) . to eq ( LfsObjectUploader . workhorse_local_upload_path )
2016-07-15 08:01:34 -04:00
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
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2016-07-15 08:01:34 -04:00
2020-02-07 07:09:13 -05:00
it 'LFS object is linked to the forked project' do
expect ( lfs_object . projects . pluck ( :id ) ) . to include ( project . id )
2016-07-15 08:01:34 -04:00
end
end
end
describe 'and user does not have push access' do
it_behaves_like 'forbidden'
end
end
2016-09-16 05:06:57 -04:00
context 'when build is authorized' do
2016-07-15 08:01:34 -04:00
let ( :authorization ) { authorize_ci_project }
2016-09-16 05:06:57 -04:00
before do
put_authorize
end
context 'build has an user' do
2019-09-27 20:06:20 -04:00
let ( :build ) { create ( :ci_build , :running , pipeline : pipeline , user : user ) }
2016-09-16 05:06:57 -04:00
context 'tries to push to own project' do
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-09-16 05:06:57 -04:00
end
context 'tries to push to other project' do
let ( :pipeline ) { create ( :ci_empty_pipeline , project : other_project ) }
2017-05-17 14:00:36 -04:00
# I'm not sure what this tests that is different from the previous test
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-09-16 05:06:57 -04:00
end
end
context 'does not have user' do
let ( :build ) { create ( :ci_build , :running , pipeline : pipeline ) }
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 403 response'
2016-09-16 05:06:57 -04:00
end
2016-07-15 08:01:34 -04:00
end
context 'for unauthenticated' do
it_behaves_like 'unauthorized'
end
describe 'and second project not related to fork or a source project' do
2017-08-02 15:55:11 -04:00
let ( :second_project ) { create ( :project ) }
2016-07-15 08:01:34 -04:00
let ( :authorization ) { authorize_user }
before do
2018-07-11 10:36:08 -04:00
second_project . add_maintainer ( user )
2016-07-15 08:01:34 -04:00
upstream_project . lfs_objects << lfs_object
end
2019-09-27 20:06:20 -04:00
context 'when pushing the same LFS object to the second project' do
2016-07-15 08:01:34 -04:00
before do
Verify that LFS upload requests are genuine
LFS uploads are handled in concert by workhorse and rails. In normal
use, workhorse:
* Authorizes the request with rails (upload_authorize)
* Handles the upload of the file to a tempfile - disk or object storage
* Validates the file size and contents
* Hands off to rails to complete the upload (upload_finalize)
In `upload_finalize`, the LFS object is linked to the project. As LFS
objects are deduplicated across all projects, it may already exist. If
not, the temporary file is copied to the correct place, and will be
used by all future LFS objects with the same OID.
Workhorse uses the Content-Type of the request to decide to follow this
routine, as the URLs are ambiguous. If the Content-Type is anything but
"application/octet-stream", the request is proxied directly to rails,
on the assumption that this is a normal file edit request. If it's an
actual LFS request with a different content-type, however, it is routed
to the Rails `upload_finalize` action, which treats it as an LFS upload
just as it would a workhorse-modified request.
The outcome is that users can upload LFS objects that don't match the
declared size or OID. They can also create links to LFS objects they
don't really own, allowing them to read the contents of files if they
know just the size or OID.
We can close this hole by requiring requests to `upload_finalize` to be
sourced from Workhorse. The mechanism to do this already exists.
2019-01-08 12:57:58 -05:00
finalize_headers = headers
. merge ( 'X-Gitlab-Lfs-Tmp' = > lfs_tmp_file )
. merge ( workhorse_internal_api_request_header )
2019-09-27 20:06:20 -04:00
put objects_url ( second_project , sample_oid , sample_size ) ,
params : { } ,
headers : finalize_headers
2016-07-15 08:01:34 -04:00
end
2019-09-27 20:06:20 -04:00
it_behaves_like 'LFS http 200 response'
2016-07-15 08:01:34 -04:00
2019-09-27 20:06:20 -04:00
it 'links the LFS object to the project' do
2016-07-15 08:01:34 -04:00
expect ( lfs_object . projects . pluck ( :id ) ) . to include ( second_project . id , upstream_project . id )
end
end
end
end
2016-08-19 13:10:41 -04:00
def put_authorize ( verified : true )
authorize_headers = headers
authorize_headers . merge! ( workhorse_internal_api_request_header ) if verified
2019-09-27 20:06:20 -04:00
put authorize_url ( project , sample_oid , sample_size ) , params : { } , headers : authorize_headers
2016-07-15 08:01:34 -04:00
end
2019-10-18 07:11:44 -04:00
def put_finalize ( lfs_tmp = lfs_tmp_file , with_tempfile : false , verified : true , remote_object : nil , args : { } )
uploaded_file = nil
2017-09-07 17:27:04 -04:00
2018-03-23 06:07:22 -04:00
if with_tempfile
2019-10-18 07:11:44 -04:00
upload_path = LfsObjectUploader . workhorse_local_upload_path
file_path = upload_path + '/' + lfs_tmp if lfs_tmp
2018-03-23 06:07:22 -04:00
FileUtils . mkdir_p ( upload_path )
FileUtils . touch ( file_path )
2019-10-18 07:11:44 -04:00
uploaded_file = UploadedFile . new ( file_path , filename : File . basename ( file_path ) )
elsif remote_object
uploaded_file = fog_to_uploaded_file ( remote_object )
end
2017-09-07 17:27:04 -04:00
Verify that LFS upload requests are genuine
LFS uploads are handled in concert by workhorse and rails. In normal
use, workhorse:
* Authorizes the request with rails (upload_authorize)
* Handles the upload of the file to a tempfile - disk or object storage
* Validates the file size and contents
* Hands off to rails to complete the upload (upload_finalize)
In `upload_finalize`, the LFS object is linked to the project. As LFS
objects are deduplicated across all projects, it may already exist. If
not, the temporary file is copied to the correct place, and will be
used by all future LFS objects with the same OID.
Workhorse uses the Content-Type of the request to decide to follow this
routine, as the URLs are ambiguous. If the Content-Type is anything but
"application/octet-stream", the request is proxied directly to rails,
on the assumption that this is a normal file edit request. If it's an
actual LFS request with a different content-type, however, it is routed
to the Rails `upload_finalize` action, which treats it as an LFS upload
just as it would a workhorse-modified request.
The outcome is that users can upload LFS objects that don't match the
declared size or OID. They can also create links to LFS objects they
don't really own, allowing them to read the contents of files if they
know just the size or OID.
We can close this hole by requiring requests to `upload_finalize` to be
sourced from Workhorse. The mechanism to do this already exists.
2019-01-08 12:57:58 -05:00
finalize_headers = headers
finalize_headers . merge! ( workhorse_internal_api_request_header ) if verified
2019-10-18 07:11:44 -04:00
workhorse_finalize (
objects_url ( project , sample_oid , sample_size ) ,
method : :put ,
file_key : :file ,
params : args . merge ( file : uploaded_file ) ,
headers : finalize_headers
)
2018-03-23 06:07:22 -04:00
end
2017-09-07 17:27:04 -04:00
2018-03-23 06:07:22 -04:00
def lfs_tmp_file
" #{ sample_oid } 012345678 "
2017-09-07 17:27:04 -04:00
end
2016-07-15 08:01:34 -04:00
end
end