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 ...@@ -9,120 +9,6 @@ RSpec.describe Repositories::GitHttpController do
let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) } let_it_be(:personal_snippet) { create(:personal_snippet, :public, :repository) }
let_it_be(:project_snippet) { create(:project_snippet, :public, :repository, project: project) } 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 context 'when repository container is a project' do
it_behaves_like Repositories::GitHttpController do it_behaves_like Repositories::GitHttpController do
let(:container) { project } let(:container) { project }
......
...@@ -6,1204 +6,1145 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -6,1204 +6,1145 @@ RSpec.describe 'Git LFS API and storage' do
include ProjectForksHelper include ProjectForksHelper
include WorkhorseHelpers include WorkhorseHelpers
let_it_be(:project, reload: true) { create(:project, :repository) } let_it_be(:project, reload: true) { create(:project, :empty_repo) }
let_it_be(:other_project) { create(:project, :repository) }
let_it_be(:user) { create(:user) } let_it_be(:user) { create(:user) }
let(:lfs_object) { create(:lfs_object, :with_file) }
let(:headers) do context 'with projects' do
{ it_behaves_like 'LFS http requests' do
'Authorization' => authorization, let_it_be(:other_project, reload: true) { create(:project, :empty_repo) }
'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 'when downloading' do let(:container) { project }
subject { get(objects_url(project, sample_oid), params: {}, headers: headers) } 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' context 'project specific LFS settings' do
end let(:body) { upload_body(sample_object) }
end
describe 'LFS enabled in project' do before do
let(:project_lfs_enabled) { true } authorize_upload
project.update_attribute(:lfs_enabled, project_lfs_enabled)
context 'when uploading' do subject
subject { post_lfs_json(batch_url(project), body, headers) } end
it_behaves_like 'LFS http 200 response' describe 'LFS disabled in project' do
end let(:project_lfs_enabled) { false }
context 'when downloading' do context 'when uploading' do
subject { get(objects_url(project, sample_oid), params: {}, headers: headers) } subject(:request) { post_lfs_json(batch_url(project), body, headers) }
it_behaves_like 'LFS http 200 blob response' it_behaves_like 'LFS http 404 response'
end end
end
end
describe 'when fetching LFS object' do context 'when downloading' do
let(:update_permissions) { } subject(:request) { get(objects_url(project, sample_oid), params: {}, headers: headers) }
let(:before_get) { }
before do it_behaves_like 'LFS http 404 response'
project.lfs_objects << lfs_object end
update_permissions end
before_get
get objects_url(project, sample_oid), params: {}, headers: headers describe 'LFS enabled in project' do
end let(:project_lfs_enabled) { true }
context 'when LFS uses object storage' do context 'when uploading' do
let(:authorization) { authorize_user } subject(:request) { post_lfs_json(batch_url(project), body, headers) }
let(:update_permissions) do it_behaves_like 'LFS http 200 response'
project.add_maintainer(user) end
end
context 'when proxy download is enabled' do context 'when downloading' do
let(:before_get) do subject(:request) { get(objects_url(project, sample_oid), params: {}, headers: headers) }
stub_lfs_object_storage(proxy_download: true)
lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
end
it 'responds with the workhorse send-url' do it_behaves_like 'LFS http 200 blob response'
expect(response).to have_gitlab_http_status(:ok) end
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
end end
end end
context 'when proxy download is disabled' do describe 'when fetching LFS object' do
let(:before_get) do subject(:request) { get objects_url(project, sample_oid), params: {}, headers: headers }
stub_lfs_object_storage(proxy_download: false)
lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
end
it 'responds with redirect' do let(:response) { request && super() }
expect(response).to have_gitlab_http_status(:found)
end
it 'responds with the file location' do before do
expect(response.location).to include(lfs_object.reload.file.path) project.lfs_objects << lfs_object
end end
end
end
context 'when deploy key is authorized' do context 'when LFS uses object storage' do
let(:key) { create(:deploy_key) } before do
let(:authorization) { authorize_deploy_key } 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 it 'responds with the workhorse send-url' do
project.deploy_keys << key expect(response).to have_gitlab_http_status(:ok)
end expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
end
end
it_behaves_like 'LFS http 200 blob response' context 'when proxy download is disabled' do
end 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 it 'responds with redirect' do
let(:authorization) { authorize_user_key } expect(response).to have_gitlab_http_status(:found)
end
context 'when user allowed' do it 'responds with the file location' do
let(:update_permissions) do expect(response.location).to include(lfs_object.reload.file.path)
project.add_maintainer(user) end
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 before do
let(:user) { create(:user, password_expires_at: 1.minute.ago)} project.deploy_keys << key
end
it_behaves_like 'LFS http 401 response' it_behaves_like 'LFS http 200 blob response'
end end
context 'when user is blocked' do context 'when using a user key (LFSToken)' do
let(:user) { create(:user, :blocked)} let(:authorization) { authorize_user_key }
it_behaves_like 'LFS http 401 response'
end
end
context 'when user not allowed' do context 'when user allowed' do
it_behaves_like 'LFS http 404 response' before do
end authorize_download
end end
context 'when build is authorized as' do it_behaves_like 'LFS http 200 blob response'
let(:authorization) { authorize_ci_project }
shared_examples 'can download LFS only from own projects' do context 'when user password is expired' do
context 'for owned project' do let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
let(:project) { create(:project, namespace: user.namespace) }
it_behaves_like 'LFS http 200 blob response' it_behaves_like 'LFS http 401 response'
end end
context 'for member of project' do context 'when user is blocked' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) } let_it_be(:user) { create(:user, :blocked)}
let(:update_permissions) do it_behaves_like 'LFS http 401 response'
project.add_reporter(user) end
end end
it_behaves_like 'LFS http 200 blob response' context 'when user not allowed' do
end it_behaves_like 'LFS http 404 response'
context 'for other project' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
it 'rejects downloading code' do
expect(response).to have_gitlab_http_status(:not_found)
end end
end end
end
context 'administrator', :enable_admin_mode do context 'when build is authorized as' do
let(:user) { create(:admin) } let(:authorization) { authorize_ci_project }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects'
end
context 'regular user' do shared_examples 'can download LFS only from own projects' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } context 'for owned project' do
let_it_be(:project) { create(:project, namespace: user.namespace) }
it_behaves_like 'can download LFS only from own projects' it_behaves_like 'LFS http 200 blob response'
end end
context 'does not have user' do context 'for member of project' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) } before do
authorize_download
end
it_behaves_like 'can download LFS only from own projects' it_behaves_like 'LFS http 200 blob response'
end end
end
end
describe 'when handling LFS batch request' do context 'for other project' do
let(:update_lfs_permissions) { } let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
let(:update_user_permissions) { }
let(:lfs_chunked_encoding) { true }
before do it 'rejects downloading code' do
update_lfs_permissions expect(response).to have_gitlab_http_status(:not_found)
update_user_permissions end
stub_feature_flags(lfs_chunked_encoding: lfs_chunked_encoding) end
post_lfs_json batch_url(project), body, headers end
end
shared_examples 'process authorization header' do |renew_authorization:| context 'administrator', :enable_admin_mode do
let(:response_authorization) do let_it_be(:user) { create(:admin) }
authorization_in_action(lfs_actions.first)
end
if renew_authorization it_behaves_like 'can download LFS only from own projects'
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 end
it 'returns a a valid token' do context 'regular user' do
username, token = ::Base64.decode64(response_authorization.split(' ', 2).last).split(':', 2) it_behaves_like 'can download LFS only from own projects'
expect(username).to eq(user.username)
expect(Gitlab::LfsToken.new(user).token_valid?(token)).to be_truthy
end end
it 'generates only one new token per each request' do context 'does not have user' do
authorizations = lfs_actions.map do |action| let(:build) { create(:ci_build, :running, pipeline: pipeline) }
authorization_in_action(action)
end.compact
expect(authorizations.uniq.count).to eq 1 it_behaves_like 'can download LFS only from own projects'
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 end
end end
def lfs_actions describe 'when handling LFS batch request' do
json_response['objects'].map { |a| a['actions'] }.compact subject(:request) { post_lfs_json batch_url(project), body, headers }
end
def authorization_in_action(action) let(:response) { request && super() }
(action['upload'] || action['download']).dig('header', 'Authorization') let(:lfs_chunked_encoding) { true }
end
end
describe 'download' do before do
let(:body) { download_body(sample_object) } stub_feature_flags(lfs_chunked_encoding: lfs_chunked_encoding)
project.lfs_objects << lfs_object
end
shared_examples 'an authorized request' do |renew_authorization:| shared_examples 'process authorization header' do |renew_authorization:|
context 'when downloading an LFS object that is assigned to our project' do let(:response_authorization) do
let(:update_lfs_permissions) do authorization_in_action(lfs_actions.first)
project.lfs_objects << lfs_object
end 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 it 'returns a valid token' do
expect(json_response['objects'].first).to include(sample_object) username, token = ::Base64.decode64(response_authorization.split(' ', 2).last).split(':', 2)
expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
end
it_behaves_like 'process authorization header', renew_authorization: renew_authorization expect(username).to eq(user.username)
end 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 expect(authorizations.uniq.count).to eq 1
let(:update_lfs_permissions) do end
other_project.lfs_objects << lfs_object 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 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 def authorization_in_action(action)
expect(json_response['objects'].first).to include(sample_object) (action['upload'] || action['download']).dig('header', 'Authorization')
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
end end
context 'when downloading a LFS object that does not exist' do describe 'download' do
let(:body) { download_body(non_existing_object) } let(:body) { download_body(sample_object) }
it_behaves_like 'LFS http 200 response'
it 'with an 404 for specific object' do shared_examples 'an authorized request' do |renew_authorization:|
expect(json_response['objects'].first).to include(non_existing_object) context 'when downloading an LFS object that is assigned to our project' do
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 'LFS http 200 response'
end
end
context 'when downloading one new and one existing LFS object' do it 'with href to download' do
let(:body) { download_body(multiple_objects) } expect(json_response['objects'].first).to include(sample_object)
let(:update_lfs_permissions) do expect(json_response['objects'].first['actions']['download']['href']).to eq(objects_url(project, sample_oid))
project.lfs_objects << lfs_object end
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 context 'when downloading an LFS object that is assigned to other project' do
expect(json_response['objects'].first).to include(sample_object) before do
expect(json_response['objects'].first['actions']['download']).to include('href' => objects_url(project, sample_oid)) lfs_object.update!(projects: [other_project])
expect(json_response['objects'].last).to eq({ end
'oid' => non_existing_object_oid,
'size' => non_existing_object_size,
'error' => {
'code' => 404,
'message' => "Object does not exist on the server or you don't have permissions to access it"
}
})
end
it_behaves_like 'process authorization header', renew_authorization: renew_authorization it_behaves_like 'LFS http 200 response'
end
context 'when downloading two existing LFS objects' do it 'with an 404 for specific object' do
let(:body) { download_body(multiple_objects) } expect(json_response['objects'].first).to include(sample_object)
let(:other_object) { create(:lfs_object, :with_file, oid: non_existing_object_oid, size: non_existing_object_size) } 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")
let(:update_lfs_permissions) do end
project.lfs_objects << [lfs_object, other_object] end
end
it 'responds with the download hypermedia link for each object' do context 'when downloading a LFS object that does not exist' do
expect(json_response['objects'].first).to include(sample_object) let(:body) { download_body(non_existing_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) it_behaves_like 'LFS http 200 response'
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 it 'with an 404 for specific object' do
end expect(json_response['objects'].first).to include(non_existing_object)
end 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 context 'when downloading one existing and one missing LFS object' do
let(:authorization) { authorize_user } let(:body) { download_body(multiple_objects) }
let(:update_user_permissions) do it_behaves_like 'LFS http 200 response'
project.add_role(user, role)
end
it_behaves_like 'an authorized request', renew_authorization: true do it 'responds with download hypermedia link for the existing object' do
let(:role) { :reporter } expect(json_response['objects'].first).to include(sample_object)
end 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 it_behaves_like 'process authorization header', renew_authorization: renew_authorization
let(:update_user_permissions) { nil } end
it_behaves_like 'LFS http 404 response' context 'when downloading two existing LFS objects' do
end 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 before do
let(:role) { :guest } project.lfs_objects << other_object
end
it_behaves_like 'LFS http 404 response' it 'responds with the download hypermedia link for each object' do
end 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 expect(json_response['objects'].last).to include(non_existing_object)
let(:role) { :reporter} expect(json_response['objects'].last['actions']['download']).to include('href' => objects_url(project, non_existing_object_oid))
let(:user) { create(:user, password_expires_at: 1.minute.ago)} end
it 'with an 404 for specific object' do it_behaves_like 'process authorization header', renew_authorization: renew_authorization
expect(json_response['objects'].first).to include(sample_object) end
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
end
context 'when user is blocked' do context 'when user is authenticated' do
let(:role) { :reporter} before do
let(:user) { create(:user, :blocked)} project.add_role(user, role) if role
end
it_behaves_like 'LFS http 401 response' it_behaves_like 'an authorized request', renew_authorization: true do
end let(:role) { :reporter }
end end
context 'when using Deploy Tokens' do context 'when user is not a member of the project' do
let(:authorization) { authorize_deploy_token } let(:role) { nil }
let(:update_user_permissions) { nil }
let(:role) { nil }
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
end
context 'when Deploy Token is not valid' do it_behaves_like 'LFS http 404 response'
let(:deploy_token) { create(:deploy_token, projects: [project], read_repository: false) } end
it_behaves_like 'LFS http 401 response' context 'when user does not have download access' do
end let(:role) { :guest }
context 'when Deploy Token is not related to the project' do it_behaves_like 'LFS http 404 response'
let(:deploy_token) { create(:deploy_token, projects: [other_project]) } end
it_behaves_like 'LFS http 401 response' context 'when user password is expired' do
end 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. # TODO: This should return a 404 response
context 'when Deploy Token is valid' do # https://gitlab.com/gitlab-org/gitlab/-/issues/292006
let(:deploy_token) { create(:deploy_token, projects: [project]) } it_behaves_like 'LFS http 200 response'
end
it_behaves_like 'an authorized request', renew_authorization: false context 'when user is blocked' do
end let_it_be(:user) { create(:user, :blocked)}
end let(:role) { :reporter}
context 'when build is authorized as' do it_behaves_like 'LFS http 401 response'
let(:authorization) { authorize_ci_project } end
end
let(:update_lfs_permissions) do context 'when using Deploy Tokens' do
project.lfs_objects << lfs_object let(:authorization) { authorize_deploy_token }
end
shared_examples 'can download LFS only from own projects' do |renew_authorization:| context 'when Deploy Token is not valid' do
context 'for own project' do let(:deploy_token) { create(:deploy_token, projects: [project], read_repository: false) }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:update_user_permissions) do it_behaves_like 'LFS http 401 response'
project.add_reporter(user)
end end
it_behaves_like 'an authorized request', renew_authorization: renew_authorization context 'when Deploy Token is not related to the project' do
end let(:deploy_token) { create(:deploy_token, projects: [other_project]) }
it_behaves_like 'LFS http 401 response'
end
context 'for other project' do # TODO: We should fix this test case that causes flakyness by alternating the result of the above test cases.
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } context 'when Deploy Token is valid' do
let(:deploy_token) { create(:deploy_token, projects: [project]) }
it 'rejects downloading code' do it_behaves_like 'an authorized request', renew_authorization: false
expect(response).to have_gitlab_http_status(:not_found)
end end
end end
end
context 'administrator', :enable_admin_mode do context 'when build is authorized as' do
let(:user) { create(:admin) } let(:authorization) { authorize_ci_project }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects', renew_authorization: true shared_examples 'can download LFS only from own projects' do |renew_authorization:|
end context 'for own project' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
context 'regular user' do before do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } authorize_download
end
it_behaves_like 'can download LFS only from own projects', renew_authorization: true it_behaves_like 'an authorized request', renew_authorization: renew_authorization
end end
context 'does not have user' do context 'for other project' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) } let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
it_behaves_like 'can download LFS only from own projects', renew_authorization: false it 'rejects downloading code' do
end expect(response).to have_gitlab_http_status(:not_found)
end end
end
end
context 'when user is not authenticated' do context 'administrator', :enable_admin_mode do
describe 'is accessing public project' do let_it_be(:user) { create(:admin) }
let(:project) { create(:project, :public) } let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
let(:update_lfs_permissions) do it_behaves_like 'can download LFS only from own projects', renew_authorization: true
project.lfs_objects << lfs_object end
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 it_behaves_like 'can download LFS only from own projects', renew_authorization: true
expect(json_response).to eq({ end
'objects' => [
{
'oid' => sample_oid,
'size' => sample_size,
'authenticated' => true,
'actions' => {
'download' => {
'href' => objects_url(project, sample_oid),
'header' => {}
}
}
}
]
})
end
end
describe 'is accessing non-public project' do context 'does not have user' do
let(:update_lfs_permissions) do let(:build) { create(:ci_build, :running, pipeline: pipeline) }
project.lfs_objects << lfs_object
end
it_behaves_like 'LFS http 401 response' it_behaves_like 'can download LFS only from own projects', renew_authorization: false
end end
end end
end
describe 'upload' do context 'when user is not authenticated' do
let(:project) { create(:project, :public) } let(:authorization) { nil }
let(:body) { upload_body(sample_object) }
shared_examples 'pushes new LFS objects' do |renew_authorization:| describe 'is accessing public project' do
let(:sample_size) { 150.megabytes } let_it_be(:project) { create(:project, :public) }
let(:sample_oid) { non_existing_object_oid }
it_behaves_like 'LFS http 200 response' it_behaves_like 'LFS http 200 response'
it 'responds with upload hypermedia link' do it 'returns href to download' do
expect(json_response['objects']).to be_kind_of(Array) expect(json_response).to eq({
expect(json_response['objects'].first).to include(sample_object) 'objects' => [
expect(json_response['objects'].first['actions']['upload']['href']).to eq(objects_url(project, sample_oid, sample_size)) {
'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'] describe 'is accessing non-public project' do
expect(headers['Content-Type']).to eq('application/octet-stream') it_behaves_like 'LFS http 401 response'
expect(headers['Transfer-Encoding']).to eq('chunked') end
end
end end
context 'when lfs_chunked_encoding feature is disabled' do describe 'upload' do
let(:lfs_chunked_encoding) { false } let_it_be(:project) { create(:project, :public) }
let(:body) { upload_body(sample_object) }
it 'responds with upload hypermedia link' do shared_examples 'pushes new LFS objects' do |renew_authorization:|
expect(json_response['objects']).to be_kind_of(Array) let(:sample_size) { 150.megabytes }
expect(json_response['objects'].first).to include(sample_object) let(:sample_oid) { non_existing_object_oid }
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'] it_behaves_like 'LFS http 200 response'
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
describe 'when request is authenticated' do it 'responds with upload hypermedia link' do
describe 'when user has project push access' do expect(json_response['objects']).to be_kind_of(Array)
let(:authorization) { authorize_user } 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 headers = json_response['objects'].first['actions']['upload']['header']
project.add_developer(user) expect(headers['Content-Type']).to eq('application/octet-stream')
end expect(headers['Transfer-Encoding']).to eq('chunked')
end
context 'when pushing an LFS object that already exists' do context 'when lfs_chunked_encoding feature is disabled' do
shared_examples_for 'batch upload with existing LFS object' do let(:lfs_chunked_encoding) { false }
it_behaves_like 'LFS http 200 response'
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']).to be_kind_of(Array)
expect(json_response['objects'].first).to include(sample_object) 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']['href']).to eq(objects_url(project, sample_oid, sample_size))
headers = json_response['objects'].first['actions']['upload']['header'] headers = json_response['objects'].first['actions']['upload']['header']
expect(headers['Content-Type']).to eq('application/octet-stream') expect(headers['Content-Type']).to eq('application/octet-stream')
expect(headers['Transfer-Encoding']).to eq('chunked') expect(headers['Transfer-Encoding']).to be_nil
end end
it_behaves_like 'process authorization header', renew_authorization: true
end end
let(:update_lfs_permissions) do it_behaves_like 'process authorization header', renew_authorization: renew_authorization
other_project.lfs_objects << lfs_object end
end
context 'in another project' do describe 'when request is authenticated' do
it_behaves_like 'batch upload with existing LFS object' describe 'when user has project push access' do
end before do
authorize_upload
end
context 'in source of fork project' do context 'when pushing an LFS object that already exists' do
let(:project) { fork_project(other_project) } 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' it 'responds with links to the object in the project' do
end expect(json_response['objects']).to be_kind_of(Array)
end 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 headers = json_response['objects'].first['actions']['upload']['header']
it_behaves_like 'pushes new LFS objects', renew_authorization: true expect(headers['Content-Type']).to eq('application/octet-stream')
end expect(headers['Transfer-Encoding']).to eq('chunked')
end
context 'when pushing one new and one existing LFS object' do it_behaves_like 'process authorization header', renew_authorization: true
let(:body) { upload_body(multiple_objects) } end
let(:update_lfs_permissions) do
project.lfs_objects << lfs_object
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 it_behaves_like 'batch upload with existing LFS object'
expect(json_response['objects']).to be_kind_of(Array) end
expect(json_response['objects'].first).to include(sample_object) context 'in source of fork project' do
expect(json_response['objects'].first).not_to have_key('actions') let(:project) { fork_project(other_project) }
expect(json_response['objects'].last).to include(non_existing_object) before do
expect(json_response['objects'].last['actions']['upload']['href']).to eq(objects_url(project, non_existing_object_oid, non_existing_object_size)) lfs_object.update!(projects: [other_project])
end
headers = json_response['objects'].last['actions']['upload']['header'] it_behaves_like 'batch upload with existing LFS object'
expect(headers['Content-Type']).to eq('application/octet-stream') end
expect(headers['Transfer-Encoding']).to eq('chunked') end
end
it_behaves_like 'process authorization header', renew_authorization: true context 'when pushing a LFS object that does not exist' do
end it_behaves_like 'pushes new LFS objects', renew_authorization: true
end end
context 'when user does not have push access' do context 'when pushing one new and one existing LFS object' do
let(:authorization) { authorize_user } let(:body) { upload_body(multiple_objects) }
it_behaves_like 'LFS http 403 response' it_behaves_like 'LFS http 200 response'
end
context 'when build is authorized' do it 'responds with upload hypermedia link for the new object' do
let(:authorization) { authorize_ci_project } expect(json_response['objects']).to be_kind_of(Array)
context 'build has an user' do expect(json_response['objects'].first).to include(sample_object)
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } expect(json_response['objects'].first).not_to have_key('actions')
context 'tries to push to own project' do expect(json_response['objects'].last).to include(non_existing_object)
it_behaves_like 'LFS http 403 response' expect(json_response['objects'].last['actions']['upload']['href']).to eq(objects_url(project, non_existing_object_oid, non_existing_object_size))
end
context 'tries to push to other project' do headers = json_response['objects'].last['actions']['upload']['header']
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } 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' it_behaves_like 'LFS http 403 response'
end end
end
context 'does not have user' do context 'when build is authorized' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) } let(:authorization) { authorize_ci_project }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
it_behaves_like 'LFS http 403 response' context 'build has an user' do
end let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
end
context 'when deploy key has project push access' do context 'tries to push to own project' do
let(:key) { create(:deploy_key) } it_behaves_like 'LFS http 403 response'
let(:authorization) { authorize_deploy_key } end
let(:update_user_permissions) do context 'tries to push to other project' do
project.deploy_keys_projects.create!(deploy_key: key, can_push: true) let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
end
it_behaves_like 'pushes new LFS objects', renew_authorization: false # I'm not sure what this tests that is different from the previous test
end it_behaves_like 'LFS http 403 response'
end end
end
context 'when user is not authenticated' do context 'does not have user' do
context 'when user has push access' do let(:build) { create(:ci_build, :running, pipeline: pipeline) }
let(:update_user_permissions) do
project.add_maintainer(user)
end
it_behaves_like 'LFS http 401 response' it_behaves_like 'LFS http 403 response'
end end
end
context 'when user does not have push access' do context 'when deploy key has project push access' do
it_behaves_like 'LFS http 401 response' let(:key) { create(:deploy_key) }
end let(:authorization) { authorize_deploy_key }
end
end
describe 'unsupported' do before do
let(:authorization) { authorize_user } project.deploy_keys_projects.create!(deploy_key: key, can_push: true)
let(:body) { request_body('other', sample_object) } end
it_behaves_like 'LFS http 404 response' it_behaves_like 'pushes new LFS objects', renew_authorization: false
end end
end end
describe 'when handling LFS batch request on a read-only GitLab instance' do context 'when user is not authenticated' do
let(:authorization) { authorize_user } 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 it_behaves_like 'LFS http 401 response'
allow(Gitlab::Database).to receive(:read_only?) { true } 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 describe 'unsupported' do
end let(:body) { request_body('other', sample_object) }
context 'when downloading' do it_behaves_like 'LFS http 404 response'
let(:body) { download_body(sample_object) } end
end
it_behaves_like 'LFS http 200 response' describe 'when handling LFS batch request on a read-only GitLab instance' do
end subject { post_lfs_json(batch_url(project), body, headers) }
context 'when uploading' do before do
let(:body) { upload_body(sample_object) } allow(Gitlab::Database).to receive(:read_only?) { true }
it_behaves_like 'LFS http expected response code and message' do project.add_maintainer(user)
let(:response_code) { 403 }
let(:message) { 'You cannot write to this read-only GitLab instance.' }
end
end
end
describe 'when pushing a LFS object' do subject
shared_examples 'unauthorized' do
context 'and request is sent by gitlab-workhorse to authorize the request' do
before do
put_authorize
end end
it_behaves_like 'LFS http 401 response' context 'when downloading' do
end let(:body) { download_body(sample_object) }
context 'and request is sent by gitlab-workhorse to finalize the upload' do it_behaves_like 'LFS http 200 response'
before do
put_finalize
end end
it_behaves_like 'LFS http 401 response' context 'when uploading' do
end let(:body) { upload_body(sample_object) }
context 'and request is sent with a malformed headers' do it_behaves_like 'LFS http expected response code and message' do
before do let(:response_code) { 403 }
put_finalize('/etc/passwd') let(:message) { 'You cannot write to this read-only GitLab instance.' }
end
end end
it_behaves_like 'LFS http 401 response'
end end
end
shared_examples 'forbidden' do describe 'when pushing a LFS object' do
context 'and request is sent by gitlab-workhorse to authorize the request' do let(:include_workhorse_jwt_header) { true }
before do
put_authorize
end
it_behaves_like 'LFS http 403 response' shared_examples 'unauthorized' do
end 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 it_behaves_like 'LFS http 401 response'
before do end
put_finalize
end
it_behaves_like 'LFS http 403 response' context 'and request is sent by gitlab-workhorse to finalize the upload' do
end before do
put_finalize
end
context 'and request is sent with a malformed headers' do it_behaves_like 'LFS http 401 response'
before do end
put_finalize('/etc/passwd')
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 end
it_behaves_like 'LFS http 403 response' shared_examples 'forbidden' do
end context 'and request is sent by gitlab-workhorse to authorize the request' do
end before do
put_authorize
end
describe 'to one project' do it_behaves_like 'LFS http 403 response'
describe 'when user is authenticated' do end
let(:authorization) { authorize_user }
describe 'when user has push access to the project' do context 'and request is sent by gitlab-workhorse to finalize the upload' do
before do before do
project.add_developer(user) put_finalize
end
it_behaves_like 'LFS http 403 response'
end end
context 'and the request bypassed workhorse' do context 'and request is sent with a malformed headers' do
it 'raises an exception' do before do
expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError put_finalize('/etc/passwd')
end end
it_behaves_like 'LFS http 403 response'
end end
end
context 'and request is sent by gitlab-workhorse to authorize the request' do describe 'to one project' do
shared_examples 'a valid response' do describe 'when user is authenticated' do
describe 'when user has push access to the project' do
before do before do
put_authorize project.add_developer(user)
end end
it_behaves_like 'LFS http 200 workhorse response' context 'and the request bypassed workhorse' do
end it 'raises an exception' do
expect { put_authorize(verified: false) }.to raise_error JWT::DecodeError
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
end
context 'when using local storage' do context 'and request is sent by gitlab-workhorse to authorize the request' do
it_behaves_like 'a local file' shared_examples 'a valid response' do
end before do
put_authorize
end
context 'when using remote storage' do it_behaves_like 'LFS http 200 workhorse response'
context 'when direct upload is enabled' do
before do
stub_lfs_object_storage(enabled: true, direct_upload: true)
end end
it_behaves_like 'a valid response' do shared_examples 'a local file' do
it 'responds with status 200, location of LFS remote store and object details' do it_behaves_like 'a valid response' do
expect(json_response).not_to have_key('TempPath') it 'responds with status 200, location of LFS store and object details' do
expect(json_response['RemoteObject']).to have_key('ID') expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
expect(json_response['RemoteObject']).to have_key('GetURL') expect(json_response['RemoteObject']).to be_nil
expect(json_response['RemoteObject']).to have_key('StoreURL') expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['RemoteObject']).to have_key('DeleteURL') expect(json_response['LfsSize']).to eq(sample_size)
expect(json_response['RemoteObject']).not_to have_key('MultipartUpload') end
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end end
end end
end
context 'when direct upload is disabled' do context 'when using local storage' do
before do it_behaves_like 'a local file'
stub_lfs_object_storage(enabled: true, direct_upload: false)
end end
it_behaves_like 'a local file' context 'when using remote storage' do
end context 'when direct upload is enabled' do
end before do
end stub_lfs_object_storage(enabled: true, direct_upload: true)
end
context 'and request is sent by gitlab-workhorse to finalize the upload' do it_behaves_like 'a valid response' do
before do it 'responds with status 200, location of LFS remote store and object details' do
put_finalize expect(json_response).not_to have_key('TempPath')
end 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 it_behaves_like 'a local file'
expect(lfs_object.projects.pluck(:id)).to include(project.id) end
end end
end end
context 'and request to finalize the upload is not sent by gitlab-workhorse' do context 'and request is sent by gitlab-workhorse to finalize the upload' do
it 'fails with a JWT decode error' do before do
expect { put_finalize(lfs_tmp_file, verified: false) }.to raise_error(JWT::DecodeError) put_finalize
end end
end
context 'and workhorse requests upload finalize for a new LFS object' do it_behaves_like 'LFS http 200 response'
before do
lfs_object.destroy!
end
context 'with object storage disabled' do it 'LFS object is linked to the project' do
it "doesn't attempt to migrate file to object storage" do expect(lfs_object.projects.pluck(:id)).to include(project.id)
expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async) 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
end
context 'with object storage enabled' do context 'and workhorse requests upload finalize for a new LFS object' do
context 'and direct upload enabled' do before do
let!(:fog_connection) do lfs_object.destroy!
stub_lfs_object_storage(direct_upload: true)
end end
let(:tmp_object) do context 'with object storage disabled' do
fog_connection.directories.new(key: 'lfs-objects').files.create( # rubocop: disable Rails/SaveBang it "doesn't attempt to migrate file to object storage" do
key: 'tmp/uploads/12312300', expect(ObjectStorage::BackgroundMoveWorker).not_to receive(:perform_async)
body: 'content'
) put_finalize(with_tempfile: true)
end
end end
['123123', '../../123123'].each do |remote_id| context 'with object storage enabled' do
context "with invalid remote_id: #{remote_id}" do context 'and direct upload enabled' do
subject do let!(:fog_connection) do
put_finalize(remote_object: tmp_object, args: { stub_lfs_object_storage(direct_upload: true)
'file.remote_id' => remote_id 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 end
it 'responds with status 403' do ['123123', '../../123123'].each do |remote_id|
subject 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
end
context 'with valid remote_id' do context 'with valid remote_id' do
subject do subject do
put_finalize(remote_object: tmp_object, args: { put_finalize(remote_object: tmp_object, args: {
'file.remote_id' => '12312300', 'file.remote_id' => '12312300',
'file.name' => 'name' 'file.name' => 'name'
}) })
end end
it 'responds with status 200' do it 'responds with status 200' do
subject subject
expect(response).to have_gitlab_http_status(:ok) expect(response).to have_gitlab_http_status(:ok)
object = LfsObject.find_by_oid(sample_oid) object = LfsObject.find_by_oid(sample_oid)
expect(object).to be_present expect(object).to be_present
expect(object.file.read).to eq(tmp_object.body) expect(object.file.read).to eq(tmp_object.body)
end 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 it 'have valid file' do
subject 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 end
it 'have valid file' do context 'and background upload enabled' do
subject 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) put_finalize(with_tempfile: true)
expect(LfsObject.last.file).to be_exists end
end end
end end
end end
context 'and background upload enabled' do context 'without the lfs object' do
before do before do
stub_lfs_object_storage(background_upload: true) lfs_object.destroy!
end end
it 'schedules migration of file to object storage' do it 'rejects slashes in the tempfile name (path traversal)' do
expect(ObjectStorage::BackgroundMoveWorker).to receive(:perform_async).with('LfsObjectUploader', 'LfsObject', :file, kind_of(Numeric)) 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
end end
end
context 'without the lfs object' do describe 'and user does not have push access' do
before do before do
lfs_object.destroy! project.add_reporter(user)
end end
it 'rejects slashes in the tempfile name (path traversal)' do it_behaves_like 'forbidden'
put_finalize('../bar', with_tempfile: true)
expect(response).to have_gitlab_http_status(:bad_request)
end end
end
context 'not sending the workhorse jwt header' do context 'when build is authorized' do
let(:include_workhorse_jwt_header) { false } let(:authorization) { authorize_ci_project }
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
it 'rejects the request' do
put_finalize(with_tempfile: true)
expect(response).to have_gitlab_http_status(:unprocessable_entity) context 'build has an user' do
end let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
end
end
end
describe 'and user does not have push access' do context 'tries to push to own project' do
before do before do
project.add_reporter(user) project.add_developer(user)
end put_authorize
end
it_behaves_like 'forbidden' it_behaves_like 'LFS http 403 response'
end end
end
context 'when build is authorized' do context 'tries to push to other project' do
let(:authorization) { authorize_ci_project } let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
context 'build has an user' do before do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } put_authorize
end
context 'tries to push to own project' do it_behaves_like 'LFS http 404 response'
before do end
project.add_developer(user)
put_authorize
end end
it_behaves_like 'LFS http 403 response' context 'does not have user' do
end let(:build) { create(:ci_build, :running, pipeline: pipeline) }
context 'tries to push to other project' do before do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } put_authorize
end
before do it_behaves_like 'LFS http 403 response'
put_authorize
end end
it_behaves_like 'LFS http 404 response'
end end
end
context 'does not have user' do describe 'when using a user key (LFSToken)' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) } let(:authorization) { authorize_user_key }
before do context 'when user allowed' do
put_authorize before do
end project.add_developer(user)
put_authorize
end
it_behaves_like 'LFS http 403 response' it_behaves_like 'LFS http 200 workhorse response'
end
end
describe 'when using a user key (LFSToken)' do context 'when user password is expired' do
let(:authorization) { authorize_user_key } let_it_be(:user) { create(:user, password_expires_at: 1.minute.ago)}
context 'when user allowed' do it_behaves_like 'LFS http 401 response'
before do end
project.add_developer(user)
put_authorize
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 context 'when user not allowed' do
let(:user) { create(:user, password_expires_at: 1.minute.ago)} before do
put_authorize
end
it_behaves_like 'LFS http 401 response' it_behaves_like 'LFS http 404 response'
end
end end
context 'when user is blocked' do context 'for unauthenticated' do
let(:user) { create(:user, :blocked)} let(:authorization) { nil }
it_behaves_like 'LFS http 401 response' it_behaves_like 'unauthorized'
end end
end end
context 'when user not allowed' do describe 'to a forked project' do
before do let_it_be(:upstream_project) { create(:project, :public) }
put_authorize let_it_be(:project_owner) { create(:user) }
end let(:project) { fork_project(upstream_project, project_owner) }
it_behaves_like 'LFS http 404 response' describe 'when user is authenticated' do
end describe 'when user has push access to the project' do
end before do
project.add_developer(user)
end
context 'for unauthenticated' do context 'and request is sent by gitlab-workhorse to authorize the request' do
it_behaves_like 'unauthorized' before do
end put_authorize
end end
describe 'to a forked project' do it_behaves_like 'LFS http 200 workhorse response'
let(:upstream_project) { create(:project, :public) }
let(:project_owner) { create(:user) }
let(:project) { fork_project(upstream_project, project_owner) }
describe 'when user is authenticated' do it 'with location of LFS store and object details' do
let(:authorization) { authorize_user } 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 context 'and request is sent by gitlab-workhorse to finalize the upload' do
before do before do
project.add_developer(user) put_finalize
end end
context 'and request is sent by gitlab-workhorse to authorize the request' do it_behaves_like 'LFS http 200 response'
before do
put_authorize
end
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 describe 'and user does not have push access' do
expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path) it_behaves_like 'forbidden'
expect(json_response['LfsOid']).to eq(sample_oid)
expect(json_response['LfsSize']).to eq(sample_size)
end end
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 before do
put_finalize put_authorize
end end
it_behaves_like 'LFS http 200 response' context 'build has an user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it 'LFS object is linked to the forked project' do
expect(lfs_object.projects.pluck(:id)).to include(project.id)
end
end
end
describe 'and user does not have push access' do context 'tries to push to own project' do
it_behaves_like 'forbidden' it_behaves_like 'LFS http 403 response'
end end
end
context 'when build is authorized' do context 'tries to push to other project' do
let(:authorization) { authorize_ci_project } let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
before do # I'm not sure what this tests that is different from the previous test
put_authorize it_behaves_like 'LFS http 403 response'
end end
end
context 'build has an user' do context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } 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 end
context 'tries to push to other project' do context 'for unauthenticated' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } let(:authorization) { nil }
# I'm not sure what this tests that is different from the previous test it_behaves_like 'unauthorized'
it_behaves_like 'LFS http 403 response'
end end
end
context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it_behaves_like 'LFS http 403 response' describe 'and second project not related to fork or a source project' do
end let_it_be(:second_project) { create(:project) }
end
context 'for unauthenticated' do before do
it_behaves_like 'unauthorized' second_project.add_maintainer(user)
end upstream_project.lfs_objects << lfs_object
end
describe 'and second project not related to fork or a source project' do context 'when pushing the same LFS object to the second project' do
let(:second_project) { create(:project) } before do
let(:authorization) { authorize_user } finalize_headers = headers
.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file)
.merge(workhorse_internal_api_request_header)
before do put objects_url(second_project, sample_oid, sample_size),
second_project.add_maintainer(user) params: {},
upstream_project.lfs_objects << lfs_object headers: finalize_headers
end end
context 'when pushing the same LFS object to the second project' do it_behaves_like 'LFS http 200 response'
before do
finalize_headers = headers
.merge('X-Gitlab-Lfs-Tmp' => lfs_tmp_file)
.merge(workhorse_internal_api_request_header)
put objects_url(second_project, sample_oid, sample_size), it 'links the LFS object to the project' do
params: {}, expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id)
headers: finalize_headers end
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 put authorize_url(project, sample_oid, sample_size), params: {}, headers: authorize_headers
expect(lfs_object.projects.pluck(:id)).to include(second_project.id, upstream_project.id)
end
end end
end
end
def put_authorize(verified: true) def put_finalize(lfs_tmp = lfs_tmp_file, with_tempfile: false, verified: true, remote_object: nil, args: {})
authorize_headers = headers uploaded_file = nil
authorize_headers.merge!(workhorse_internal_api_request_header) if verified
put authorize_url(project, sample_oid, sample_size), params: {}, headers: authorize_headers if with_tempfile
end 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: {}) FileUtils.mkdir_p(upload_path)
uploaded_file = nil FileUtils.touch(file_path)
if with_tempfile uploaded_file = UploadedFile.new(file_path, filename: File.basename(file_path))
upload_path = LfsObjectUploader.workhorse_local_upload_path elsif remote_object
file_path = upload_path + '/' + lfs_tmp if lfs_tmp uploaded_file = fog_to_uploaded_file(remote_object)
end
FileUtils.mkdir_p(upload_path) finalize_headers = headers
FileUtils.touch(file_path) 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)) def lfs_tmp_file
elsif remote_object "#{sample_oid}012345678"
uploaded_file = fog_to_uploaded_file(remote_object) end
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
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 ...@@ -65,12 +65,19 @@ end
RSpec.shared_examples 'LFS http requests' do RSpec.shared_examples 'LFS http requests' do
include LfsHttpHelpers include LfsHttpHelpers
let(:lfs_enabled) { true }
let(:authorize_guest) {} let(:authorize_guest) {}
let(:authorize_download) {} let(:authorize_download) {}
let(:authorize_upload) {} let(:authorize_upload) {}
let(:lfs_object) { create(:lfs_object, :with_file) } let(:lfs_object) { create(:lfs_object, :with_file) }
let(:sample_oid) { lfs_object.oid } 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(:authorization) { authorize_user }
let(:headers) do let(:headers) do
...@@ -89,13 +96,11 @@ RSpec.shared_examples 'LFS http requests' do ...@@ -89,13 +96,11 @@ RSpec.shared_examples 'LFS http requests' do
end end
before do before do
stub_lfs_setting(enabled: true) stub_lfs_setting(enabled: lfs_enabled)
end end
context 'when LFS is disabled globally' do context 'when LFS is disabled globally' do
before do let(:lfs_enabled) { false }
stub_lfs_setting(enabled: false)
end
describe 'download request' do describe 'download request' do
before 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