As we reevaluate how to best support and maintain Staging Ref in the future, we encourage development teams using this environment to highlight their use cases in the following issue: https://gitlab.com/gitlab-com/gl-infra/software-delivery/framework/software-delivery-framework-issue-tracker/-/issues/36.

Skip to content
Snippets Groups Projects
Unverified Commit 18096b54 authored by Nick Thomas's avatar Nick Thomas
Browse files

Remove dead Ruby code

parent 8db304b4
No related branches found
No related tags found
No related merge requests found
Showing
with 2 additions and 1252 deletions
Loading
Loading
@@ -14,15 +14,9 @@ verify_golang:
test: test_ruby test_golang
# The Ruby tests are now all integration specs that test the Go implementation.
test_ruby:
# bin/gitlab-shell, bin/gitlab-shell-authorized-keys-check and
# bin/gitlab-shell-authorized-principals-check must exist and need to be
# the Ruby version for rspec to be able to test.
cp bin/gitlab-shell-ruby bin/gitlab-shell
cp bin/gitlab-shell-authorized-keys-check-ruby bin/gitlab-shell-authorized-keys-check
cp bin/gitlab-shell-authorized-principals-check-ruby bin/gitlab-shell-authorized-principals-check
bundle exec rspec --color --tag '~go' --format d spec
rm -f bin/gitlab-shell bin/gitlab-shell-authorized-keys-check bin/gitlab-shell-authorized-principals-check
bundle exec rspec --color --format d spec
test_golang:
support/go-test
Loading
Loading
#!/usr/bin/env ruby
#
# GitLab shell authorized_keys helper. Query GitLab API to get the authorized
# command for a given ssh key fingerprint
#
# Ex.
# bin/gitlab-shell-authorized-keys-check <username> <public-key>
#
# Returns
# command="/bin/gitlab-shell key-#",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAA...
#
# Expects to be called by the SSH daemon, via configuration like:
# AuthorizedKeysCommandUser git
# AuthorizedKeysCommand /bin/gitlab-shell-authorized-keys-check git %u %k
abort "# Wrong number of arguments. #{ARGV.size}. Usage:
# gitlab-shell-authorized-keys-check <expected-username> <actual-username> <key>" unless ARGV.size == 3
expected_username = ARGV[0]
abort '# No username provided' if expected_username.nil? || expected_username == ''
actual_username = ARGV[1]
abort '# No username provided' if actual_username.nil? || actual_username == ''
# Only check access if the requested username matches the configured username.
# Normally, these would both be 'git', but it can be configured by the user
exit 0 unless expected_username == actual_username
key = ARGV[2]
abort "# No key provided" if key.nil? || key == ''
require_relative '../lib/gitlab_init'
require_relative '../lib/gitlab_net'
require_relative '../lib/gitlab_keys'
authorized_key = GitlabNet.new.authorized_key(key)
if authorized_key.nil?
puts "# No key was found for #{key}"
else
puts GitlabKeys.key_line("key-#{authorized_key['id']}", authorized_key['key'])
end
#!/usr/bin/env ruby
#
# GitLab shell authorized principals helper. Emits the same sort of
# command="..." line as gitlab-shell-authorized-principals-check, with
# the right options.
#
# Ex.
# bin/gitlab-shell-authorized-keys-check <key-id> <principal1> [<principal2>...]
#
# Returns one line per principal passed in, e.g.:
# command="/bin/gitlab-shell username-{KEY_ID}",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty {PRINCIPAL}
# [command="/bin/gitlab-shell username-{KEY_ID}",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty {PRINCIPAL2}]
#
# Expects to be called by the SSH daemon, via configuration like:
# AuthorizedPrincipalsCommandUser root
# AuthorizedPrincipalsCommand /bin/gitlab-shell-authorized-principals-check git %i sshUsers
abort "# Wrong number of arguments. #{ARGV.size}. Usage:
# gitlab-shell-authorized-principals-check <key-id> <principal1> [<principal2>...]" unless ARGV.size >= 2
key_id = ARGV[0]
abort '# No key_id provided' if key_id.nil? || key_id == ''
principals = ARGV[1..-1]
principals.each { |principal|
abort '# An invalid principal was provided' if principal.nil? || principal == ''
}
require_relative '../lib/gitlab_init'
require_relative '../lib/gitlab_net'
require_relative '../lib/gitlab_keys'
principals.each { |principal|
puts GitlabKeys.principal_line("username-#{key_id}", principal.dup)
}
#!/usr/bin/env ruby
unless ENV['SSH_CONNECTION']
puts "Only ssh allowed"
exit
end
original_cmd = ENV.delete('SSH_ORIGINAL_COMMAND')
require_relative '../lib/gitlab_init'
#
#
# GitLab shell, invoked from ~/.ssh/authorized_keys or from an
# AuthorizedPrincipalsCommand in the key-less SSH CERT mode.
#
#
require File.join(ROOT_PATH, 'lib', 'gitlab_shell')
# We must match e.g. "key-12345" anywhere on the command-line. See
# https://gitlab.com/gitlab-org/gitlab-shell/issues/145
who = /\b(?:(?:key)-[0-9]+|username-\S+)\b/.match(ARGV.join(' ')).to_s
if GitlabShell.new(who).exec(original_cmd)
exit 0
else
exit 1
end
#!/usr/bin/env ruby
# The purpose of this executable is to test that gitlab-shell logging
# works correctly in combination with Kernel.exec.
require_relative '../lib/gitlab_init'
require_relative '../lib/gitlab_logger'
$logger.info(ARGV.first)
Kernel.exec('true')
require_relative 'action/custom'
module Action
end
require 'base64'
require_relative '../http_helper'
require_relative '../console_helper'
module Action
class Custom
include HTTPHelper
include ConsoleHelper
class BaseError < StandardError; end
class MissingPayloadError < BaseError; end
class MissingAPIEndpointsError < BaseError; end
class MissingDataError < BaseError; end
class UnsuccessfulError < BaseError; end
NO_MESSAGE_TEXT = 'No message'.freeze
DEFAULT_HEADERS = { 'Content-Type' => CONTENT_TYPE_JSON }.freeze
def initialize(gl_id, payload)
@gl_id = gl_id
@payload = payload
end
def execute
validate!
inform_client(info_message) if info_message
process_api_endpoints!
end
private
attr_reader :gl_id, :payload
def process_api_endpoints!
output = ''
resp = nil
data_with_gl_id = data.merge('gl_id' => gl_id)
api_endpoints.each do |endpoint|
url = "#{base_url}#{endpoint}"
json = { 'data' => data_with_gl_id, 'output' => output }
resp = post(url, {}, headers: DEFAULT_HEADERS, options: { json: json })
# Net::HTTPSuccess is the parent of Net::HTTPOK, Net::HTTPCreated etc.
case resp
when Net::HTTPSuccess, Net::HTTPMultipleChoices
true
else
raise_unsuccessful!(resp)
end
begin
body = JSON.parse(resp.body)
rescue JSON::ParserError
raise UnsuccessfulError, 'Response was not valid JSON'
end
print_flush(body['result'])
# In the context of the git push sequence of events, it's necessary to read
# stdin in order to capture output to pass onto subsequent commands
output = read_stdin
end
resp
end
def base_url
config.gitlab_url
end
def data
@data ||= payload['data']
end
def api_endpoints
data['api_endpoints']
end
def info_message
data['info_message']
end
def config
@config ||= GitlabConfig.new
end
def api
@api ||= GitlabNet.new
end
def read_stdin
Base64.encode64($stdin.read)
end
def print_flush(str)
return false unless str
$stdout.print(Base64.decode64(str))
$stdout.flush
end
def inform_client(str)
$stderr.puts(format_gitlab_output(str))
end
def format_gitlab_output(str)
format_for_stderr(str.split("\n")).join("\n")
end
def validate!
validate_payload!
validate_data!
validate_api_endpoints!
end
def validate_payload!
raise MissingPayloadError if !payload.is_a?(Hash) || payload.empty?
end
def validate_data!
raise MissingDataError unless data.is_a?(Hash)
end
def validate_api_endpoints!
raise MissingAPIEndpointsError if !api_endpoints.is_a?(Array) ||
api_endpoints.empty?
end
def raise_unsuccessful!(result)
message = "#{exception_message_for(result.body)} (#{result.code})"
raise UnsuccessfulError, format_gitlab_output(message)
end
def exception_message_for(body)
body = JSON.parse(body)
return body['message'] unless body['message'].to_s.empty?
body['result'].to_s.empty? ? NO_MESSAGE_TEXT : Base64.decode64(body['result'])
rescue JSON::ParserError
NO_MESSAGE_TEXT
end
end
end
# frozen_string_literal: true
module ConsoleHelper
LINE_PREFACE = '> GitLab:'
def write_stderr(messages)
format_for_stderr(messages).each do |message|
$stderr.puts(message)
end
end
def format_for_stderr(messages)
Array(messages).each_with_object([]) do |message, all|
all << "#{LINE_PREFACE} #{message}" unless message.empty?
end
end
end
require 'json'
class GitAccessStatus
HTTP_MULTIPLE_CHOICES = '300'.freeze
attr_reader :message, :gl_repository, :gl_project_path, :gl_id, :gl_username,
:gitaly, :git_protocol, :git_config_options, :payload,
:gl_console_messages
def initialize(status, status_code, message, gl_repository: nil,
gl_project_path: nil, gl_id: nil,
gl_username: nil, gitaly: nil, git_protocol: nil,
git_config_options: nil, payload: nil, gl_console_messages: [])
@status = status
@status_code = status_code
@message = message
@gl_repository = gl_repository
@gl_project_path = gl_project_path
@gl_id = gl_id
@gl_username = gl_username
@git_config_options = git_config_options
@gitaly = gitaly
@git_protocol = git_protocol
@payload = payload
@gl_console_messages = gl_console_messages
end
def self.create_from_json(json, status_code)
values = JSON.parse(json)
new(values["status"],
status_code,
values["message"],
gl_repository: values["gl_repository"],
gl_project_path: values["gl_project_path"],
gl_id: values["gl_id"],
gl_username: values["gl_username"],
git_config_options: values["git_config_options"],
gitaly: values["gitaly"],
git_protocol: values["git_protocol"],
payload: values["payload"],
gl_console_messages: values["gl_console_messages"])
end
def allowed?
@status
end
def custom_action?
@status_code == HTTP_MULTIPLE_CHOICES
end
end
require 'yaml'
class GitlabConfig
attr_reader :config
def initialize
@config = YAML.load_file(File.join(ROOT_PATH, 'config.yml'))
end
def home
ENV['HOME']
end
def auth_file
@config['auth_file'] ||= File.join(home, ".ssh/authorized_keys")
end
def secret_file
@config['secret_file'] ||= File.join(ROOT_PATH, '.gitlab_shell_secret')
end
# Pass a default value because this is called from a repo's context; in which
# case, the repo's hooks directory should be the default.
#
def custom_hooks_dir(default: nil)
@config['custom_hooks_dir'] || default
end
def gitlab_url
(@config['gitlab_url'] ||= "http://localhost:8080").sub(%r{/*$}, '')
end
def http_settings
@config['http_settings'] ||= {}
end
def log_file
@config['log_file'] ||= File.join(ROOT_PATH, 'gitlab-shell.log')
end
def log_level
@config['log_level'] ||= 'INFO'
end
def log_format
@config['log_format'] ||= 'text'
end
def audit_usernames
@config['audit_usernames'] ||= false
end
def metrics_log_file
@config['metrics_log_file'] ||= File.join(ROOT_PATH, 'gitlab-shell-metrics.log')
end
end
ROOT_PATH = ENV.fetch('GITLAB_SHELL_DIR', File.expand_path('..', __dir__))
# We are transitioning parts of gitlab-shell into the gitaly project. In
# gitaly, GITALY_EMBEDDED will be true.
GITALY_EMBEDDED = false
require_relative 'gitlab_config'
module GitlabKeys
class KeyError < StandardError; end
def self.command(whatever)
"#{ROOT_PATH}/bin/gitlab-shell #{whatever}"
end
def self.command_key(key_id)
unless /\A[a-z0-9-]+\z/ =~ key_id
raise KeyError, "Invalid key_id: #{key_id.inspect}"
end
command(key_id)
end
def self.whatever_line(command, trailer)
"command=\"#{command}\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty #{trailer}"
end
def self.key_line(key_id, public_key)
public_key.chomp!
if public_key.include?("\n")
raise KeyError, "Invalid public_key: #{public_key.inspect}"
end
whatever_line(command_key(key_id), public_key)
end
def self.principal_line(username_key_id, principal)
principal.chomp!
if principal.include?("\n")
raise KeyError, "Invalid principal: #{principal.inspect}"
end
whatever_line(command_key(username_key_id), principal)
end
end
require 'base64'
require 'json'
class GitlabLfsAuthentication
# TODO: These don't need to be public
attr_accessor :username, :lfs_token, :repository_http_path
def initialize(username, lfs_token, repository_http_path, expires_in = nil)
@username = username
@lfs_token = lfs_token
@repository_http_path = repository_http_path
@expires_in = expires_in
end
def self.build_from_json(json)
values = JSON.parse(json)
new(values['username'],
values['lfs_token'],
values['repository_http_path'],
values['expires_in'])
rescue
nil
end
# Source: https://github.com/git-lfs/git-lfs/blob/master/docs/api/server-discovery.md#ssh
#
def authentication_payload
payload = { header: { Authorization: authorization }, href: href }
payload[:expires_in] = @expires_in if @expires_in
JSON.generate(payload)
end
private
def authorization
"Basic #{Base64.strict_encode64("#{username}:#{lfs_token}")}"
end
def href
"#{repository_http_path}/info/lfs"
end
end
require 'json'
require 'logger'
require 'time'
require_relative 'gitlab_config'
def convert_log_level(log_level)
Logger.const_get(log_level.upcase)
rescue NameError
$stderr.puts "WARNING: Unrecognized log level #{log_level.inspect}."
$stderr.puts "WARNING: Falling back to INFO."
Logger::INFO
end
class GitlabLogger
# Emulate the quoting logic of logrus
# https://github.com/sirupsen/logrus/blob/v1.0.5/text_formatter.go#L143-L156
SHOULD_QUOTE = /[^a-zA-Z0-9\-._\/@^+]/
LEVELS = {
Logger::INFO => 'info'.freeze,
Logger::DEBUG => 'debug'.freeze,
Logger::WARN => 'warn'.freeze,
Logger::ERROR => 'error'.freeze
}.freeze
def initialize(level, path, log_format)
@level = level
@log_file = File.open(path, 'ab')
# By default Ruby will buffer writes. This is a problem when we exec
# into a new command before Ruby flushed its buffers. Setting 'sync' to
# true disables Ruby's buffering.
@log_file.sync = true
@log_format = log_format
end
def info(message, data = {})
log_at(Logger::INFO, message, data)
end
def debug(message, data = {})
log_at(Logger::DEBUG, message, data)
end
def warn(message, data = {})
log_at(Logger::WARN, message, data)
end
def error(message, data = {})
log_at(Logger::ERROR, message, data)
end
private
attr_reader :log_file, :log_format
def log_at(level, message, data)
return unless @level <= level
data[:pid] = pid
data[:level] = LEVELS[level]
data[:msg] = message
# Use RFC3339 to match logrus in the Go parts of gitlab-shell
data[:time] = time_now.to_datetime.rfc3339
case log_format
when 'json'
# Don't use IO#puts because of https://bugs.ruby-lang.org/issues/14042
log_file.print("#{format_json(data)}\n")
else
log_file.print("#{format_text(data)}\n")
end
end
def pid
Process.pid
end
def time_now
Time.now
end
def format_text(data)
# We start the line with these fields to match the behavior of logrus
result = [
format_key_value(:time, data.delete(:time)),
format_key_value(:level, data.delete(:level)),
format_key_value(:msg, data.delete(:msg))
]
data.sort.each { |k, v| result << format_key_value(k, v) }
result.join(' ')
end
def format_key_value(key, value)
value_string = value.to_s
value_string = value_string.inspect if SHOULD_QUOTE =~ value_string
"#{key}=#{value_string}"
end
def format_json(data)
data.each do |key, value|
next unless value.is_a?(String)
value = value.dup.force_encoding('utf-8')
value = value.inspect unless value.valid_encoding?
data[key] = value.freeze
end
data.to_json
end
end
config = GitlabConfig.new
$logger = GitlabLogger.new(convert_log_level(config.log_level), config.log_file, config.log_format)
require_relative 'gitlab_config'
require_relative 'gitlab_logger'
module GitlabMetrics
module System
# THREAD_CPUTIME is not supported on OS X
if Process.const_defined?(:CLOCK_THREAD_CPUTIME_ID)
def self.cpu_time
Process.
clock_gettime(Process::CLOCK_THREAD_CPUTIME_ID, :millisecond)
end
else
def self.cpu_time
Process.
clock_gettime(Process::CLOCK_PROCESS_CPUTIME_ID, :millisecond)
end
end
# Returns the current monotonic clock time in a given precision.
#
# Returns the time as a Fixnum.
def self.monotonic_time
if defined?(Process::CLOCK_MONOTONIC)
Process.clock_gettime(Process::CLOCK_MONOTONIC, :millisecond)
else
Process.clock_gettime(Process::CLOCK_REALTIME, :millisecond)
end
end
end
def self.logger
$logger
end
# Measures the execution time of a block.
#
# Example:
#
# GitlabMetrics.measure(:find_by_username_duration) do
# User.find_by_username(some_username)
# end
#
# name - The name of the field to store the execution time in.
#
# Returns the value yielded by the supplied block.
def self.measure(name)
start_real = System.monotonic_time
start_cpu = System.cpu_time
retval = yield
real_time = System.monotonic_time - start_real
cpu_time = System.cpu_time - start_cpu
logger.debug('metrics', name: name, wall_time: real_time, cpu_time: cpu_time)
retval
end
end
require 'net/http'
require 'openssl'
require 'json'
require_relative 'gitlab_config'
require_relative 'gitlab_access_status'
require_relative 'gitlab_lfs_authentication'
require_relative 'http_helper'
class GitlabNet # rubocop:disable Metrics/ClassLength
include HTTPHelper
CHECK_TIMEOUT = 5
API_INACCESSIBLE_MESSAGE = 'API is not accessible'.freeze
def check_access(cmd, gl_repository, repo, who, changes, protocol, env: {})
changes = changes.join("\n") unless changes.is_a?(String)
params = {
action: cmd,
changes: changes,
gl_repository: gl_repository,
project: sanitize_path(repo),
protocol: protocol,
env: env
}
who_sym, _, who_v = self.class.parse_who(who)
params[who_sym] = who_v
url = "#{internal_api_endpoint}/allowed"
resp = post(url, params)
case resp
when Net::HTTPSuccess, Net::HTTPMultipleChoices, Net::HTTPUnauthorized,
Net::HTTPNotFound, Net::HTTPServiceUnavailable
if resp.content_type == CONTENT_TYPE_JSON
return GitAccessStatus.create_from_json(resp.body, resp.code)
end
end
GitAccessStatus.new(false, resp.code, API_INACCESSIBLE_MESSAGE)
end
def discover(who)
_, who_k, who_v = self.class.parse_who(who)
resp = get("#{internal_api_endpoint}/discover?#{who_k}=#{who_v}")
JSON.parse(resp.body) rescue nil
end
def lfs_authenticate(gl_id, repo, operation)
id_sym, _, id = self.class.parse_who(gl_id)
params = { project: sanitize_path(repo), operation: operation }
case id_sym
when :key_id
params[:key_id] = id
when :user_id
params[:user_id] = id
else
raise ArgumentError, "lfs_authenticate() got unsupported GL_ID='#{gl_id}'!"
end
resp = post("#{internal_api_endpoint}/lfs_authenticate", params)
GitlabLfsAuthentication.build_from_json(resp.body) if resp.code == '200'
end
def broadcast_message
resp = get("#{internal_api_endpoint}/broadcast_message")
JSON.parse(resp.body) rescue {}
end
def merge_request_urls(gl_repository, repo_path, changes)
changes = changes.join("\n") unless changes.is_a?(String)
changes = changes.encode('UTF-8', 'ASCII', invalid: :replace, replace: '')
url = "#{internal_api_endpoint}/merge_request_urls?project=#{URI.escape(repo_path)}&changes=#{URI.escape(changes)}"
url += "&gl_repository=#{URI.escape(gl_repository)}" if gl_repository
resp = get(url)
if resp.code == '200'
JSON.parse(resp.body)
else
[]
end
rescue
[]
end
def check
get("#{internal_api_endpoint}/check", options: { read_timeout: CHECK_TIMEOUT })
end
def authorized_key(key)
resp = get("#{internal_api_endpoint}/authorized_keys?key=#{URI.escape(key, '+/=')}")
JSON.parse(resp.body) if resp.code == "200"
rescue
nil
end
def two_factor_recovery_codes(gl_id)
id_sym, _, id = self.class.parse_who(gl_id)
resp = post("#{internal_api_endpoint}/two_factor_recovery_codes", id_sym => id)
JSON.parse(resp.body) if resp.code == '200'
rescue
{}
end
def notify_post_receive(gl_repository, repo_path)
params = { gl_repository: gl_repository, project: repo_path }
resp = post("#{internal_api_endpoint}/notify_post_receive", params)
resp.code == '200'
rescue
false
end
def post_receive(gl_repository, identifier, changes, push_options)
params = {
gl_repository: gl_repository,
identifier: identifier,
changes: changes,
:"push_options[]" => push_options, # rubocop:disable Style/HashSyntax
}
resp = post("#{internal_api_endpoint}/post_receive", params)
raise NotFound if resp.code == '404'
JSON.parse(resp.body) if resp.code == '200'
end
def pre_receive(gl_repository)
resp = post("#{internal_api_endpoint}/pre_receive", gl_repository: gl_repository)
raise NotFound if resp.code == '404'
JSON.parse(resp.body) if resp.code == '200'
end
def self.parse_who(who)
if who.start_with?("key-")
value = who.gsub("key-", "")
raise ArgumentError, "who='#{who}' is invalid!" unless value =~ /\A[0-9]+\z/
[:key_id, 'key_id', value]
elsif who.start_with?("user-")
value = who.gsub("user-", "")
raise ArgumentError, "who='#{who}' is invalid!" unless value =~ /\A[0-9]+\z/
[:user_id, 'user_id', value]
elsif who.start_with?("username-")
[:username, 'username', who.gsub("username-", "")]
else
raise ArgumentError, "who='#{who}' is invalid!"
end
end
protected
def sanitize_path(repo)
repo.delete("'")
end
end
class GitlabNet
class ApiUnreachableError < StandardError; end
class NotFound < StandardError; end
end
# frozen_string_literal: true
require 'shellwords'
require 'pathname'
require_relative 'gitlab_net'
require_relative 'gitlab_metrics'
require_relative 'action'
require_relative 'console_helper'
class GitlabShell # rubocop:disable Metrics/ClassLength
include ConsoleHelper
class AccessDeniedError < StandardError; end
class DisallowedCommandError < StandardError; end
class InvalidRepositoryPathError < StandardError; end
GIT_UPLOAD_PACK_COMMAND = 'git-upload-pack'
GIT_RECEIVE_PACK_COMMAND = 'git-receive-pack'
GIT_UPLOAD_ARCHIVE_COMMAND = 'git-upload-archive'
GIT_LFS_AUTHENTICATE_COMMAND = 'git-lfs-authenticate'
GITALY_COMMANDS = {
GIT_UPLOAD_PACK_COMMAND => File.join(ROOT_PATH, 'bin', 'gitaly-upload-pack'),
GIT_UPLOAD_ARCHIVE_COMMAND => File.join(ROOT_PATH, 'bin', 'gitaly-upload-archive'),
GIT_RECEIVE_PACK_COMMAND => File.join(ROOT_PATH, 'bin', 'gitaly-receive-pack')
}.freeze
GIT_COMMANDS = (GITALY_COMMANDS.keys + [GIT_LFS_AUTHENTICATE_COMMAND]).freeze
TWO_FACTOR_RECOVERY_COMMAND = '2fa_recovery_codes'
GL_PROTOCOL = 'ssh'
attr_accessor :gl_id, :gl_repository, :gl_project_path, :repo_name, :command, :git_access, :git_protocol
def initialize(who)
who_sym, = GitlabNet.parse_who(who)
if who_sym == :username
@who = who
else
@gl_id = who
end
@config = GitlabConfig.new
end
# The origin_cmd variable contains UNTRUSTED input. If the user ran
# ssh git@gitlab.example.com 'evil command', then origin_cmd contains
# 'evil command'.
def exec(origin_cmd)
unless origin_cmd
puts "Welcome to GitLab, #{username}!"
return true
end
args = Shellwords.shellwords(origin_cmd)
args = parse_cmd(args)
access_status = nil
if GIT_COMMANDS.include?(args.first)
access_status = GitlabMetrics.measure('verify-access') { verify_access }
@gl_repository = access_status.gl_repository
@git_protocol = ENV['GIT_PROTOCOL']
@gl_project_path = access_status.gl_project_path
@gitaly = access_status.gitaly
@username = access_status.gl_username
@git_config_options = access_status.git_config_options
@gl_id = access_status.gl_id if defined?(@who)
write_stderr(access_status.gl_console_messages)
elsif !defined?(@gl_id)
# We're processing an API command like 2fa_recovery_codes, but
# don't have a @gl_id yet, that means we're in the "username"
# mode and need to materialize it, calling the "user" method
# will do that and call the /discover method.
user
end
if @command == GIT_RECEIVE_PACK_COMMAND && access_status.custom_action?
# If the response from /api/v4/allowed is a HTTP 300, we need to perform
# a Custom Action and therefore should return and not call process_cmd()
#
return process_custom_action(access_status)
end
process_cmd(args)
true
rescue GitlabNet::ApiUnreachableError
write_stderr('Failed to authorize your Git request: internal API unreachable')
false
rescue AccessDeniedError => ex
$logger.warn('Access denied', command: origin_cmd, user: log_username)
write_stderr(ex.message)
false
rescue DisallowedCommandError
$logger.warn('Denied disallowed command', command: origin_cmd, user: log_username)
write_stderr('Disallowed command')
false
rescue InvalidRepositoryPathError
write_stderr('Invalid repository path')
false
rescue Action::Custom::BaseError => ex
$logger.warn('Custom action error', exception: ex.class, message: ex.message,
command: origin_cmd, user: log_username)
$stderr.puts ex.message
false
end
protected
def parse_cmd(args)
# Handle Git for Windows 2.14 using "git upload-pack" instead of git-upload-pack
if args.length == 3 && args.first == 'git'
@command = "git-#{args[1]}"
args = [@command, args.last]
else
@command = args.first
end
@git_access = @command
return args if TWO_FACTOR_RECOVERY_COMMAND == @command
raise DisallowedCommandError unless GIT_COMMANDS.include?(@command)
case @command
when GIT_LFS_AUTHENTICATE_COMMAND
raise DisallowedCommandError unless args.count >= 2
@repo_name = args[1]
case args[2]
when 'download'
@git_access = GIT_UPLOAD_PACK_COMMAND
when 'upload'
@git_access = GIT_RECEIVE_PACK_COMMAND
else
raise DisallowedCommandError
end
else
raise DisallowedCommandError unless args.count == 2
@repo_name = args.last
end
args
end
def verify_access
status = api.check_access(@git_access, nil, @repo_name, @who || @gl_id, '_any', GL_PROTOCOL)
raise AccessDeniedError, status.message unless status.allowed?
status
end
def process_custom_action(access_status)
Action::Custom.new(@gl_id, access_status.payload).execute
end
def process_cmd(args)
return api_2fa_recovery_codes if TWO_FACTOR_RECOVERY_COMMAND == @command
if @command == GIT_LFS_AUTHENTICATE_COMMAND
GitlabMetrics.measure('lfs-authenticate') do
operation = args[2]
$logger.info('Processing LFS authentication', operation: operation, user: log_username)
lfs_authenticate(operation)
end
return
end
# TODO: instead of building from pieces here in gitlab-shell, build the
# entire gitaly_request in gitlab-ce and pass on as-is here.
args = JSON.dump(
'repository' => @gitaly['repository'],
'gl_repository' => @gl_repository,
'gl_project_path' => @gl_project_path,
'gl_id' => @gl_id,
'gl_username' => @username,
'git_config_options' => @git_config_options,
'git_protocol' => @git_protocol
)
gitaly_address = @gitaly['address']
executable = GITALY_COMMANDS.fetch(@command)
gitaly_bin = File.basename(executable)
args_string = [gitaly_bin, gitaly_address, args].join(' ')
$logger.info('executing git command', command: args_string, user: log_username)
exec_cmd(executable, gitaly_address: gitaly_address, token: @gitaly['token'], json_args: args)
end
# This method is not covered by Rspec because it ends the current Ruby process.
def exec_cmd(executable, gitaly_address:, token:, json_args:)
env = { 'GITALY_TOKEN' => token }
args = [executable, gitaly_address, json_args]
# We use 'chdir: ROOT_PATH' to let the next executable know where config.yml is.
Kernel.exec(env, *args, unsetenv_others: true, chdir: ROOT_PATH)
end
def api
GitlabNet.new
end
def user
return @user if defined?(@user)
begin
if defined?(@who)
@user = api.discover(@who)
@gl_id = "user-#{@user['id']}" if @user && @user.key?('id')
else
@user = api.discover(@gl_id)
end
rescue GitlabNet::ApiUnreachableError
@user = nil
end
end
def username_from_discover
return nil unless user && user['username']
"@#{user['username']}"
end
def username
@username ||= username_from_discover || 'Anonymous'
end
# User identifier to be used in log messages.
def log_username
@config.audit_usernames ? username : "user with id #{@gl_id}"
end
def lfs_authenticate(operation)
lfs_access = api.lfs_authenticate(@gl_id, @repo_name, operation)
return unless lfs_access
puts lfs_access.authentication_payload
end
private
def continue?(question)
puts "#{question} (yes/no)"
STDOUT.flush # Make sure the question gets output before we wait for input
continue = STDIN.gets.chomp
puts '' # Add a buffer in the output
continue == 'yes'
end
def api_2fa_recovery_codes
continue = continue?(
"Are you sure you want to generate new two-factor recovery codes?\n" \
"Any existing recovery codes you saved will be invalidated."
)
unless continue
puts 'New recovery codes have *not* been generated. Existing codes will remain valid.'
return
end
resp = api.two_factor_recovery_codes(@gl_id)
if resp['success']
codes = resp['recovery_codes'].join("\n")
puts "Your two-factor authentication recovery codes are:\n\n" \
"#{codes}\n\n" \
"During sign in, use one of the codes above when prompted for\n" \
"your two-factor code. Then, visit your Profile Settings and add\n" \
"a new device so you do not lose access to your account again."
else
puts "An error occurred while trying to generate new recovery codes.\n" \
"#{resp['message']}"
end
end
end
module HooksUtils
module_function
# Gets an array of Git push options from the environment
def get_push_options
count = ENV['GIT_PUSH_OPTION_COUNT'].to_i
result = []
count.times do |i|
result.push(ENV["GIT_PUSH_OPTION_#{i}"])
end
result
end
end
require_relative 'httpunix'
require_relative 'gitlab_logger'
require_relative 'gitlab_net/errors'
module HTTPHelper
READ_TIMEOUT = 300
CONTENT_TYPE_JSON = 'application/json'.freeze
protected
def config
@config ||= GitlabConfig.new
end
def base_api_endpoint
"#{config.gitlab_url}/api/v4"
end
def internal_api_endpoint
"#{base_api_endpoint}/internal"
end
def http_client_for(uri, options = {})
http = if uri.is_a?(URI::HTTPUNIX)
Net::HTTPUNIX.new(uri.hostname)
else
Net::HTTP.new(uri.host, uri.port)
end
http.read_timeout = options[:read_timeout] || read_timeout
if uri.is_a?(URI::HTTPS)
http.use_ssl = true
http.cert_store = cert_store
http.verify_mode = OpenSSL::SSL::VERIFY_NONE if config.http_settings['self_signed_cert']
end
http
end
def http_request_for(method, uri, params: {}, headers: {}, options: {})
request_klass = method == :get ? Net::HTTP::Get : Net::HTTP::Post
request = request_klass.new(uri.request_uri, headers)
user = config.http_settings['user']
password = config.http_settings['password']
request.basic_auth(user, password) if user && password
if options[:json]
request.body = options[:json].merge(secret_token: secret_token).to_json
else
request.set_form_data(params.merge(secret_token: secret_token))
end
if uri.is_a?(URI::HTTPUNIX)
# The HTTPUNIX HTTP client does not set a correct Host header. This can
# lead to 400 Bad Request responses.
request['Host'] = 'localhost'
end
request
end
def request(method, url, params: {}, headers: {}, options: {})
$logger.debug('Performing request', method: method.to_s.upcase, url: url)
uri = URI.parse(url)
http = http_client_for(uri, options)
request = http_request_for(method, uri,
params: params,
headers: headers,
options: options)
begin
start_time = Time.new
response = http.start { http.request(request) }
rescue => e
$logger.warn('Failed to connect', method: method.to_s.upcase, url: url, error: e)
raise GitlabNet::ApiUnreachableError
ensure
fields = { method: method.to_s.upcase, url: url, duration: Time.new - start_time, gitaly_embedded: GITALY_EMBEDDED }
$logger.info('finished HTTP request', fields)
end
case response
when Net::HTTPSuccess, Net::HTTPMultipleChoices
$logger.debug('Received response', code: response.code, body: response.body)
else
$logger.error('Call failed', method: method.to_s.upcase, url: url, code: response.code, body: response.body)
end
response
end
def get(url, headers: {}, options: {})
request(:get, url, headers: headers, options: options)
end
def post(url, params, headers: {}, options: {})
request(:post, url, params: params, headers: headers, options: options)
end
def cert_store
@cert_store ||= begin
store = OpenSSL::X509::Store.new
store.set_default_paths
ca_file = config.http_settings['ca_file']
store.add_file(ca_file) if ca_file
ca_path = config.http_settings['ca_path']
store.add_path(ca_path) if ca_path
store
end
end
def secret_token
@secret_token ||= File.read config.secret_file
end
def read_timeout
config.http_settings['read_timeout'] || READ_TIMEOUT
end
end
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment