summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorMarin Jankovski <maxlazio@gmail.com>2015-10-12 16:42:14 +0200
committerMarin Jankovski <maxlazio@gmail.com>2015-11-16 12:39:13 +0100
commit14032d8eb1a60ae5920286249c1044be2fa27278 (patch)
tree78d27cfb47590f5c30b1ae75de60368c5e269cc9
parent9179fcece813d0fe54abb5d7a28874cfa799f9c5 (diff)
downloadgitlab-ce-14032d8eb1a60ae5920286249c1044be2fa27278.tar.gz
Add support for git lfs.lfs
-rw-r--r--.gitignore1
-rw-r--r--app/controllers/projects_controller.rb5
-rw-r--r--app/models/lfs_object.rb8
-rw-r--r--app/models/lfs_objects_project.rb8
-rw-r--r--app/models/project.rb12
-rw-r--r--app/uploaders/lfs_object_uploader.rb29
-rw-r--r--config/gitlab.yml.example10
-rw-r--r--config/initializers/1_settings.rb7
-rw-r--r--config/routes.rb2
-rw-r--r--db/migrate/20151103134857_create_lfs_objects.rb10
-rw-r--r--db/migrate/20151103134958_create_lfs_objects_projects.rb12
-rw-r--r--db/migrate/20151104105513_add_file_to_lfs_objects.rb5
-rw-r--r--db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb6
-rw-r--r--db/schema.rb22
-rw-r--r--lib/gitlab/backend/grack_auth.rb5
-rw-r--r--lib/gitlab/git_access.rb6
-rw-r--r--lib/gitlab/lfs/response.rb308
-rw-r--r--lib/gitlab/lfs/router.rb95
-rw-r--r--lib/support/nginx/gitlab9
-rw-r--r--lib/support/nginx/gitlab-ssl9
-rw-r--r--spec/factories/lfs_objects.rb12
-rw-r--r--spec/factories/lfs_objects_projects.rb8
-rw-r--r--spec/lib/gitlab/lfs/lfs_router_spec.rb650
23 files changed, 1226 insertions, 13 deletions
diff --git a/.gitignore b/.gitignore
index 39ff95c50ee..f5b6427ca03 100644
--- a/.gitignore
+++ b/.gitignore
@@ -43,3 +43,4 @@ rails_best_practices_output.html
tmp/
vendor/bundle/*
builds/*
+shared/*
diff --git a/app/controllers/projects_controller.rb b/app/controllers/projects_controller.rb
index 30b166334a9..23453195e85 100644
--- a/app/controllers/projects_controller.rb
+++ b/app/controllers/projects_controller.rb
@@ -72,8 +72,7 @@ class ProjectsController < ApplicationController
def remove_fork
return access_denied! unless can?(current_user, :remove_fork_project, @project)
- if @project.forked?
- @project.forked_project_link.destroy
+ if @project.unlink_fork
flash[:notice] = 'The fork relationship has been removed.'
end
end
@@ -243,7 +242,7 @@ class ProjectsController < ApplicationController
project.repository_exists? && !project.empty_repo?
end
- # Override get_id from ExtractsPath, which returns the branch and file path
+ # Override get_id from ExtractsPath, which returns the branch and file path
# for the blob/tree, which in this case is just the root of the default branch.
def get_id
project.repository.root_ref
diff --git a/app/models/lfs_object.rb b/app/models/lfs_object.rb
new file mode 100644
index 00000000000..3c1426f59d0
--- /dev/null
+++ b/app/models/lfs_object.rb
@@ -0,0 +1,8 @@
+class LfsObject < ActiveRecord::Base
+ has_many :lfs_objects_projects, dependent: :destroy
+ has_many :projects, through: :lfs_objects_projects
+
+ validates :oid, presence: true, uniqueness: true
+
+ mount_uploader :file, LfsObjectUploader
+end
diff --git a/app/models/lfs_objects_project.rb b/app/models/lfs_objects_project.rb
new file mode 100644
index 00000000000..0fd5f089db9
--- /dev/null
+++ b/app/models/lfs_objects_project.rb
@@ -0,0 +1,8 @@
+class LfsObjectsProject < ActiveRecord::Base
+ belongs_to :project
+ belongs_to :lfs_object
+
+ validates :lfs_object_id, presence: true
+ validates :lfs_object_id, uniqueness: { scope: [:project_id], message: "already exists in project" }
+ validates :project_id, presence: true
+end
diff --git a/app/models/project.rb b/app/models/project.rb
index 3e72a9a46a0..9ea0d15497a 100644
--- a/app/models/project.rb
+++ b/app/models/project.rb
@@ -124,6 +124,8 @@ class Project < ActiveRecord::Base
has_many :ci_commits, dependent: :destroy, class_name: 'Ci::Commit', foreign_key: :gl_project_id
has_many :ci_builds, through: :ci_commits, source: :builds, dependent: :destroy, class_name: 'Ci::Build'
has_many :releases, dependent: :destroy
+ has_many :lfs_objects_projects, dependent: :destroy
+ has_many :lfs_objects, through: :lfs_objects_projects
has_one :import_data, dependent: :destroy, class_name: "ProjectImportData"
has_one :gitlab_ci_project, dependent: :destroy, class_name: "Ci::Project", foreign_key: :gitlab_id
@@ -798,4 +800,14 @@ class Project < ActiveRecord::Base
def enable_ci
self.builds_enabled = true
end
+
+ def unlink_fork
+ if forked?
+ forked_from_project.lfs_objects.find_each do |lfs_object|
+ lfs_object.projects << self
+ end
+
+ forked_project_link.destroy
+ end
+ end
end
diff --git a/app/uploaders/lfs_object_uploader.rb b/app/uploaders/lfs_object_uploader.rb
new file mode 100644
index 00000000000..28085b31083
--- /dev/null
+++ b/app/uploaders/lfs_object_uploader.rb
@@ -0,0 +1,29 @@
+# encoding: utf-8
+
+class LfsObjectUploader < CarrierWave::Uploader::Base
+ storage :file
+
+ def store_dir
+ "#{Gitlab.config.lfs.storage_path}/#{model.oid[0,2]}/#{model.oid[2,2]}"
+ end
+
+ def cache_dir
+ "#{Gitlab.config.lfs.storage_path}/tmp/cache"
+ end
+
+ def move_to_cache
+ true
+ end
+
+ def move_to_store
+ true
+ end
+
+ def exists?
+ file.try(:exists?)
+ end
+
+ def filename
+ model.oid[4..-1]
+ end
+end
diff --git a/config/gitlab.yml.example b/config/gitlab.yml.example
index 5bd98e3e42d..8fdb2603ce8 100644
--- a/config/gitlab.yml.example
+++ b/config/gitlab.yml.example
@@ -124,6 +124,12 @@ production: &base
# The mailbox where incoming mail will end up. Usually "inbox".
mailbox: "inbox"
+ ## Git LFS
+ lfs:
+ enabled: false
+ # The location where LFS objects are stored (default: shared/lfs-objects).
+ # storage_path: shared/lfs-objects
+
## Gravatar
## For Libravatar see: http://doc.gitlab.com/ce/customization/libravatar.html
gravatar:
@@ -317,8 +323,6 @@ production: &base
# path: /mnt/gitlab # Default: shared
-
-
#
# 4. Advanced settings
# ==========================
@@ -419,6 +423,8 @@ test:
<<: *base
gravatar:
enabled: true
+ lfs:
+ enabled: false
gitlab:
host: localhost
port: 80
diff --git a/config/initializers/1_settings.rb b/config/initializers/1_settings.rb
index 847d9f21898..6b7990c0ab0 100644
--- a/config/initializers/1_settings.rb
+++ b/config/initializers/1_settings.rb
@@ -200,6 +200,13 @@ Settings.incoming_email['start_tls'] = false if Settings.incoming_email['start_
Settings.incoming_email['mailbox'] = "inbox" if Settings.incoming_email['mailbox'].nil?
#
+# Git LFS
+#
+Settings['lfs'] ||= Settingslogic.new({})
+Settings.lfs['enabled'] = false if Settings.lfs['enabled'].nil?
+Settings.lfs['storage_path'] = File.expand_path(Settings.lfs['storage_path'] || File.join(Settings.shared['path'], "lfs-objects"), Rails.root)
+
+#
# Gravatar
#
Settings['gravatar'] ||= Settingslogic.new({})
diff --git a/config/routes.rb b/config/routes.rb
index 095c562be8d..bd85f4e3c69 100644
--- a/config/routes.rb
+++ b/config/routes.rb
@@ -93,7 +93,7 @@ Gitlab::Application.routes.draw do
end
# Enable Grack support
- mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post]
+ mount Grack::AuthSpawner, at: '/', constraints: lambda { |request| /[-\/\w\.]+\.git\//.match(request.path_info) }, via: [:get, :post, :put]
# Help
get 'help' => 'help#index'
diff --git a/db/migrate/20151103134857_create_lfs_objects.rb b/db/migrate/20151103134857_create_lfs_objects.rb
new file mode 100644
index 00000000000..2d04c170a88
--- /dev/null
+++ b/db/migrate/20151103134857_create_lfs_objects.rb
@@ -0,0 +1,10 @@
+class CreateLfsObjects < ActiveRecord::Migration
+ def change
+ create_table :lfs_objects do |t|
+ t.string :oid, null: false, unique: true
+ t.integer :size, null: false
+
+ t.timestamps
+ end
+ end
+end
diff --git a/db/migrate/20151103134958_create_lfs_objects_projects.rb b/db/migrate/20151103134958_create_lfs_objects_projects.rb
new file mode 100644
index 00000000000..f3f58b931ec
--- /dev/null
+++ b/db/migrate/20151103134958_create_lfs_objects_projects.rb
@@ -0,0 +1,12 @@
+class CreateLfsObjectsProjects < ActiveRecord::Migration
+ def change
+ create_table :lfs_objects_projects do |t|
+ t.integer :lfs_object_id, null: false
+ t.integer :project_id, null: false
+
+ t.timestamps
+ end
+
+ add_index :lfs_objects_projects, :project_id
+ end
+end
diff --git a/db/migrate/20151104105513_add_file_to_lfs_objects.rb b/db/migrate/20151104105513_add_file_to_lfs_objects.rb
new file mode 100644
index 00000000000..7c57f3f0df6
--- /dev/null
+++ b/db/migrate/20151104105513_add_file_to_lfs_objects.rb
@@ -0,0 +1,5 @@
+class AddFileToLfsObjects < ActiveRecord::Migration
+ def change
+ add_column :lfs_objects, :file, :string
+ end
+end
diff --git a/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb
new file mode 100644
index 00000000000..d10f1f6e605
--- /dev/null
+++ b/db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb
@@ -0,0 +1,6 @@
+class AddIndexForLfsOidAndSize < ActiveRecord::Migration
+ def change
+ add_index :lfs_objects, :oid
+ add_index :lfs_objects, [:oid, :size]
+ end
+end
diff --git a/db/schema.rb b/db/schema.rb
index f631d73f334..a8e8dfe6bbf 100644
--- a/db/schema.rb
+++ b/db/schema.rb
@@ -11,7 +11,7 @@
#
# It's strongly recommended that you check this file into your version control system.
-ActiveRecord::Schema.define(version: 20151109100728) do
+ActiveRecord::Schema.define(version: 20151114113410) do
# These are extensions that must be enabled in order to support this database
enable_extension "plpgsql"
@@ -422,6 +422,26 @@ ActiveRecord::Schema.define(version: 20151109100728) do
add_index "labels", ["project_id"], name: "index_labels_on_project_id", using: :btree
+ create_table "lfs_objects", force: true do |t|
+ t.string "oid", null: false, unique: true
+ t.integer "size", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ t.string "file"
+ end
+
+ add_index "lfs_objects", ["oid", "size"], name: "index_lfs_objects_on_oid_and_size", using: :btree
+ add_index "lfs_objects", ["oid"], name: "index_lfs_objects_on_oid", using: :btree
+
+ create_table "lfs_objects_projects", force: true do |t|
+ t.integer "lfs_object_id", null: false
+ t.integer "project_id", null: false
+ t.datetime "created_at"
+ t.datetime "updated_at"
+ end
+
+ add_index "lfs_objects_projects", ["project_id"], name: "index_lfs_objects_projects_on_project_id", using: :btree
+
create_table "members", force: true do |t|
t.integer "access_level", null: false
t.integer "source_id", null: false
diff --git a/lib/gitlab/backend/grack_auth.rb b/lib/gitlab/backend/grack_auth.rb
index 440ef5a3cb3..0d156047ff0 100644
--- a/lib/gitlab/backend/grack_auth.rb
+++ b/lib/gitlab/backend/grack_auth.rb
@@ -33,6 +33,9 @@ module Grack
auth!
+ lfs_response = Gitlab::Lfs::Router.new(project, @user, @request).try_call
+ return lfs_response unless lfs_response.nil?
+
if project && authorized_request?
# Tell gitlab-workhorse the request is OK, and what the GL_ID is
render_grack_auth_ok
@@ -72,7 +75,7 @@ module Grack
matched_login = /(?<s>^[a-zA-Z]*-ci)-token$/.match(login)
if project && matched_login.present? && git_cmd == 'git-upload-pack'
- underscored_service = matched_login['s'].underscore
+ underscored_service = matched_login['s'].underscore
if Service.available_services_names.include?(underscored_service)
service_method = "#{underscored_service}_service"
diff --git a/lib/gitlab/git_access.rb b/lib/gitlab/git_access.rb
index c90184d31cf..3ed1eec517c 100644
--- a/lib/gitlab/git_access.rb
+++ b/lib/gitlab/git_access.rb
@@ -13,7 +13,7 @@ module Gitlab
def user
return @user if defined?(@user)
- @user =
+ @user =
case actor
when User
actor
@@ -125,7 +125,7 @@ module Gitlab
def change_access_check(change)
oldrev, newrev, ref = change.split(' ')
- action =
+ action =
if project.protected_branch?(branch_name(ref))
protected_branch_action(oldrev, newrev, branch_name(ref))
elsif protected_tag?(tag_name(ref))
@@ -148,7 +148,7 @@ module Gitlab
build_status_object(false, "You are not allowed to change existing tags on this project.")
else # :push_code
build_status_object(false, "You are not allowed to push code to this project.")
- end
+ end
return status
end
diff --git a/lib/gitlab/lfs/response.rb b/lib/gitlab/lfs/response.rb
new file mode 100644
index 00000000000..4202c786466
--- /dev/null
+++ b/lib/gitlab/lfs/response.rb
@@ -0,0 +1,308 @@
+module Gitlab
+ module Lfs
+ class Response
+
+ def initialize(project, user, request)
+ @origin_project = project
+ @project = storage_project(project)
+ @user = user
+ @env = request.env
+ @request = request
+ end
+
+ # Return a response for a download request
+ # Can be a response to:
+ # Request from a user to get the file
+ # Request from gitlab-workhorse which file to serve to the user
+ def render_download_hypermedia_response(oid)
+ render_response_to_download do
+ if check_download_accept_header?
+ render_lfs_download_hypermedia(oid)
+ else
+ render_not_found
+ end
+ end
+ end
+
+ def render_download_object_response(oid)
+ render_response_to_download do
+ if check_download_sendfile_header? && check_download_accept_header?
+ render_lfs_sendfile(oid)
+ else
+ render_not_found
+ end
+ end
+ end
+
+ def render_lfs_api_auth
+ render_response_to_push do
+ request_body = JSON.parse(@request.body.read)
+ return render_not_found if request_body.empty? || request_body['objects'].empty?
+
+ response = build_response(request_body['objects'])
+ [
+ 200,
+ {
+ "Content-Type" => "application/json; charset=utf-8",
+ "Cache-Control" => "private",
+ },
+ [JSON.dump(response)]
+ ]
+ end
+ end
+
+ def render_storage_upload_authorize_response(oid, size)
+ render_response_to_push do
+ [
+ 200,
+ { "Content-Type" => "application/json; charset=utf-8" },
+ [JSON.dump({
+ 'StoreLFSPath' => "#{Gitlab.config.lfs.storage_path}/tmp/upload",
+ 'LfsOid' => oid,
+ 'LfsSize' => size
+ })]
+ ]
+ end
+ end
+
+ def render_storage_upload_store_response(oid, size, tmp_file_name)
+ render_response_to_push do
+ render_lfs_upload_ok(oid, size, tmp_file_name)
+ end
+ end
+
+ private
+
+ def render_not_enabled
+ [
+ 501,
+ {
+ "Content-Type" => "application/vnd.git-lfs+json",
+ },
+ [JSON.dump({
+ 'message' => 'Git LFS is not enabled on this GitLab server, contact your admin.',
+ 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
+ })]
+ ]
+ end
+
+ def render_unauthorized
+ [
+ 401,
+ {
+ 'Content-Type' => 'text/plain'
+ },
+ ['Unauthorized']
+ ]
+ end
+
+ def render_not_found
+ [
+ 404,
+ {
+ "Content-Type" => "application/vnd.git-lfs+json"
+ },
+ [JSON.dump({
+ 'message' => 'Not found.',
+ 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
+ })]
+ ]
+ end
+
+ def render_forbidden
+ [
+ 403,
+ {
+ "Content-Type" => "application/vnd.git-lfs+json"
+ },
+ [JSON.dump({
+ 'message' => 'Access forbidden. Check your access level.',
+ 'documentation_url' => "#{Gitlab.config.gitlab.url}/help",
+ })]
+ ]
+ end
+
+ def render_lfs_sendfile(oid)
+ return render_not_found unless oid.present?
+
+ lfs_object = object_for_download(oid)
+
+ if lfs_object && lfs_object.file.exists?
+ [
+ 200,
+ {
+ # GitLab-workhorse will forward Content-Type header
+ "Content-Type" => "application/octet-stream",
+ "X-Sendfile" => lfs_object.file.path
+ },
+ []
+ ]
+ else
+ render_not_found
+ end
+ end
+
+ def render_lfs_download_hypermedia(oid)
+ return render_not_found unless oid.present?
+
+ lfs_object = object_for_download(oid)
+ if lfs_object
+ [
+ 200,
+ { "Content-Type" => "application/vnd.git-lfs+json" },
+ [JSON.dump(download_hypermedia(oid))]
+ ]
+ else
+ render_not_found
+ end
+ end
+
+ def render_lfs_upload_ok(oid, size, tmp_file)
+ if store_file(oid, size, tmp_file)
+ [
+ 200,
+ {
+ 'Content-Type' => 'text/plain',
+ 'Content-Length' => 0
+ },
+ []
+ ]
+ else
+ [
+ 422,
+ { 'Content-Type' => 'text/plain' },
+ ["Unprocessable entity"]
+ ]
+ end
+ end
+
+ def render_response_to_download
+ return render_not_enabled unless Gitlab.config.lfs.enabled
+
+ unless @project.public?
+ return render_unauthorized unless @user
+ return render_forbidden unless user_can_fetch?
+ end
+
+ yield
+ end
+
+ def render_response_to_push
+ return render_not_enabled unless Gitlab.config.lfs.enabled
+ return render_unauthorized unless @user
+ return render_forbidden unless user_can_push?
+
+ yield
+ end
+
+ def check_download_sendfile_header?
+ @env['HTTP_X_SENDFILE_TYPE'].to_s == "X-Sendfile"
+ end
+
+ def check_download_accept_header?
+ @env['HTTP_ACCEPT'].to_s == "application/vnd.git-lfs+json; charset=utf-8"
+ end
+
+ def user_can_fetch?
+ # Check user access against the project they used to initiate the pull
+ @user.can?(:download_code, @origin_project)
+ end
+
+ def user_can_push?
+ # Check user access against the project they used to initiate the push
+ @user.can?(:push_code, @origin_project)
+ end
+
+ def storage_project(project)
+ if project.forked?
+ project.forked_from_project
+ else
+ project
+ end
+ end
+
+ def store_file(oid, size, tmp_file)
+ tmp_file_path = File.join("#{Gitlab.config.lfs.storage_path}/tmp/upload", tmp_file)
+
+ object = LfsObject.find_or_create_by(oid: oid, size: size)
+ if object.file.exists?
+ success = true
+ else
+ success = move_tmp_file_to_storage(object, tmp_file_path)
+ end
+
+ if success
+ success = link_to_project(object)
+ end
+
+ success
+ ensure
+ # Ensure that the tmp file is removed
+ FileUtils.rm_f(tmp_file_path)
+ end
+
+ def object_for_download(oid)
+ @project.lfs_objects.find_by(oid: oid)
+ end
+
+ def move_tmp_file_to_storage(object, path)
+ File.open(path) do |f|
+ object.file = f
+ end
+
+ object.file.store!
+ object.save
+ end
+
+ def link_to_project(object)
+ if object && !object.projects.exists?(@project)
+ object.projects << @project
+ object.save
+ end
+ end
+
+ def select_existing_objects(objects)
+ objects_oids = objects.map { |o| o['oid'] }
+ @project.lfs_objects.where(oid: objects_oids).pluck(:oid).to_set
+ end
+
+ def build_response(objects)
+ selected_objects = select_existing_objects(objects)
+
+ upload_hypermedia(objects, selected_objects)
+ end
+
+ def download_hypermedia(oid)
+ {
+ '_links' => {
+ 'download' =>
+ {
+ 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{oid}",
+ 'header' => {
+ 'Accept' => "application/vnd.git-lfs+json; charset=utf-8",
+ 'Authorization' => @env['HTTP_AUTHORIZATION']
+ }.compact
+ }
+ }
+ }
+ end
+
+ def upload_hypermedia(all_objects, existing_objects)
+ all_objects.each do |object|
+ object['_links'] = hypermedia_links(object) unless existing_objects.include?(object['oid'])
+ end
+
+ { 'objects' => all_objects }
+ end
+
+ def hypermedia_links(object)
+ {
+ "upload" => {
+ 'href' => "#{@origin_project.http_url_to_repo}/gitlab-lfs/objects/#{object['oid']}/#{object['size']}",
+ 'header' => { 'Authorization' => @env['HTTP_AUTHORIZATION'] }
+ }.compact
+ }
+ end
+ end
+ end
+end
diff --git a/lib/gitlab/lfs/router.rb b/lib/gitlab/lfs/router.rb
new file mode 100644
index 00000000000..4809e834984
--- /dev/null
+++ b/lib/gitlab/lfs/router.rb
@@ -0,0 +1,95 @@
+module Gitlab
+ module Lfs
+ class Router
+ def initialize(project, user, request)
+ @project = project
+ @user = user
+ @env = request.env
+ @request = request
+ end
+
+ def try_call
+ return unless @request && @request.path.present?
+
+ case @request.request_method
+ when 'GET'
+ get_response
+ when 'POST'
+ post_response
+ when 'PUT'
+ put_response
+ else
+ nil
+ end
+ end
+
+ private
+
+ def get_response
+ path_match = @request.path.match(/\/(info\/lfs|gitlab-lfs)\/objects\/([0-9a-f]{64})$/)
+ return nil unless path_match
+
+ oid = path_match[2]
+ return nil unless oid
+
+ case path_match[1]
+ when "info/lfs"
+ lfs.render_download_hypermedia_response(oid)
+ when "gitlab-lfs"
+ lfs.render_download_object_response(oid)
+ else
+ nil
+ end
+ end
+
+ def post_response
+ post_path = @request.path.match(/\/info\/lfs\/objects(\/batch)?$/)
+ return nil unless post_path
+
+ # Check for Batch API
+ if post_path[0].ends_with?("/info/lfs/objects/batch")
+ lfs.render_lfs_api_auth
+ else
+ nil
+ end
+ end
+
+ def put_response
+ object_match = @request.path.match(/\/gitlab-lfs\/objects\/([0-9a-f]{64})\/([0-9]+)(|\/authorize){1}$/)
+ return nil if object_match.nil?
+
+ oid = object_match[1]
+ size = object_match[2].try(:to_i)
+ return nil if oid.nil? || size.nil?
+
+ # GitLab-workhorse requests
+ # 1. Try to authorize the request
+ # 2. send a request with a header containing the name of the temporary file
+ if object_match[3] && object_match[3] == '/authorize'
+ lfs.render_storage_upload_authorize_response(oid, size)
+ else
+ tmp_file_name = sanitize_tmp_filename(@request.env['HTTP_X_GITLAB_LFS_TMP'])
+ return nil unless tmp_file_name
+
+ lfs.render_storage_upload_store_response(oid, size, tmp_file_name)
+ end
+ end
+
+ def lfs
+ return unless @project
+
+ Gitlab::Lfs::Response.new(@project, @user, @request)
+ end
+
+ def sanitize_tmp_filename(name)
+ if name.present?
+ name.gsub!(/^.*(\\|\/)/, '')
+ name = name.match(/[0-9a-f]{73}/)
+ name[0] if name
+ else
+ nil
+ end
+ end
+ end
+ end
+end
diff --git a/lib/support/nginx/gitlab b/lib/support/nginx/gitlab
index 0a7a4118077..93f2ad07aeb 100644
--- a/lib/support/nginx/gitlab
+++ b/lib/support/nginx/gitlab
@@ -44,7 +44,7 @@ upstream gitlab-workhorse {
## Normal HTTP host
server {
- ## Either remove "default_server" from the listen line below,
+ ## Either remove "default_server" from the listen line below,
## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
## to be served if you visit any address that your server responds to, eg.
## the ip address of the server (http://x.x.x.x/)n 0.0.0.0:80 default_server;
@@ -113,6 +113,13 @@ server {
proxy_pass http://gitlab;
}
+ location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ {
# 'Error' 418 is a hack to re-use the @gitlab-workhorse block
error_page 418 = @gitlab-workhorse;
diff --git a/lib/support/nginx/gitlab-ssl b/lib/support/nginx/gitlab-ssl
index b463d5b6aa9..90749947fa4 100644
--- a/lib/support/nginx/gitlab-ssl
+++ b/lib/support/nginx/gitlab-ssl
@@ -48,7 +48,7 @@ upstream gitlab-workhorse {
## Redirects all HTTP traffic to the HTTPS host
server {
- ## Either remove "default_server" from the listen line below,
+ ## Either remove "default_server" from the listen line below,
## or delete the /etc/nginx/sites-enabled/default file. This will cause gitlab
## to be served if you visit any address that your server responds to, eg.
## the ip address of the server (http://x.x.x.x/)
@@ -160,6 +160,13 @@ server {
proxy_pass http://gitlab;
}
+ location ~ ^/[\w\.-]+/[\w\.-]+/gitlab-lfs/objects {
+ client_max_body_size 0;
+ # 'Error' 418 is a hack to re-use the @gitlab-workhorse block
+ error_page 418 = @gitlab-workhorse;
+ return 418;
+ }
+
location ~ ^/[\w\.-]+/[\w\.-]+/(info/refs|git-upload-pack|git-receive-pack)$ {
# 'Error' 418 is a hack to re-use the @gitlab-workhorse block
error_page 418 = @gitlab-workhorse;
diff --git a/spec/factories/lfs_objects.rb b/spec/factories/lfs_objects.rb
new file mode 100644
index 00000000000..7fb2d77ca32
--- /dev/null
+++ b/spec/factories/lfs_objects.rb
@@ -0,0 +1,12 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :lfs_object do
+ oid "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80"
+ size 499013
+ end
+
+ trait :with_file do
+ file { fixture_file_upload(Rails.root + "spec/fixtures/dk.png", "`/png") }
+ end
+end
diff --git a/spec/factories/lfs_objects_projects.rb b/spec/factories/lfs_objects_projects.rb
new file mode 100644
index 00000000000..93de6607df8
--- /dev/null
+++ b/spec/factories/lfs_objects_projects.rb
@@ -0,0 +1,8 @@
+# Read about factories at https://github.com/thoughtbot/factory_girl
+
+FactoryGirl.define do
+ factory :lfs_objects_project do
+ lfs_object
+ project
+ end
+end
diff --git a/spec/lib/gitlab/lfs/lfs_router_spec.rb b/spec/lib/gitlab/lfs/lfs_router_spec.rb
new file mode 100644
index 00000000000..cebcb5bc887
--- /dev/null
+++ b/spec/lib/gitlab/lfs/lfs_router_spec.rb
@@ -0,0 +1,650 @@
+require 'spec_helper'
+
+describe Gitlab::Lfs::Router do
+ let(:project) { create(:project) }
+ let(:public_project) { create(:project, :public) }
+ let(:forked_project) { fork_project(public_project, user) }
+
+ let(:user) { create(:user) }
+ let(:user_two) { create(:user) }
+ let!(:lfs_object) { create(:lfs_object, :with_file) }
+
+ let(:request) { Rack::Request.new(env) }
+ let(:env) do
+ {
+ 'rack.input' => '',
+ 'REQUEST_METHOD' => 'GET',
+ }
+ end
+
+ let(:lfs_router_auth) { new_lfs_router(project, user) }
+ let(:lfs_router_noauth) { new_lfs_router(project, nil) }
+ let(:lfs_router_public_auth) { new_lfs_router(public_project, user) }
+ let(:lfs_router_public_noauth) { new_lfs_router(public_project, nil) }
+ let(:lfs_router_forked_noauth) { new_lfs_router(forked_project, nil) }
+ let(:lfs_router_forked_auth) { new_lfs_router(forked_project, user_two) }
+
+ let(:sample_oid) { "b68143e6463773b1b6c6fd009a76c32aeec041faff32ba2ed42fd7f708a17f80" }
+ let(:sample_size) { 499013 }
+
+ describe 'when lfs is disabled' do
+ before do
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(false)
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}"
+ end
+
+ it 'responds with 501' do
+ respond_with_disabled = [ 501,
+ { "Content-Type"=>"application/vnd.git-lfs+json" },
+ ["{\"message\":\"Git LFS is not enabled on this GitLab server, contact your admin.\",\"documentation_url\":\"#{Gitlab.config.gitlab.url}/help\"}"]
+ ]
+ expect(lfs_router_auth.try_call).to match_array(respond_with_disabled)
+ end
+ end
+
+ describe 'when fetching lfs object' do
+ before do
+ enable_lfs
+ env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8"
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}"
+ end
+
+ describe 'when user is authenticated' do
+ context 'and user has project download access' do
+ before do
+ @auth = authorize(user)
+ env["HTTP_AUTHORIZATION"] = @auth
+ project.lfs_objects << lfs_object
+ project.team << [user, :master]
+ end
+
+ it "responds with status 200" do
+ expect(lfs_router_auth.try_call.first).to eq(200)
+ end
+
+ it "responds with download hypermedia" do
+ json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first)
+
+ expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}")
+ expect(json_response['_links']['download']['header']).to eq("Authorization" => @auth, "Accept" => "application/vnd.git-lfs+json; charset=utf-8")
+ end
+ end
+
+ context 'and user does not have project access' do
+ it "responds with status 403" do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+ end
+
+ describe 'when user is unauthenticated' do
+ context 'and user does not have download access' do
+ it "responds with status 401" do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and user has download access' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it "responds with status 401" do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+
+ describe 'and project is public' do
+ context 'and project has access to the lfs object' do
+ before do
+ public_project.lfs_objects << lfs_object
+ end
+
+ context 'and user is authenticated' do
+ it "responds with status 200 and sends download hypermedia" do
+ expect(lfs_router_public_auth.try_call.first).to eq(200)
+ json_response = ActiveSupport::JSON.decode(lfs_router_public_auth.try_call.last.first)
+
+ expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{public_project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}")
+ expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8")
+ end
+ end
+
+ context 'and user is unauthenticated' do
+ it "responds with status 200 and sends download hypermedia" do
+ expect(lfs_router_public_noauth.try_call.first).to eq(200)
+ json_response = ActiveSupport::JSON.decode(lfs_router_public_noauth.try_call.last.first)
+
+ expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{public_project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}")
+ expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8")
+ end
+ end
+ end
+
+ context 'and project does not have access to the lfs object' do
+ it "responds with status 404" do
+ expect(lfs_router_public_auth.try_call.first).to eq(404)
+ end
+ end
+ end
+
+ describe 'and request comes from gitlab-workhorse' do
+ before do
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}"
+ end
+ context 'without user being authorized' do
+ it "responds with status 401" do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'with required headers' do
+ before do
+ env['HTTP_X_SENDFILE_TYPE'] = "X-Sendfile"
+ end
+
+ context 'when user does not have project access' do
+ it "responds with status 403" do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+
+ context 'when user has project access' do
+ before do
+ project.lfs_objects << lfs_object
+ project.team << [user, :master]
+ end
+
+ it "responds with status 200" do
+ expect(lfs_router_auth.try_call.first).to eq(200)
+ end
+
+ it "responds with the file location" do
+ expect(lfs_router_auth.try_call[1]['Content-Type']).to eq("application/octet-stream")
+ expect(lfs_router_auth.try_call[1]['X-Sendfile']).to eq(lfs_object.file.path)
+ end
+ end
+ end
+
+ context 'without required headers' do
+ it "responds with status 403" do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+ end
+
+ describe 'from a forked public project' do
+ before do
+ env['HTTP_ACCEPT'] = "application/vnd.git-lfs+json; charset=utf-8"
+ env["PATH_INFO"] = "#{forked_project.repository.path_with_namespace}.git/info/lfs/objects/#{sample_oid}"
+ end
+
+ context "when fetching a lfs object" do
+ context "and user has project download access" do
+ before do
+ public_project.lfs_objects << lfs_object
+ end
+
+ it "can download the lfs object" do
+ expect(lfs_router_forked_auth.try_call.first).to eq(200)
+ json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first)
+
+ expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{forked_project.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}")
+ expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8")
+ end
+ end
+
+ context "and user is not authenticated but project is public" do
+ before do
+ public_project.lfs_objects << lfs_object
+ end
+
+ it "can download the lfs object" do
+ expect(lfs_router_forked_auth.try_call.first).to eq(200)
+ end
+ end
+
+ context "and user has project download access" do
+ before do
+ env["PATH_INFO"] = "#{forked_project.repository.path_with_namespace}.git/info/lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897"
+ @auth = ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
+ env["HTTP_AUTHORIZATION"] = @auth
+ lfs_object_two = create(:lfs_object, :with_file, oid: "91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897", size: 1575078)
+ public_project.lfs_objects << lfs_object_two
+ end
+
+ it "can get a lfs object that is not in the forked project" do
+ expect(lfs_router_forked_auth.try_call.first).to eq(200)
+
+ json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first)
+ expect(json_response['_links']['download']['href']).to eq("#{Gitlab.config.gitlab.url}/#{forked_project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
+ expect(json_response['_links']['download']['header']).to eq("Accept" => "application/vnd.git-lfs+json; charset=utf-8", "Authorization" => @auth)
+ end
+ end
+
+ context "and user has project download access" do
+ before do
+ env["PATH_INFO"] = "#{forked_project.repository.path_with_namespace}.git/info/lfs/objects/267c8b1d876743971e3a9978405818ff5ca731c4c870b06507619cd9b1847b6b"
+ lfs_object_three = create(:lfs_object, :with_file, oid: "267c8b1d876743971e3a9978405818ff5ca731c4c870b06507619cd9b1847b6b", size: 127192524)
+ project.lfs_objects << lfs_object_three
+ end
+
+ it "cannot get a lfs object that is not in the project" do
+ expect(lfs_router_forked_auth.try_call.first).to eq(404)
+ end
+ end
+ end
+ end
+ end
+
+ describe 'when initiating pushing of the lfs object' do
+ before do
+ enable_lfs
+ env['REQUEST_METHOD'] = 'POST'
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/info/lfs/objects/batch"
+ end
+
+ describe 'when user is authenticated' do
+ before do
+ body = { 'objects' => [{
+ 'oid' => sample_oid,
+ 'size' => sample_size
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+
+ describe 'when user has project push access' do
+ before do
+ @auth = authorize(user)
+ env["HTTP_AUTHORIZATION"] = @auth
+ project.team << [user, :master]
+ end
+
+ context 'when pushing an lfs object that already exists' do
+ before do
+ public_project.lfs_objects << lfs_object
+ end
+
+ it "responds with status 200 and links the object to the project" do
+ response_body = lfs_router_auth.try_call.last
+ response = ActiveSupport::JSON.decode(response_body.first)
+
+ expect(response['objects']).to be_kind_of(Array)
+ expect(response['objects'].first['oid']).to eq(sample_oid)
+ expect(response['objects'].first['size']).to eq(sample_size)
+ expect(lfs_object.projects.pluck(:id)).to_not include(project.id)
+ expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
+ expect(response['objects'].first).to have_key('_links')
+ end
+ end
+
+ context 'when pushing a lfs object that does not exist' do
+ before do
+ body = {
+ 'objects' => [{
+ 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ }]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ end
+
+ it "responds with status 200 and upload hypermedia link" do
+ response = lfs_router_auth.try_call
+ expect(response.first).to eq(200)
+
+ response_body = ActiveSupport::JSON.decode(response.last.first)
+ expect(response_body['objects']).to be_kind_of(Array)
+ expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
+ expect(response_body['objects'].first['size']).to eq(1575078)
+ expect(lfs_object.projects.pluck(:id)).not_to include(project.id)
+ expect(response_body['objects'].first['_links']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
+ expect(response_body['objects'].first['_links']['upload']['header']).to eq("Authorization" => @auth)
+ end
+ end
+
+ context 'when pushing one new and one existing lfs object' do
+ before do
+ body = {
+ 'objects' => [
+ { 'oid' => '91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897',
+ 'size' => 1575078
+ },
+ { 'oid' => sample_oid,
+ 'size' => sample_size
+ }
+ ]
+ }.to_json
+ env['rack.input'] = StringIO.new(body)
+ public_project.lfs_objects << lfs_object
+ end
+
+ it "responds with status 200 with upload hypermedia link for the new object" do
+ response = lfs_router_auth.try_call
+ expect(response.first).to eq(200)
+
+ response_body = ActiveSupport::JSON.decode(response.last.first)
+ expect(response_body['objects']).to be_kind_of(Array)
+
+
+ expect(response_body['objects'].first['oid']).to eq("91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897")
+ expect(response_body['objects'].first['size']).to eq(1575078)
+ expect(response_body['objects'].first['_links']['upload']['href']).to eq("#{Gitlab.config.gitlab.url}/#{project.path_with_namespace}.git/gitlab-lfs/objects/91eff75a492a3ed0dfcb544d7f31326bc4014c8551849c192fd1e48d4dd2c897/1575078")
+ expect(response_body['objects'].first['_links']['upload']['header']).to eq("Authorization" => @auth)
+
+ expect(response_body['objects'].last['oid']).to eq(sample_oid)
+ expect(response_body['objects'].last['size']).to eq(sample_size)
+ expect(lfs_object.projects.pluck(:id)).to_not include(project.id)
+ expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
+ expect(response_body['objects'].last).to have_key('_links')
+ end
+ end
+ end
+
+ context 'when user does not have push access' do
+ it 'responds with 403' do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+ end
+
+ context 'when user is not authenticated' do
+ context 'when user has push access' do
+ before do
+ project.team << [user, :master]
+ end
+
+ it "responds with status 401" do
+ expect(lfs_router_public_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'when user does not have push access' do
+ it "responds with status 401" do
+ expect(lfs_router_public_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ end
+
+ describe 'when pushing a lfs object' do
+ before do
+ enable_lfs
+ env['REQUEST_METHOD'] = 'PUT'
+ end
+
+ describe 'to one project' do
+ describe 'when user has push access to the project' do
+ before do
+ project.team << [user, :master]
+ end
+
+ describe 'when user is authenticated' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(project)
+ end
+
+ it 'responds with status 200, location of lfs store and object details' do
+ json_response = ActiveSupport::JSON.decode(lfs_router_auth.try_call.last.first)
+
+ expect(lfs_router_auth.try_call.first).to eq(200)
+ expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(project)
+ end
+
+ it 'responds with status 200 and lfs object is linked to the project' do
+ expect(lfs_router_auth.try_call.first).to eq(200)
+ expect(lfs_object.projects.pluck(:id)).to include(project.id)
+ end
+ end
+ end
+
+ describe 'when user is unauthenticated' do
+ let(:lfs_router_noauth) { new_lfs_router(project, nil) }
+
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(project)
+ end
+
+ it 'responds with status 401' do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(project)
+ end
+
+ it 'responds with status 401' do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent with a malformed headers' do
+ before do
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
+ env["HTTP_X_GITLAB_LFS_TMP"] = "cat /etc/passwd"
+ end
+
+ it 'does not recognize it as a valid lfs command' do
+ expect(lfs_router_noauth.try_call).to eq(nil)
+ end
+ end
+ end
+ end
+
+ describe 'and user does not have push access' do
+ describe 'when user is authenticated' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(project)
+ end
+
+ it 'responds with 403' do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(project)
+ end
+
+ it 'responds with 403' do
+ expect(lfs_router_auth.try_call.first).to eq(403)
+ end
+ end
+ end
+
+ describe 'when user is unauthenticated' do
+ let(:lfs_router_noauth) { new_lfs_router(project, nil) }
+
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(project)
+ end
+
+ it 'responds with 401' do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(project)
+ end
+
+ it 'responds with 401' do
+ expect(lfs_router_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ end
+ end
+
+ describe "to a forked project" do
+ let(:forked_project) { fork_project(public_project, user) }
+
+ describe 'when user has push access to the project' do
+ before do
+ forked_project.team << [user_two, :master]
+ end
+
+ describe 'when user is authenticated' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(forked_project)
+ end
+
+ it 'responds with status 200, location of lfs store and object details' do
+ json_response = ActiveSupport::JSON.decode(lfs_router_forked_auth.try_call.last.first)
+
+ expect(lfs_router_forked_auth.try_call.first).to eq(200)
+ expect(json_response['StoreLFSPath']).to eq("#{Gitlab.config.shared.path}/lfs-objects/tmp/upload")
+ expect(json_response['LfsOid']).to eq(sample_oid)
+ expect(json_response['LfsSize']).to eq(sample_size)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(forked_project)
+ end
+
+ it 'responds with status 200 and lfs object is linked to the source project' do
+ expect(lfs_router_forked_auth.try_call.first).to eq(200)
+ expect(lfs_object.projects.pluck(:id)).to include(public_project.id)
+ end
+ end
+ end
+
+ describe 'when user is unauthenticated' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(forked_project)
+ end
+
+ it 'responds with status 401' do
+ expect(lfs_router_forked_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(forked_project)
+ end
+
+ it 'responds with status 401' do
+ expect(lfs_router_forked_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ end
+
+ describe 'and user does not have push access' do
+ describe 'when user is authenticated' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(forked_project)
+ end
+
+ it 'responds with 403' do
+ expect(lfs_router_forked_auth.try_call.first).to eq(403)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(forked_project)
+ end
+
+ it 'responds with 403' do
+ expect(lfs_router_forked_auth.try_call.first).to eq(403)
+ end
+ end
+ end
+
+ describe 'when user is unauthenticated' do
+ context 'and request is sent by gitlab-workhorse to authorize the request' do
+ before do
+ header_for_upload_authorize(forked_project)
+ end
+
+ it 'responds with 401' do
+ expect(lfs_router_forked_noauth.try_call.first).to eq(401)
+ end
+ end
+
+ context 'and request is sent by gitlab-workhorse to finalize the upload' do
+ before do
+ headers_for_upload_finalize(forked_project)
+ end
+
+ it 'responds with 401' do
+ expect(lfs_router_forked_noauth.try_call.first).to eq(401)
+ end
+ end
+ end
+ end
+
+ describe 'and second project not related to fork or a source project' do
+ let(:second_project) { create(:project) }
+ let(:lfs_router_second_project) { new_lfs_router(second_project, user) }
+
+ before do
+ public_project.lfs_objects << lfs_object
+ headers_for_upload_finalize(second_project)
+ end
+
+ context 'when pushing the same lfs object to the second project' do
+ before do
+ second_project.team << [user, :master]
+ end
+
+ it 'responds with 200 and links the lfs object to the project' do
+ expect(lfs_router_second_project.try_call.first).to eq(200)
+ expect(lfs_object.projects.pluck(:id)).to include(second_project.id, public_project.id)
+ end
+ end
+ end
+ end
+ end
+
+ def enable_lfs
+ allow(Gitlab.config.lfs).to receive(:enabled).and_return(true)
+ end
+
+ def authorize(user)
+ ActionController::HttpAuthentication::Basic.encode_credentials(user.username, user.password)
+ end
+
+ def new_lfs_router(project, user)
+ Gitlab::Lfs::Router.new(project, user, request)
+ end
+
+ def header_for_upload_authorize(project)
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}/authorize"
+ end
+
+ def headers_for_upload_finalize(project)
+ env["PATH_INFO"] = "#{project.repository.path_with_namespace}.git/gitlab-lfs/objects/#{sample_oid}/#{sample_size}"
+ env["HTTP_X_GITLAB_LFS_TMP"] = "#{sample_oid}6e561c9d4"
+ end
+
+ def fork_project(project, user, object = nil)
+ allow(RepositoryForkWorker).to receive(:perform_async).and_return(true)
+ Projects::ForkService.new(project, user, {}).execute
+ end
+end