Merge branch '26388-push-to-create-a-new-project' into 'master'
Resolve "Push to create a new project" Closes #26388 See merge request gitlab-org/gitlab-ce!16547
This commit is contained in:
commit
b0f0ad050b
|
@ -5,6 +5,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
|
|||
|
||||
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403
|
||||
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404
|
||||
rescue_from Gitlab::GitAccess::ProjectCreationError, with: :render_422
|
||||
|
||||
# GET /foo/bar.git/info/refs?service=git-upload-pack (git pull)
|
||||
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
|
||||
|
@ -55,8 +56,15 @@ class Projects::GitHttpController < Projects::GitHttpClientController
|
|||
render plain: exception.message, status: :not_found
|
||||
end
|
||||
|
||||
def render_422(exception)
|
||||
render plain: exception.message, status: :unprocessable_entity
|
||||
end
|
||||
|
||||
def access
|
||||
@access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities, redirected_path: redirected_path)
|
||||
@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)
|
||||
end
|
||||
|
||||
def access_actor
|
||||
|
@ -68,12 +76,17 @@ class Projects::GitHttpController < Projects::GitHttpClientController
|
|||
# Use the magic string '_any' to indicate we do not know what the
|
||||
# changes are. This is also what gitlab-shell does.
|
||||
access.check(git_command, '_any')
|
||||
@project ||= access.project
|
||||
end
|
||||
|
||||
def access_klass
|
||||
@access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
|
||||
end
|
||||
|
||||
def project_path
|
||||
@project_path ||= params[:project_id].sub(/\.git$/, '')
|
||||
end
|
||||
|
||||
def log_user_activity
|
||||
Users::ActivityService.new(user, 'pull').execute
|
||||
end
|
||||
|
|
|
@ -0,0 +1,5 @@
|
|||
---
|
||||
title: User can now git push to create a new project
|
||||
merge_request: 16547
|
||||
author:
|
||||
type: added
|
|
@ -33,5 +33,40 @@
|
|||
|
||||
1. Click **Create project**.
|
||||
|
||||
## Push to create a new project
|
||||
|
||||
> [Introduced](https://gitlab.com/gitlab-org/gitlab-ce/issues/26388) in GitLab 10.5.
|
||||
|
||||
When you create a new repo locally, instead of going to GitLab to manually
|
||||
create a new project and then push the repo, you can directly push it to
|
||||
GitLab to create the new project, all without leaving your terminal. If you have access to that
|
||||
namespace, we will automatically create a new project under that GitLab namespace with its
|
||||
visibility set to private by default (you can later change it in the UI).
|
||||
|
||||
This can be done by using either SSH or HTTP:
|
||||
|
||||
```
|
||||
## Git push using SSH
|
||||
git push git@gitlab.example.com:namespace/nonexistent-project.git
|
||||
|
||||
## Git push using HTTP
|
||||
git push https://gitlab.example.com/namespace/nonexistent-project.git
|
||||
```
|
||||
|
||||
Once the push finishes successfully, a remote message will indicate
|
||||
the command to set the remote and the URL to the new project:
|
||||
|
||||
```
|
||||
remote:
|
||||
remote: The private project namespace/nonexistent-project was created.
|
||||
remote:
|
||||
remote: To configure the remote, run:
|
||||
remote: git remote add origin https://gitlab.example.com/namespace/nonexistent-project.git
|
||||
remote:
|
||||
remote: To view the project, visit:
|
||||
remote: https://gitlab.example.com/namespace/nonexistent-project
|
||||
remote:
|
||||
```
|
||||
|
||||
[import it]: ../workflow/importing/README.md
|
||||
[reserved]: ../user/reserved_names.md
|
||||
|
|
|
@ -60,8 +60,20 @@ module API
|
|||
false
|
||||
end
|
||||
|
||||
def project_path
|
||||
project&.path || project_path_match[:project_path]
|
||||
end
|
||||
|
||||
def namespace_path
|
||||
project&.namespace&.full_path || project_path_match[:namespace_path]
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def project_path_match
|
||||
@project_path_match ||= params[:project].match(Gitlab::PathRegex.full_project_git_path_regex) || {}
|
||||
end
|
||||
|
||||
# rubocop:disable Gitlab/ModuleWithInstanceVariables
|
||||
def set_project
|
||||
if params[:gl_repository]
|
||||
|
|
|
@ -42,11 +42,14 @@ module API
|
|||
end
|
||||
|
||||
access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
|
||||
access_checker = access_checker_klass
|
||||
.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities, redirected_path: redirected_path)
|
||||
access_checker = access_checker_klass.new(actor, project,
|
||||
protocol, authentication_abilities: ssh_authentication_abilities,
|
||||
namespace_path: namespace_path, project_path: project_path,
|
||||
redirected_path: redirected_path)
|
||||
|
||||
begin
|
||||
access_checker.check(params[:action], params[:changes])
|
||||
@project ||= access_checker.project
|
||||
rescue Gitlab::GitAccess::UnauthorizedError, Gitlab::GitAccess::NotFoundError => e
|
||||
return { status: false, message: e.message }
|
||||
end
|
||||
|
@ -207,8 +210,11 @@ module API
|
|||
# A user is not guaranteed to be returned; an orphaned write deploy
|
||||
# key could be used
|
||||
if user
|
||||
redirect_message = Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)
|
||||
redirect_message = Gitlab::Checks::ProjectMoved.fetch_message(user.id, project.id)
|
||||
project_created_message = Gitlab::Checks::ProjectCreated.fetch_message(user.id, project.id)
|
||||
|
||||
output[:redirected_message] = redirect_message if redirect_message
|
||||
output[:project_created_message] = project_created_message if project_created_message
|
||||
end
|
||||
|
||||
output
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
module Gitlab
|
||||
module Checks
|
||||
class PostPushMessage
|
||||
def initialize(project, user, protocol)
|
||||
@project = project
|
||||
@user = user
|
||||
@protocol = protocol
|
||||
end
|
||||
|
||||
def self.fetch_message(user_id, project_id)
|
||||
key = message_key(user_id, project_id)
|
||||
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
message = redis.get(key)
|
||||
redis.del(key)
|
||||
message
|
||||
end
|
||||
end
|
||||
|
||||
def add_message
|
||||
return unless user.present? && project.present?
|
||||
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
key = self.class.message_key(user.id, project.id)
|
||||
redis.setex(key, 5.minutes, message)
|
||||
end
|
||||
end
|
||||
|
||||
def message
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
protected
|
||||
|
||||
attr_reader :project, :user, :protocol
|
||||
|
||||
def self.message_key(user_id, project_id)
|
||||
raise NotImplementedError
|
||||
end
|
||||
|
||||
def url_to_repo
|
||||
protocol == 'ssh' ? project.ssh_url_to_repo : project.http_url_to_repo
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -0,0 +1,31 @@
|
|||
module Gitlab
|
||||
module Checks
|
||||
class ProjectCreated < PostPushMessage
|
||||
PROJECT_CREATED = "project_created".freeze
|
||||
|
||||
def message
|
||||
<<~MESSAGE
|
||||
|
||||
The private project #{project.full_path} was successfully created.
|
||||
|
||||
To configure the remote, run:
|
||||
git remote add origin #{url_to_repo}
|
||||
|
||||
To view the project, visit:
|
||||
#{project_url}
|
||||
|
||||
MESSAGE
|
||||
end
|
||||
|
||||
private
|
||||
|
||||
def self.message_key(user_id, project_id)
|
||||
"#{PROJECT_CREATED}:#{user_id}:#{project_id}"
|
||||
end
|
||||
|
||||
def project_url
|
||||
Gitlab::Routing.url_helpers.project_url(project)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
|
@ -1,38 +1,16 @@
|
|||
module Gitlab
|
||||
module Checks
|
||||
class ProjectMoved
|
||||
class ProjectMoved < PostPushMessage
|
||||
REDIRECT_NAMESPACE = "redirect_namespace".freeze
|
||||
|
||||
def initialize(project, user, redirected_path, protocol)
|
||||
@project = project
|
||||
@user = user
|
||||
def initialize(project, user, protocol, redirected_path)
|
||||
@redirected_path = redirected_path
|
||||
@protocol = protocol
|
||||
|
||||
super(project, user, protocol)
|
||||
end
|
||||
|
||||
def self.fetch_redirect_message(user_id, project_id)
|
||||
redirect_key = redirect_message_key(user_id, project_id)
|
||||
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
message = redis.get(redirect_key)
|
||||
redis.del(redirect_key)
|
||||
message
|
||||
end
|
||||
end
|
||||
|
||||
def add_redirect_message
|
||||
# Don't bother with sending a redirect message for anonymous clones
|
||||
# because they never see it via the `/internal/post_receive` endpoint
|
||||
return unless user.present? && project.present?
|
||||
|
||||
Gitlab::Redis::SharedState.with do |redis|
|
||||
key = self.class.redirect_message_key(user.id, project.id)
|
||||
redis.setex(key, 5.minutes, redirect_message)
|
||||
end
|
||||
end
|
||||
|
||||
def redirect_message(rejected: false)
|
||||
<<~MESSAGE.strip_heredoc
|
||||
def message(rejected: false)
|
||||
<<~MESSAGE
|
||||
Project '#{redirected_path}' was moved to '#{project.full_path}'.
|
||||
|
||||
Please update your Git remote:
|
||||
|
@ -47,17 +25,17 @@ module Gitlab
|
|||
|
||||
private
|
||||
|
||||
attr_reader :project, :redirected_path, :protocol, :user
|
||||
attr_reader :redirected_path
|
||||
|
||||
def self.redirect_message_key(user_id, project_id)
|
||||
def self.message_key(user_id, project_id)
|
||||
"#{REDIRECT_NAMESPACE}:#{user_id}:#{project_id}"
|
||||
end
|
||||
|
||||
def remote_url_message(rejected)
|
||||
if rejected
|
||||
"git remote set-url origin #{url} and try again."
|
||||
"git remote set-url origin #{url_to_repo} and try again."
|
||||
else
|
||||
"git remote set-url origin #{url}"
|
||||
"git remote set-url origin #{url_to_repo}"
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -2,15 +2,19 @@
|
|||
# class return an instance of `GitlabAccessStatus`
|
||||
module Gitlab
|
||||
class GitAccess
|
||||
include Gitlab::Utils::StrongMemoize
|
||||
|
||||
UnauthorizedError = Class.new(StandardError)
|
||||
NotFoundError = Class.new(StandardError)
|
||||
ProjectCreationError = Class.new(StandardError)
|
||||
ProjectMovedError = Class.new(NotFoundError)
|
||||
|
||||
ERROR_MESSAGES = {
|
||||
upload: 'You are not allowed to upload code for this project.',
|
||||
download: 'You are not allowed to download code from this project.',
|
||||
deploy_key_upload:
|
||||
'This deploy key does not have write access to this project.',
|
||||
auth_upload: 'You are not allowed to upload code.',
|
||||
auth_download: 'You are not allowed to download code.',
|
||||
deploy_key_upload: 'This deploy key does not have write access to this project.',
|
||||
no_repo: 'A repository for this project does not exist yet.',
|
||||
project_not_found: 'The project you were looking for could not be found.',
|
||||
account_blocked: 'Your account has been blocked.',
|
||||
|
@ -25,24 +29,31 @@ module Gitlab
|
|||
PUSH_COMMANDS = %w{ git-receive-pack }.freeze
|
||||
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS
|
||||
|
||||
attr_reader :actor, :project, :protocol, :authentication_abilities, :redirected_path
|
||||
attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path
|
||||
|
||||
def initialize(actor, project, protocol, authentication_abilities:, redirected_path: nil)
|
||||
def initialize(actor, project, protocol, authentication_abilities:, namespace_path: nil, project_path: nil, redirected_path: nil)
|
||||
@actor = actor
|
||||
@project = project
|
||||
@protocol = protocol
|
||||
@redirected_path = redirected_path
|
||||
@authentication_abilities = authentication_abilities
|
||||
@namespace_path = namespace_path
|
||||
@project_path = project_path
|
||||
@redirected_path = redirected_path
|
||||
end
|
||||
|
||||
def check(cmd, changes)
|
||||
check_protocol!
|
||||
check_valid_actor!
|
||||
check_active_user!
|
||||
check_project_accessibility!
|
||||
check_project_moved!
|
||||
check_authentication_abilities!(cmd)
|
||||
check_command_disabled!(cmd)
|
||||
check_command_existence!(cmd)
|
||||
check_db_accessibility!(cmd)
|
||||
|
||||
ensure_project_on_push!(cmd, changes)
|
||||
|
||||
check_project_accessibility!
|
||||
check_project_moved!
|
||||
check_repository_existence!
|
||||
|
||||
case cmd
|
||||
|
@ -95,6 +106,19 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def check_authentication_abilities!(cmd)
|
||||
case cmd
|
||||
when *DOWNLOAD_COMMANDS
|
||||
unless authentication_abilities.include?(:download_code) || authentication_abilities.include?(:build_download_code)
|
||||
raise UnauthorizedError, ERROR_MESSAGES[:auth_download]
|
||||
end
|
||||
when *PUSH_COMMANDS
|
||||
unless authentication_abilities.include?(:push_code)
|
||||
raise UnauthorizedError, ERROR_MESSAGES[:auth_upload]
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
def check_project_accessibility!
|
||||
if project.blank? || !can_read_project?
|
||||
raise NotFoundError, ERROR_MESSAGES[:project_not_found]
|
||||
|
@ -104,12 +128,12 @@ module Gitlab
|
|||
def check_project_moved!
|
||||
return if redirected_path.nil?
|
||||
|
||||
project_moved = Checks::ProjectMoved.new(project, user, redirected_path, protocol)
|
||||
project_moved = Checks::ProjectMoved.new(project, user, protocol, redirected_path)
|
||||
|
||||
if project_moved.permanent_redirect?
|
||||
project_moved.add_redirect_message
|
||||
project_moved.add_message
|
||||
else
|
||||
raise ProjectMovedError, project_moved.redirect_message(rejected: true)
|
||||
raise ProjectMovedError, project_moved.message(rejected: true)
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -139,6 +163,40 @@ module Gitlab
|
|||
end
|
||||
end
|
||||
|
||||
def check_db_accessibility!(cmd)
|
||||
return unless receive_pack?(cmd)
|
||||
|
||||
if Gitlab::Database.read_only?
|
||||
raise UnauthorizedError, push_to_read_only_message
|
||||
end
|
||||
end
|
||||
|
||||
def ensure_project_on_push!(cmd, changes)
|
||||
return if project || deploy_key?
|
||||
return unless receive_pack?(cmd) && changes == '_any' && authentication_abilities.include?(:push_code)
|
||||
|
||||
namespace = Namespace.find_by_full_path(namespace_path)
|
||||
|
||||
return unless user&.can?(:create_projects, namespace)
|
||||
|
||||
project_params = {
|
||||
path: project_path,
|
||||
namespace_id: namespace.id,
|
||||
visibility_level: Gitlab::VisibilityLevel::PRIVATE
|
||||
}
|
||||
|
||||
project = Projects::CreateService.new(user, project_params).execute
|
||||
|
||||
unless project.saved?
|
||||
raise ProjectCreationError, "Could not create project: #{project.errors.full_messages.join(', ')}"
|
||||
end
|
||||
|
||||
@project = project
|
||||
user_access.project = @project
|
||||
|
||||
Checks::ProjectCreated.new(project, user, protocol).add_message
|
||||
end
|
||||
|
||||
def check_repository_existence!
|
||||
unless project.repository.exists?
|
||||
raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
|
||||
|
@ -146,9 +204,8 @@ module Gitlab
|
|||
end
|
||||
|
||||
def check_download_access!
|
||||
return if deploy_key?
|
||||
|
||||
passed = user_can_download_code? ||
|
||||
passed = deploy_key? ||
|
||||
user_can_download_code? ||
|
||||
build_can_download_code? ||
|
||||
guest_can_download_code?
|
||||
|
||||
|
@ -162,35 +219,21 @@ module Gitlab
|
|||
raise UnauthorizedError, ERROR_MESSAGES[:read_only]
|
||||
end
|
||||
|
||||
if Gitlab::Database.read_only?
|
||||
raise UnauthorizedError, push_to_read_only_message
|
||||
end
|
||||
|
||||
if deploy_key
|
||||
check_deploy_key_push_access!
|
||||
unless deploy_key.can_push_to?(project)
|
||||
raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload]
|
||||
end
|
||||
elsif user
|
||||
check_user_push_access!
|
||||
# User access is verified in check_change_access!
|
||||
else
|
||||
raise UnauthorizedError, ERROR_MESSAGES[:upload]
|
||||
end
|
||||
|
||||
return if changes.blank? # Allow access.
|
||||
return if changes.blank? # Allow access this is needed for EE.
|
||||
|
||||
check_change_access!(changes)
|
||||
end
|
||||
|
||||
def check_user_push_access!
|
||||
unless authentication_abilities.include?(:push_code)
|
||||
raise UnauthorizedError, ERROR_MESSAGES[:upload]
|
||||
end
|
||||
end
|
||||
|
||||
def check_deploy_key_push_access!
|
||||
unless deploy_key.can_push_to?(project)
|
||||
raise UnauthorizedError, ERROR_MESSAGES[:deploy_key_upload]
|
||||
end
|
||||
end
|
||||
|
||||
def check_change_access!(changes)
|
||||
changes_list = Gitlab::ChangesList.new(changes)
|
||||
|
||||
|
|
|
@ -179,6 +179,10 @@ module Gitlab
|
|||
@full_project_path_regex ||= %r{\A#{full_namespace_route_regex}/#{project_route_regex}/\z}
|
||||
end
|
||||
|
||||
def full_project_git_path_regex
|
||||
@full_project_git_path_regex ||= %r{\A\/?(?<namespace_path>#{full_namespace_route_regex})\/(?<project_path>#{project_route_regex})\.git\z}
|
||||
end
|
||||
|
||||
def namespace_format_regex
|
||||
@namespace_format_regex ||= /\A#{NAMESPACE_FORMAT_REGEX}\z/.freeze
|
||||
end
|
||||
|
|
|
@ -6,7 +6,8 @@ module Gitlab
|
|||
[user&.id, project&.id]
|
||||
end
|
||||
|
||||
attr_reader :user, :project
|
||||
attr_reader :user
|
||||
attr_accessor :project
|
||||
|
||||
def initialize(user, project: nil)
|
||||
@user = user
|
||||
|
|
|
@ -0,0 +1,46 @@
|
|||
require 'rails_helper'
|
||||
|
||||
describe Gitlab::Checks::ProjectCreated, :clean_gitlab_redis_shared_state do
|
||||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
|
||||
describe '.fetch_message' do
|
||||
context 'with a project created message queue' do
|
||||
let(:project_created) { described_class.new(project, user, 'http') }
|
||||
|
||||
before do
|
||||
project_created.add_message
|
||||
end
|
||||
|
||||
it 'returns project created message' do
|
||||
expect(described_class.fetch_message(user.id, project.id)).to eq(project_created.message)
|
||||
end
|
||||
|
||||
it 'deletes the project created message from redis' do
|
||||
expect(Gitlab::Redis::SharedState.with { |redis| redis.get("project_created:#{user.id}:#{project.id}") }).not_to be_nil
|
||||
described_class.fetch_message(user.id, project.id)
|
||||
expect(Gitlab::Redis::SharedState.with { |redis| redis.get("project_created:#{user.id}:#{project.id}") }).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no project created message queue' do
|
||||
it 'returns nil' do
|
||||
expect(described_class.fetch_message(1, 2)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#add_message' do
|
||||
it 'queues a project created message' do
|
||||
project_created = described_class.new(project, user, 'http')
|
||||
|
||||
expect(project_created.add_message).to eq('OK')
|
||||
end
|
||||
|
||||
it 'handles anonymous push' do
|
||||
project_created = described_class.new(nil, user, 'http')
|
||||
|
||||
expect(project_created.add_message).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
|
@ -4,82 +4,82 @@ describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do
|
|||
let(:user) { create(:user) }
|
||||
let(:project) { create(:project) }
|
||||
|
||||
describe '.fetch_redirct_message' do
|
||||
describe '.fetch_message' do
|
||||
context 'with a redirect message queue' do
|
||||
it 'should return the redirect message' do
|
||||
project_moved = described_class.new(project, user, 'foo/bar', 'http')
|
||||
project_moved.add_redirect_message
|
||||
it 'returns the redirect message' do
|
||||
project_moved = described_class.new(project, user, 'http', 'foo/bar')
|
||||
project_moved.add_message
|
||||
|
||||
expect(described_class.fetch_redirect_message(user.id, project.id)).to eq(project_moved.redirect_message)
|
||||
expect(described_class.fetch_message(user.id, project.id)).to eq(project_moved.message)
|
||||
end
|
||||
|
||||
it 'should delete the redirect message from redis' do
|
||||
project_moved = described_class.new(project, user, 'foo/bar', 'http')
|
||||
project_moved.add_redirect_message
|
||||
it 'deletes the redirect message from redis' do
|
||||
project_moved = described_class.new(project, user, 'http', 'foo/bar')
|
||||
project_moved.add_message
|
||||
|
||||
expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).not_to be_nil
|
||||
described_class.fetch_redirect_message(user.id, project.id)
|
||||
described_class.fetch_message(user.id, project.id)
|
||||
expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
context 'with no redirect message queue' do
|
||||
it 'should return nil' do
|
||||
expect(described_class.fetch_redirect_message(1, 2)).to be_nil
|
||||
it 'returns nil' do
|
||||
expect(described_class.fetch_message(1, 2)).to be_nil
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#add_redirect_message' do
|
||||
it 'should queue a redirect message' do
|
||||
project_moved = described_class.new(project, user, 'foo/bar', 'http')
|
||||
expect(project_moved.add_redirect_message).to eq("OK")
|
||||
describe '#add_message' do
|
||||
it 'queues a redirect message' do
|
||||
project_moved = described_class.new(project, user, 'http', 'foo/bar')
|
||||
expect(project_moved.add_message).to eq("OK")
|
||||
end
|
||||
|
||||
it 'should handle anonymous clones' do
|
||||
project_moved = described_class.new(project, nil, 'foo/bar', 'http')
|
||||
it 'handles anonymous clones' do
|
||||
project_moved = described_class.new(project, nil, 'http', 'foo/bar')
|
||||
|
||||
expect(project_moved.add_redirect_message).to eq(nil)
|
||||
expect(project_moved.add_message).to eq(nil)
|
||||
end
|
||||
end
|
||||
|
||||
describe '#redirect_message' do
|
||||
describe '#message' do
|
||||
context 'when the push is rejected' do
|
||||
it 'should return a redirect message telling the user to try again' do
|
||||
project_moved = described_class.new(project, user, 'foo/bar', 'http')
|
||||
it 'returns a redirect message telling the user to try again' do
|
||||
project_moved = described_class.new(project, user, 'http', 'foo/bar')
|
||||
message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
|
||||
"\n\nPlease update your Git remote:" +
|
||||
"\n\n git remote set-url origin #{project.http_url_to_repo} and try again.\n"
|
||||
|
||||
expect(project_moved.redirect_message(rejected: true)).to eq(message)
|
||||
expect(project_moved.message(rejected: true)).to eq(message)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when the push is not rejected' do
|
||||
it 'should return a redirect message' do
|
||||
project_moved = described_class.new(project, user, 'foo/bar', 'http')
|
||||
it 'returns a redirect message' do
|
||||
project_moved = described_class.new(project, user, 'http', 'foo/bar')
|
||||
message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
|
||||
"\n\nPlease update your Git remote:" +
|
||||
"\n\n git remote set-url origin #{project.http_url_to_repo}\n"
|
||||
|
||||
expect(project_moved.redirect_message).to eq(message)
|
||||
expect(project_moved.message).to eq(message)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#permanent_redirect?' do
|
||||
context 'with a permanent RedirectRoute' do
|
||||
it 'should return true' do
|
||||
it 'returns true' do
|
||||
project.route.create_redirect('foo/bar', permanent: true)
|
||||
project_moved = described_class.new(project, user, 'foo/bar', 'http')
|
||||
project_moved = described_class.new(project, user, 'http', 'foo/bar')
|
||||
expect(project_moved.permanent_redirect?).to be_truthy
|
||||
end
|
||||
end
|
||||
|
||||
context 'without a permanent RedirectRoute' do
|
||||
it 'should return false' do
|
||||
it 'returns false' do
|
||||
project.route.create_redirect('foo/bar')
|
||||
project_moved = described_class.new(project, user, 'foo/bar', 'http')
|
||||
project_moved = described_class.new(project, user, 'http', 'foo/bar')
|
||||
expect(project_moved.permanent_redirect?).to be_falsy
|
||||
end
|
||||
end
|
||||
|
|
|
@ -5,11 +5,19 @@ describe Gitlab::GitAccess do
|
|||
|
||||
let(:actor) { user }
|
||||
let(:project) { create(:project, :repository) }
|
||||
let(:project_path) { project.path }
|
||||
let(:namespace_path) { project&.namespace&.path }
|
||||
let(:protocol) { 'ssh' }
|
||||
let(:authentication_abilities) { %i[read_project download_code push_code] }
|
||||
let(:redirected_path) { nil }
|
||||
|
||||
let(:access) { described_class.new(actor, project, protocol, authentication_abilities: authentication_abilities, redirected_path: redirected_path) }
|
||||
let(:access) do
|
||||
described_class.new(actor, project,
|
||||
protocol, authentication_abilities: authentication_abilities,
|
||||
namespace_path: namespace_path, project_path: project_path,
|
||||
redirected_path: redirected_path)
|
||||
end
|
||||
|
||||
let(:push_access_check) { access.check('git-receive-pack', '_any') }
|
||||
let(:pull_access_check) { access.check('git-upload-pack', '_any') }
|
||||
|
||||
|
@ -111,7 +119,7 @@ describe Gitlab::GitAccess do
|
|||
end
|
||||
|
||||
it 'does not block pushes with "not found"' do
|
||||
expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload])
|
||||
expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload])
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -145,6 +153,7 @@ describe Gitlab::GitAccess do
|
|||
|
||||
context 'when the project is nil' do
|
||||
let(:project) { nil }
|
||||
let(:project_path) { "new-project" }
|
||||
|
||||
it 'blocks push and pull with "not found"' do
|
||||
aggregate_failures do
|
||||
|
@ -152,6 +161,42 @@ describe Gitlab::GitAccess do
|
|||
expect { push_access_check }.to raise_not_found
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is allowed to create project in namespace' do
|
||||
let(:namespace_path) { user.namespace.path }
|
||||
let(:access) do
|
||||
described_class.new(actor, nil,
|
||||
protocol, authentication_abilities: authentication_abilities,
|
||||
project_path: project_path, namespace_path: namespace_path,
|
||||
redirected_path: redirected_path)
|
||||
end
|
||||
|
||||
it 'blocks pull access with "not found"' do
|
||||
expect { pull_access_check }.to raise_not_found
|
||||
end
|
||||
|
||||
it 'allows push access' do
|
||||
expect { push_access_check }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user is not allowed to create project in namespace' do
|
||||
let(:user2) { create(:user) }
|
||||
let(:namespace_path) { user2.namespace.path }
|
||||
let(:access) do
|
||||
described_class.new(actor, nil,
|
||||
protocol, authentication_abilities: authentication_abilities,
|
||||
project_path: project_path, namespace_path: namespace_path,
|
||||
redirected_path: redirected_path)
|
||||
end
|
||||
|
||||
it 'blocks push and pull with "not found"' do
|
||||
aggregate_failures do
|
||||
expect { pull_access_check }.to raise_not_found
|
||||
expect { push_access_check }.to raise_not_found
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -197,7 +242,7 @@ describe Gitlab::GitAccess do
|
|||
it 'enqueues a redirected message' do
|
||||
push_access_check
|
||||
|
||||
expect(Gitlab::Checks::ProjectMoved.fetch_redirect_message(user.id, project.id)).not_to be_nil
|
||||
expect(Gitlab::Checks::ProjectMoved.fetch_message(user.id, project.id)).not_to be_nil
|
||||
end
|
||||
end
|
||||
|
||||
|
@ -273,6 +318,52 @@ describe Gitlab::GitAccess do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#check_authentication_abilities!' do
|
||||
before do
|
||||
project.add_master(user)
|
||||
end
|
||||
|
||||
context 'when download' do
|
||||
let(:authentication_abilities) { [] }
|
||||
|
||||
it 'raises unauthorized with download error' do
|
||||
expect { pull_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_download])
|
||||
end
|
||||
|
||||
context 'when authentication abilities include download code' do
|
||||
let(:authentication_abilities) { [:download_code] }
|
||||
|
||||
it 'does not raise any errors' do
|
||||
expect { pull_access_check }.not_to raise_error
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authentication abilities include build download code' do
|
||||
let(:authentication_abilities) { [:build_download_code] }
|
||||
|
||||
it 'does not raise any errors' do
|
||||
expect { pull_access_check }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when upload' do
|
||||
let(:authentication_abilities) { [] }
|
||||
|
||||
it 'raises unauthorized with push error' do
|
||||
expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload])
|
||||
end
|
||||
|
||||
context 'when authentication abilities include push code' do
|
||||
let(:authentication_abilities) { [:push_code] }
|
||||
|
||||
it 'does not raise any errors' do
|
||||
expect { push_access_check }.not_to raise_error
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#check_command_disabled!' do
|
||||
before do
|
||||
project.add_master(user)
|
||||
|
@ -311,6 +402,117 @@ describe Gitlab::GitAccess do
|
|||
end
|
||||
end
|
||||
|
||||
describe '#check_db_accessibility!' do
|
||||
context 'when in a read-only GitLab instance' do
|
||||
before do
|
||||
create(:protected_branch, name: 'feature', project: project)
|
||||
allow(Gitlab::Database).to receive(:read_only?) { true }
|
||||
end
|
||||
|
||||
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:cannot_push_to_read_only]) }
|
||||
end
|
||||
end
|
||||
|
||||
describe '#ensure_project_on_push!' do
|
||||
let(:access) do
|
||||
described_class.new(actor, project,
|
||||
protocol, authentication_abilities: authentication_abilities,
|
||||
project_path: project_path, namespace_path: namespace_path,
|
||||
redirected_path: redirected_path)
|
||||
end
|
||||
|
||||
context 'when push' do
|
||||
let(:cmd) { 'git-receive-pack' }
|
||||
|
||||
context 'when project does not exist' do
|
||||
let(:project_path) { "nonexistent" }
|
||||
let(:project) { nil }
|
||||
|
||||
context 'when changes is _any' do
|
||||
let(:changes) { '_any' }
|
||||
|
||||
context 'when authentication abilities include push code' do
|
||||
let(:authentication_abilities) { [:push_code] }
|
||||
|
||||
context 'when user can create project in namespace' do
|
||||
let(:namespace_path) { user.namespace.path }
|
||||
|
||||
it 'creates a new project' do
|
||||
expect { access.send(:ensure_project_on_push!, cmd, changes) }.to change { Project.count }.by(1)
|
||||
end
|
||||
end
|
||||
|
||||
context 'when user cannot create project in namespace' do
|
||||
let(:user2) { create(:user) }
|
||||
let(:namespace_path) { user2.namespace.path }
|
||||
|
||||
it 'does not create a new project' do
|
||||
expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when authentication abilities do not include push code' do
|
||||
let(:authentication_abilities) { [] }
|
||||
|
||||
context 'when user can create project in namespace' do
|
||||
let(:namespace_path) { user.namespace.path }
|
||||
|
||||
it 'does not create a new project' do
|
||||
expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when check contains actual changes' do
|
||||
let(:changes) { "#{Gitlab::Git::BLANK_SHA} 570e7b2abdd848b95f2f578043fc23bd6f6fd24d refs/heads/new_branch" }
|
||||
|
||||
it 'does not create a new project' do
|
||||
expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when project exists' do
|
||||
let(:changes) { '_any' }
|
||||
let!(:project) { create(:project) }
|
||||
|
||||
it 'does not create a new project' do
|
||||
expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
|
||||
end
|
||||
end
|
||||
|
||||
context 'when deploy key is used' do
|
||||
let(:key) { create(:deploy_key, user: user) }
|
||||
let(:actor) { key }
|
||||
let(:project_path) { "nonexistent" }
|
||||
let(:project) { nil }
|
||||
let(:namespace_path) { user.namespace.path }
|
||||
let(:changes) { '_any' }
|
||||
|
||||
it 'does not create a new project' do
|
||||
expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when pull' do
|
||||
let(:cmd) { 'git-upload-pack' }
|
||||
let(:changes) { '_any' }
|
||||
|
||||
context 'when project does not exist' do
|
||||
let(:project_path) { "new-project" }
|
||||
let(:namespace_path) { user.namespace.path }
|
||||
let(:project) { nil }
|
||||
|
||||
it 'does not create a new project' do
|
||||
expect { access.send(:ensure_project_on_push!, cmd, changes) }.not_to change { Project.count }
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
describe '#check_download_access!' do
|
||||
it 'allows masters to pull' do
|
||||
project.add_master(user)
|
||||
|
@ -338,7 +540,9 @@ describe Gitlab::GitAccess do
|
|||
|
||||
context 'when project is public' do
|
||||
let(:public_project) { create(:project, :public, :repository) }
|
||||
let(:access) { described_class.new(nil, public_project, 'web', authentication_abilities: []) }
|
||||
let(:project_path) { public_project.path }
|
||||
let(:namespace_path) { public_project.namespace.path }
|
||||
let(:access) { described_class.new(nil, public_project, 'web', authentication_abilities: [:download_code], project_path: project_path, namespace_path: namespace_path) }
|
||||
|
||||
context 'when repository is enabled' do
|
||||
it 'give access to download code' do
|
||||
|
@ -638,19 +842,6 @@ describe Gitlab::GitAccess do
|
|||
admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }))
|
||||
end
|
||||
end
|
||||
|
||||
context "when in a read-only GitLab instance" do
|
||||
before do
|
||||
create(:protected_branch, name: 'feature', project: project)
|
||||
allow(Gitlab::Database).to receive(:read_only?) { true }
|
||||
end
|
||||
|
||||
# Only check admin; if an admin can't do it, other roles can't either
|
||||
matrix = permissions_matrix[:admin].dup
|
||||
matrix.each { |key, _| matrix[key] = false }
|
||||
|
||||
run_permission_checks(admin: matrix)
|
||||
end
|
||||
end
|
||||
|
||||
describe 'build authentication abilities' do
|
||||
|
@ -661,26 +852,26 @@ describe Gitlab::GitAccess do
|
|||
project.add_reporter(user)
|
||||
end
|
||||
|
||||
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) }
|
||||
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) }
|
||||
end
|
||||
|
||||
context 'when unauthorized' do
|
||||
context 'to public project' do
|
||||
let(:project) { create(:project, :public, :repository) }
|
||||
|
||||
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) }
|
||||
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) }
|
||||
end
|
||||
|
||||
context 'to internal project' do
|
||||
let(:project) { create(:project, :internal, :repository) }
|
||||
|
||||
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:upload]) }
|
||||
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) }
|
||||
end
|
||||
|
||||
context 'to private project' do
|
||||
let(:project) { create(:project, :private, :repository) }
|
||||
|
||||
it { expect { push_access_check }.to raise_not_found }
|
||||
it { expect { push_access_check }.to raise_unauthorized(described_class::ERROR_MESSAGES[:auth_upload]) }
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -767,8 +958,7 @@ describe Gitlab::GitAccess do
|
|||
end
|
||||
|
||||
def raise_not_found
|
||||
raise_error(Gitlab::GitAccess::NotFoundError,
|
||||
Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found])
|
||||
raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found])
|
||||
end
|
||||
|
||||
def build_authentication_abilities
|
||||
|
|
|
@ -368,7 +368,7 @@ describe API::Internal do
|
|||
|
||||
context 'project as /namespace/project' do
|
||||
it do
|
||||
pull(key, project_with_repo_path('/' + project.full_path))
|
||||
push(key, project_with_repo_path('/' + project.full_path))
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(json_response["status"]).to be_truthy
|
||||
|
@ -379,7 +379,7 @@ describe API::Internal do
|
|||
|
||||
context 'project as namespace/project' do
|
||||
it do
|
||||
pull(key, project_with_repo_path(project.full_path))
|
||||
push(key, project_with_repo_path(project.full_path))
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(json_response["status"]).to be_truthy
|
||||
|
@ -807,14 +807,27 @@ describe API::Internal do
|
|||
|
||||
context 'with a redirected data' do
|
||||
it 'returns redirected message on the response' do
|
||||
project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'foo/baz', 'http')
|
||||
project_moved.add_redirect_message
|
||||
project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'http', 'foo/baz')
|
||||
project_moved.add_message
|
||||
|
||||
post api("/internal/post_receive"), valid_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(json_response["redirected_message"]).to be_present
|
||||
expect(json_response["redirected_message"]).to eq(project_moved.redirect_message)
|
||||
expect(json_response["redirected_message"]).to eq(project_moved.message)
|
||||
end
|
||||
end
|
||||
|
||||
context 'with new project data' do
|
||||
it 'returns new project message on the response' do
|
||||
project_created = Gitlab::Checks::ProjectCreated.new(project, user, 'http')
|
||||
project_created.add_message
|
||||
|
||||
post api("/internal/post_receive"), valid_params
|
||||
|
||||
expect(response).to have_gitlab_http_status(200)
|
||||
expect(json_response["project_created_message"]).to be_present
|
||||
expect(json_response["project_created_message"]).to eq(project_created.message)
|
||||
end
|
||||
end
|
||||
|
||||
|
|
|
@ -107,15 +107,39 @@ describe 'Git HTTP requests' do
|
|||
let(:user) { create(:user) }
|
||||
|
||||
context "when the project doesn't exist" do
|
||||
let(:path) { 'doesnt/exist.git' }
|
||||
context "when namespace doesn't exist" do
|
||||
let(:path) { 'doesnt/exist.git' }
|
||||
|
||||
it_behaves_like 'pulls require Basic HTTP Authentication'
|
||||
it_behaves_like 'pushes require Basic HTTP Authentication'
|
||||
it_behaves_like 'pulls require Basic HTTP Authentication'
|
||||
it_behaves_like 'pushes require Basic HTTP Authentication'
|
||||
|
||||
context 'when authenticated' do
|
||||
it 'rejects downloads and uploads with 404 Not Found' do
|
||||
download_or_upload(path, user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
context 'when authenticated' do
|
||||
it 'rejects downloads and uploads with 404 Not Found' do
|
||||
download_or_upload(path, user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_gitlab_http_status(:not_found)
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
|
||||
context 'when namespace exists' do
|
||||
let(:path) { "#{user.namespace.path}/new-project.git"}
|
||||
|
||||
context 'when authenticated' do
|
||||
it 'creates a new project under the existing namespace' do
|
||||
expect do
|
||||
upload(path, user: user.username, password: user.password) do |response|
|
||||
expect(response).to have_gitlab_http_status(:ok)
|
||||
end
|
||||
end.to change { user.projects.count }.by(1)
|
||||
end
|
||||
|
||||
it 'rejects push with 422 Unprocessable Entity when project is invalid' do
|
||||
path = "#{user.namespace.path}/new.git"
|
||||
|
||||
push_get(path, user: user.username, password: user.password)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:unprocessable_entity)
|
||||
end
|
||||
end
|
||||
end
|
||||
|
@ -596,7 +620,7 @@ describe 'Git HTTP requests' do
|
|||
push_get(path, env)
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
expect(response.body).to eq(git_access_error(:upload))
|
||||
expect(response.body).to eq(git_access_error(:auth_upload))
|
||||
end
|
||||
|
||||
# We are "authenticated" as CI using a valid token here. But we are
|
||||
|
@ -636,7 +660,7 @@ describe 'Git HTTP requests' do
|
|||
push_get path, env
|
||||
|
||||
expect(response).to have_gitlab_http_status(:forbidden)
|
||||
expect(response.body).to eq(git_access_error(:upload))
|
||||
expect(response.body).to eq(git_access_error(:auth_upload))
|
||||
end
|
||||
end
|
||||
|
||||
|
|
Loading…
Reference in New Issue