Commit b0f0ad05 authored by Douwe Maan's avatar Douwe Maan

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
parents bc59a5d0 c51eb79b
...@@ -5,6 +5,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -5,6 +5,7 @@ class Projects::GitHttpController < Projects::GitHttpClientController
rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403 rescue_from Gitlab::GitAccess::UnauthorizedError, with: :render_403
rescue_from Gitlab::GitAccess::NotFoundError, with: :render_404 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-upload-pack (git pull)
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
...@@ -55,8 +56,15 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -55,8 +56,15 @@ class Projects::GitHttpController < Projects::GitHttpClientController
render plain: exception.message, status: :not_found render plain: exception.message, status: :not_found
end end
def render_422(exception)
render plain: exception.message, status: :unprocessable_entity
end
def access 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 end
def access_actor def access_actor
...@@ -68,12 +76,17 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -68,12 +76,17 @@ class Projects::GitHttpController < Projects::GitHttpClientController
# Use the magic string '_any' to indicate we do not know what the # Use the magic string '_any' to indicate we do not know what the
# changes are. This is also what gitlab-shell does. # changes are. This is also what gitlab-shell does.
access.check(git_command, '_any') access.check(git_command, '_any')
@project ||= access.project
end end
def access_klass def access_klass
@access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess @access_klass ||= wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
end end
def project_path
@project_path ||= params[:project_id].sub(/\.git$/, '')
end
def log_user_activity def log_user_activity
Users::ActivityService.new(user, 'pull').execute Users::ActivityService.new(user, 'pull').execute
end end
......
---
title: User can now git push to create a new project
merge_request: 16547
author:
type: added
...@@ -33,5 +33,40 @@ ...@@ -33,5 +33,40 @@
1. Click **Create project**. 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 [import it]: ../workflow/importing/README.md
[reserved]: ../user/reserved_names.md [reserved]: ../user/reserved_names.md
...@@ -60,8 +60,20 @@ module API ...@@ -60,8 +60,20 @@ module API
false false
end 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 private
def project_path_match
@project_path_match ||= params[:project].match(Gitlab::PathRegex.full_project_git_path_regex) || {}
end
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
def set_project def set_project
if params[:gl_repository] if params[:gl_repository]
......
...@@ -42,11 +42,14 @@ module API ...@@ -42,11 +42,14 @@ module API
end end
access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
access_checker = access_checker_klass access_checker = access_checker_klass.new(actor, project,
.new(actor, project, protocol, authentication_abilities: ssh_authentication_abilities, redirected_path: redirected_path) protocol, authentication_abilities: ssh_authentication_abilities,
namespace_path: namespace_path, project_path: project_path,
redirected_path: redirected_path)
begin begin
access_checker.check(params[:action], params[:changes]) access_checker.check(params[:action], params[:changes])
@project ||= access_checker.project
rescue Gitlab::GitAccess::UnauthorizedError, Gitlab::GitAccess::NotFoundError => e rescue Gitlab::GitAccess::UnauthorizedError, Gitlab::GitAccess::NotFoundError => e
return { status: false, message: e.message } return { status: false, message: e.message }
end end
...@@ -207,8 +210,11 @@ module API ...@@ -207,8 +210,11 @@ module API
# A user is not guaranteed to be returned; an orphaned write deploy # A user is not guaranteed to be returned; an orphaned write deploy
# key could be used # key could be used
if user 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[:redirected_message] = redirect_message if redirect_message
output[:project_created_message] = project_created_message if project_created_message
end end
output output
......
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
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
module Gitlab module Gitlab
module Checks module Checks
class ProjectMoved class ProjectMoved < PostPushMessage
REDIRECT_NAMESPACE = "redirect_namespace".freeze REDIRECT_NAMESPACE = "redirect_namespace".freeze
def initialize(project, user, redirected_path, protocol) def initialize(project, user, protocol, redirected_path)
@project = project
@user = user
@redirected_path = redirected_path @redirected_path = redirected_path
@protocol = 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| super(project, user, protocol)
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 end
def redirect_message(rejected: false) def message(rejected: false)
<<~MESSAGE.strip_heredoc <<~MESSAGE
Project '#{redirected_path}' was moved to '#{project.full_path}'. Project '#{redirected_path}' was moved to '#{project.full_path}'.
Please update your Git remote: Please update your Git remote:
...@@ -47,17 +25,17 @@ module Gitlab ...@@ -47,17 +25,17 @@ module Gitlab
private 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}" "#{REDIRECT_NAMESPACE}:#{user_id}:#{project_id}"
end end
def remote_url_message(rejected) def remote_url_message(rejected)
if rejected if rejected
"git remote set-url origin #{url} and try again." "git remote set-url origin #{url_to_repo} and try again."
else else
"git remote set-url origin #{url}" "git remote set-url origin #{url_to_repo}"
end end
end end
......
...@@ -2,15 +2,19 @@ ...@@ -2,15 +2,19 @@
# class return an instance of `GitlabAccessStatus` # class return an instance of `GitlabAccessStatus`
module Gitlab module Gitlab
class GitAccess class GitAccess
include Gitlab::Utils::StrongMemoize
UnauthorizedError = Class.new(StandardError) UnauthorizedError = Class.new(StandardError)
NotFoundError = Class.new(StandardError) NotFoundError = Class.new(StandardError)
ProjectCreationError = Class.new(StandardError)
ProjectMovedError = Class.new(NotFoundError) ProjectMovedError = Class.new(NotFoundError)
ERROR_MESSAGES = { ERROR_MESSAGES = {
upload: 'You are not allowed to upload code for this project.', upload: 'You are not allowed to upload code for this project.',
download: 'You are not allowed to download code from this project.', download: 'You are not allowed to download code from this project.',
deploy_key_upload: auth_upload: 'You are not allowed to upload code.',
'This deploy key does not have write access to this project.', 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.', no_repo: 'A repository for this project does not exist yet.',
project_not_found: 'The project you were looking for could not be found.', project_not_found: 'The project you were looking for could not be found.',
account_blocked: 'Your account has been blocked.', account_blocked: 'Your account has been blocked.',
...@@ -25,24 +29,31 @@ module Gitlab ...@@ -25,24 +29,31 @@ module Gitlab
PUSH_COMMANDS = %w{ git-receive-pack }.freeze PUSH_COMMANDS = %w{ git-receive-pack }.freeze
ALL_COMMANDS = DOWNLOAD_COMMANDS + PUSH_COMMANDS 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 @actor = actor
@project = project @project = project
@protocol = protocol @protocol = protocol
@redirected_path = redirected_path
@authentication_abilities = authentication_abilities @authentication_abilities = authentication_abilities
@namespace_path = namespace_path
@project_path = project_path
@redirected_path = redirected_path
end end
def check(cmd, changes) def check(cmd, changes)
check_protocol! check_protocol!
check_valid_actor! check_valid_actor!
check_active_user! check_active_user!
check_project_accessibility! check_authentication_abilities!(cmd)
check_project_moved!
check_command_disabled!(cmd) check_command_disabled!(cmd)
check_command_existence!(cmd) check_command_existence!(cmd)
check_db_accessibility!(cmd)
ensure_project_on_push!(cmd, changes)
check_project_accessibility!
check_project_moved!
check_repository_existence! check_repository_existence!
case cmd case cmd
...@@ -95,6 +106,19 @@ module Gitlab ...@@ -95,6 +106,19 @@ module Gitlab
end end
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! def check_project_accessibility!
if project.blank? || !can_read_project? if project.blank? || !can_read_project?
raise NotFoundError, ERROR_MESSAGES[:project_not_found] raise NotFoundError, ERROR_MESSAGES[:project_not_found]
...@@ -104,12 +128,12 @@ module Gitlab ...@@ -104,12 +128,12 @@ module Gitlab
def check_project_moved! def check_project_moved!
return if redirected_path.nil? 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? if project_moved.permanent_redirect?
project_moved.add_redirect_message project_moved.add_message
else else
raise ProjectMovedError, project_moved.redirect_message(rejected: true) raise ProjectMovedError, project_moved.message(rejected: true)
end end
end end
...@@ -139,6 +163,40 @@ module Gitlab ...@@ -139,6 +163,40 @@ module Gitlab
end end
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! def check_repository_existence!
unless project.repository.exists? unless project.repository.exists?
raise UnauthorizedError, ERROR_MESSAGES[:no_repo] raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
...@@ -146,9 +204,8 @@ module Gitlab ...@@ -146,9 +204,8 @@ module Gitlab
end end
def check_download_access! def check_download_access!
return if deploy_key? passed = deploy_key? ||
user_can_download_code? ||
passed = user_can_download_code? ||
build_can_download_code? || build_can_download_code? ||
guest_can_download_code? guest_can_download_code?
...@@ -162,35 +219,21 @@ module Gitlab ...@@ -162,35 +219,21 @@ module Gitlab
raise UnauthorizedError, ERROR_MESSAGES[:read_only] raise UnauthorizedError, ERROR_MESSAGES[:read_only]
end end
if Gitlab::Database.read_only?
raise UnauthorizedError, push_to_read_only_message
end
if deploy_key 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 elsif user
check_user_push_access! # User access is verified in check_change_access!
else else
raise UnauthorizedError, ERROR_MESSAGES[:upload] raise UnauthorizedError, ERROR_MESSAGES[:upload]
end end
return if changes.blank? # Allow access. return if changes.blank? # Allow access this is needed for EE.
check_change_access!(changes) check_change_access!(changes)
end 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) def check_change_access!(changes)
changes_list = Gitlab::ChangesList.new(changes) changes_list = Gitlab::ChangesList.new(changes)
......
...@@ -179,6 +179,10 @@ module Gitlab ...@@ -179,6 +179,10 @@ module Gitlab
@full_project_path_regex ||= %r{\A#{full_namespace_route_regex}/#{project_route_regex}/\z} @full_project_path_regex ||= %r{\A#{full_namespace_route_regex}/#{project_route_regex}/\z}
end 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 def namespace_format_regex
@namespace_format_regex ||= /\A#{NAMESPACE_FORMAT_REGEX}\z/.freeze @namespace_format_regex ||= /\A#{NAMESPACE_FORMAT_REGEX}\z/.freeze
end end
......
...@@ -6,7 +6,8 @@ module Gitlab ...@@ -6,7 +6,8 @@ module Gitlab
[user&.id, project&.id] [user&.id, project&.id]
end end
attr_reader :user, :project attr_reader :user
attr_accessor :project
def initialize(user, project: nil) def initialize(user, project: nil)
@user = user @user = user
......
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 ...@@ -4,82 +4,82 @@ describe Gitlab::Checks::ProjectMoved, :clean_gitlab_redis_shared_state do
let(:user) { create(:user) } let(:user) { create(:user) }
let(:project) { create(:project) } let(:project) { create(:project) }
describe '.fetch_redirct_message' do describe '.fetch_message' do
context 'with a redirect message queue' do context 'with a redirect message queue' do
it 'should return the redirect message' do it 'returns the redirect message' do
project_moved = described_class.new(project, user, 'foo/bar', 'http') project_moved = described_class.new(project, user, 'http', 'foo/bar')
project_moved.add_redirect_message 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 end
it 'should delete the redirect message from redis' do it 'deletes the redirect message from redis' do
project_moved = described_class.new(project, user, 'foo/bar', 'http') project_moved = described_class.new(project, user, 'http', 'foo/bar')
project_moved.add_redirect_message project_moved.add_message
expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).not_to be_nil 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 expect(Gitlab::Redis::SharedState.with { |redis| redis.get("redirect_namespace:#{user.id}:#{project.id}") }).to be_nil
end end
end end
context 'with no redirect message queue' do context 'with no redirect message queue' do
it 'should return nil' do it 'returns nil' do
expect(described_class.fetch_redirect_message(1, 2)).to be_nil expect(described_class.fetch_message(1, 2)).to be_nil
end end
end end
end end
describe '#add_redirect_message' do describe '#add_message' do
it 'should queue a redirect message' do it 'queues a redirect message' do
project_moved = described_class.new(project, user, 'foo/bar', 'http') project_moved = described_class.new(project, user, 'http', 'foo/bar')
expect(project_moved.add_redirect_message).to eq("OK") expect(project_moved.add_message).to eq("OK")
end end
it 'should handle anonymous clones' do it 'handles anonymous clones' do
project_moved = described_class.new(project, nil, 'foo/bar', 'http') 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
end end
describe '#redirect_message' do describe '#message' do
context 'when the push is rejected' do context 'when the push is rejected' do
it 'should return a redirect message telling the user to try again' do it 'returns a redirect message telling the user to try again' do
project_moved = described_class.new(project, user, 'foo/bar', 'http') project_moved = described_class.new(project, user, 'http', 'foo/bar')
message = "Project 'foo/bar' was moved to '#{project.full_path}'." + message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
"\n\nPlease update your Git remote:" + "\n\nPlease update your Git remote:" +
"\n\n git remote set-url origin #{project.http_url_to_repo} and try again.\n" "\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
end end
context 'when the push is not rejected' do context 'when the push is not rejected' do
it 'should return a redirect message' do it 'returns a redirect message' do
project_moved = described_class.new(project, user, 'foo/bar', 'http') project_moved = described_class.new(project, user, 'http', 'foo/bar')
message = "Project 'foo/bar' was moved to '#{project.full_path}'." + message = "Project 'foo/bar' was moved to '#{project.full_path}'." +
"\n\nPlease update your Git remote:" + "\n\nPlease update your Git remote:" +
"\n\n git remote set-url origin #{project.http_url_to_repo}\n" "\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 end
end end
describe '#permanent_redirect?' do describe '#permanent_redirect?' do
context 'with a permanent RedirectRoute' 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.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 expect(project_moved.permanent_redirect?).to be_truthy
end end
end end
context 'without a permanent RedirectRoute' do context 'without a permanent RedirectRoute' do
it 'should return false' do it 'returns false' do
project.route.create_redirect('foo/bar') 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 expect(project_moved.permanent_redirect?).to be_falsy
end end
end end
......
...@@ -5,11 +5,19 @@ describe Gitlab::GitAccess do ...@@ -5,11 +5,19 @@ describe Gitlab::GitAccess do
let(:actor) { user } let(:actor) { user }
let(:project) { create(:project, :repository) } let(:project) { create(:project, :repository) }
let(:project_path) { project.path }
let(:namespace_path) { project&.namespace&.path }
let(:protocol) { 'ssh' } let(:protocol) { 'ssh' }
let(:authentication_abilities) { %i[read_project download_code push_code] } let(:authentication_abilities) { %i[read_project download_code push_code] }
let(:redirected_path) { nil } 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(:push_access_check) { access.check('git-receive-pack', '_any') }
let(:pull_access_check) { access.check('git-upload-pack', '_any') } let(:pull_access_check) { access.check('git-upload-pack', '_any') }
...@@ -111,7 +119,7 @@ describe Gitlab::GitAccess do ...@@ -111,7 +119,7 @@ describe Gitlab::GitAccess do
end end
it 'does not block pushes with "not found"' do 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 end
end end
...@@ -145,6 +153,7 @@ describe Gitlab::GitAccess do ...@@ -145,6 +153,7 @@ describe Gitlab::GitAccess do
context 'when the project is nil' do context 'when the project is nil' do
let(:project) { nil } let(:project) { nil }
let(:project_path) { "new-project" }
it 'blocks push and pull with "not found"' do it 'blocks push and pull with "not found"' do
aggregate_failures do aggregate_failures do
...@@ -152,6 +161,42 @@ describe Gitlab::GitAccess do ...@@ -152,6 +161,42 @@ describe Gitlab::GitAccess do
expect { push_access_check }.to raise_not_found expect { push_access_check }.to raise_not_found
end end
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
end end
...@@ -197,7 +242,7 @@ describe Gitlab::GitAccess do ...@@ -197,7 +242,7 @@ describe Gitlab::GitAccess do
it 'enqueues a redirected message' do it 'enqueues a redirected message' do
push_access_check 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
end end
...@@ -273,6 +318,52 @@ describe Gitlab::GitAccess do ...@@ -273,6 +318,52 @@ describe Gitlab::GitAccess do
end end
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 describe '#check_command_disabled!' do
before do before do
project.add_master(user) project.add_master(user)
...@@ -311,6 +402,117 @@ describe Gitlab::GitAccess do ...@@ -311,6 +402,117 @@ describe Gitlab::GitAccess do
end end
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 describe '#check_download_access!' do
it 'allows masters to pull' do it 'allows masters to pull' do
project.add_master(user) project.add_master(user)
...@@ -338,7 +540,9 @@ describe Gitlab::GitAccess do ...@@ -338,7 +540,9 @@ describe Gitlab::GitAccess do
context 'when project is public' do context 'when project is public' do
let(:public_project) { create(:project, :public, :repository) } 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 context 'when repository is enabled' do
it 'give access to download code' do it 'give access to download code' do
...@@ -638,19 +842,6 @@ describe Gitlab::GitAccess do ...@@ -638,19 +842,6 @@ describe Gitlab::GitAccess do
admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false })) admin: { push_protected_branch: false, push_all: false, merge_into_protected_branch: false }))
end end
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 end
describe 'build authentication abilities' do describe 'build authentication abilities' do
...@@ -661,26 +852,26 @@ describe Gitlab::GitAccess do ...@@ -661,26 +852,26 @@ describe Gitlab::GitAccess do
project.add_reporter(user) project.add_reporter(user)
end 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 end
context 'when unauthorized' do context 'when unauthorized' do
context 'to public project' do context 'to public project' do
let(:project) { create(:project, :public, :repository) } 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 end
context 'to internal project' do context 'to internal project' do
let(:project) { create(:project, :internal, :repository) } 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 end
context 'to private project' do context 'to private project' do
let(:project) { create(:project, :private, :repository) } 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 end
end end
...@@ -767,8 +958,7 @@ describe Gitlab::GitAccess do ...@@ -767,8 +958,7 @@ describe Gitlab::GitAccess do
end end
def raise_not_found def raise_not_found
raise_error(Gitlab::GitAccess::NotFoundError, raise_error(Gitlab::GitAccess::NotFoundError, Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found])
Gitlab::GitAccess::ERROR_MESSAGES[:project_not_found])
end end
def build_authentication_abilities def build_authentication_abilities
......
...@@ -368,7 +368,7 @@ describe API::Internal do ...@@ -368,7 +368,7 @@ describe API::Internal do
context 'project as /namespace/project' do context 'project as /namespace/project' do
it 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(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy expect(json_response["status"]).to be_truthy
...@@ -379,7 +379,7 @@ describe API::Internal do ...@@ -379,7 +379,7 @@ describe API::Internal do
context 'project as namespace/project' do context 'project as namespace/project' do
it 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(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy expect(json_response["status"]).to be_truthy
...@@ -807,14 +807,27 @@ describe API::Internal do ...@@ -807,14 +807,27 @@ describe API::Internal do
context 'with a redirected data' do context 'with a redirected data' do
it 'returns redirected message on the response' do it 'returns redirected message on the response' do
project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'foo/baz', 'http') project_moved = Gitlab::Checks::ProjectMoved.new(project, user, 'http', 'foo/baz')
project_moved.add_redirect_message project_moved.add_message
post api("/internal/post_receive"), valid_params post api("/internal/post_receive"), valid_params
expect(response).to have_gitlab_http_status(200) expect(response).to have_gitlab_http_status(200)
expect(json_response["redirected_message"]).to be_present 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
end end
......
...@@ -107,15 +107,39 @@ describe 'Git HTTP requests' do ...@@ -107,15 +107,39 @@ describe 'Git HTTP requests' do
let(:user) { create(:user) } let(:user) { create(:user) }
context "when the project doesn't exist" do 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 'pulls require Basic HTTP Authentication'
it_behaves_like 'pushes require Basic HTTP Authentication' it_behaves_like 'pushes require Basic HTTP Authentication'
context 'when authenticated' do context 'when authenticated' do
it 'rejects downloads and uploads with 404 Not Found' do it 'rejects downloads and uploads with 404 Not Found' do
download_or_upload(path, user: user.username, password: user.password) do |response| download_or_upload(path, user: user.username, password: user.password) do |response|
expect(response).to have_gitlab_http_status(:not_found) 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 end
end end
...@@ -596,7 +620,7 @@ describe 'Git HTTP requests' do ...@@ -596,7 +620,7 @@ describe 'Git HTTP requests' do
push_get(path, env) push_get(path, env)
expect(response).to have_gitlab_http_status(:forbidden) 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
# We are "authenticated" as CI using a valid token here. But we are # We are "authenticated" as CI using a valid token here. But we are
...@@ -636,7 +660,7 @@ describe 'Git HTTP requests' do ...@@ -636,7 +660,7 @@ describe 'Git HTTP requests' do
push_get path, env push_get path, env
expect(response).to have_gitlab_http_status(:forbidden) 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
end end
......
Markdown is supported
0%
or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment