From 14032d8eb1a60ae5920286249c1044be2fa27278 Mon Sep 17 00:00:00 2001
From: Marin Jankovski <maxlazio@gmail.com>
Date: Mon, 12 Oct 2015 16:42:14 +0200
Subject: [PATCH] Add support for git lfs.

---
 .gitignore                                    |   1 +
 app/controllers/projects_controller.rb        |   5 +-
 app/models/lfs_object.rb                      |   8 +
 app/models/lfs_objects_project.rb             |   8 +
 app/models/project.rb                         |  12 +
 app/uploaders/lfs_object_uploader.rb          |  29 +
 config/gitlab.yml.example                     |  10 +-
 config/initializers/1_settings.rb             |   7 +
 config/routes.rb                              |   2 +-
 .../20151103134857_create_lfs_objects.rb      |  10 +
 ...51103134958_create_lfs_objects_projects.rb |  12 +
 .../20151104105513_add_file_to_lfs_objects.rb |   5 +
 ...14113410_add_index_for_lfs_oid_and_size.rb |   6 +
 db/schema.rb                                  |  22 +-
 lib/gitlab/backend/grack_auth.rb              |   5 +-
 lib/gitlab/git_access.rb                      |   6 +-
 lib/gitlab/lfs/response.rb                    | 308 +++++++++
 lib/gitlab/lfs/router.rb                      |  95 +++
 lib/support/nginx/gitlab                      |   9 +-
 lib/support/nginx/gitlab-ssl                  |   9 +-
 spec/factories/lfs_objects.rb                 |  12 +
 spec/factories/lfs_objects_projects.rb        |   8 +
 spec/lib/gitlab/lfs/lfs_router_spec.rb        | 650 ++++++++++++++++++
 23 files changed, 1226 insertions(+), 13 deletions(-)
 create mode 100644 app/models/lfs_object.rb
 create mode 100644 app/models/lfs_objects_project.rb
 create mode 100644 app/uploaders/lfs_object_uploader.rb
 create mode 100644 db/migrate/20151103134857_create_lfs_objects.rb
 create mode 100644 db/migrate/20151103134958_create_lfs_objects_projects.rb
 create mode 100644 db/migrate/20151104105513_add_file_to_lfs_objects.rb
 create mode 100644 db/migrate/20151114113410_add_index_for_lfs_oid_and_size.rb
 create mode 100644 lib/gitlab/lfs/response.rb
 create mode 100644 lib/gitlab/lfs/router.rb
 create mode 100644 spec/factories/lfs_objects.rb
 create mode 100644 spec/factories/lfs_objects_projects.rb
 create mode 100644 spec/lib/gitlab/lfs/lfs_router_spec.rb

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
@@ -199,6 +199,13 @@ Settings.incoming_email['ssl']        = false if Settings.incoming_email['ssl'].
 Settings.incoming_email['start_tls']  = false if Settings.incoming_email['start_tls'].nil?
 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
 #
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
-- 
2.30.9