gitlab-org--gitlab-foss/lib/api/internal.rb
Jonathon Reinhart ba781484c7 Add support for Git push options, specifically ci.skip
gitlab-org/gitlab-shell!166 added support for collecting push options
from the environment, and passing them along to the
/internal/post_receive API endpoint.

This change handles the new push_options JSON element in the payload,
and passes them on through to the GitPushService and GitTagPushService
services.

Futhermore, it adds support for the first push option, ci.skip.  With
this change, one can use 'git push -o ci.skip' to skip CI pipe
execution. Note that the pipeline is still created, but in the "skipped"
state, just like with the 'ci skip' commit message text.

Implements #18667
2018-12-31 13:57:53 -05:00

288 lines
9.6 KiB
Ruby

# frozen_string_literal: true
module API
# Internal access API
class Internal < Grape::API
before { authenticate_by_gitlab_shell_token! }
helpers ::API::Helpers::InternalHelpers
helpers ::Gitlab::Identifier
UNKNOWN_CHECK_RESULT_ERROR = 'Unknown check result'.freeze
helpers do
def response_with_status(code: 200, success: true, message: nil, **extra_options)
status code
{ status: success, message: message }.merge(extra_options).compact
end
end
namespace 'internal' do
# Check if git command is allowed for project
#
# Params:
# key_id - ssh key id for Git over SSH
# user_id - user id for Git over HTTP or over SSH in keyless SSH CERT mode
# username - user name for Git over SSH in keyless SSH cert mode
# protocol - Git access protocol being used, e.g. HTTP or SSH
# project - project full_path (not path on disk)
# action - git action (git-upload-pack or git-receive-pack)
# changes - changes as "oldrev newrev ref", see Gitlab::ChangesList
# rubocop: disable CodeReuse/ActiveRecord
post "/allowed" do
# Stores some Git-specific env thread-safely
env = parse_env
Gitlab::Git::HookEnv.set(gl_repository, env) if project
actor =
if params[:key_id]
Key.find_by(id: params[:key_id])
elsif params[:user_id]
User.find_by(id: params[:user_id])
elsif params[:username]
UserFinder.new(params[:username]).find_by_username
end
protocol = params[:protocol]
actor.update_last_used_at if actor.is_a?(Key)
user =
if actor.is_a?(Key)
actor.user
else
actor
end
access_checker_klass = wiki? ? Gitlab::GitAccessWiki : Gitlab::GitAccess
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)
check_result = begin
result = access_checker.check(params[:action], params[:changes])
@project ||= access_checker.project
result
rescue Gitlab::GitAccess::UnauthorizedError => e
break response_with_status(code: 401, success: false, message: e.message)
rescue Gitlab::GitAccess::TimeoutError => e
break response_with_status(code: 503, success: false, message: e.message)
rescue Gitlab::GitAccess::NotFoundError => e
break response_with_status(code: 404, success: false, message: e.message)
end
log_user_activity(actor)
case check_result
when ::Gitlab::GitAccessResult::Success
payload = {
gl_repository: gl_repository,
gl_id: Gitlab::GlId.gl_id(user),
gl_username: user&.username,
git_config_options: [],
# This repository_path is a bogus value but gitlab-shell still requires
# its presence. https://gitlab.com/gitlab-org/gitlab-shell/issues/135
repository_path: '/',
gitaly: gitaly_payload(params[:action])
}
# Custom option for git-receive-pack command
receive_max_input_size = Gitlab::CurrentSettings.receive_max_input_size.to_i
if receive_max_input_size > 0
payload[:git_config_options] << "receive.maxInputSize=#{receive_max_input_size.megabytes}"
end
response_with_status(**payload)
when ::Gitlab::GitAccessResult::CustomAction
response_with_status(code: 300, message: check_result.message, payload: check_result.payload)
else
response_with_status(code: 500, success: false, message: UNKNOWN_CHECK_RESULT_ERROR)
end
end
# rubocop: enable CodeReuse/ActiveRecord
# rubocop: disable CodeReuse/ActiveRecord
post "/lfs_authenticate" do
status 200
if params[:key_id]
actor = Key.find(params[:key_id])
actor.update_last_used_at
elsif params[:user_id]
actor = User.find_by(id: params[:user_id])
raise ActiveRecord::RecordNotFound.new("No such user id!") unless actor
else
raise ActiveRecord::RecordNotFound.new("No key_id or user_id passed!")
end
token_handler = Gitlab::LfsToken.new(actor)
{
username: token_handler.actor_name,
lfs_token: token_handler.token,
repository_http_path: project.http_url_to_repo
}
end
# rubocop: enable CodeReuse/ActiveRecord
get "/merge_request_urls" do
merge_request_urls
end
#
# Get a ssh key using the fingerprint
#
# rubocop: disable CodeReuse/ActiveRecord
get "/authorized_keys" do
fingerprint = params.fetch(:fingerprint) do
Gitlab::InsecureKeyFingerprint.new(params.fetch(:key)).fingerprint
end
key = Key.find_by(fingerprint: fingerprint)
not_found!("Key") if key.nil?
present key, with: Entities::SSHKey
end
# rubocop: enable CodeReuse/ActiveRecord
#
# Discover user by ssh key, user id or username
#
# rubocop: disable CodeReuse/ActiveRecord
get "/discover" do
if params[:key_id]
key = Key.find(params[:key_id])
user = key.user
elsif params[:user_id]
user = User.find_by(id: params[:user_id])
elsif params[:username]
user = UserFinder.new(params[:username]).find_by_username
end
present user, with: Entities::UserSafe
end
# rubocop: enable CodeReuse/ActiveRecord
get "/check" do
{
api_version: API.version,
gitlab_version: Gitlab::VERSION,
gitlab_rev: Gitlab.revision,
redis: redis_ping
}
end
get "/broadcast_messages" do
if messages = BroadcastMessage.current
present messages, with: Entities::BroadcastMessage
else
[]
end
end
get "/broadcast_message" do
if message = BroadcastMessage.current&.last
present message, with: Entities::BroadcastMessage
else
{}
end
end
# rubocop: disable CodeReuse/ActiveRecord
post '/two_factor_recovery_codes' do
status 200
if params[:key_id]
key = Key.find_by(id: params[:key_id])
if key
key.update_last_used_at
else
break { 'success' => false, 'message' => 'Could not find the given key' }
end
if key.is_a?(DeployKey)
break { success: false, message: 'Deploy keys cannot be used to retrieve recovery codes' }
end
user = key.user
unless user
break { success: false, message: 'Could not find a user for the given key' }
end
elsif params[:user_id]
user = User.find_by(id: params[:user_id])
unless user
break { success: false, message: 'Could not find the given user' }
end
end
unless user.two_factor_enabled?
break { success: false, message: 'Two-factor authentication is not enabled for this user' }
end
codes = nil
::Users::UpdateService.new(current_user, user: user).execute! do |user|
codes = user.generate_otp_backup_codes!
end
{ success: true, recovery_codes: codes }
end
# rubocop: enable CodeReuse/ActiveRecord
post '/pre_receive' do
status 200
reference_counter_increased = Gitlab::ReferenceCounter.new(params[:gl_repository]).increase
{ reference_counter_increased: reference_counter_increased }
end
post "/notify_post_receive" do
status 200
# TODO: Re-enable when Gitaly is processing the post-receive notification
# return unless Gitlab::GitalyClient.enabled?
#
# begin
# repository = wiki? ? project.wiki.repository : project.repository
# Gitlab::GitalyClient::NotificationService.new(repository.raw_repository).post_receive
# rescue GRPC::Unavailable => e
# render_api_error!(e, 500)
# end
end
post '/post_receive' do
status 200
PostReceive.perform_async(params[:gl_repository], params[:identifier],
params[:changes], params[:push_options].to_a)
broadcast_message = BroadcastMessage.current&.last&.message
reference_counter_decreased = Gitlab::ReferenceCounter.new(params[:gl_repository]).decrease
output = {
merge_request_urls: merge_request_urls,
broadcast_message: broadcast_message,
reference_counter_decreased: reference_counter_decreased
}
project = Gitlab::GlRepository.parse(params[:gl_repository]).first
user = identify(params[:identifier])
# A user is not guaranteed to be returned; an orphaned write deploy
# key could be used
if user
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
end
end
end
end