Commit 78977b06 authored by Stan Hu's avatar Stan Hu

Merge branch '207869-lfs-enabled-checks' into 'master'

Block LFS requests on snippets

See merge request gitlab-org/gitlab!45874
parents 7721a229 63e1fcb7
# frozen_string_literal: true # frozen_string_literal: true
# This concern assumes: # This concern assumes:
# - a `#container` accessor
# - a `#project` accessor # - a `#project` accessor
# - a `#user` accessor # - a `#user` accessor
# - a `#authentication_result` accessor # - a `#authentication_result` accessor
...@@ -11,6 +12,7 @@ ...@@ -11,6 +12,7 @@
# - a `#has_authentication_ability?(ability)` method # - a `#has_authentication_ability?(ability)` method
module LfsRequest module LfsRequest
extend ActiveSupport::Concern extend ActiveSupport::Concern
include Gitlab::Utils::StrongMemoize
CONTENT_TYPE = 'application/vnd.git-lfs+json' CONTENT_TYPE = 'application/vnd.git-lfs+json'
...@@ -29,16 +31,19 @@ module LfsRequest ...@@ -29,16 +31,19 @@ module LfsRequest
message: _('Git LFS is not enabled on this GitLab server, contact your admin.'), message: _('Git LFS is not enabled on this GitLab server, contact your admin.'),
documentation_url: help_url documentation_url: help_url
}, },
content_type: CONTENT_TYPE,
status: :not_implemented status: :not_implemented
) )
end end
def lfs_check_access! def lfs_check_access!
return render_lfs_not_found unless project return render_lfs_not_found unless container&.lfs_enabled?
return if download_request? && lfs_download_access? return if download_request? && lfs_download_access?
return if upload_request? && lfs_upload_access? return if upload_request? && lfs_upload_access?
if project.public? || can?(user, :read_project, project) # Only return a 403 response if the user has download access permission,
# otherwise return a 404 to avoid exposing the existence of the container.
if lfs_download_access?
lfs_forbidden! lfs_forbidden!
else else
render_lfs_not_found render_lfs_not_found
...@@ -72,9 +77,9 @@ module LfsRequest ...@@ -72,9 +77,9 @@ module LfsRequest
end end
def lfs_download_access? def lfs_download_access?
return false unless project.lfs_enabled? strong_memoize(:lfs_download_access) do
ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? || deploy_token_can_download_code?
ci? || lfs_deploy_token? || user_can_download_code? || build_can_download_code? || deploy_token_can_download_code? end
end end
def deploy_token_can_download_code? def deploy_token_can_download_code?
...@@ -93,11 +98,12 @@ module LfsRequest ...@@ -93,11 +98,12 @@ module LfsRequest
end end
def lfs_upload_access? def lfs_upload_access?
return false unless project.lfs_enabled? strong_memoize(:lfs_upload_access) do
return false unless has_authentication_ability?(:push_code) next false unless has_authentication_ability?(:push_code)
return false if limit_exceeded? next false if limit_exceeded?
lfs_deploy_token? || can?(user, :push_code, project) lfs_deploy_token? || can?(user, :push_code, project)
end
end end
def lfs_deploy_token? def lfs_deploy_token?
......
...@@ -6,7 +6,7 @@ module Repositories ...@@ -6,7 +6,7 @@ module Repositories
include KerberosSpnegoHelper include KerberosSpnegoHelper
include Gitlab::Utils::StrongMemoize include Gitlab::Utils::StrongMemoize
attr_reader :authentication_result, :redirected_path, :container attr_reader :authentication_result, :redirected_path
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result
...@@ -75,6 +75,12 @@ module Repositories ...@@ -75,6 +75,12 @@ module Repositories
headers['Www-Authenticate'] = challenges.join("\n") if challenges.any? headers['Www-Authenticate'] = challenges.join("\n") if challenges.any?
end end
def container
parse_repo_path unless defined?(@container)
@container
end
def project def project
parse_repo_path unless defined?(@project) parse_repo_path unless defined?(@project)
......
...@@ -17,9 +17,9 @@ module Repositories ...@@ -17,9 +17,9 @@ module Repositories
end end
if download_request? if download_request?
render json: { objects: download_objects! } render json: { objects: download_objects! }, content_type: LfsRequest::CONTENT_TYPE
elsif upload_request? elsif upload_request?
render json: { objects: upload_objects! } render json: { objects: upload_objects! }, content_type: LfsRequest::CONTENT_TYPE
else else
raise "Never reached" raise "Never reached"
end end
...@@ -31,6 +31,7 @@ module Repositories ...@@ -31,6 +31,7 @@ module Repositories
message: _('Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.'), message: _('Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.'),
documentation_url: "#{Gitlab.config.gitlab.url}/help" documentation_url: "#{Gitlab.config.gitlab.url}/help"
}, },
content_type: LfsRequest::CONTENT_TYPE,
status: :not_implemented status: :not_implemented
) )
end end
......
...@@ -29,7 +29,7 @@ module Repositories ...@@ -29,7 +29,7 @@ module Repositories
def upload_finalize def upload_finalize
if store_file!(oid, size) if store_file!(oid, size)
head 200 head 200, content_type: LfsRequest::CONTENT_TYPE
else else
render plain: 'Unprocessable entity', status: :unprocessable_entity render plain: 'Unprocessable entity', status: :unprocessable_entity
end end
......
---
title: Block LFS requests on snippets
merge_request: 45874
author:
type: fixed
# frozen_string_literal: true
require 'spec_helper'
RSpec.describe LfsRequest do
include ProjectForksHelper
controller(Repositories::GitHttpClientController) do
# `described_class` is not available in this context
include LfsRequest
def show
head :ok
end
def project
@project ||= Project.find_by(id: params[:id])
end
def download_request?
true
end
def upload_request?
false
end
def ci?
false
end
end
let(:project) { create(:project, :public) }
before do
stub_lfs_setting(enabled: true)
end
context 'user is authenticated without access to lfs' do
before do
allow(controller).to receive(:authenticate_user)
allow(controller).to receive(:authentication_result) do
Gitlab::Auth::Result.new
end
end
context 'with access to the project' do
it 'returns 403' do
get :show, params: { id: project.id }
expect(response).to have_gitlab_http_status(:forbidden)
end
end
context 'without access to the project' do
context 'project does not exist' do
it 'returns 404' do
get :show, params: { id: 'does not exist' }
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'project is private' do
let(:project) { create(:project, :private) }
it 'returns 404' do
get :show, params: { id: project.id }
expect(response).to have_gitlab_http_status(:not_found)
end
end
end
end
end
...@@ -17,5 +17,9 @@ FactoryBot.define do ...@@ -17,5 +17,9 @@ FactoryBot.define do
container { project } container { project }
end end
trait :empty_repo do
after(:create, &:create_wiki_repository)
end
end end
end end
...@@ -9,18 +9,17 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -9,18 +9,17 @@ RSpec.describe 'Git LFS API and storage' do
let_it_be(:project, reload: true) { create(:project, :repository) } let_it_be(:project, reload: true) { create(:project, :repository) }
let_it_be(:other_project) { create(:project, :repository) } 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(:lfs_object) { create(:lfs_object, :with_file) }
let(:headers) do let(:headers) do
{ {
'Authorization' => authorization, 'Authorization' => authorization,
'X-Sendfile-Type' => sendfile 'X-Sendfile-Type' => 'X-Sendfile'
}.compact }.compact
end end
let(:include_workhorse_jwt_header) { true } let(:include_workhorse_jwt_header) { true }
let(:authorization) { } let(:authorization) { }
let(:sendfile) { }
let(:pipeline) { create(:ci_empty_pipeline, project: project) } let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:sample_oid) { lfs_object.oid } let(:sample_oid) { lfs_object.oid }
...@@ -37,18 +36,6 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -37,18 +36,6 @@ RSpec.describe 'Git LFS API and storage' do
stub_lfs_setting(enabled: lfs_enabled) stub_lfs_setting(enabled: lfs_enabled)
end end
describe 'when LFS is disabled' do
let(:lfs_enabled) { false }
let(:body) { upload_body(multiple_objects) }
let(:authorization) { authorize_user }
before do
post_lfs_json batch_url(project), body, headers
end
it_behaves_like 'LFS http 501 response'
end
context 'project specific LFS settings' do context 'project specific LFS settings' do
let(:body) { upload_body(sample_object) } let(:body) { upload_body(sample_object) }
let(:authorization) { authorize_user } let(:authorization) { authorize_user }
...@@ -60,105 +47,36 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -60,105 +47,36 @@ RSpec.describe 'Git LFS API and storage' do
subject subject
end end
context 'with LFS disabled globally' do describe 'LFS disabled in project' do
let(:lfs_enabled) { false } let(:project_lfs_enabled) { false }
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 501 response'
end
context 'when downloading' do context 'when uploading' do
subject { get(objects_url(project, sample_oid), params: {}, headers: headers) } subject { post_lfs_json(batch_url(project), body, headers) }
it_behaves_like 'LFS http 501 response' it_behaves_like 'LFS http 404 response'
end
end end
describe 'LFS enabled in project' do context 'when downloading' do
let(:project_lfs_enabled) { true } subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
context 'when uploading' do
subject { post_lfs_json(batch_url(project), body, headers) }
it_behaves_like 'LFS http 501 response'
end
context 'when downloading' do it_behaves_like 'LFS http 404 response'
subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
it_behaves_like 'LFS http 501 response'
end
end end
end end
context 'with LFS enabled globally' do describe 'LFS enabled in project' do
describe 'LFS disabled in project' do let(:project_lfs_enabled) { true }
let(:project_lfs_enabled) { false }
context 'when uploading' do
subject { post_lfs_json(batch_url(project), body, headers) }
it_behaves_like 'LFS http 403 response'
end
context 'when downloading' do
subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
it_behaves_like 'LFS http 403 response'
end
end
describe 'LFS enabled in project' do
let(:project_lfs_enabled) { true }
context 'when uploading' do
subject { post_lfs_json(batch_url(project), body, headers) }
it_behaves_like 'LFS http 200 response'
end
context 'when downloading' do context 'when uploading' do
subject { get(objects_url(project, sample_oid), params: {}, headers: headers) } subject { post_lfs_json(batch_url(project), body, headers) }
it_behaves_like 'LFS http 200 response' it_behaves_like 'LFS http 200 response'
end
end end
end
end
describe 'deprecated API' do context 'when downloading' do
let(:authorization) { authorize_user } subject { get(objects_url(project, sample_oid), params: {}, headers: headers) }
shared_examples 'deprecated request' do it_behaves_like 'LFS http 200 blob response'
before do
subject
end end
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 501 }
let(:message) { 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.' }
end
end
context 'when fetching LFS object using deprecated API' do
subject { get(deprecated_objects_url(project, sample_oid), params: {}, headers: headers) }
it_behaves_like 'deprecated request'
end
context 'when handling LFS request using deprecated API' do
subject { post_lfs_json(deprecated_objects_url(project), nil, headers) }
it_behaves_like 'deprecated request'
end
def deprecated_objects_url(project, oid = nil)
File.join(["#{project.http_url_to_repo}/info/lfs/objects/", oid].compact)
end end
end end
...@@ -167,196 +85,133 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -167,196 +85,133 @@ RSpec.describe 'Git LFS API and storage' do
let(:before_get) { } let(:before_get) { }
before do before do
project.lfs_objects << lfs_object
update_permissions update_permissions
before_get before_get
get objects_url(project, sample_oid), params: {}, headers: headers get objects_url(project, sample_oid), params: {}, headers: headers
end end
context 'and request comes from gitlab-workhorse' do context 'when LFS uses object storage' do
context 'without user being authorized' do let(:authorization) { authorize_user }
it_behaves_like 'LFS http 401 response'
end
context 'with required headers' do let(:update_permissions) do
shared_examples 'responds with a file' do project.add_maintainer(user)
let(:sendfile) { 'X-Sendfile' } end
it_behaves_like 'LFS http 200 response' 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
it 'responds with the file location' do it 'responds with the workhorse send-url' do
expect(response.headers['Content-Type']).to eq('application/octet-stream') expect(response).to have_gitlab_http_status(:ok)
expect(response.headers['X-Sendfile']).to eq(lfs_object.file.path) expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:")
end
end end
end
context 'with user is authorized' do context 'when proxy download is disabled' do
let(:authorization) { authorize_user } let(:before_get) do
stub_lfs_object_storage(proxy_download: false)
lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
end
context 'and does not have project access' do it 'responds with redirect' do
let(:update_permissions) do expect(response).to have_gitlab_http_status(:found)
project.lfs_objects << lfs_object end
end
it_behaves_like 'LFS http 404 response' it 'responds with the file location' do
end expect(response.location).to include(lfs_object.reload.file.path)
end
end
end
context 'and does have project access' do context 'when deploy key is authorized' do
let(:update_permissions) do let(:key) { create(:deploy_key) }
project.add_maintainer(user) let(:authorization) { authorize_deploy_key }
project.lfs_objects << lfs_object
end
it_behaves_like 'responds with a file' let(:update_permissions) do
project.deploy_keys << key
end
context 'when LFS uses object storage' do it_behaves_like 'LFS http 200 blob response'
context 'when proxy download is enabled' do end
let(:before_get) do
stub_lfs_object_storage(proxy_download: true)
lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
end
it_behaves_like 'LFS http 200 response' context 'when using a user key (LFSToken)' do
let(:authorization) { authorize_user_key }
it 'responds with the workhorse send-url' do context 'when user allowed' do
expect(response.headers[Gitlab::Workhorse::SEND_DATA_HEADER]).to start_with("send-url:") let(:update_permissions) do
end project.add_maintainer(user)
end end
context 'when proxy download is disabled' do it_behaves_like 'LFS http 200 blob response'
let(:before_get) do
stub_lfs_object_storage(proxy_download: false)
lfs_object.file.migrate!(LfsObjectUploader::Store::REMOTE)
end
it 'responds with redirect' do context 'when user password is expired' do
expect(response).to have_gitlab_http_status(:found) let(:user) { create(:user, password_expires_at: 1.minute.ago)}
end
it 'responds with the file location' do it_behaves_like 'LFS http 401 response'
expect(response.location).to include(lfs_object.reload.file.path)
end
end
end
end
end end
context 'when deploy key is authorized' do context 'when user is blocked' do
let(:key) { create(:deploy_key) } let(:user) { create(:user, :blocked)}
let(:authorization) { authorize_deploy_key }
let(:update_permissions) do
project.deploy_keys << key
project.lfs_objects << lfs_object
end
it_behaves_like 'responds with a file' it_behaves_like 'LFS http 401 response'
end end
end
describe 'when using a user key (LFSToken)' do context 'when user not allowed' do
let(:authorization) { authorize_user_key } it_behaves_like 'LFS http 404 response'
end
context 'when user allowed' do end
let(:update_permissions) do
project.add_maintainer(user)
project.lfs_objects << lfs_object
end
it_behaves_like 'responds with a file' context 'when build is authorized as' do
let(:authorization) { authorize_ci_project }
context 'when user password is expired' do shared_examples 'can download LFS only from own projects' do
let(:user) { create(:user, password_expires_at: 1.minute.ago)} context 'for owned project' do
let(:project) { create(:project, namespace: user.namespace) }
it_behaves_like 'LFS http 401 response' it_behaves_like 'LFS http 200 blob response'
end end
context 'when user is blocked' do context 'for member of project' do
let(:user) { create(:user, :blocked)} let(:pipeline) { create(:ci_empty_pipeline, project: project) }
it_behaves_like 'LFS http 401 response' let(:update_permissions) do
end project.add_reporter(user)
end end
context 'when user not allowed' do it_behaves_like 'LFS http 200 blob response'
let(:update_permissions) do
project.lfs_objects << lfs_object
end
it_behaves_like 'LFS http 404 response'
end
end end
context 'when build is authorized as' do context 'for other project' do
let(:authorization) { authorize_ci_project } let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
shared_examples 'can download LFS only from own projects' do
context 'for owned project' do
let(:project) { create(:project, namespace: user.namespace) }
let(:update_permissions) do
project.lfs_objects << lfs_object
end
it_behaves_like 'responds with a file'
end
context 'for member of project' do
let(:pipeline) { create(:ci_empty_pipeline, project: project) }
let(:update_permissions) do
project.add_reporter(user)
project.lfs_objects << lfs_object
end
it_behaves_like 'responds with a file'
end
context 'for other project' do it 'rejects downloading code' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } expect(response).to have_gitlab_http_status(:not_found)
let(:update_permissions) do
project.lfs_objects << lfs_object
end
it 'rejects downloading code' do
expect(response).to have_gitlab_http_status(other_project_status)
end
end
end
context 'administrator' do
let(:user) { create(:admin) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects' do
# We render 403, because administrator does have normally access
let(:other_project_status) { 403 }
end
end end
end
end
context 'regular user' do context 'administrator' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } let(:user) { create(:admin) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects' do it_behaves_like 'can download LFS only from own projects'
# We render 404, to prevent data leakage about existence of the project end
let(:other_project_status) { 404 }
end
end
context 'does not have user' do context 'regular user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) } let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects' do it_behaves_like 'can download LFS only from own projects'
# We render 404, to prevent data leakage about existence of the project
let(:other_project_status) { 404 }
end
end
end
end end
context 'without required headers' do context 'does not have user' do
let(:authorization) { authorize_user } let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it_behaves_like 'LFS http 404 response' it_behaves_like 'can download LFS only from own projects'
end end
end end
end end
...@@ -511,7 +366,7 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -511,7 +366,7 @@ RSpec.describe 'Git LFS API and storage' do
let(:role) { :reporter } let(:role) { :reporter }
end end
context 'when user does is not member of the project' do context 'when user is not a member of the project' do
let(:update_user_permissions) { nil } let(:update_user_permissions) { nil }
it_behaves_like 'LFS http 404 response' it_behaves_like 'LFS http 404 response'
...@@ -520,7 +375,7 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -520,7 +375,7 @@ RSpec.describe 'Git LFS API and storage' do
context 'when user does not have download access' do context 'when user does not have download access' do
let(:role) { :guest } let(:role) { :guest }
it_behaves_like 'LFS http 403 response' it_behaves_like 'LFS http 404 response'
end end
context 'when user password is expired' do context 'when user password is expired' do
...@@ -591,7 +446,7 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -591,7 +446,7 @@ RSpec.describe 'Git LFS API and storage' do
let(:pipeline) { create(:ci_empty_pipeline, project: other_project) } let(:pipeline) { create(:ci_empty_pipeline, project: other_project) }
it 'rejects downloading code' do it 'rejects downloading code' do
expect(response).to have_gitlab_http_status(other_project_status) expect(response).to have_gitlab_http_status(:not_found)
end end
end end
end end
...@@ -600,28 +455,19 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -600,28 +455,19 @@ RSpec.describe 'Git LFS API and storage' do
let(:user) { create(:admin) } let(:user) { create(:admin) }
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects', renew_authorization: true do it_behaves_like 'can download LFS only from own projects', renew_authorization: true
# We render 403, because administrator does have normally access
let(:other_project_status) { 403 }
end
end end
context 'regular user' do context 'regular user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) } let(:build) { create(:ci_build, :running, pipeline: pipeline, user: user) }
it_behaves_like 'can download LFS only from own projects', renew_authorization: true do it_behaves_like 'can download LFS only from own projects', renew_authorization: true
# We render 404, to prevent data leakage about existence of the project
let(:other_project_status) { 404 }
end
end end
context 'does not have user' do context 'does not have user' do
let(:build) { create(:ci_build, :running, pipeline: pipeline) } let(:build) { create(:ci_build, :running, pipeline: pipeline) }
it_behaves_like 'can download LFS only from own projects', renew_authorization: false do it_behaves_like 'can download LFS only from own projects', renew_authorization: false
# We render 404, to prevent data leakage about existence of the project
let(:other_project_status) { 404 }
end
end end
end end
...@@ -919,11 +765,7 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -919,11 +765,7 @@ RSpec.describe 'Git LFS API and storage' do
put_authorize put_authorize
end end
it_behaves_like 'LFS http 200 response' it_behaves_like 'LFS http 200 workhorse response'
it 'uses the gitlab-workhorse content type' do
expect(response.media_type).to eq(Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE)
end
end end
shared_examples 'a local file' do shared_examples 'a local file' do
...@@ -1142,7 +984,7 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -1142,7 +984,7 @@ RSpec.describe 'Git LFS API and storage' do
put_authorize put_authorize
end end
it_behaves_like 'LFS http 404 response' it_behaves_like 'LFS http 403 response'
end end
end end
...@@ -1155,7 +997,7 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -1155,7 +997,7 @@ RSpec.describe 'Git LFS API and storage' do
put_authorize put_authorize
end end
it_behaves_like 'LFS http 200 response' it_behaves_like 'LFS http 200 workhorse response'
context 'when user password is expired' do context 'when user password is expired' do
let(:user) { create(:user, password_expires_at: 1.minute.ago)} let(:user) { create(:user, password_expires_at: 1.minute.ago)}
...@@ -1202,7 +1044,7 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -1202,7 +1044,7 @@ RSpec.describe 'Git LFS API and storage' do
put_authorize put_authorize
end end
it_behaves_like 'LFS http 200 response' it_behaves_like 'LFS http 200 workhorse response'
it 'with location of LFS store and object details' do it 'with location of LFS store and object details' do
expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path) expect(json_response['TempPath']).to eq(LfsObjectUploader.workhorse_local_upload_path)
...@@ -1330,4 +1172,50 @@ RSpec.describe 'Git LFS API and storage' do ...@@ -1330,4 +1172,50 @@ RSpec.describe 'Git LFS API and storage' do
"#{sample_oid}012345678" "#{sample_oid}012345678"
end end
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
context 'with project wikis' do
it_behaves_like 'LFS http requests' do
let(:container) { create(:project_wiki, :empty_repo, project: project) }
let(:authorize_guest) { project.add_guest(user) }
let(:authorize_download) { project.add_reporter(user) }
let(:authorize_upload) { project.add_developer(user) }
end
end
context 'with snippets' do
# LFS is not supported on snippets, so we override the shared examples
# to expect 404 responses instead.
[
'LFS http 200 response',
'LFS http 200 blob response',
'LFS http 403 response'
].each do |examples|
shared_examples_for(examples) { it_behaves_like 'LFS http 404 response' }
end
context 'with project snippets' do
it_behaves_like 'LFS http requests' do
let(:container) { create(:project_snippet, :empty_repo, project: project) }
let(:authorize_guest) { project.add_guest(user) }
let(:authorize_download) { project.add_reporter(user) }
let(:authorize_upload) { project.add_developer(user) }
end
end
context 'with personal snippets' do
it_behaves_like 'LFS http requests' do
let(:container) { create(:personal_snippet, :empty_repo) }
let(:authorize_upload) { container.update!(author: user) }
end
end
end
end end
...@@ -3,24 +3,38 @@ ...@@ -3,24 +3,38 @@
require 'spec_helper' require 'spec_helper'
RSpec.describe 'Git LFS File Locking API' do RSpec.describe 'Git LFS File Locking API' do
include LfsHttpHelpers
include WorkhorseHelpers include WorkhorseHelpers
let(:project) { create(:project) } let_it_be(:project) { create(:project) }
let(:maintainer) { create(:user) } let_it_be(:maintainer) { create(:user) }
let(:developer) { create(:user) } let_it_be(:developer) { create(:user) }
let(:guest) { create(:user) } let_it_be(:reporter) { create(:user) }
let(:path) { 'README.md' } let_it_be(:guest) { create(:user) }
let_it_be(:path) { 'README.md' }
let(:user) { developer }
let(:headers) do let(:headers) do
{ {
'Authorization' => authorization 'Authorization' => authorize_user
}.compact }.compact
end end
shared_examples 'unauthorized request' do shared_examples 'unauthorized request' do
context 'when user is not authorized' do context 'when user does not have download permission' do
let(:authorization) { authorize_user(guest) } let(:user) { guest }
it 'returns a forbidden 403 response' do it 'returns a 404 response' do
post_lfs_json url, body, headers
expect(response).to have_gitlab_http_status(:not_found)
end
end
context 'when user does not have upload permission' do
let(:user) { reporter }
it 'returns a 403 response' do
post_lfs_json url, body, headers post_lfs_json url, body, headers
expect(response).to have_gitlab_http_status(:forbidden) expect(response).to have_gitlab_http_status(:forbidden)
...@@ -31,15 +45,15 @@ RSpec.describe 'Git LFS File Locking API' do ...@@ -31,15 +45,15 @@ RSpec.describe 'Git LFS File Locking API' do
before do before do
allow(Gitlab.config.lfs).to receive(:enabled).and_return(true) allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
project.add_developer(maintainer) project.add_maintainer(maintainer)
project.add_developer(developer) project.add_developer(developer)
project.add_reporter(reporter)
project.add_guest(guest) project.add_guest(guest)
end end
describe 'Create File Lock endpoint' do describe 'Create File Lock endpoint' do
let(:url) { "#{project.http_url_to_repo}/info/lfs/locks" } let(:url) { "#{project.http_url_to_repo}/info/lfs/locks" }
let(:authorization) { authorize_user(developer) } let(:body) { { path: path } }
let(:body) { { path: path } }
include_examples 'unauthorized request' include_examples 'unauthorized request'
...@@ -76,8 +90,7 @@ RSpec.describe 'Git LFS File Locking API' do ...@@ -76,8 +90,7 @@ RSpec.describe 'Git LFS File Locking API' do
end end
describe 'Listing File Locks endpoint' do describe 'Listing File Locks endpoint' do
let(:url) { "#{project.http_url_to_repo}/info/lfs/locks" } let(:url) { "#{project.http_url_to_repo}/info/lfs/locks" }
let(:authorization) { authorize_user(developer) }
include_examples 'unauthorized request' include_examples 'unauthorized request'
...@@ -95,8 +108,7 @@ RSpec.describe 'Git LFS File Locking API' do ...@@ -95,8 +108,7 @@ RSpec.describe 'Git LFS File Locking API' do
end end
describe 'List File Locks for verification endpoint' do describe 'List File Locks for verification endpoint' do
let(:url) { "#{project.http_url_to_repo}/info/lfs/locks/verify" } let(:url) { "#{project.http_url_to_repo}/info/lfs/locks/verify" }
let(:authorization) { authorize_user(developer) }
include_examples 'unauthorized request' include_examples 'unauthorized request'
...@@ -116,9 +128,8 @@ RSpec.describe 'Git LFS File Locking API' do ...@@ -116,9 +128,8 @@ RSpec.describe 'Git LFS File Locking API' do
end end
describe 'Delete File Lock endpoint' do describe 'Delete File Lock endpoint' do
let!(:lock) { lock_file('README.md', developer) } let!(:lock) { lock_file('README.md', developer) }
let(:url) { "#{project.http_url_to_repo}/info/lfs/locks/#{lock[:id]}/unlock" } let(:url) { "#{project.http_url_to_repo}/info/lfs/locks/#{lock[:id]}/unlock" }
let(:authorization) { authorize_user(developer) }
include_examples 'unauthorized request' include_examples 'unauthorized request'
...@@ -136,7 +147,7 @@ RSpec.describe 'Git LFS File Locking API' do ...@@ -136,7 +147,7 @@ RSpec.describe 'Git LFS File Locking API' do
end end
context 'when a maintainer uses force' do context 'when a maintainer uses force' do
let(:authorization) { authorize_user(maintainer) } let(:user) { maintainer }
it 'deletes the lock' do it 'deletes the lock' do
project.add_maintainer(maintainer) project.add_maintainer(maintainer)
...@@ -154,14 +165,6 @@ RSpec.describe 'Git LFS File Locking API' do ...@@ -154,14 +165,6 @@ RSpec.describe 'Git LFS File Locking API' do
result[:lock] result[:lock]
end end
def authorize_user(user)
ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
end
def post_lfs_json(url, body = nil, headers = nil)
post(url, params: body.try(:to_json), headers: (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE))
end
def do_get(url, params = nil, headers = nil) def do_get(url, params = nil, headers = nil)
get(url, params: (params || {}), headers: (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE)) get(url, params: (params || {}), headers: (headers || {}).merge('Content-Type' => LfsRequest::CONTENT_TYPE))
end end
......
...@@ -31,16 +31,16 @@ module LfsHttpHelpers ...@@ -31,16 +31,16 @@ module LfsHttpHelpers
post(url, params: params, headers: headers) post(url, params: params, headers: headers)
end end
def batch_url(project) def batch_url(container)
"#{project.http_url_to_repo}/info/lfs/objects/batch" "#{container.http_url_to_repo}/info/lfs/objects/batch"
end end
def objects_url(project, oid = nil, size = nil) def objects_url(container, oid = nil, size = nil)
File.join(["#{project.http_url_to_repo}/gitlab-lfs/objects", oid, size].compact.map(&:to_s)) File.join(["#{container.http_url_to_repo}/gitlab-lfs/objects", oid, size].compact.map(&:to_s))
end end
def authorize_url(project, oid, size) def authorize_url(container, oid, size)
File.join(objects_url(project, oid, size), 'authorize') File.join(objects_url(container, oid, size), 'authorize')
end end
def download_body(objects) def download_body(objects)
......
...@@ -2,42 +2,252 @@ ...@@ -2,42 +2,252 @@
RSpec.shared_examples 'LFS http 200 response' do RSpec.shared_examples 'LFS http 200 response' do
it_behaves_like 'LFS http expected response code and message' do it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 200 } let(:response_code) { :ok }
end
end
RSpec.shared_examples 'LFS http 200 blob response' do
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { :ok }
let(:content_type) { Repositories::LfsApiController::LFS_TRANSFER_CONTENT_TYPE }
let(:response_headers) { { 'X-Sendfile' => lfs_object.file.path } }
end
end
RSpec.shared_examples 'LFS http 200 workhorse response' do
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { :ok }
let(:content_type) { Gitlab::Workhorse::INTERNAL_API_CONTENT_TYPE }
end end
end end
RSpec.shared_examples 'LFS http 401 response' do RSpec.shared_examples 'LFS http 401 response' do
it_behaves_like 'LFS http expected response code and message' do it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 401 } let(:response_code) { :unauthorized }
let(:content_type) { 'text/plain' }
end end
end end
RSpec.shared_examples 'LFS http 403 response' do RSpec.shared_examples 'LFS http 403 response' do
it_behaves_like 'LFS http expected response code and message' do it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 403 } let(:response_code) { :forbidden }
let(:message) { 'Access forbidden. Check your access level.' } let(:message) { 'Access forbidden. Check your access level.' }
end end
end end
RSpec.shared_examples 'LFS http 501 response' do RSpec.shared_examples 'LFS http 501 response' do
it_behaves_like 'LFS http expected response code and message' do it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 501 } let(:response_code) { :not_implemented }
let(:message) { 'Git LFS is not enabled on this GitLab server, contact your admin.' } let(:message) { 'Git LFS is not enabled on this GitLab server, contact your admin.' }
end end
end end
RSpec.shared_examples 'LFS http 404 response' do RSpec.shared_examples 'LFS http 404 response' do
it_behaves_like 'LFS http expected response code and message' do it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 404 } let(:response_code) { :not_found }
end end
end end
RSpec.shared_examples 'LFS http expected response code and message' do RSpec.shared_examples 'LFS http expected response code and message' do
let(:response_code) { } let(:response_code) { }
let(:message) { } let(:response_headers) { {} }
let(:content_type) { LfsRequest::CONTENT_TYPE }
let(:message) {}
it 'responds with the expected response code and message' do specify do
expect(response).to have_gitlab_http_status(response_code) expect(response).to have_gitlab_http_status(response_code)
expect(response.headers.to_hash).to include(response_headers)
expect(response.media_type).to match(content_type)
expect(json_response['message']).to eq(message) if message expect(json_response['message']).to eq(message) if message
end end
end end
RSpec.shared_examples 'LFS http requests' do
include LfsHttpHelpers
let(:authorize_guest) {}
let(:authorize_download) {}
let(:authorize_upload) {}
let(:lfs_object) { create(:lfs_object, :with_file) }
let(:sample_oid) { lfs_object.oid }
let(:authorization) { authorize_user }
let(:headers) do
{
'Authorization' => authorization,
'X-Sendfile-Type' => 'X-Sendfile'
}
end
let(:request_download) do
get objects_url(container, sample_oid), params: {}, headers: headers
end
let(:request_upload) do
post_lfs_json batch_url(container), upload_body(multiple_objects), headers
end
before do
stub_lfs_setting(enabled: true)
end
context 'when LFS is disabled globally' do
before do
stub_lfs_setting(enabled: false)
end
describe 'download request' do
before do
request_download
end
it_behaves_like 'LFS http 501 response'
end
describe 'upload request' do
before do
request_upload
end
it_behaves_like 'LFS http 501 response'
end
end
context 'unauthenticated' do
let(:headers) { {} }
describe 'download request' do
before do
request_download
end
it_behaves_like 'LFS http 401 response'
end
describe 'upload request' do
before do
request_upload
end
it_behaves_like 'LFS http 401 response'
end
end
context 'without access' do
describe 'download request' do
before do
request_download
end
it_behaves_like 'LFS http 404 response'
end
describe 'upload request' do
before do
request_upload
end
it_behaves_like 'LFS http 404 response'
end
end
context 'with guest access' do
before do
authorize_guest
end
describe 'download request' do
before do
request_download
end
it_behaves_like 'LFS http 404 response'
end
describe 'upload request' do
before do
request_upload
end
it_behaves_like 'LFS http 404 response'
end
end
context 'with download permission' do
before do
authorize_download
end
describe 'download request' do
before do
request_download
end
it_behaves_like 'LFS http 200 blob response'
context 'when container does not exist' do
def objects_url(*args)
super.sub(container.full_path, 'missing/path')
end
it_behaves_like 'LFS http 404 response'
end
end
describe 'upload request' do
before do
request_upload
end
it_behaves_like 'LFS http 403 response'
end
end
context 'with upload permission' do
before do
authorize_upload
end
describe 'upload request' do
before do
request_upload
end
it_behaves_like 'LFS http 200 response'
end
end
describe 'deprecated API' do
shared_examples 'deprecated request' do
before do
request
end
it_behaves_like 'LFS http expected response code and message' do
let(:response_code) { 501 }
let(:message) { 'Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.' }
end
end
context 'when fetching LFS object using deprecated API' do
subject(:request) do
get deprecated_objects_url(container, sample_oid), params: {}, headers: headers
end
it_behaves_like 'deprecated request'
end
context 'when handling LFS request using deprecated API' do
subject(:request) do
post_lfs_json deprecated_objects_url(container), nil, headers
end
it_behaves_like 'deprecated request'
end
def deprecated_objects_url(container, oid = nil)
File.join(["#{container.http_url_to_repo}/info/lfs/objects/", oid].compact)
end
end
end
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