Commit f5d0c7c5 authored by Nick Thomas's avatar Nick Thomas

Merge branch 'fj-refactor-git-routes' into 'master'

Refactor git controllers

See merge request gitlab-org/gitlab!23164
parents c50b6bd7 2fd80528
# frozen_string_literal: true
class Projects::GitHttpClientController < Projects::ApplicationController
include ActionController::HttpAuthentication::Basic
include KerberosSpnegoHelper
include Gitlab::Utils::StrongMemoize
attr_reader :authentication_result, :redirected_path
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result
alias_method :user, :actor
alias_method :authenticated_user, :actor
# Git clients will not know what authenticity token to send along
skip_around_action :set_session_storage
skip_before_action :verify_authenticity_token
skip_before_action :repository
before_action :authenticate_user
private
def download_request?
raise NotImplementedError
end
def upload_request?
raise NotImplementedError
end
def authenticate_user
@authentication_result = Gitlab::Auth::Result.new
if allow_basic_auth? && basic_auth_provided?
login, password = user_name_and_password(request)
if handle_basic_authentication(login, password)
return # Allow access
end
elsif allow_kerberos_spnego_auth? && spnego_provided?
kerberos_user = find_kerberos_user
if kerberos_user
@authentication_result = Gitlab::Auth::Result.new(
kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
send_final_spnego_response
return # Allow access
end
elsif http_download_allowed?
@authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code])
return # Allow access
end
send_challenges
render plain: "HTTP Basic: Access denied\n", status: :unauthorized
rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_access_token
end
def basic_auth_provided?
has_basic_credentials?(request)
end
def send_challenges
challenges = []
challenges << 'Basic realm="GitLab"' if allow_basic_auth?
challenges << spnego_challenge if allow_kerberos_spnego_auth?
headers['Www-Authenticate'] = challenges.join("\n") if challenges.any?
end
def project
parse_repo_path unless defined?(@project)
@project
end
def parse_repo_path
@project, @repo_type, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:project_id]}")
end
def render_missing_personal_access_token
render plain: "HTTP Basic: Access denied\n" \
"You must use a personal access token with 'read_repository' or 'write_repository' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
status: :unauthorized
end
def repository
strong_memoize(:repository) do
repo_type.repository_for(project)
end
end
def repo_type
parse_repo_path unless defined?(@repo_type)
@repo_type
end
def handle_basic_authentication(login, password)
@authentication_result = Gitlab::Auth.find_for_git_client(
login, password, project: project, ip: request.ip)
@authentication_result.success?
end
def ci?
authentication_result.ci?(project)
end
def http_download_allowed?
Gitlab::ProtocolAccess.allowed?('http') &&
download_request? &&
project && Guest.can?(:download_code, project)
end
end
Projects::GitHttpClientController.prepend_if_ee('EE::Projects::GitHttpClientController')
# frozen_string_literal: true
class Projects::GitHttpController < Projects::GitHttpClientController
include WorkhorseRequest
before_action :access_check
prepend_before_action :deny_head_requests, only: [:info_refs]
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403_with_exception
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404_with_exception
rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422_with_exception
rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs
log_user_activity if upload_pack?
render_ok
end
# POST /foo/bar.git/git-upload-pack (git pull)
def git_upload_pack
enqueue_fetch_statistics_update
render_ok
end
# POST /foo/bar.git/git-receive-pack" (git push)
def git_receive_pack
render_ok
end
private
def deny_head_requests
head :forbidden if request.head?
end
def download_request?
upload_pack?
end
def upload_pack?
git_command == 'git-upload-pack'
end
def git_command
if action_name == 'info_refs'
params[:service]
else
action_name.dasherize
end
end
def render_ok
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.git_http_ok(repository, repo_type, user, action_name)
end
def render_403_with_exception(exception)
render plain: exception.message, status: :forbidden
end
def render_404_with_exception(exception)
render plain: exception.message, status: :not_found
end
def render_422_with_exception(exception)
render plain: exception.message, status: :unprocessable_entity
end
def render_503_with_exception(exception)
render plain: exception.message, status: :service_unavailable
end
def enqueue_fetch_statistics_update
return if Gitlab::Database.read_only?
return if repo_type.wiki?
return unless project&.daily_statistics_enabled?
ProjectDailyStatisticsWorker.perform_async(project.id)
end
def access
@access ||= access_klass.new(access_actor, project, 'http',
authentication_abilities: authentication_abilities,
namespace_path: params[:namespace_id],
project_path: project_path,
redirected_path: redirected_path,
auth_result_type: auth_result_type)
end
def access_actor
return user if user
return :ci if ci?
end
def access_check
access.check(git_command, Gitlab::GitAccess::ANY)
@project ||= access.project
end
def access_klass
@access_klass ||= repo_type.access_checker_class
end
def project_path
@project_path ||= params[:project_id].sub(/\.git$/, '')
end
def log_user_activity
Users::ActivityService.new(user).execute
end
end
Projects::GitHttpController.prepend_if_ee('EE::Projects::GitHttpController')
# frozen_string_literal: true
class Projects::LfsApiController < Projects::GitHttpClientController
include LfsRequest
include Gitlab::Utils::StrongMemoize
LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream'
skip_before_action :lfs_check_access!, only: [:deprecated]
before_action :lfs_check_batch_operation!, only: [:batch]
def batch
unless objects.present?
render_lfs_not_found
return
end
if download_request?
render json: { objects: download_objects! }
elsif upload_request?
render json: { objects: upload_objects! }
else
raise "Never reached"
end
end
def deprecated
render(
json: {
message: _('Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.'),
documentation_url: "#{Gitlab.config.gitlab.url}/help"
},
status: :not_implemented
)
end
private
def download_request?
params[:operation] == 'download'
end
def upload_request?
params[:operation] == 'upload'
end
# rubocop: disable CodeReuse/ActiveRecord
def existing_oids
@existing_oids ||= begin
project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def download_objects!
objects.each do |object|
if existing_oids.include?(object[:oid])
object[:actions] = download_actions(object)
if Guest.can?(:download_code, project)
object[:authenticated] = true
end
else
object[:error] = {
code: 404,
message: _("Object does not exist on the server or you don't have permissions to access it")
}
end
end
objects
end
def upload_objects!
objects.each do |object|
object[:actions] = upload_actions(object) unless existing_oids.include?(object[:oid])
end
objects
end
def download_actions(object)
{
download: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}",
header: {
Authorization: authorization_header
}.compact
}
}
end
def upload_actions(object)
{
upload: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}",
header: {
Authorization: authorization_header,
# git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This
# ensures that Workhorse can intercept the request.
'Content-Type': LFS_TRANSFER_CONTENT_TYPE
}.compact
}
}
end
def lfs_check_batch_operation!
if batch_operation_disallowed?
render(
json: {
message: lfs_read_only_message
},
content_type: LfsRequest::CONTENT_TYPE,
status: :forbidden
)
end
end
# Overridden in EE
def batch_operation_disallowed?
upload_request? && Gitlab::Database.read_only?
end
# Overridden in EE
def lfs_read_only_message
_('You cannot write to this read-only GitLab instance.')
end
def authorization_header
strong_memoize(:authorization_header) do
lfs_auth_header || request.headers['Authorization']
end
end
def lfs_auth_header
return unless user.is_a?(User)
Gitlab::LfsToken.new(user).basic_encoding
end
end
Projects::LfsApiController.prepend_if_ee('EE::Projects::LfsApiController')
# frozen_string_literal: true
class Projects::LfsLocksApiController < Projects::GitHttpClientController
include LfsRequest
def create
@result = Lfs::LockFileService.new(project, user, lfs_params).execute
render_json(@result[:lock])
end
def unlock
@result = Lfs::UnlockFileService.new(project, user, lfs_params).execute
render_json(@result[:lock])
end
def index
@result = Lfs::LocksFinderService.new(project, user, lfs_params).execute
render_json(@result[:locks])
end
def verify
@result = Lfs::LocksFinderService.new(project, user, {}).execute
ours, theirs = split_by_owner(@result[:locks])
render_json({ ours: ours, theirs: theirs }, false)
end
private
def render_json(data, process = true)
render json: build_payload(data, process),
content_type: LfsRequest::CONTENT_TYPE,
status: @result[:http_status]
end
def build_payload(data, process)
data = LfsFileLockSerializer.new.represent(data) if process
return data if @result[:status] == :success
# When the locking failed due to an existent Lock, the existent record
# is returned in `@result[:lock]`
error_payload(@result[:message], @result[:lock] ? data : {})
end
def error_payload(message, custom_attrs = {})
custom_attrs.merge({
message: message,
documentation_url: help_url
})
end
def split_by_owner(locks)
groups = locks.partition { |lock| lock.user_id == user.id }
groups.map! do |records|
LfsFileLockSerializer.new.represent(records, root: false)
end
end
def download_request?
params[:action] == 'index'
end
def upload_request?
%w(create unlock verify).include?(params[:action])
end
def lfs_params
params.permit(:id, :path, :force)
end
end
# frozen_string_literal: true
class Projects::LfsStorageController < Projects::GitHttpClientController
include LfsRequest
include WorkhorseRequest
include SendFileUpload
skip_before_action :verify_workhorse_api!, only: :download
def download
lfs_object = LfsObject.find_by_oid(oid)
unless lfs_object && lfs_object.file.exists?
render_lfs_not_found
return
end
send_upload(lfs_object.file, send_params: { content_type: "application/octet-stream" })
end
def upload_authorize
set_workhorse_internal_api_content_type
authorized = LfsObjectUploader.workhorse_authorize(has_length: true)
authorized.merge!(LfsOid: oid, LfsSize: size)
render json: authorized
end
def upload_finalize
if store_file!(oid, size)
head 200
else
render plain: 'Unprocessable entity', status: :unprocessable_entity
end
rescue ActiveRecord::RecordInvalid
render_lfs_forbidden
rescue UploadedFile::InvalidPathError
render_lfs_forbidden
rescue ObjectStorage::RemoteStoreError
render_lfs_forbidden
end
private
def download_request?
action_name == 'download'
end
def upload_request?
%w[upload_authorize upload_finalize].include? action_name
end
def oid
params[:oid].to_s
end
def size
params[:size].to_i
end
# rubocop: disable CodeReuse/ActiveRecord
def store_file!(oid, size)
object = LfsObject.find_by(oid: oid, size: size)
unless object&.file&.exists?
object = create_file!(oid, size)
end
return unless object
link_to_project!(object)
end
# rubocop: enable CodeReuse/ActiveRecord
def create_file!(oid, size)
uploaded_file = UploadedFile.from_params(
params, :file, LfsObjectUploader.workhorse_local_upload_path)
return unless uploaded_file
LfsObject.create!(oid: oid, size: size, file: uploaded_file)
end
# rubocop: disable CodeReuse/ActiveRecord
def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id)
object.lfs_objects_projects.create!(project: storage_project)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
# frozen_string_literal: true
module Repositories
class ApplicationController < ::ApplicationController
skip_before_action :authenticate_user!
end
end
# frozen_string_literal: true
module Repositories
class GitHttpClientController < Repositories::ApplicationController
include ActionController::HttpAuthentication::Basic
include KerberosSpnegoHelper
include Gitlab::Utils::StrongMemoize
attr_reader :authentication_result, :redirected_path
delegate :actor, :authentication_abilities, to: :authentication_result, allow_nil: true
delegate :type, to: :authentication_result, allow_nil: true, prefix: :auth_result
alias_method :user, :actor
alias_method :authenticated_user, :actor
# Git clients will not know what authenticity token to send along
skip_around_action :set_session_storage
skip_before_action :verify_authenticity_token
before_action :parse_repo_path
before_action :authenticate_user
private
def download_request?
raise NotImplementedError
end
def upload_request?
raise NotImplementedError
end
def authenticate_user
@authentication_result = Gitlab::Auth::Result.new
if allow_basic_auth? && basic_auth_provided?
login, password = user_name_and_password(request)
if handle_basic_authentication(login, password)
return # Allow access
end
elsif allow_kerberos_spnego_auth? && spnego_provided?
kerberos_user = find_kerberos_user
if kerberos_user
@authentication_result = Gitlab::Auth::Result.new(
kerberos_user, nil, :kerberos, Gitlab::Auth.full_authentication_abilities)
send_final_spnego_response
return # Allow access
end
elsif http_download_allowed?
@authentication_result = Gitlab::Auth::Result.new(nil, project, :none, [:download_code])
return # Allow access
end
send_challenges
render plain: "HTTP Basic: Access denied\n", status: :unauthorized
rescue Gitlab::Auth::MissingPersonalAccessTokenError
render_missing_personal_access_token
end
def basic_auth_provided?
has_basic_credentials?(request)
end
def send_challenges
challenges = []
challenges << 'Basic realm="GitLab"' if allow_basic_auth?
challenges << spnego_challenge if allow_kerberos_spnego_auth?
headers['Www-Authenticate'] = challenges.join("\n") if challenges.any?
end
def project
parse_repo_path unless defined?(@project)
@project
end
def parse_repo_path
@project, @repo_type, @redirected_path = Gitlab::RepoPath.parse("#{params[:namespace_id]}/#{params[:repository_id]}")
end
def render_missing_personal_access_token
render plain: "HTTP Basic: Access denied\n" \
"You must use a personal access token with 'read_repository' or 'write_repository' scope for Git over HTTP.\n" \
"You can generate one at #{profile_personal_access_tokens_url}",
status: :unauthorized
end
def repository
strong_memoize(:repository) do
repo_type.repository_for(project)
end
end
def repo_type
parse_repo_path unless defined?(@repo_type)
@repo_type
end
def handle_basic_authentication(login, password)
@authentication_result = Gitlab::Auth.find_for_git_client(
login, password, project: project, ip: request.ip)
@authentication_result.success?
end
def ci?
authentication_result.ci?(project)
end
def http_download_allowed?
Gitlab::ProtocolAccess.allowed?('http') &&
download_request? &&
project && Guest.can?(:download_code, project)
end
end
end
Repositories::GitHttpClientController.prepend_if_ee('EE::Repositories::GitHttpClientController')
# frozen_string_literal: true
module Repositories
class GitHttpController < Repositories::GitHttpClientController
include WorkhorseRequest
before_action :access_check
prepend_before_action :deny_head_requests, only: [:info_refs]
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403_with_exception
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404_with_exception
rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422_with_exception
rescue_from Gitlab::GitAccess::TimeoutError, with: :render_503_with_exception
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs
log_user_activity if upload_pack?
render_ok
end
# POST /foo/bar.git/git-upload-pack (git pull)
def git_upload_pack
enqueue_fetch_statistics_update
render_ok
end
# POST /foo/bar.git/git-receive-pack" (git push)
def git_receive_pack
render_ok
end
private
def deny_head_requests
head :forbidden if request.head?
end
def download_request?
upload_pack?
end
def upload_pack?
git_command == 'git-upload-pack'
end
def git_command
if action_name == 'info_refs'
params[:service]
else
action_name.dasherize
end
end
def render_ok
set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.git_http_ok(repository, repo_type, user, action_name)
end
def render_403_with_exception(exception)
render plain: exception.message, status: :forbidden
end
def render_404_with_exception(exception)
render plain: exception.message, status: :not_found
end
def render_422_with_exception(exception)
render plain: exception.message, status: :unprocessable_entity
end
def render_503_with_exception(exception)
render plain: exception.message, status: :service_unavailable
end
def enqueue_fetch_statistics_update
return if Gitlab::Database.read_only?
return unless repo_type.project?
return unless project&.daily_statistics_enabled?
ProjectDailyStatisticsWorker.perform_async(project.id)
end
def access
@access ||= access_klass.new(access_actor, project, 'http',
authentication_abilities: authentication_abilities,
namespace_path: params[:namespace_id],
project_path: project_path,
redirected_path: redirected_path,
auth_result_type: auth_result_type)
end
def access_actor
return user if user
return :ci if ci?
end
def access_check
access.check(git_command, Gitlab::GitAccess::ANY)
@project ||= access.project
end
def access_klass
@access_klass ||= repo_type.access_checker_class
end
def project_path
@project_path ||= params[:repository_id].sub(/\.git$/, '')
end
def log_user_activity
Users::ActivityService.new(user).execute
end
end
end
Repositories::GitHttpController.prepend_if_ee('EE::Repositories::GitHttpController')
# frozen_string_literal: true
module Repositories
class LfsApiController < Repositories::GitHttpClientController
include LfsRequest
include Gitlab::Utils::StrongMemoize
LFS_TRANSFER_CONTENT_TYPE = 'application/octet-stream'
skip_before_action :lfs_check_access!, only: [:deprecated]
before_action :lfs_check_batch_operation!, only: [:batch]
def batch
unless objects.present?
render_lfs_not_found
return
end
if download_request?
render json: { objects: download_objects! }
elsif upload_request?
render json: { objects: upload_objects! }
else
raise "Never reached"
end
end
def deprecated
render(
json: {
message: _('Server supports batch API only, please update your Git LFS client to version 1.0.1 and up.'),
documentation_url: "#{Gitlab.config.gitlab.url}/help"
},
status: :not_implemented
)
end
private
def download_request?
params[:operation] == 'download'
end
def upload_request?
params[:operation] == 'upload'
end
# rubocop: disable CodeReuse/ActiveRecord
def existing_oids
@existing_oids ||= begin
project.all_lfs_objects.where(oid: objects.map { |o| o['oid'].to_s }).pluck(:oid)
end
end
# rubocop: enable CodeReuse/ActiveRecord
def download_objects!
objects.each do |object|
if existing_oids.include?(object[:oid])
object[:actions] = download_actions(object)
if Guest.can?(:download_code, project)
object[:authenticated] = true
end
else
object[:error] = {
code: 404,
message: _("Object does not exist on the server or you don't have permissions to access it")
}
end
end
objects
end
def upload_objects!
objects.each do |object|
object[:actions] = upload_actions(object) unless existing_oids.include?(object[:oid])
end
objects
end
def download_actions(object)
{
download: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}",
header: {
Authorization: authorization_header
}.compact
}
}
end
def upload_actions(object)
{
upload: {
href: "#{project.http_url_to_repo}/gitlab-lfs/objects/#{object[:oid]}/#{object[:size]}",
header: {
Authorization: authorization_header,
# git-lfs v2.5.0 sets the Content-Type based on the uploaded file. This
# ensures that Workhorse can intercept the request.
'Content-Type': LFS_TRANSFER_CONTENT_TYPE
}.compact
}
}
end
def lfs_check_batch_operation!
if batch_operation_disallowed?
render(
json: {
message: lfs_read_only_message
},
content_type: LfsRequest::CONTENT_TYPE,
status: :forbidden
)
end
end
# Overridden in EE
def batch_operation_disallowed?
upload_request? && Gitlab::Database.read_only?
end
# Overridden in EE
def lfs_read_only_message
_('You cannot write to this read-only GitLab instance.')
end
def authorization_header
strong_memoize(:authorization_header) do
lfs_auth_header || request.headers['Authorization']
end
end
def lfs_auth_header
return unless user.is_a?(User)
Gitlab::LfsToken.new(user).basic_encoding
end
end
end
Repositories::LfsApiController.prepend_if_ee('EE::Repositories::LfsApiController')
# frozen_string_literal: true
module Repositories
class LfsLocksApiController < Repositories::GitHttpClientController
include LfsRequest
def create
@result = Lfs::LockFileService.new(project, user, lfs_params).execute
render_json(@result[:lock])
end
def unlock
@result = Lfs::UnlockFileService.new(project, user, lfs_params).execute
render_json(@result[:lock])
end
def index
@result = Lfs::LocksFinderService.new(project, user, lfs_params).execute
render_json(@result[:locks])
end
def verify
@result = Lfs::LocksFinderService.new(project, user, {}).execute
ours, theirs = split_by_owner(@result[:locks])
render_json({ ours: ours, theirs: theirs }, false)
end
private
def render_json(data, process = true)
render json: build_payload(data, process),
content_type: LfsRequest::CONTENT_TYPE,
status: @result[:http_status]
end
def build_payload(data, process)
data = LfsFileLockSerializer.new.represent(data) if process
return data if @result[:status] == :success
# When the locking failed due to an existent Lock, the existent record
# is returned in `@result[:lock]`
error_payload(@result[:message], @result[:lock] ? data : {})
end
def error_payload(message, custom_attrs = {})
custom_attrs.merge({
message: message,
documentation_url: help_url
})
end
def split_by_owner(locks)
groups = locks.partition { |lock| lock.user_id == user.id }
groups.map! do |records|
LfsFileLockSerializer.new.represent(records, root: false)
end
end
def download_request?
params[:action] == 'index'
end
def upload_request?
%w(create unlock verify).include?(params[:action])
end
def lfs_params
params.permit(:id, :path, :force)
end
end
end
# frozen_string_literal: true
module Repositories
class LfsStorageController < Repositories::GitHttpClientController
include LfsRequest
include WorkhorseRequest
include SendFileUpload
skip_before_action :verify_workhorse_api!, only: :download
def download
lfs_object = LfsObject.find_by_oid(oid)
unless lfs_object && lfs_object.file.exists?
render_lfs_not_found
return
end
send_upload(lfs_object.file, send_params: { content_type: "application/octet-stream" })
end
def upload_authorize
set_workhorse_internal_api_content_type
authorized = LfsObjectUploader.workhorse_authorize(has_length: true)
authorized.merge!(LfsOid: oid, LfsSize: size)
render json: authorized
end
def upload_finalize
if store_file!(oid, size)
head 200
else
render plain: 'Unprocessable entity', status: :unprocessable_entity
end
rescue ActiveRecord::RecordInvalid
render_lfs_forbidden
rescue UploadedFile::InvalidPathError
render_lfs_forbidden
rescue ObjectStorage::RemoteStoreError
render_lfs_forbidden
end
private
def download_request?
action_name == 'download'
end
def upload_request?
%w[upload_authorize upload_finalize].include? action_name
end
def oid
params[:oid].to_s
end
def size
params[:size].to_i
end
# rubocop: disable CodeReuse/ActiveRecord
def store_file!(oid, size)
object = LfsObject.find_by(oid: oid, size: size)
unless object&.file&.exists?
object = create_file!(oid, size)
end
return unless object
link_to_project!(object)
end
# rubocop: enable CodeReuse/ActiveRecord
def create_file!(oid, size)
uploaded_file = UploadedFile.from_params(
params, :file, LfsObjectUploader.workhorse_local_upload_path)
return unless uploaded_file
LfsObject.create!(oid: oid, size: size, file: uploaded_file)
end
# rubocop: disable CodeReuse/ActiveRecord
def link_to_project!(object)
if object && !object.projects.exists?(storage_project.id)
object.lfs_objects_projects.create!(project: storage_project)
end
end
# rubocop: enable CodeReuse/ActiveRecord
end
end
scope(path: '*namespace_id/:project_id', concern :gitactionable do
format: nil,
constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do
scope(constraints: { project_id: Gitlab::PathRegex.project_git_route_regex }, module: :projects) do
# Git HTTP clients ('git clone' etc.)
scope(controller: :git_http) do scope(controller: :git_http) do
get '/info/refs', action: :info_refs get '/info/refs', action: :info_refs
post '/git-upload-pack', action: :git_upload_pack post '/git-upload-pack', action: :git_upload_pack
post '/git-receive-pack', action: :git_receive_pack post '/git-receive-pack', action: :git_receive_pack
end end
end
concern :lfsable do
# Git LFS API (metadata) # Git LFS API (metadata)
scope(path: 'info/lfs/objects', controller: :lfs_api) do scope(path: 'info/lfs/objects', controller: :lfs_api) do
post :batch post :batch
...@@ -32,12 +30,22 @@ scope(path: '*namespace_id/:project_id', ...@@ -32,12 +30,22 @@ scope(path: '*namespace_id/:project_id',
put '/*size', action: :upload_finalize put '/*size', action: :upload_finalize
end end
end end
end
scope(path: '*namespace_id/:repository_id',
format: nil,
constraints: { namespace_id: Gitlab::PathRegex.full_namespace_route_regex }) do
scope(constraints: { repository_id: Gitlab::PathRegex.project_git_route_regex }) do
scope(module: :repositories) do
concerns :gitactionable
concerns :lfsable
end
end end
# Redirect /group/project.wiki.git to the project wiki # Redirect /group/project.wiki.git to the project wiki
scope(format: true, constraints: { project_id: Gitlab::PathRegex.project_wiki_git_route_regex, format: :git }) do scope(format: true, constraints: { repository_id: Gitlab::PathRegex.project_wiki_git_route_regex, format: :git }) do
wiki_redirect = redirect do |params, request| wiki_redirect = redirect do |params, request|
project_id = params[:project_id].delete_suffix('.wiki') project_id = params[:repository_id].delete_suffix('.wiki')
path = [params[:namespace_id], project_id, 'wikis'].join('/') path = [params[:namespace_id], project_id, 'wikis'].join('/')
path << "?#{request.query_string}" unless request.query_string.blank? path << "?#{request.query_string}" unless request.query_string.blank?
path path
...@@ -47,7 +55,7 @@ scope(path: '*namespace_id/:project_id', ...@@ -47,7 +55,7 @@ scope(path: '*namespace_id/:project_id',
end end
# Redirect /group/project/info/refs to /group/project.git/info/refs # Redirect /group/project/info/refs to /group/project.git/info/refs
scope(constraints: { project_id: Gitlab::PathRegex.project_route_regex }) do scope(constraints: { repository_id: Gitlab::PathRegex.project_route_regex }) do
# Allow /info/refs, /info/refs?service=git-upload-pack, and # Allow /info/refs, /info/refs?service=git-upload-pack, and
# /info/refs?service=git-receive-pack, but nothing else. # /info/refs?service=git-receive-pack, but nothing else.
# #
...@@ -58,7 +66,7 @@ scope(path: '*namespace_id/:project_id', ...@@ -58,7 +66,7 @@ scope(path: '*namespace_id/:project_id',
end end
ref_redirect = redirect do |params, request| ref_redirect = redirect do |params, request|
path = "#{params[:namespace_id]}/#{params[:project_id]}.git/info/refs" path = "#{params[:namespace_id]}/#{params[:repository_id]}.git/info/refs"
path << "?#{request.query_string}" unless request.query_string.blank? path << "?#{request.query_string}" unless request.query_string.blank?
path path
end end
......
# frozen_string_literal: true # frozen_string_literal: true
module EE module EE
module Projects module Repositories
module GitHttpClientController module GitHttpClientController
extend ActiveSupport::Concern extend ActiveSupport::Concern
......
# frozen_string_literal: true # frozen_string_literal: true
module EE module EE
module Projects module Repositories
module GitHttpController module GitHttpController
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
include ::Gitlab::Utils::StrongMemoize include ::Gitlab::Utils::StrongMemoize
......
# frozen_string_literal: true # frozen_string_literal: true
module EE module EE
module Projects module Repositories
module LfsApiController module LfsApiController
extend ::Gitlab::Utils::Override extend ::Gitlab::Utils::Override
include GitlabRoutingHelper include GitlabRoutingHelper
......
...@@ -33,12 +33,12 @@ describe 'Git HTTP requests' do ...@@ -33,12 +33,12 @@ describe 'Git HTTP requests' do
let(:env) { { spnego_request_token: 'opaque_request_token' } } let(:env) { { spnego_request_token: 'opaque_request_token' } }
before do before do
allow_any_instance_of(Projects::GitHttpController).to receive(:allow_kerberos_spnego_auth?).and_return(true) allow_any_instance_of(Repositories::GitHttpController).to receive(:allow_kerberos_spnego_auth?).and_return(true)
end end
context "when authentication fails because of invalid Kerberos token" do context "when authentication fails because of invalid Kerberos token" do
before do before do
allow_any_instance_of(Projects::GitHttpController).to receive(:spnego_credentials!).and_return(nil) allow_any_instance_of(Repositories::GitHttpController).to receive(:spnego_credentials!).and_return(nil)
end end
it "responds with status 401 Unauthorized" do it "responds with status 401 Unauthorized" do
...@@ -50,7 +50,7 @@ describe 'Git HTTP requests' do ...@@ -50,7 +50,7 @@ describe 'Git HTTP requests' do
context "when authentication fails because of unknown Kerberos identity" do context "when authentication fails because of unknown Kerberos identity" do
before do before do
allow_any_instance_of(Projects::GitHttpController).to receive(:spnego_credentials!).and_return("mylogin@FOO.COM") allow_any_instance_of(Repositories::GitHttpController).to receive(:spnego_credentials!).and_return("mylogin@FOO.COM")
end end
it "responds with status 401 Unauthorized" do it "responds with status 401 Unauthorized" do
...@@ -62,7 +62,7 @@ describe 'Git HTTP requests' do ...@@ -62,7 +62,7 @@ describe 'Git HTTP requests' do
context "when authentication succeeds" do context "when authentication succeeds" do
before do before do
allow_any_instance_of(Projects::GitHttpController).to receive(:spnego_credentials!).and_return("mylogin@FOO.COM") allow_any_instance_of(Repositories::GitHttpController).to receive(:spnego_credentials!).and_return("mylogin@FOO.COM")
user.identities.create!(provider: "kerberos", extern_uid: "mylogin@FOO.COM") user.identities.create!(provider: "kerberos", extern_uid: "mylogin@FOO.COM")
end end
...@@ -101,7 +101,7 @@ describe 'Git HTTP requests' do ...@@ -101,7 +101,7 @@ describe 'Git HTTP requests' do
end end
it "complies with RFC4559" do it "complies with RFC4559" do
allow_any_instance_of(Projects::GitHttpController).to receive(:spnego_response_token).and_return("opaque_response_token") allow_any_instance_of(Repositories::GitHttpController).to receive(:spnego_response_token).and_return("opaque_response_token")
download(path, env) do |response| download(path, env) do |response|
expect(response.headers['WWW-Authenticate'].split("\n")).to include("Negotiate #{::Base64.strict_encode64('opaque_response_token')}") expect(response.headers['WWW-Authenticate'].split("\n")).to include("Negotiate #{::Base64.strict_encode64('opaque_response_token')}")
end end
...@@ -116,7 +116,7 @@ describe 'Git HTTP requests' do ...@@ -116,7 +116,7 @@ describe 'Git HTTP requests' do
end end
it "complies with RFC4559" do it "complies with RFC4559" do
allow_any_instance_of(Projects::GitHttpController).to receive(:spnego_response_token).and_return("opaque_response_token") allow_any_instance_of(Repositories::GitHttpController).to receive(:spnego_response_token).and_return("opaque_response_token")
download(path, env) do |response| download(path, env) do |response|
expect(response.headers['WWW-Authenticate'].split("\n")).to include("Negotiate #{::Base64.strict_encode64('opaque_response_token')}") expect(response.headers['WWW-Authenticate'].split("\n")).to include("Negotiate #{::Base64.strict_encode64('opaque_response_token')}")
end end
......
...@@ -2,7 +2,7 @@ ...@@ -2,7 +2,7 @@
require 'spec_helper' require 'spec_helper'
describe Projects::GitHttpController, type: :request do describe Repositories::GitHttpController, type: :request do
include GitHttpHelpers include GitHttpHelpers
describe 'GET #info_refs' do describe 'GET #info_refs' do
......
...@@ -104,13 +104,11 @@ module API ...@@ -104,13 +104,11 @@ module API
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def set_project def set_project
@project, @repo_type, @redirected_path =
if params[:gl_repository] if params[:gl_repository]
@project, @repo_type = Gitlab::GlRepository.parse(params[:gl_repository]) Gitlab::GlRepository.parse(params[:gl_repository])
@redirected_path = nil
elsif params[:project] elsif params[:project]
@project, @repo_type, @redirected_path = Gitlab::RepoPath.parse(params[:project]) Gitlab::RepoPath.parse(params[:project])
else
@project, @repo_type, @redirected_path = nil, nil, nil
end end
end end
# rubocop:enable Gitlab/ModuleWithInstanceVariables # rubocop:enable Gitlab/ModuleWithInstanceVariables
......
...@@ -4,7 +4,7 @@ module Constraints ...@@ -4,7 +4,7 @@ module Constraints
class ProjectUrlConstrainer class ProjectUrlConstrainer
def matches?(request, existence_check: true) def matches?(request, existence_check: true)
namespace_path = request.params[:namespace_id] namespace_path = request.params[:namespace_id]
project_path = request.params[:project_id] || request.params[:id] project_path = request.params[:project_id] || request.params[:id] || request.params[:repository_id]
full_path = [namespace_path, project_path].join('/') full_path = [namespace_path, project_path].join('/')
return false unless ProjectPathValidator.valid_path?(full_path) return false unless ProjectPathValidator.valid_path?(full_path)
......
...@@ -12,12 +12,12 @@ module Gitlab ...@@ -12,12 +12,12 @@ module Gitlab
ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance' ERROR_MESSAGE = 'You cannot perform write operations on a read-only instance'
WHITELISTED_GIT_ROUTES = { WHITELISTED_GIT_ROUTES = {
'projects/git_http' => %w{git_upload_pack git_receive_pack} 'repositories/git_http' => %w{git_upload_pack git_receive_pack}
}.freeze }.freeze
WHITELISTED_GIT_LFS_ROUTES = { WHITELISTED_GIT_LFS_ROUTES = {
'projects/lfs_api' => %w{batch}, 'repositories/lfs_api' => %w{batch},
'projects/lfs_locks_api' => %w{verify create unlock} 'repositories/lfs_locks_api' => %w{verify create unlock}
}.freeze }.freeze
WHITELISTED_GIT_REVISION_ROUTES = { WHITELISTED_GIT_REVISION_ROUTES = {
......
...@@ -5,7 +5,7 @@ require 'spec_helper' ...@@ -5,7 +5,7 @@ require 'spec_helper'
describe LfsRequest do describe LfsRequest do
include ProjectForksHelper include ProjectForksHelper
controller(Projects::GitHttpClientController) do controller(Repositories::GitHttpClientController) do
# `described_class` is not available in this context # `described_class` is not available in this context
include LfsRequest include LfsRequest
......
# frozen_string_literal: true
require 'spec_helper'
describe Projects::GitHttpController do
include GitHttpHelpers
let_it_be(:project) { create(:project, :public, :repository) }
let(:project_params) do
{
namespace_id: project.namespace.to_param,
project_id: project.path + '.git'
}
end
let(:params) { project_params }
describe 'HEAD #info_refs' do
it 'returns 403' do
head :info_refs, params: { namespace_id: project.namespace.to_param, project_id: project.path + '.git' }
expect(response.status).to eq(403)
end
end
describe 'GET #info_refs' do
let(:params) { project_params.merge(service: 'git-upload-pack') }
it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do
stub_application_setting(enabled_git_access_protocol: 'ssh')
get :info_refs, params: params
expect(response.status).to eq(401)
end
context 'with authorized user' do
let(:user) { project.owner }
before do
request.headers.merge! auth_env(user.username, user.password, nil)
end
it 'returns 200' do
get :info_refs, params: params
expect(response.status).to eq(200)
end
it 'updates the user activity' do
expect_next_instance_of(Users::ActivityService) do |activity_service|
expect(activity_service).to receive(:execute)
end
get :info_refs, params: params
end
end
context 'with exceptions' do
before do
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
end
it 'returns 503 with GRPC Unavailable' do
allow(controller).to receive(:access_check).and_raise(GRPC::Unavailable)
get :info_refs, params: params
expect(response.status).to eq(503)
end
it 'returns 503 with timeout error' do
allow(controller).to receive(:access_check).and_raise(Gitlab::GitAccess::TimeoutError)
get :info_refs, params: params
expect(response.status).to eq(503)
expect(response.body).to eq 'Gitlab::GitAccess::TimeoutError'
end
end
end
describe 'POST #git_upload_pack' do
before do
allow(controller).to receive(:authenticate_user).and_return(true)
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
allow(controller).to receive(:access_check).and_return(nil)
end
after do
post :git_upload_pack, params: params
end
context 'on a read-only instance' do
before do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
end
it 'does not update project statistics' do
expect(ProjectDailyStatisticsWorker).not_to receive(:perform_async)
end
end
it 'updates project statistics' do
expect(ProjectDailyStatisticsWorker).to receive(:perform_async)
end
end
end
# frozen_string_literal: true
require 'spec_helper'
describe Repositories::GitHttpController do
include GitHttpHelpers
let_it_be(:project) { create(:project, :public, :repository) }
let(:namespace_id) { project.namespace.to_param }
let(:repository_id) { project.path + '.git' }
let(:project_params) do
{
namespace_id: namespace_id,
repository_id: repository_id
}
end
let(:params) { project_params }
describe 'HEAD #info_refs' do
it 'returns 403' do
head :info_refs, params: params
expect(response.status).to eq(403)
end
end
shared_examples 'info_refs behavior' do
describe 'GET #info_refs' do
let(:params) { project_params.merge(service: 'git-upload-pack') }
it 'returns 401 for unauthenticated requests to public repositories when http protocol is disabled' do
stub_application_setting(enabled_git_access_protocol: 'ssh')
allow(controller).to receive(:basic_auth_provided?).and_call_original
expect(controller).to receive(:http_download_allowed?).and_call_original
get :info_refs, params: params
expect(response.status).to eq(401)
end
context 'with authorized user' do
let(:user) { project.owner }
before do
request.headers.merge! auth_env(user.username, user.password, nil)
end
it 'returns 200' do
get :info_refs, params: params
expect(response.status).to eq(200)
end
it 'updates the user activity' do
expect_next_instance_of(Users::ActivityService) do |activity_service|
expect(activity_service).to receive(:execute)
end
get :info_refs, params: params
end
end
context 'with exceptions' do
before do
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
end
it 'returns 503 with GRPC Unavailable' do
allow(controller).to receive(:access_check).and_raise(GRPC::Unavailable)
get :info_refs, params: params
expect(response.status).to eq(503)
end
it 'returns 503 with timeout error' do
allow(controller).to receive(:access_check).and_raise(Gitlab::GitAccess::TimeoutError)
get :info_refs, params: params
expect(response.status).to eq(503)
expect(response.body).to eq 'Gitlab::GitAccess::TimeoutError'
end
end
end
end
shared_examples 'git_upload_pack behavior' do |expected|
describe 'POST #git_upload_pack' do
before do
allow(controller).to receive(:authenticate_user).and_return(true)
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
allow(controller).to receive(:access_check).and_return(nil)
end
after do
post :git_upload_pack, params: params
end
context 'on a read-only instance' do
before do
allow(Gitlab::Database).to receive(:read_only?).and_return(true)
end
it 'does not update project statistics' do
expect(ProjectDailyStatisticsWorker).not_to receive(:perform_async)
end
end
if expected
it 'updates project statistics' do
expect(ProjectDailyStatisticsWorker).to receive(:perform_async)
end
else
it 'does not update project statistics' do
expect(ProjectDailyStatisticsWorker).not_to receive(:perform_async)
end
end
end
end
shared_examples 'access checker class' do
let(:params) { project_params.merge(service: 'git-upload-pack') }
it 'calls the right access class checker with the right object' do
allow(controller).to receive(:verify_workhorse_api!).and_return(true)
access_double = double
expect(expected_class).to receive(:new).with(anything, expected_object, 'http', anything).and_return(access_double)
allow(access_double).to receive(:check).and_return(false)
get :info_refs, params: params
end
end
context 'when repository container is a project' do
it_behaves_like 'info_refs behavior'
it_behaves_like 'git_upload_pack behavior', true
it_behaves_like 'access checker class' do
let(:expected_class) { Gitlab::GitAccess }
let(:expected_object) { project }
end
end
end
...@@ -108,7 +108,7 @@ describe 'Git HTTP requests' do ...@@ -108,7 +108,7 @@ describe 'Git HTTP requests' do
shared_examples_for 'project path without .git suffix' do shared_examples_for 'project path without .git suffix' do
context "GET info/refs" do context "GET info/refs" do
let(:path) { "/#{project_path}/info/refs" } let(:path) { "/#{repository_path}/info/refs" }
context "when no params are added" do context "when no params are added" do
before do before do
...@@ -116,7 +116,7 @@ describe 'Git HTTP requests' do ...@@ -116,7 +116,7 @@ describe 'Git HTTP requests' do
end end
it "redirects to the .git suffix version" do it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project_path}.git/info/refs") expect(response).to redirect_to("/#{repository_path}.git/info/refs")
end end
end end
...@@ -128,7 +128,7 @@ describe 'Git HTTP requests' do ...@@ -128,7 +128,7 @@ describe 'Git HTTP requests' do
end end
it "redirects to the .git suffix version" do it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}") expect(response).to redirect_to("/#{repository_path}.git/info/refs?service=#{params[:service]}")
end end
end end
...@@ -140,7 +140,7 @@ describe 'Git HTTP requests' do ...@@ -140,7 +140,7 @@ describe 'Git HTTP requests' do
end end
it "redirects to the .git suffix version" do it "redirects to the .git suffix version" do
expect(response).to redirect_to("/#{project_path}.git/info/refs?service=#{params[:service]}") expect(response).to redirect_to("/#{repository_path}.git/info/refs?service=#{params[:service]}")
end end
end end
...@@ -159,13 +159,13 @@ describe 'Git HTTP requests' do ...@@ -159,13 +159,13 @@ describe 'Git HTTP requests' do
context "POST git-upload-pack" do context "POST git-upload-pack" do
it "fails to find a route" do it "fails to find a route" do
expect { clone_post(project_path) }.to raise_error(ActionController::RoutingError) expect { clone_post(repository_path) }.to raise_error(ActionController::RoutingError)
end end
end end
context "POST git-receive-pack" do context "POST git-receive-pack" do
it "fails to find a route" do it "fails to find a route" do
expect { push_post(project_path) }.to raise_error(ActionController::RoutingError) expect { push_post(repository_path) }.to raise_error(ActionController::RoutingError)
end end
end end
end end
...@@ -211,7 +211,7 @@ describe 'Git HTTP requests' do ...@@ -211,7 +211,7 @@ describe 'Git HTTP requests' do
end end
it_behaves_like 'project path without .git suffix' do it_behaves_like 'project path without .git suffix' do
let(:project_path) { "#{user.namespace.path}/project.git-project" } let(:repository_path) { "#{user.namespace.path}/project.git-project" }
end end
end end
end end
...@@ -820,7 +820,7 @@ describe 'Git HTTP requests' do ...@@ -820,7 +820,7 @@ describe 'Git HTTP requests' do
end end
it_behaves_like 'project path without .git suffix' do it_behaves_like 'project path without .git suffix' do
let(:project_path) { create(:project, :repository, :public, path: 'project.git-project').full_path } let(:repository_path) { create(:project, :repository, :public, path: 'project.git-project').full_path }
end end
context "retrieving an info/refs file" do context "retrieving an info/refs file" do
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment