Commit 63e1fcb7 authored by Markus Koller's avatar Markus Koller Committed by Stan Hu

Block LFS requests on snippets

The repository routes for project repositories are ambiguous and also
match project snippet repositories, so LFS requests for project snippets
will work but snippets are not ready yet to properly support LFS.

We can work around this by checking `#lfs_enabled?` on the `container`
instead of the `project`, which for snippets will be the snippet itself,
and `Snippet#lfs_enabled?` is currently hard-coded to return `false`.

To simplify things, we also remove the project-specific access check and
use `lfs_download_access?` instead to determine wether to expose the
existence of the project (404 response) or not (403 response), when
sending an error response. When LFS is disabled on the container we now
also send a 404 instead of a 403.
parent 22271907
# 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