Commit 1e56b3f4 authored by Tiago Botelho's avatar Tiago Botelho

Moves project creationg to git access check for git push

parent 839829a7
...@@ -11,7 +11,6 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -11,7 +11,6 @@ class Projects::GitHttpController < Projects::GitHttpClientController
# GET /foo/bar.git/info/refs?service=git-receive-pack (git push) # GET /foo/bar.git/info/refs?service=git-receive-pack (git push)
def info_refs def info_refs
log_user_activity if upload_pack? log_user_activity if upload_pack?
create_new_project if receive_pack? && project.blank?
render_ok render_ok
end end
...@@ -36,10 +35,6 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -36,10 +35,6 @@ class Projects::GitHttpController < Projects::GitHttpClientController
git_command == 'git-upload-pack' git_command == 'git-upload-pack'
end end
def receive_pack?
git_command == 'git-receive-pack'
end
def git_command def git_command
if action_name == 'info_refs' if action_name == 'info_refs'
params[:service] params[:service]
...@@ -48,10 +43,6 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -48,10 +43,6 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end end
end end
def create_new_project
@project = ::Projects::CreateFromPushService.new(user, params[:project_id], namespace, 'http').execute
end
def render_ok def render_ok
set_workhorse_internal_api_content_type set_workhorse_internal_api_content_type
render json: Gitlab::Workhorse.git_http_ok(repository, wiki?, user, action_name) render json: Gitlab::Workhorse.git_http_ok(repository, wiki?, user, action_name)
...@@ -70,7 +61,10 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -70,7 +61,10 @@ class Projects::GitHttpController < Projects::GitHttpClientController
end end
def access def access
@access ||= access_klass.new(access_actor, project, 'http', authentication_abilities: authentication_abilities, redirected_path: redirected_path, target_namespace: namespace) @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
...@@ -82,14 +76,15 @@ class Projects::GitHttpController < Projects::GitHttpClientController ...@@ -82,14 +76,15 @@ 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 namespace def project_path
@namespace ||= Namespace.find_by_full_path(params[:namespace_id]) @project_path ||= params[:project_id].sub(/\.git$/, '')
end end
def log_user_activity def log_user_activity
......
module Projects
class CreateFromPushService < BaseService
attr_reader :user, :project_path, :namespace, :protocol
def initialize(user, project_path, namespace, protocol)
@user = user
@project_path = project_path
@namespace = namespace
@protocol = protocol
end
def execute
return unless user
project = Projects::CreateService.new(user, project_params).execute
if project.saved?
Gitlab::Checks::ProjectCreated.new(project, user, protocol).add_message
else
raise Gitlab::GitAccess::ProjectCreationError, "Could not create project: #{project.errors.full_messages.join(', ')}"
end
project
end
private
def project_params
{
description: "",
path: project_path.gsub(/\.git$/, ''),
namespace_id: namespace&.id,
visibility_level: Gitlab::VisibilityLevel::PRIVATE.to_s
}
end
end
end
...@@ -39,18 +39,18 @@ ...@@ -39,18 +39,18 @@
When you create a new repo locally, instead of going to GitLab to manually 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 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. That GitLab to create the new project, all without leaving your terminal. If you have access to that
will automatically create a new project under a GitLab namespace that you have access to namespace, we will automatically create a new project under that GitLab namespace with its
with its visibility set to private by default (you can later change it). visibility set to private by default (you can later change it in the UI).
This can be done by using either SSH or HTTP: This can be done by using either SSH or HTTP:
``` ```
## Git push using SSH ## Git push using SSH
git push git@gitlab.com:namespace/nonexistent-project.git branch_name git push git@gitlab.example.com:namespace/nonexistent-project.git
## Git push using HTTP ## Git push using HTTP
git push https://gitlab.com/namespace/nonexistent-project.git branch_name git push https://gitlab.example.com/namespace/nonexistent-project.git
``` ```
Once the push finishes successfully, a remote message will indicate Once the push finishes successfully, a remote message will indicate
...@@ -61,15 +61,12 @@ remote: ...@@ -61,15 +61,12 @@ remote:
remote: The private project namespace/nonexistent-project was created. remote: The private project namespace/nonexistent-project was created.
remote: remote:
remote: To configure the remote, run: remote: To configure the remote, run:
remote: git remote add origin https://gitlab.com/namespace/nonexistent-project.git remote: git remote add origin https://gitlab.example.com/namespace/nonexistent-project.git
remote: remote:
remote: To view the project, visit: remote: To view the project, visit:
remote: https://gitlab.com/namespace/nonexistent-project remote: https://gitlab.example.com/namespace/nonexistent-project
remote: remote:
``` ```
If the project name is already in use, your push will be rejected
to prevent accidental overwriting the existing project.
[import it]: ../workflow/importing/README.md [import it]: ../workflow/importing/README.md
[reserved]: ../user/reserved_names.md [reserved]: ../user/reserved_names.md
module API module API
module Helpers module Helpers
module InternalHelpers module InternalHelpers
include Gitlab::Utils::StrongMemoize
attr_reader :redirected_path attr_reader :redirected_path
def wiki? def wiki?
...@@ -49,10 +47,6 @@ module API ...@@ -49,10 +47,6 @@ module API
::Users::ActivityService.new(actor, 'Git SSH').execute if commands.include?(params[:action]) ::Users::ActivityService.new(actor, 'Git SSH').execute if commands.include?(params[:action])
end end
def receive_pack?
params[:action] == 'git-receive-pack'
end
def merge_request_urls def merge_request_urls
::MergeRequests::GetUrlsService.new(project).execute(params[:changes]) ::MergeRequests::GetUrlsService.new(project).execute(params[:changes])
end end
...@@ -66,16 +60,18 @@ module API ...@@ -66,16 +60,18 @@ module API
false false
end end
def project_namespace def project_path
strong_memoize(:project_namespace) do project&.path || project_path_match[:project_path]
project&.namespace || Namespace.find_by_full_path(project_match[:namespace_path])
end end
def namespace_path
project&.namespace&.full_path || project_path_match[:namespace_path]
end end
private private
def project_match def project_path_match
@project_match ||= params[:project].match(Gitlab::PathRegex.full_project_git_path_regex) || {} @project_path_match ||= params[:project].match(Gitlab::PathRegex.full_project_git_path_regex) || {}
end end
# rubocop:disable Gitlab/ModuleWithInstanceVariables # rubocop:disable Gitlab/ModuleWithInstanceVariables
......
...@@ -42,23 +42,18 @@ module API ...@@ -42,23 +42,18 @@ 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, target_namespace: project_namespace) 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
if receive_pack? && project.blank?
begin
@project = ::Projects::CreateFromPushService.new(user, project_match[:project_path], project_namespace, protocol).execute
rescue Gitlab::GitAccess::ProjectCreationError => e
return { status: false, message: e.message }
end
end
log_user_activity(actor) log_user_activity(actor)
{ {
......
module Gitlab module Gitlab
module Checks module Checks
class BaseProject class PostPushMessage
def initialize(project, user, protocol) def initialize(project, user, protocol)
@project = project @project = project
@user = user @user = user
......
module Gitlab module Gitlab
module Checks module Checks
class ProjectCreated < BaseProject class ProjectCreated < PostPushMessage
PROJECT_CREATED = "project_created".freeze PROJECT_CREATED = "project_created".freeze
def message def message
<<~MESSAGE.strip_heredoc <<~MESSAGE
The private project #{project.full_path} was created. The private project #{project.full_path} was successfully created.
To configure the remote, run: To configure the remote, run:
git remote add origin #{url_to_repo} git remote add origin #{url_to_repo}
......
module Gitlab module Gitlab
module Checks module Checks
class ProjectMoved < BaseProject class ProjectMoved < PostPushMessage
REDIRECT_NAMESPACE = "redirect_namespace".freeze REDIRECT_NAMESPACE = "redirect_namespace".freeze
def initialize(project, user, protocol, redirected_path) def initialize(project, user, protocol, redirected_path)
...@@ -10,7 +10,7 @@ module Gitlab ...@@ -10,7 +10,7 @@ module Gitlab
end end
def 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:
......
...@@ -28,32 +28,37 @@ module Gitlab ...@@ -28,32 +28,37 @@ 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, :target_namespace attr_reader :actor, :project, :protocol, :authentication_abilities, :namespace_path, :project_path, :redirected_path
def initialize(actor, project, protocol, authentication_abilities:, redirected_path: nil, target_namespace: 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
@target_namespace = target_namespace @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!(cmd)
check_project_moved!
check_command_disabled!(cmd) check_command_disabled!(cmd)
check_command_existence!(cmd) check_command_existence!(cmd)
check_repository_existence!(cmd) check_db_accessibility!(cmd)
ensure_project_on_push!(cmd, changes)
check_project_accessibility!
check_project_moved!
check_repository_existence!
case cmd case cmd
when *DOWNLOAD_COMMANDS when *DOWNLOAD_COMMANDS
check_download_access! check_download_access!
when *PUSH_COMMANDS when *PUSH_COMMANDS
check_push_access!(cmd, changes) check_push_access!(changes)
end end
true true
...@@ -99,8 +104,8 @@ module Gitlab ...@@ -99,8 +104,8 @@ module Gitlab
end end
end end
def check_project_accessibility!(cmd) def check_project_accessibility!
unless can_create_project_in_namespace?(cmd) || can_read_project? if project.blank? || !can_read_project?
raise NotFoundError, ERROR_MESSAGES[:project_not_found] raise NotFoundError, ERROR_MESSAGES[:project_not_found]
end end
end end
...@@ -143,16 +148,49 @@ module Gitlab ...@@ -143,16 +148,49 @@ module Gitlab
end end
end end
def check_repository_existence!(cmd) def check_db_accessibility!(cmd)
unless can_create_project_in_namespace?(cmd) || project.repository.exists? 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] raise UnauthorizedError, ERROR_MESSAGES[:no_repo]
end end
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?
...@@ -161,13 +199,7 @@ module Gitlab ...@@ -161,13 +199,7 @@ module Gitlab
end end
end end
def check_push_access!(cmd, changes) def check_push_access!(changes)
if Gitlab::Database.read_only?
raise UnauthorizedError, push_to_read_only_message
end
return if can_create_project_in_namespace?(cmd)
if project.repository_read_only? if project.repository_read_only?
raise UnauthorizedError, ERROR_MESSAGES[:read_only] raise UnauthorizedError, ERROR_MESSAGES[:read_only]
end end
...@@ -180,8 +212,6 @@ module Gitlab ...@@ -180,8 +212,6 @@ module Gitlab
raise UnauthorizedError, ERROR_MESSAGES[:upload] raise UnauthorizedError, ERROR_MESSAGES[:upload]
end end
return if changes.blank? # Allow access.
check_change_access!(changes) check_change_access!(changes)
end end
...@@ -198,6 +228,8 @@ module Gitlab ...@@ -198,6 +228,8 @@ module Gitlab
end end
def check_change_access!(changes) def check_change_access!(changes)
return if changes.blank? # Allow access.
changes_list = Gitlab::ChangesList.new(changes) changes_list = Gitlab::ChangesList.new(changes)
# Iterate over all changes to find if user allowed all of them to be applied # Iterate over all changes to find if user allowed all of them to be applied
...@@ -240,14 +272,6 @@ module Gitlab ...@@ -240,14 +272,6 @@ module Gitlab
end || Guest.can?(:read_project, project) end || Guest.can?(:read_project, project)
end end
def can_create_project_in_namespace?(cmd)
strong_memoize(:can_create_project_in_namespace) do
return false unless push?(cmd) && target_namespace && project.blank?
user.can?(:create_projects, target_namespace)
end
end
def http? def http?
protocol == 'http' protocol == 'http'
end end
...@@ -260,10 +284,6 @@ module Gitlab ...@@ -260,10 +284,6 @@ module Gitlab
command == 'git-receive-pack' command == 'git-receive-pack'
end end
def push?(cmd)
PUSH_COMMANDS.include?(cmd)
end
def upload_pack_disabled_over_http? def upload_pack_disabled_over_http?
!Gitlab.config.gitlab_shell.upload_pack !Gitlab.config.gitlab_shell.upload_pack
end end
......
...@@ -188,7 +188,7 @@ module Gitlab ...@@ -188,7 +188,7 @@ module Gitlab
end end
def full_project_git_path_regex def full_project_git_path_regex
@full_project_git_path_regex ||= /\A\/?(?<namespace_path>#{full_namespace_route_regex})\/(?<project_path>#{project_git_route_regex})\z/.freeze @full_project_git_path_regex ||= %r{\A\/?(?<namespace_path>#{full_namespace_route_regex})\/(?<project_path>#{project_route_regex})\.git\z}
end end
def full_namespace_format_regex def full_namespace_format_regex
......
...@@ -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
......
This diff is collapsed.
...@@ -386,32 +386,6 @@ describe API::Internal do ...@@ -386,32 +386,6 @@ describe API::Internal do
expect(json_response["repository_path"]).to eq(project.repository.path_to_repo) expect(json_response["repository_path"]).to eq(project.repository.path_to_repo)
expect(json_response["gl_repository"]).to eq("project-#{project.id}") expect(json_response["gl_repository"]).to eq("project-#{project.id}")
end end
context 'when project does not exist' do
it 'creates a new project' do
path = "#{user.namespace.path}/notexist.git"
expect do
push_with_path(key, path)
end.to change { Project.count }.by(1)
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_truthy
expect(json_response["gitaly"]["repository"]["relative_path"]).to eq(path)
end
it 'handles project creation failure' do
path = "#{user.namespace.path}/new.git"
expect do
push_with_path(key, path)
end.not_to change { Project.count }
expect(response).to have_gitlab_http_status(200)
expect(json_response["status"]).to be_falsey
expect(json_response["message"]).to eq("Could not create project: Path new is a reserved name")
end
end
end end
end end
end end
......
require 'spec_helper'
describe Projects::CreateFromPushService do
let(:user) { create(:user) }
let(:project_path) { "nonexist" }
let(:namespace) { user&.namespace }
let(:protocol) { 'http' }
subject { described_class.new(user, project_path, namespace, protocol) }
it 'creates project' do
expect_any_instance_of(Projects::CreateService).to receive(:execute).and_call_original
expect { subject.execute }.to change { Project.count }.by(1)
end
it 'raises project creation error when project creation fails' do
allow_any_instance_of(Project).to receive(:saved?).and_return(false)
expect { subject.execute }.to raise_error(Gitlab::GitAccess::ProjectCreationError)
end
context 'when user is nil' do
let(:user) { nil }
subject { described_class.new(user, project_path, namespace, protocol) }
it 'returns nil' do
expect_any_instance_of(Projects::CreateService).not_to receive(:execute)
expect(subject.execute).to be_nil
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