Commit ec81ecae authored by Markus Koller's avatar Markus Koller Committed by Mark Chao

Refactor Git/LFS request specs into shared examples

This allows us to reuse them in EE.

- The shared examples for `Repositories::GitHttpController` were moved
  without any changes.

- The LFS request specs were cleaned up and simplified a bit.
parent c69f85f2
......@@ -9,120 +9,6 @@ RSpec.describe Repositories::GitHttpController do
let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) }
let_it_be(:project_snippet) { create(:project_snippet, :public, :repository, project: project) }
shared_examples Repositories::GitHttpController do
let(:repository_path) { "#{container.full_path}.git" }
let(:params) { { repository_path: repository_path } }
describe 'HEAD #info_refs' do
it 'returns 403' do
head :info_refs, params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
describe 'GET #info_refs' do
let(:params) { super().merge(service: 'git-upload-pack') }
it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do
stub_application_setting(enabled_git_access_protocol: 'ssh')
allow(controller).to receive(:basic_auth_provided?).and_call_original
expect(controller).to receive(:http_download_allowed?).and_call_original
get :info_refs, params: params
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'calls the right access checker class with the right object' do
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
access_double = double
options = {
authentication_abilities: [:download_code],
repository_path: repository_path,
redirected_path: nil,
auth_result_type: :none
}
expect(access_checker_class).to receive(:new)
.with(nil, container, 'http', hash_including(options))
.and_return(access_double)
allow(access_double).to receive(:check).and_return(false)
get :info_refs, params: params
end
context 'with authorized user' do
before do
request.headers.merge! auth_env(user.username, user.password, nil)
end
it 'returns 200' do
get :info_refs, params: params
expect(response).to have_gitlab_http_status(:ok)
end
it 'updates the user activity' do
expect_next_instance_of(Users::ActivityService) do |activity_service|
expect(activity_service).to receive(:execute)
end
get :info_refs, params: params
end
include_context 'parsed logs' do
it 'adds user info to the logs' do
get :info_refs, params: params
expect(log_data).to include('username' => user.username,
'user_id' => user.id,
'meta.user' => user.username)
end
end
end
context 'with exceptions' do
before do
allow(controller).to receive(:authenticate_user).and_return(true)
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
end
it 'returns 503 with GRPC Unavailable' do
allow(controller).to receive(:access_check).and_raise(GRPC::Unavailable)
get :info_refs, params: params
expect(response).to have_gitlab_http_status(:service_unavailable)
end
it 'returns 503 with timeout error' do
allow(controller).to receive(:access_check).and_raise(Gitlab::GitAccess::TimeoutError)
get :info_refs, params: params
expect(response).to have_gitlab_http_status(:service_unavailable)
expect(response.body).to eq 'Gitlab::GitAccess::TimeoutError'
end
end
end
describe 'POST #git_upload_pack' do
before do
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
end
it 'returns 200' do
post :git_upload_pack, params: params
expect(response).to have_gitlab_http_status(:ok)
end
end
end
context 'when repository container is a project' do
it_behaves_like Repositories::GitHttpController do
let(:container) { project }
......
......@@ -6,1204 +6,1145 @@ RSpec.describe 'Git LFS API and storage' do
include ProjectForksHelper
include WorkhorseHelpers
let_it_be(:project, reload: true) { create(:project, :repository) }
let_it_be(:other_project) { create(:project, :repository) }
let_it_be(:project, reload: true) { create(:project, :empty_repo) }
let_it_be(:user) { create(:user) }
let(:lfs_object) { create(:lfs_object, :with_file) }
let(:headers) do
{
'Authorization' => authorization,
'X-Sendfile-Type' => 'X-Sendfile'
}.compact
end
let(:include_workhorse_jwt_header) { true }
let(:authorization) { }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:sample_oid) { lfs_object.oid }
let(:sample_size) { lfs_object.size }
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] }
let(:lfs_enabled) { true }
before do
stub_lfs_setting(enabled: lfs_enabled)
end
context 'project specific LFS settings' do
let(:body) { upload_body(sample_object) }
let(:authorization) { authorize_user }
before do
project.add_maintainer(user)
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 { post_lfs_json(batch_url(project), body, headers) }
it_behaves_like 'LFS http 404 response'
end
context 'with projects' do
it_behaves_like 'LFS http requests' do
let_it_be(:other_project, reload: true) { create(:project, :empty_repo) }
context 'when downloading' do
subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
let(:container) { project }
let(:authorize_guest) { project.add_guest(user) }
let(:authorize_download) { project.add_reporter(user) }
let(:authorize_upload) { project.add_developer(user) }
it_behaves_like 'LFS http 404 response'
end
end
context 'project specific LFS settings' do
let(:body) { upload_body(sample_object) }
describe 'LFS enabled in project' do
let(:project_lfs_enabled) { true }
before do
authorize_upload
project.update_attribute(:lfs_enabled, project_lfs_enabled)
context 'when uploading' do
subject { post_lfs_json(batch_url(project), body, headers) }
subject
end
it_behaves_like 'LFS http 200 response'
end
describe 'LFS disabled in project' do
let(:project_lfs_enabled) { false }
context 'when downloading' do
subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
context 'when uploading' do
subject(:request) { post_lfs_json(batch_url(project), body, headers) }
it_behaves_like 'LFS http 200 blob response'
end
end
end
it_behaves_like 'LFS http 404 response'
end
describe 'when fetching LFS object' do
let(:update_permissions) { }
let(:before_get) { }
context 'when downloading' do
subject(:request) { get(objects_url(project, sample_oid), params: {}, headers: headers) }
before do
project.lfs_objects << lfs_object
update_permissions
before_get
it_behaves_like 'LFS http 404 response'
end
end
get objects_url(project, sample_oid), params: {}, headers: headers
end
describe 'LFS enabled in project' do
let(:project_lfs_enabled) { true }
context 'when LFS uses object storage' do
let(:authorization) { authorize_user }
context 'when uploading' do
subject(:request) { post_lfs_json(batch_url(project), body, headers) }
let(:update_permissions) do
project.add_maintainer(user)
end
it_behaves_like 'LFS http 200 response'
end
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
context 'when downloading' do
subject(:request) { get(objects_url(project, sample_oid), params: {}, headers: headers) }
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:")
it_behaves_like 'LFS http 200 blob response'
end
end
end
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
describe 'when fetching LFS object' do
subject(:request) { get objects_url(project, sample_oid), params: {}, headers: headers }
it 'responds with redirect' do
expect(response).to have_gitlab_http_status(:found)
end
let(:response) { request && super() }
it 'responds with the file location' do
expect(response.location).to include(lfs_object.reload.file.path)
before do
project.lfs_objects << lfs_object
end
end
end
context 'when deploy key is authorized' do
let(:key) { create(:deploy_key) }
let(:authorization) { authorize_deploy_key }
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
let(:update_permissions) do
project.deploy_keys << key
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
it_behaves_like 'LFS http 200 blob response'
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
context 'when using a user key (LFSToken)' do
let(:authorization) { authorize_user_key }
it 'responds with redirect' do
expect(response).to have_gitlab_http_status(:found)
end
context 'when user allowed' do
let(:update_permissions) do
project.add_maintainer(user)
it 'responds with the file location' do
expect(response.location).to include(lfs_object.reload.file.path)
end
end
end
it_behaves_like 'LFS http 200 blob response'
context 'when deploy key is authorized' do
let_it_be(:key) { create(:deploy_key) }
let(:authorization) { authorize_deploy_key }
context 'when user password is expired' do
let(:user) { create(:user, password_expires_at: 1.minute.ago)}
before do
project.deploy_keys << key
end
it_behaves_like 'LFS http 401 response'
it_behaves_like 'LFS http 200 blob response'
end
context 'when user is blocked' do
let(:user) { create(:user, :blocked)}
it_behaves_like 'LFS http 401 response'
end
end
context 'when using a user key (LFSToken)' do
let(:authorization) { authorize_user_key }
context 'when user not allowed' do
it_behaves_like 'LFS http 404 response'
end
end
context 'when user allowed' do
before do
authorize_download
end
context 'when build is authorized as' do
let(:authorization) { authorize_ci_project }
it_behaves_like 'LFS http 200 blob response'
shared_examples 'can download LFS only from own projects' do
context 'for owned project' do
let(:project) { create(:project, namespace: user.namespace) }
context 'when user password is expired' do
let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
it_behaves_like 'LFS http 200 blob response'
end
it_behaves_like 'LFS http 401 response'
end
context 'for member of project' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
context 'when user is blocked' do
let_it_be(:user) { create(:user, :blocked)}
let(:update_permissions) do
project.add_reporter(user)
it_behaves_like 'LFS http 401 response'
end
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)
context 'when user not allowed' do
it_behaves_like 'LFS http 404 response'
end
end
end
context 'administrator', :enable_admin_mode 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'
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) }
context 'regular user' do
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 'can download LFS only from own projects'
end
it_behaves_like 'LFS http 200 blob response'
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
context 'for member of project' do
before do
authorize_download
end
it_behaves_like 'can download LFS only from own projects'
end
end
end
it_behaves_like 'LFS http 200 blob response'
end
describe 'when handling LFS batch request' do
let(:update_lfs_permissions) { }
let(:update_user_permissions) { }
let(:lfs_chunked_encoding) { true }
context 'for other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
before do
update_lfs_permissions
update_user_permissions
stub_feature_flags(lfs_chunked_encoding: lfs_chunked_encoding)
post_lfs_json batch_url(project), body, headers
end
it 'rejects downloading code' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
shared_examples 'process authorization header' do |renew_authorization:|
let(:response_authorization) do
authorization_in_action(lfs_actions.first)
end
context 'administrator', :enable_admin_mode do
let_it_be(:user) { create(:admin) }
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)
it_behaves_like 'can download LFS only from own projects'
end
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
context 'regular user' do
it_behaves_like 'can download LFS only from own projects'
end
it 'generates only one new token per each request' do
authorizations = lfs_actions.map do |action|
authorization_in_action(action)
end.compact
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
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)
it_behaves_like 'can download LFS only from own projects'
end
end
end
def lfs_actions
json_response['objects'].map { |a| a['actions'] }.compact
end
describe 'when handling LFS batch request' do
subject(:request) { post_lfs_json batch_url(project), body, headers }
def authorization_in_action(action)
(action['upload'] || action['download']).dig('header', 'Authorization')
end
end
let(:response) { request && super() }
let(:lfs_chunked_encoding) { true }
describe 'download' do
let(:body) { download_body(sample_object) }
before do
stub_feature_flags(lfs_chunked_encoding: lfs_chunked_encoding)
project.lfs_objects << lfs_object
end
shared_examples 'an authorized request' do |renew_authorization:|
context 'when downloading an LFS object that is assigned to our project' do
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
shared_examples 'process authorization header' do |renew_authorization:|
let(:response_authorization) do
authorization_in_action(lfs_actions.first)
end
it_behaves_like 'LFS http 200 response'
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 '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 'returns a valid token' do
username, token = ::Base64.decode64(response_authorization.split(' ', 2).last).split(':', 2)
it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
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
context 'when downloading an LFS object that is assigned to other project' do
let(:update_lfs_permissions) do
other_project.lfs_objects << lfs_object
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
it_behaves_like 'LFS http 200 response'
def lfs_actions
json_response['objects'].map { |a| a['actions'] }.compact
end
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")
def authorization_in_action(action)
(action['upload'] || action['download']).dig('header', 'Authorization')
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'
describe 'download' do
let(:body) { download_body(sample_object) }
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
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'
context 'when downloading one new and one existing LFS object' do
let(:body) { download_body(multiple_objects) }
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
end
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 'LFS http 200 response'
it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
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"
}
})
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 'process authorization header', renew_authorization: renew_authorization
end
it_behaves_like 'LFS http 200 response'
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 '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
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))
context 'when downloading a LFS object that does not exist' do
let(:body) { download_body(non_existing_object) }
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 'LFS http 200 response'
it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
end
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 user is authenticated' do
let(:authorization) { authorize_user }
context 'when downloading one existing and one missing LFS object' do
let(:body) { download_body(multiple_objects) }
let(:update_user_permissions) do
project.add_role(user, role)
end
it_behaves_like 'LFS http 200 response'
it_behaves_like 'an authorized request', renew_authorization: true do
let(:role) { :reporter }
end
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
context 'when user is not a member of the project' do
let(:update_user_permissions) { nil }
it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
it_behaves_like 'LFS http 404 response'
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) }
context 'when user does not have download access' do
let(:role) { :guest }
before do
project.lfs_objects << other_object
end
it_behaves_like 'LFS http 404 response'
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))
context 'when user password is expired' do
let(:role) { :reporter}
let(:user) { create(:user, password_expires_at: 1.minute.ago)}
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 '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")
it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
end
end
context 'when user is blocked' do
let(:role) { :reporter}
let(:user) { create(:user, :blocked)}
context 'when user is authenticated' do
before do
project.add_role(user, role) if role
end
it_behaves_like 'LFS http 401 response'
end
end
it_behaves_like 'an authorized request', renew_authorization: true do
let(:role) { :reporter }
end
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 user is not a member of the project' do
let(:role) { nil }
context 'when Deploy Token is not valid' do
let(:deploy_token) { create(:deploy_token, projects: [project], read_repository: false) }
it_behaves_like 'LFS http 404 response'
end
it_behaves_like 'LFS http 401 response'
end
context 'when user does not have download access' do
let(:role) { :guest }
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 404 response'
end
it_behaves_like 'LFS http 401 response'
end
context 'when user password is expired' do
let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
let(:role) { :reporter}
# 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]) }
# TODO: This should return a 404 response
# https://gitlab.com/gitlab-org/gitlab/-/issues/292006
it_behaves_like 'LFS http 200 response'
end
it_behaves_like 'an authorized request', renew_authorization: false
end
end
context 'when user is blocked' do
let_it_be(:user) { create(:user, :blocked)}
let(:role) { :reporter}
context 'when build is authorized as' do
let(:authorization) { authorize_ci_project }
it_behaves_like 'LFS http 401 response'
end
end
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
end
context 'when using Deploy Tokens' do
let(:authorization) { authorize_deploy_token }
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) }
context 'when Deploy Token is not valid' do
let(:deploy_token) { create(:deploy_token, projects: [project], read_repository: false) }
let(:update_user_permissions) do
project.add_reporter(user)
it_behaves_like 'LFS http 401 response'
end
it_behaves_like 'an authorized request', renew_authorization: renew_authorization
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 'for other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
# 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 'rejects downloading code' do
expect(response).to have_gitlab_http_status(:not_found)
it_behaves_like 'an authorized request', renew_authorization: false
end
end
end
context 'administrator', :enable_admin_mode do
let(:user) { create(:admin) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
context 'when build is authorized as' do
let(:authorization) { authorize_ci_project }
it_behaves_like 'can download LFS only from own projects', renew_authorization: true
end
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) }
context 'regular user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
before do
authorize_download
end
it_behaves_like 'can download LFS only from own projects', renew_authorization: true
end
it_behaves_like 'an authorized request', renew_authorization: renew_authorization
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
context 'for other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
it_behaves_like 'can download LFS only from own projects', renew_authorization: false
end
end
it 'rejects downloading code' do
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
context 'when user is not authenticated' do
describe 'is accessing public project' do
let(:project) { create(:project, :public) }
context 'administrator', :enable_admin_mode do
let_it_be(:user) { create(:admin) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
end
it_behaves_like 'can download LFS only from own projects', renew_authorization: true
end
it_behaves_like 'LFS http 200 response'
context 'regular user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
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
it_behaves_like 'can download LFS only from own projects', renew_authorization: true
end
describe 'is accessing non-public project' do
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it_behaves_like 'LFS http 401 response'
end
end
end
it_behaves_like 'can download LFS only from own projects', renew_authorization: false
end
end
describe 'upload' do
let(:project) { create(:project, :public) }
let(:body) { upload_body(sample_object) }
context 'when user is not authenticated' do
let(:authorization) { nil }
shared_examples 'pushes new LFS objects' do |renew_authorization:|
let(:sample_size) { 150.megabytes }
let(:sample_oid) { non_existing_object_oid }
describe 'is accessing public project' do
let_it_be(:project) { create(:project, :public) }
it_behaves_like 'LFS http 200 response'
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))
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
headers = json_response['objects'].first['actions']['upload']['header']
expect(headers['Content-Type']).to eq('application/octet-stream')
expect(headers['Transfer-Encoding']).to eq('chunked')
describe 'is accessing non-public project' do
it_behaves_like 'LFS http 401 response'
end
end
end
context 'when lfs_chunked_encoding feature is disabled' do
let(:lfs_chunked_encoding) { false }
describe 'upload' do
let_it_be(:project) { create(:project, :public) }
let(:body) { upload_body(sample_object) }
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))
shared_examples 'pushes new LFS objects' do |renew_authorization:|
let(:sample_size) { 150.megabytes }
let(:sample_oid) { non_existing_object_oid }
headers = json_response['objects'].first['actions']['upload']['header']
expect(headers['Content-Type']).to eq('application/octet-stream')
expect(headers['Transfer-Encoding']).to be_nil
end
end
it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
it_behaves_like 'LFS http 200 response'
describe 'when request is authenticated' do
describe 'when user has project push access' do
let(:authorization) { authorize_user }
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))
let(:update_user_permissions) do
project.add_developer(user)
end
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
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'
context 'when lfs_chunked_encoding feature is disabled' do
let(:lfs_chunked_encoding) { false }
it 'responds with links the object to the project' do
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(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')
expect(headers['Transfer-Encoding']).to be_nil
end
it_behaves_like 'process authorization header', renew_authorization: true
end
let(:update_lfs_permissions) do
other_project.lfs_objects << lfs_object
end
it_behaves_like 'process authorization header', renew_authorization: renew_authorization
end
context 'in another project' do
it_behaves_like 'batch upload with existing LFS object'
end
describe 'when request is authenticated' do
describe 'when user has project push access' do
before do
authorize_upload
end
context 'in source of fork project' do
let(:project) { fork_project(other_project) }
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_behaves_like 'batch upload with existing LFS object'
end
end
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))
context 'when pushing a LFS object that does not exist' do
it_behaves_like 'pushes new LFS objects', renew_authorization: true
end
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
context 'when pushing one new and one existing LFS object' do
let(:body) { upload_body(multiple_objects) }
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
end
it_behaves_like 'process authorization header', renew_authorization: true
end
it_behaves_like 'LFS http 200 response'
context 'in another project' do
before do
lfs_object.update!(projects: [other_project])
end
it 'responds with upload hypermedia link for the new object' do
expect(json_response['objects']).to be_kind_of(Array)
it_behaves_like 'batch upload with existing LFS object'
end
expect(json_response['objects'].first).to include(sample_object)
expect(json_response['objects'].first).not_to have_key('actions')
context 'in source of fork project' do
let(:project) { fork_project(other_project) }
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))
before do
lfs_object.update!(projects: [other_project])
end
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 'batch upload with existing LFS object'
end
end
it_behaves_like 'process authorization header', renew_authorization: true
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 user does not have push access' do
let(:authorization) { authorize_user }
context 'when pushing one new and one existing LFS object' do
let(:body) { upload_body(multiple_objects) }
it_behaves_like 'LFS http 403 response'
end
it_behaves_like 'LFS http 200 response'
context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
it 'responds with upload hypermedia link for the new object' do
expect(json_response['objects']).to be_kind_of(Array)
context 'build has an user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
expect(json_response['objects'].first).to include(sample_object)
expect(json_response['objects'].first).not_to have_key('actions')
context 'tries to push to own project' do
it_behaves_like 'LFS http 403 response'
end
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))
context 'tries to push to other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
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
# I'm not sure what this tests that is different from the previous test
context 'when user does not have push access' do
it_behaves_like 'LFS http 403 response'
end
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
it_behaves_like 'LFS http 403 response'
end
end
context 'build has an user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
context 'when deploy key has project push access' do
let(:key) { create(:deploy_key) }
let(:authorization) { authorize_deploy_key }
context 'tries to push to own project' do
it_behaves_like 'LFS http 403 response'
end
let(:update_user_permissions) do
project.deploy_keys_projects.create!(deploy_key: key, can_push: true)
end
context 'tries to push to other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
it_behaves_like 'pushes new LFS objects', renew_authorization: false
end
end
# I'm not sure what this tests that is different from the previous test
it_behaves_like 'LFS http 403 response'
end
end
context 'when user is not authenticated' do
context 'when user has push access' do
let(:update_user_permissions) do
project.add_maintainer(user)
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it_behaves_like 'LFS http 401 response'
end
it_behaves_like 'LFS http 403 response'
end
end
context 'when user does not have push access' do
it_behaves_like 'LFS http 401 response'
end
end
end
context 'when deploy key has project push access' do
let(:key) { create(:deploy_key) }
let(:authorization) { authorize_deploy_key }
describe 'unsupported' do
let(:authorization) { authorize_user }
let(:body) { request_body('other', sample_object) }
before do
project.deploy_keys_projects.create!(deploy_key: key, can_push: true)
end
it_behaves_like 'LFS http 404 response'
end
end
it_behaves_like 'pushes new LFS objects', renew_authorization: false
end
end
describe 'when handling LFS batch request on a read-only GitLab instance' do
let(:authorization) { authorize_user }
context 'when user is not authenticated' do
let(:authorization) { nil }
subject { post_lfs_json(batch_url(project), body, headers) }
context 'when user has push access' do
before do
authorize_upload
end
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
it_behaves_like 'LFS http 401 response'
end
project.add_maintainer(user)
context 'when user does not have push access' do
it_behaves_like 'LFS http 401 response'
end
end
end
subject
end
describe 'unsupported' do
let(:body) { request_body('other', sample_object) }
context 'when downloading' do
let(:body) { download_body(sample_object) }
it_behaves_like 'LFS http 404 response'
end
end
it_behaves_like 'LFS http 200 response'
end
describe 'when handling LFS batch request on a read-only GitLab instance' do
subject { post_lfs_json(batch_url(project), body, headers) }
context 'when uploading' do
let(:body) { upload_body(sample_object) }
before do
allow(Gitlab::Database).to receive(:read_only?) { true }
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
project.add_maintainer(user)
describe 'when pushing a LFS object' do
shared_examples 'unauthorized' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
put_authorize
subject
end
it_behaves_like 'LFS http 401 response'
end
context 'when downloading' do
let(:body) { download_body(sample_object) }
context 'and request is sent by gitlab-workhorse to finalize the upload' do
before do
put_finalize
it_behaves_like 'LFS http 200 response'
end
it_behaves_like 'LFS http 401 response'
end
context 'when uploading' do
let(:body) { upload_body(sample_object) }
context 'and request is sent with a malformed headers' do
before do
put_finalize('/etc/passwd')
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
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
describe 'when pushing a LFS object' do
let(:include_workhorse_jwt_header) { true }
it_behaves_like 'LFS http 403 response'
end
shared_examples 'unauthorized' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
put_authorize
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
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
context 'and request is sent with a malformed headers' do
before do
put_finalize('/etc/passwd')
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
it_behaves_like 'LFS http 403 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
describe 'to one project' do
describe 'when user is authenticated' do
let(:authorization) { authorize_user }
it_behaves_like 'LFS http 403 response'
end
describe 'when user has push access to the project' do
before do
project.add_developer(user)
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 the request bypassed workhorse' do
it 'raises an exception' do
expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError
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
context 'and request is sent by gitlab-workhorse to authorize the request' do
shared_examples 'a valid response' do
describe 'to one project' do
describe 'when user is authenticated' do
describe 'when user has push access to the project' do
before do
put_authorize
project.add_developer(user)
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)
context 'and the request bypassed workhorse' do
it 'raises an exception' do
expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError
end
end
end
context 'when using local storage' do
it_behaves_like 'a local file'
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
context 'when using remote storage' do
context 'when direct upload is enabled' do
before do
stub_lfs_object_storage(enabled: true, direct_upload: true)
it_behaves_like 'LFS http 200 workhorse response'
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']).not_to have_key('MultipartUpload')
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
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
end
context 'when direct upload is disabled' do
before do
stub_lfs_object_storage(enabled: true, direct_upload: false)
context 'when using local storage' do
it_behaves_like 'a local file'
end
it_behaves_like 'a local file'
end
end
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
context 'and request is sent by gitlab-workhorse to finalize the upload' do
before do
put_finalize
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']).not_to have_key('MultipartUpload')
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end
end
end
it_behaves_like 'LFS http 200 response'
context 'when direct upload is disabled' do
before do
stub_lfs_object_storage(enabled: true, direct_upload: false)
end
it 'LFS object is linked to the project' do
expect(lfs_object.projects.pluck(:id)).to include(project.id)
end
end
it_behaves_like 'a local file'
end
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 request is sent by gitlab-workhorse to finalize the upload' do
before do
put_finalize
end
context 'and workhorse requests upload finalize for a new LFS object' do
before do
lfs_object.destroy!
end
it_behaves_like 'LFS http 200 response'
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)
it 'LFS object is linked to the project' do
expect(lfs_object.projects.pluck(:id)).to include(project.id)
end
end
put_finalize(with_tempfile: true)
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
end
context 'with object storage enabled' do
context 'and direct upload enabled' do
let!(:fog_connection) do
stub_lfs_object_storage(direct_upload: true)
context 'and workhorse requests upload finalize for a new LFS object' do
before do
lfs_object.destroy!
end
let(:tmp_object) do
fog_connection.directories.new(key: 'lfs-objects').files.create( # rubocop: disable Rails/SaveBang
key: 'tmp/uploads/12312300',
body: 'content'
)
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
['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
})
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
it 'responds with status 403' do
subject
['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)
expect(response).to have_gitlab_http_status(:forbidden)
end
end
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
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
it 'responds with status 200' do
subject
expect(response).to have_gitlab_http_status(:ok)
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
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 'schedules migration of file to object storage' do
subject
it 'have valid file' do
subject
expect(LfsObject.last.projects).to include(project)
expect(LfsObject.last.file_store).to eq(ObjectStorage::Store::REMOTE)
expect(LfsObject.last.file).to be_exists
end
end
end
it 'have valid file' do
subject
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))
expect(LfsObject.last.file_store).to eq(ObjectStorage::Store::REMOTE)
expect(LfsObject.last.file).to be_exists
put_finalize(with_tempfile: true)
end
end
end
end
context 'and background upload enabled' do
context 'without the lfs object' do
before do
stub_lfs_object_storage(background_upload: true)
lfs_object.destroy!
end
it 'schedules migration of file to object storage' do
expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('LfsObjectUploader', 'LfsObject', :file, kind_of(Numeric))
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)
put_finalize(with_tempfile: true)
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
end
end
end
context 'without the lfs object' do
before do
lfs_object.destroy!
end
describe 'and user does not have push access' do
before do
project.add_reporter(user)
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)
it_behaves_like 'forbidden'
end
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)
context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
expect(response).to have_gitlab_http_status(:unprocessable_entity)
end
end
end
end
context 'build has an user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
describe 'and user does not have push access' do
before do
project.add_reporter(user)
end
context 'tries to push to own project' do
before do
project.add_developer(user)
put_authorize
end
it_behaves_like 'forbidden'
end
end
it_behaves_like 'LFS http 403 response'
end
context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
context 'tries to push to other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
context 'build has an user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
before do
put_authorize
end
context 'tries to push to own project' do
before do
project.add_developer(user)
put_authorize
it_behaves_like 'LFS http 404 response'
end
end
it_behaves_like 'LFS http 403 response'
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
context 'tries to push to other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
before do
put_authorize
end
before do
put_authorize
it_behaves_like 'LFS http 403 response'
end
it_behaves_like 'LFS http 404 response'
end
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
describe 'when using a user key (LFSToken)' do
let(:authorization) { authorize_user_key }
before do
put_authorize
end
context 'when user allowed' do
before do
project.add_developer(user)
put_authorize
end
it_behaves_like 'LFS http 403 response'
end
end
it_behaves_like 'LFS http 200 workhorse response'
describe 'when using a user key (LFSToken)' do
let(:authorization) { authorize_user_key }
context 'when user password is expired' do
let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
context 'when user allowed' do
before do
project.add_developer(user)
put_authorize
end
it_behaves_like 'LFS http 401 response'
end
it_behaves_like 'LFS http 200 workhorse response'
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 password is expired' do
let(:user) { create(:user, password_expires_at: 1.minute.ago)}
context 'when user not allowed' do
before do
put_authorize
end
it_behaves_like 'LFS http 401 response'
it_behaves_like 'LFS http 404 response'
end
end
context 'when user is blocked' do
let(:user) { create(:user, :blocked)}
context 'for unauthenticated' do
let(:authorization) { nil }
it_behaves_like 'LFS http 401 response'
it_behaves_like 'unauthorized'
end
end
context 'when user not allowed' do
before do
put_authorize
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) }
it_behaves_like 'LFS http 404 response'
end
end
describe 'when user is authenticated' do
describe 'when user has push access to the project' do
before do
project.add_developer(user)
end
context 'for unauthenticated' do
it_behaves_like 'unauthorized'
end
end
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
put_authorize
end
describe 'to a forked project' do
let(:upstream_project) { create(:project, :public) }
let(:project_owner) { create(:user) }
let(:project) { fork_project(upstream_project, project_owner) }
it_behaves_like 'LFS http 200 workhorse response'
describe 'when user is authenticated' do
let(:authorization) { authorize_user }
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
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 finalize the upload' do
before do
put_finalize
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 response'
it_behaves_like 'LFS http 200 workhorse response'
it 'LFS object is linked to the forked project' do
expect(lfs_object.projects.pluck(:id)).to include(project.id)
end
end
end
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)
describe 'and user does not have push access' do
it_behaves_like 'forbidden'
end
end
context 'and request is sent by gitlab-workhorse to finalize the upload' do
context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
before do
put_finalize
put_authorize
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
context 'build has an user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
describe 'and user does not have push access' do
it_behaves_like 'forbidden'
end
end
context 'tries to push to own project' do
it_behaves_like 'LFS http 403 response'
end
context 'when build is authorized' do
let(:authorization) { authorize_ci_project }
context 'tries to push to other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
before do
put_authorize
end
# I'm not sure what this tests that is different from the previous test
it_behaves_like 'LFS http 403 response'
end
end
context 'build has an user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
context 'tries to push to own project' do
it_behaves_like 'LFS http 403 response'
it_behaves_like 'LFS http 403 response'
end
end
context 'tries to push to other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
context 'for unauthenticated' do
let(:authorization) { nil }
# I'm not sure what this tests that is different from the previous test
it_behaves_like 'LFS http 403 response'
it_behaves_like 'unauthorized'
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
describe 'and second project not related to fork or a source project' do
let_it_be(:second_project) { create(:project) }
context 'for unauthenticated' do
it_behaves_like 'unauthorized'
end
before do
second_project.add_maintainer(user)
upstream_project.lfs_objects << lfs_object
end
describe 'and second project not related to fork or a source project' do
let(:second_project) { create(:project) }
let(:authorization) { authorize_user }
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)
before do
second_project.add_maintainer(user)
upstream_project.lfs_objects << lfs_object
end
put objects_url(second_project, sample_oid, sample_size),
params: {},
headers: finalize_headers
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)
it_behaves_like 'LFS http 200 response'
put objects_url(second_project, sample_oid, sample_size),
params: {},
headers: finalize_headers
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
it_behaves_like 'LFS http 200 response'
def put_authorize(verified: true)
authorize_headers = headers
authorize_headers.merge!(workhorse_internal_api_request_header) if verified
it 'links the LFS object to the project' do
expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id)
end
put authorize_url(project, sample_oid, sample_size), params: {}, headers: authorize_headers
end
end
end
def put_authorize(verified: true)
authorize_headers = headers
authorize_headers.merge!(workhorse_internal_api_request_header) if verified
def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, remote_object: nil, args: {})
uploaded_file = nil
put authorize_url(project, sample_oid, sample_size), params: {}, headers: authorize_headers
end
if with_tempfile
upload_path = LfsObjectUploader.workhorse_local_upload_path
file_path = upload_path + '/' + lfs_tmp if lfs_tmp
def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, remote_object: nil, args: {})
uploaded_file = nil
FileUtils.mkdir_p(upload_path)
FileUtils.touch(file_path)
if with_tempfile
upload_path = LfsObjectUploader.workhorse_local_upload_path
file_path = upload_path + '/' + lfs_tmp if lfs_tmp
uploaded_file = UploadedFile.new(file_path, filename: File.basename(file_path))
elsif remote_object
uploaded_file = fog_to_uploaded_file(remote_object)
end
FileUtils.mkdir_p(upload_path)
FileUtils.touch(file_path)
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
uploaded_file = UploadedFile.new(file_path, filename: File.basename(file_path))
elsif remote_object
uploaded_file = fog_to_uploaded_file(remote_object)
def lfs_tmp_file
"#{sample_oid}012345678"
end
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
context 'with projects' do
it_behaves_like 'LFS http requests' do
let(:container) { project }
let(:authorize_guest) { project.add_guest(user) }
let(:authorize_download) { project.add_reporter(user) }
let(:authorize_upload) { project.add_developer(user) }
end
end
......
# frozen_string_literal: true
RSpec.shared_examples Repositories::GitHttpController do
include GitHttpHelpers
let(:repository_path) { "#{container.full_path}.git" }
let(:params) { { repository_path: repository_path } }
describe 'HEAD #info_refs' do
it 'returns 403' do
head :info_refs, params: params
expect(response).to have_gitlab_http_status(:forbidden)
end
end
describe 'GET #info_refs' do
let(:params) { super().merge(service: 'git-upload-pack') }
it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do
stub_application_setting(enabled_git_access_protocol: 'ssh')
allow(controller).to receive(:basic_auth_provided?).and_call_original
expect(controller).to receive(:http_download_allowed?).and_call_original
get :info_refs, params: params
expect(response).to have_gitlab_http_status(:unauthorized)
end
it 'calls the right access checker class with the right object' do
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
access_double = double
options = {
authentication_abilities: [:download_code],
repository_path: repository_path,
redirected_path: nil,
auth_result_type: :none
}
expect(access_checker_class).to receive(:new)
.with(nil, container, 'http', hash_including(options))
.and_return(access_double)
allow(access_double).to receive(:check).and_return(false)
get :info_refs, params: params
end
context 'with authorized user' do
before do
request.headers.merge! auth_env(user.username, user.password, nil)
end
it 'returns 200' do
get :info_refs, params: params
expect(response).to have_gitlab_http_status(:ok)
end
it 'updates the user activity' do
expect_next_instance_of(Users::ActivityService) do |activity_service|
expect(activity_service).to receive(:execute)
end
get :info_refs, params: params
end
include_context 'parsed logs' do
it 'adds user info to the logs' do
get :info_refs, params: params
expect(log_data).to include('username' => user.username,
'user_id' => user.id,
'meta.user' => user.username)
end
end
end
context 'with exceptions' do
before do
allow(controller).to receive(:authenticate_user).and_return(true)
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
end
it 'returns 503 with GRPC Unavailable' do
allow(controller).to receive(:access_check).and_raise(GRPC::Unavailable)
get :info_refs, params: params
expect(response).to have_gitlab_http_status(:service_unavailable)
end
it 'returns 503 with timeout error' do
allow(controller).to receive(:access_check).and_raise(Gitlab::GitAccess::TimeoutError)
get :info_refs, params: params
expect(response).to have_gitlab_http_status(:service_unavailable)
expect(response.body).to eq 'Gitlab::GitAccess::TimeoutError'
end
end
end
describe 'POST #git_upload_pack' do
before do
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
end
it 'returns 200' do
post :git_upload_pack, params: params
expect(response).to have_gitlab_http_status(:ok)
end
end
end
......@@ -65,12 +65,19 @@ end
RSpec.shared_examples 'LFS http requests' do
include LfsHttpHelpers
let(:lfs_enabled) { true }
let(:authorize_guest) {}
let(:authorize_download) {}
let(:authorize_upload) {}
let(:lfs_object) { create(:lfs_object, :with_file) }
let(:sample_oid) { lfs_object.oid }
let(:sample_size) { lfs_object.size }
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] }
let(:authorization) { authorize_user }
let(:headers) do
......@@ -89,13 +96,11 @@ RSpec.shared_examples 'LFS http requests' do
end
before do
stub_lfs_setting(enabled: true)
stub_lfs_setting(enabled: lfs_enabled)
end
context 'when LFS is disabled globally' do
before do
stub_lfs_setting(enabled: false)
end
let(:lfs_enabled) { false }
describe 'download request' do
before do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment