Commit 18096b54 authored by Nick Thomas's avatar Nick Thomas

Remove dead Ruby code

parent 8db304b4
......@@ -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
......
#!/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
# support for http+unix://... connection scheme
#
# The URI scheme has the same structure as the similar one for python requests. See:
# http://fixall.online/theres-no-need-to-reinvent-the-wheelhttpsgithubcommsabramorequests-unixsocketurl/241810/
# https://github.com/msabramo/requests-unixsocket
require 'uri'
require 'net/http'
module URI
class HTTPUNIX < HTTP
def hostname
# decode %XX from path to file
v = host
URI.decode(v)
end
# port is not allowed in URI
DEFAULT_PORT = nil
def set_port(v)
return v unless v
raise InvalidURIError, "http+unix:// cannot contain port"
end
end
@@schemes['HTTP+UNIX'] = HTTPUNIX
end
# Based on:
# - http://stackoverflow.com/questions/15637226/ruby-1-9-3-simple-get-request-to-unicorn-through-socket
# - Net::HTTP::connect
module Net
class HTTPUNIX < HTTP
def initialize(socketpath, port = nil)
super(socketpath, port)
@port = nil # HTTP will set it to default - override back -> set DEFAULT_PORT
end
# override to prevent ":<port>" being appended to HTTP_HOST
def addr_port
address
end
def connect
D "opening connection to #{address} ..."
s = UNIXSocket.new(address)
D "opened"
@socket = BufferedIO.new(s)
@socket.read_timeout = @read_timeout
@socket.continue_timeout = @continue_timeout
@socket.debug_output = @debug_output
on_connect
end
end
end
require 'pathname'
class ObjectDirsHelper
class << self
def all_attributes
{
"GIT_ALTERNATE_OBJECT_DIRECTORIES" => absolute_alt_object_dirs,
"GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE" => relative_alt_object_dirs,
"GIT_OBJECT_DIRECTORY" => absolute_object_dir,
"GIT_OBJECT_DIRECTORY_RELATIVE" => relative_object_dir
}
end
def absolute_object_dir
ENV['GIT_OBJECT_DIRECTORY']
end
def relative_object_dir
relative_path(absolute_object_dir)
end
def absolute_alt_object_dirs
ENV['GIT_ALTERNATE_OBJECT_DIRECTORIES'].to_s.split(File::PATH_SEPARATOR)
end
def relative_alt_object_dirs
absolute_alt_object_dirs.map { |dir| relative_path(dir) }.compact
end
private
def relative_path(absolute_path)
return if absolute_path.nil?
repo_dir = Dir.pwd
Pathname.new(absolute_path).relative_path_from(Pathname.new(repo_dir)).to_s
end
end
end
require_relative '../spec_helper'
require_relative '../../lib/action/custom'
describe Action::Custom do
let(:repo_name) { 'gitlab-ci.git' }
let(:gl_id) { 'key-1' }
let(:secret) { "0a3938d9d95d807e94d937af3a4fbbea" }
let(:base_url) { 'http://localhost:3000' }
subject { described_class.new(gl_id, payload) }
describe '#execute' do
context 'with an empty payload' do
let(:payload) { {} }
it 'raises a MissingPayloadError exception' do
expect { subject.execute }.to raise_error(Action::Custom::MissingPayloadError)
end
end
context 'with api_endpoints defined' do
before do
allow(subject).to receive(:base_url).and_return(base_url)
allow(subject).to receive(:secret_token).and_return(secret)
allow($stdin).to receive(:read).and_return('')
end
context 'that are valid' do
let(:payload) do
{
'action' => 'geo_proxy_to_primary',
'data' => {
'api_endpoints' => %w{/api/v4/fake/info_refs /api/v4/fake/push},
'primary_repo' => 'http://localhost:3001/user1/repo1.git'
}
}
end
context 'and responds correctly' do
it 'prints a Base64 encoded result to $stdout' do
VCR.use_cassette("custom-action-ok") do
expect($stdout).to receive(:print).with('info_refs-result').ordered
expect($stdout).to receive(:print).with('push-result').ordered
subject.execute
end
end
context 'with results printed to $stdout' do
before do
allow($stdout).to receive(:print).with('info_refs-result')
allow($stdout).to receive(:print).with('push-result')
end
it 'returns an instance of Net::HTTPCreated' do
VCR.use_cassette("custom-action-ok") do
expect(subject.execute).to be_instance_of(Net::HTTPCreated)
end
end
context 'and with an information message provided' do
before do
payload['data']['info_message'] = 'Important message here.'
end
it 'prints said informational message to $stderr' do
VCR.use_cassette("custom-action-ok-with-message") do
expect { subject.execute }.to output(/Important message here./).to_stderr
end
end
end
end
end
context 'but responds incorrectly' do
it 'raises an UnsuccessfulError exception' do
VCR.use_cassette("custom-action-ok-not-json") do
expect do
subject.execute
end.to raise_error(Action::Custom::UnsuccessfulError, 'Response was not valid JSON')
end
end
end
end
context 'that are invalid' do
context 'where api_endpoints gl_id is missing' do
let(:payload) do
{
'action' => 'geo_proxy_to_primary',
'data' => {
'primary_repo' => 'http://localhost:3001/user1/repo1.git'
}
}
end
it 'raises a MissingAPIEndpointsError exception' do
expect { subject.execute }.to raise_error(Action::Custom::MissingAPIEndpointsError)
end
end
context 'where api_endpoints are empty' do
let(:payload) do
{
'action' => 'geo_proxy_to_primary',
'data' => {
'api_endpoints' => [],
'primary_repo' => 'http://localhost:3001/user1/repo1.git'
}
}
end
it 'raises a MissingAPIEndpointsError exception' do
expect { subject.execute }.to raise_error(Action::Custom::MissingAPIEndpointsError)
end
end
context 'where data gl_id is missing' do
let(:payload) { { 'api_endpoints' => %w{/api/v4/fake/info_refs /api/v4/fake/push} } }
it 'raises a MissingDataError exception' do
expect { subject.execute }.to raise_error(Action::Custom::MissingDataError)
end
end
context 'where API endpoints are bad' do
let(:payload) do
{
'action' => 'geo_proxy_to_primary',
'data' => {
'api_endpoints' => %w{/api/v4/fake/info_refs_bad /api/v4/fake/push_bad},
'primary_repo' => 'http://localhost:3001/user1/repo1.git'
}
}
end
context 'and response is JSON' do
it 'raises an UnsuccessfulError exception' do
VCR.use_cassette("custom-action-not-ok-json") do
expect do
subject.execute
end.to raise_error(Action::Custom::UnsuccessfulError, '> GitLab: You cannot perform write operations on a read-only instance (403)')
end
end
end
context 'and response is not JSON' do
it 'raises an UnsuccessfulError exception' do
VCR.use_cassette("custom-action-not-ok-not-json") do
expect do
subject.execute
end.to raise_error(Action::Custom::UnsuccessfulError, '> GitLab: No message (403)')
end
end
end
end
end
end
end
end
require_relative 'spec_helper'
require_relative '../lib/console_helper'
describe ConsoleHelper do
using RSpec::Parameterized::TableSyntax
class DummyClass
include ConsoleHelper
end
subject { DummyClass.new }
describe '#write_stderr' do
where(:messages, :stderr_output) do
'test' | "> GitLab: test\n"
%w{test1 test2} | "> GitLab: test1\n> GitLab: test2\n"
end
with_them do
it 'puts to $stderr, prefaced with > GitLab:' do
expect { subject.write_stderr(messages) }.to output(stderr_output).to_stderr
end
end
end
describe '#format_for_stderr' do
where(:messages, :result) do
'test' | ['> GitLab: test']
%w{test1 test2} | ['> GitLab: test1', '> GitLab: test2']
end
with_them do
it 'returns message(s), prefaced with > GitLab:' do
expect(subject.format_for_stderr(messages)).to eq(result)
end
end
end
end
require_relative 'spec_helper'
require_relative '../lib/gitlab_config'
describe GitlabConfig do
let(:config) { GitlabConfig.new }
let(:config_data) { {} }
before { expect(YAML).to receive(:load_file).and_return(config_data) }
describe '#gitlab_url' do
let(:url) { 'http://test.com' }
subject { config.gitlab_url }
before { config_data['gitlab_url'] = url }
it { is_expected.not_to be_empty }
it { is_expected.to eq(url) }
context 'remove trailing slashes' do
before { config_data['gitlab_url'] = url + '//' }
it { is_expected.to eq(url) }
end
end
describe '#audit_usernames' do
subject { config.audit_usernames }
it("returns false by default") { is_expected.to eq(false) }
end
describe '#log_format' do
subject { config.log_format }
it 'returns "text" by default' do
is_expected.to eq('text')
end
end
end
require_relative 'spec_helper'
require_relative '../lib/gitlab_keys'
require 'stringio'
describe GitlabKeys do
before do
$logger = double('logger').as_null_object
end
describe '.command' do
it 'the internal "command" utility function' do
command = "#{ROOT_PATH}/bin/gitlab-shell does-not-validate"
expect(described_class.command('does-not-validate')).to eq(command)
end
it 'does not raise a KeyError on invalid input' do
command = "#{ROOT_PATH}/bin/gitlab-shell foo\nbar\nbaz\n"
expect(described_class.command("foo\nbar\nbaz\n")).to eq(command)
end
end
describe '.command_key' do
it 'returns the "command" part of the key line' do
command = "#{ROOT_PATH}/bin/gitlab-shell key-123"
expect(described_class.command_key('key-123')).to eq(command)
end
it 'raises KeyError on invalid input' do
expect { described_class.command_key("\nssh-rsa AAA") }.to raise_error(described_class::KeyError)
end
end
describe '.key_line' do
let(:line) { %(command="#{ROOT_PATH}/bin/gitlab-shell key-741",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty ssh-rsa AAAAB3NzaDAxx2E) }
it 'returns the key line' do
expect(described_class.key_line('key-741', 'ssh-rsa AAAAB3NzaDAxx2E')).to eq(line)
end
it 'silently removes a trailing newline' do
expect(described_class.key_line('key-741', "ssh-rsa AAAAB3NzaDAxx2E\n")).to eq(line)
end
it 'raises KeyError on invalid input' do
expect { described_class.key_line('key-741', "ssh-rsa AAA\nssh-rsa AAA") }.to raise_error(described_class::KeyError)
end
end
describe '.principal_line' do
let(:line) { %(command="#{ROOT_PATH}/bin/gitlab-shell username-someuser",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty sshUsers) }
it 'returns the key line' do
expect(described_class.principal_line('username-someuser', 'sshUsers')).to eq(line)
end
it 'silently removes a trailing newline' do
expect(described_class.principal_line('username-someuser', "sshUsers\n")).to eq(line)
end
it 'raises KeyError on invalid input' do
expect { described_class.principal_line('username-someuser', "sshUsers\nloginUsers") }.to raise_error(described_class::KeyError)
end
end
end
require 'spec_helper'
require 'gitlab_lfs_authentication'
require 'json'
describe GitlabLfsAuthentication do
let(:payload_from_gitlab_api) do
{
username: 'dzaporozhets',
lfs_token: 'wsnys8Zm8Jn7zyhHTAAK',
repository_http_path: 'http://gitlab.dev/repo'
}
end
subject do
GitlabLfsAuthentication.build_from_json(
JSON.generate(payload_from_gitlab_api)
)
end
describe '#build_from_json' do
it { expect(subject.username).to eq('dzaporozhets') }
it { expect(subject.lfs_token).to eq('wsnys8Zm8Jn7zyhHTAAK') }
it { expect(subject.repository_http_path).to eq('http://gitlab.dev/repo') }
end
describe '#authentication_payload' do
shared_examples 'a valid payload' do
it 'should be proper JSON' do
payload = subject.authentication_payload
json_payload = JSON.parse(payload)
expect(json_payload['header']['Authorization']).to eq('Basic ZHphcG9yb3poZXRzOndzbnlzOFptOEpuN3p5aEhUQUFL')
expect(json_payload['href']).to eq('http://gitlab.dev/repo/info/lfs')
end
end
context 'without expires_in' do
let(:result) { { 'header' => { 'Authorization' => 'Basic ZHphcG9yb3poZXRzOndzbnlzOFptOEpuN3p5aEhUQUFL' }, 'href' => 'http://gitlab.dev/repo/info/lfs' }.to_json }
it { expect(subject.authentication_payload).to eq(result) }
it_behaves_like 'a valid payload'
end
context 'with expires_in' do
let(:result) { { 'header' => { 'Authorization' => 'Basic ZHphcG9yb3poZXRzOndzbnlzOFptOEpuN3p5aEhUQUFL' }, 'href' => 'http://gitlab.dev/repo/info/lfs', 'expires_in' => 1800 }.to_json }
before do
payload_from_gitlab_api[:expires_in] = 1800
end
it { expect(subject.authentication_payload).to eq(result) }
it_behaves_like 'a valid payload'
end
end
end
require_relative 'spec_helper'
require_relative '../lib/gitlab_logger'
require 'securerandom'
describe :convert_log_level do
subject { convert_log_level :extreme }
it "converts invalid log level to Logger::INFO" do
expect($stderr).to receive(:puts).at_least(:once)
is_expected.to eq(Logger::INFO)
end
end
describe GitlabLogger do
subject { described_class.new(level, '/dev/null', format) }
let(:format) { 'text' }
let(:output) { StringIO.new }
let(:level) { Logger::INFO }
let(:time) { Time.at(123_456_789).utc } # '1973-11-29T21:33:09+00:00'
let(:pid) { 1234 }
before do
allow(subject).to receive(:log_file).and_return(output)
allow(subject).to receive(:time_now).and_return(time)
allow(subject).to receive(:pid).and_return(pid)
end
def first_line
output.string.lines.first.chomp
end
describe 'field sorting' do
it 'sorts fields, except time, level, msg' do
# Intentionally put 'foo' before 'baz' to see the effect of sorting
subject.info('hello world', foo: 'bar', baz: 'qux')
expect(first_line).to eq('time="1973-11-29T21:33:09+00:00" level=info msg="hello world" baz=qux foo=bar pid=1234')
end
end
describe '#error' do
context 'when the log level is too high' do
let(:level) { Logger::FATAL }
it 'does nothing' do
subject.info('hello world')
expect(output.string).to eq('')
end
end
it 'logs data' do
subject.error('hello world', foo: 'bar')
expect(first_line).to eq('time="1973-11-29T21:33:09+00:00" level=error msg="hello world" foo=bar pid=1234')
end
end
describe '#info' do
context 'when the log level is too high' do
let(:level) { Logger::ERROR }
it 'does nothing' do
subject.info('hello world')
expect(output.string).to eq('')
end
end
it 'logs data' do
subject.info('hello world', foo: 'bar')
expect(first_line).to eq('time="1973-11-29T21:33:09+00:00" level=info msg="hello world" foo=bar pid=1234')
end
end
describe '#warn' do
context 'when the log level is too high' do
let(:level) { Logger::ERROR }
it 'does nothing' do
subject.warn('hello world')
expect(output.string).to eq('')
end
end
it 'logs data' do
subject.warn('hello world', foo: 'bar')
expect(first_line).to eq('time="1973-11-29T21:33:09+00:00" level=warn msg="hello world" foo=bar pid=1234')
end
end
describe '#debug' do
it 'does nothing' do
subject.debug('hello world')
expect(output.string).to eq('')
end
context 'when the log level is low enough' do
let(:level) { Logger::DEBUG }
it 'logs data' do
subject.debug('hello world', foo: 'bar')
expect(first_line).to eq('time="1973-11-29T21:33:09+00:00" level=debug msg="hello world" foo=bar pid=1234')
end
end
end
describe 'json logging' do
let(:format) { 'json' }
it 'writes valid JSON data' do
subject.info('hello world', foo: 'bar')
expect(JSON.parse(first_line)).to eq(
'foo' => 'bar',
'level' => 'info',
'msg' => 'hello world',
'pid' => 1234,
'time' => '1973-11-29T21:33:09+00:00'
)
end
it 'handles non-UTF8 string values' do
subject.info("hello\x80world")
expect(JSON.parse(first_line)).to include('msg' => '"hello\x80world"')
end
end
describe 'log flushing' do
it 'logs get written even when calling Kernel.exec' do
msg = SecureRandom.hex(12)
test_logger_status = system('bin/test-logger', msg)
expect(test_logger_status).to eq(true)
grep_status = system('grep', '-q', '-e', msg, GitlabConfig.new.log_file)
expect(grep_status).to eq(true)
end
end
end
require_relative 'spec_helper'
require_relative '../lib/gitlab_metrics'
describe GitlabMetrics do
describe '.measure' do
before do
$logger = double('logger').as_null_object
end
it 'returns the return value of the block' do
val = described_class.measure('foo') { 10 }
expect(val).to eq(10)
end
it 'writes the metrics data to a log file' do
expect($logger).to receive(:debug).
with('metrics', a_metrics_log_message('foo'))
described_class.measure('foo') { 10 }
end
it 'calls proper measure methods' do
expect(described_class::System).to receive(:monotonic_time).twice.and_call_original
expect(described_class::System).to receive(:cpu_time).twice.and_call_original
described_class.measure('foo') { 10 }
end
end
end
RSpec::Matchers.define :a_metrics_log_message do |x|
match do |actual|
[
actual.fetch(:name) == x,
actual.fetch(:wall_time).is_a?(Numeric),
actual.fetch(:cpu_time).is_a?(Numeric),
].all?
end
end
require_relative 'spec_helper'
require_relative '../lib/gitlab_net'
require_relative '../lib/gitlab_access_status'
describe GitlabNet, vcr: true do
using RSpec::Parameterized::TableSyntax
let(:gitlab_net) { described_class.new }
let(:changes) { ['0000000000000000000000000000000000000000 92d0970eefd7acb6d548878925ce2208cfe2d2ec refs/heads/branch4'] }
let(:base_api_endpoint) { 'http://localhost:3000/api/v4' }
let(:internal_api_endpoint) { 'http://localhost:3000/api/v4/internal' }
let(:project) { 'gitlab-org/gitlab-test.git' }
let(:key) { 'key-1' }
let(:key2) { 'key-2' }
let(:secret) { "0a3938d9d95d807e94d937af3a4fbbea\n" }
before do
$logger = double('logger').as_null_object
allow(gitlab_net).to receive(:base_api_endpoint).and_return(base_api_endpoint)
allow(gitlab_net).to receive(:secret_token).and_return(secret)
end
describe '#check' do
it 'should return 200 code for gitlab check' do
VCR.use_cassette("check-ok") do
result = gitlab_net.check
expect(result.code).to eq('200')
end
end
it 'adds the secret_token to request' do
VCR.use_cassette("check-ok") do
expect_any_instance_of(Net::HTTP::Get).to receive(:set_form_data).with(hash_including(secret_token: secret))
gitlab_net.check
end
end
it "raises an exception if the connection fails" do
allow_any_instance_of(Net::HTTP).to receive(:request).and_raise(StandardError)
expect { gitlab_net.check }.to raise_error(GitlabNet::ApiUnreachableError)
end
end
describe '#discover' do
it 'should return user has based on key id' do
VCR.use_cassette("discover-ok") do
user = gitlab_net.discover(key)
expect(user['name']).to eq('Administrator')
expect(user['username']).to eq('root')
end
end
it 'adds the secret_token to request' do
VCR.use_cassette("discover-ok") do
expect_any_instance_of(Net::HTTP::Get).to receive(:set_form_data).with(hash_including(secret_token: secret))
gitlab_net.discover(key)
end
end
it "raises an exception if the connection fails" do
VCR.use_cassette("discover-ok") do
allow_any_instance_of(Net::HTTP).to receive(:request).and_raise(StandardError)
expect { gitlab_net.discover(key) }.to raise_error(GitlabNet::ApiUnreachableError)
end
end
end
describe '#lfs_authenticate' do
context 'lfs authentication succeeded' do
let(:repository_http_path) { URI.join(internal_api_endpoint.sub('/api/v4', ''), project).to_s }
context 'for download operation' do
it 'should return the correct data' do
VCR.use_cassette('lfs-authenticate-ok-download') do
lfs_access = gitlab_net.lfs_authenticate(key, project, 'download')
expect(lfs_access.username).to eq('root')
expect(lfs_access.lfs_token).to eq('Hyzhyde_wLUeyUQsR3tHGTG8eNocVQm4ssioTEsBSdb6KwCSzQ')
expect(lfs_access.repository_http_path).to eq(repository_http_path)
end
end
end
context 'for upload operation' do
it 'should return the correct data' do
VCR.use_cassette('lfs-authenticate-ok-upload') do
lfs_access = gitlab_net.lfs_authenticate(key, project, 'upload')
expect(lfs_access.username).to eq('root')
expect(lfs_access.lfs_token).to eq('Hyzhyde_wLUeyUQsR3tHGTG8eNocVQm4ssioTEsBSdb6KwCSzQ')
expect(lfs_access.repository_http_path).to eq(repository_http_path)
end
end
end
end
end
describe '#broadcast_message' do
context "broadcast message exists" do
it 'should return message' do
VCR.use_cassette("broadcast_message-ok") do
result = gitlab_net.broadcast_message
expect(result["message"]).to eq("Message")
end
end
end
context "broadcast message doesn't exist" do
it 'should return nil' do
VCR.use_cassette("broadcast_message-none") do
result = gitlab_net.broadcast_message
expect(result).to eq({})
end
end
end
end
describe '#merge_request_urls' do
let(:gl_repository) { "project-1" }
let(:changes) { "123456 789012 refs/heads/test\n654321 210987 refs/tags/tag" }
let(:encoded_changes) { "123456%20789012%20refs/heads/test%0A654321%20210987%20refs/tags/tag" }
it "sends the given arguments as encoded URL parameters" do
expect(gitlab_net).to receive(:get).with("#{internal_api_endpoint}/merge_request_urls?project=#{project}&changes=#{encoded_changes}&gl_repository=#{gl_repository}")
gitlab_net.merge_request_urls(gl_repository, project, changes)
end
it "omits the gl_repository parameter if it's nil" do
expect(gitlab_net).to receive(:get).with("#{internal_api_endpoint}/merge_request_urls?project=#{project}&changes=#{encoded_changes}")
gitlab_net.merge_request_urls(nil, project, changes)
end
it "returns an empty array when the result cannot be parsed as JSON" do
response = double(:response, code: '200', body: '')
allow(gitlab_net).to receive(:get).and_return(response)
expect(gitlab_net.merge_request_urls(gl_repository, project, changes)).to eq([])
end
it "returns an empty array when the result's status is not 200" do
response = double(:response, code: '500', body: '[{}]')
allow(gitlab_net).to receive(:get).and_return(response)
expect(gitlab_net.merge_request_urls(gl_repository, project, changes)).to eq([])
end
end
describe '#pre_receive' do
let(:gl_repository) { "project-1" }
let(:params) { { gl_repository: gl_repository } }
subject { gitlab_net.pre_receive(gl_repository) }
it 'sends the correct parameters and returns the request body parsed' do
expect_any_instance_of(Net::HTTP::Post).to receive(:set_form_data)
.with(hash_including(params))
VCR.use_cassette("pre-receive") { subject }
end
it 'calls /internal/pre-receive' do
VCR.use_cassette("pre-receive") do
expect(subject['reference_counter_increased']).to be(true)
end
end
it 'throws a NotFound error when pre-receive is not available' do
VCR.use_cassette("pre-receive-not-found") do
expect { subject }.to raise_error(GitlabNet::NotFound)
end
end
end
describe '#post_receive' do
let(:gl_repository) { "project-1" }
let(:changes) { "123456 789012 refs/heads/test\n654321 210987 refs/tags/tag" }
let(:push_options) { ["ci-skip", "something unexpected"] }
let(:params) do
{ gl_repository: gl_repository, identifier: key, changes: changes, :"push_options[]" => push_options }
end
let(:merge_request_urls) do
[{
"branch_name" => "test",
"url" => "http://localhost:3000/gitlab-org/gitlab-test/merge_requests/7",
"new_merge_request" => false
}]
end
subject { gitlab_net.post_receive(gl_repository, key, changes, push_options) }
it 'sends the correct parameters' do
expect_any_instance_of(Net::HTTP::Post).to receive(:set_form_data).with(hash_including(params))
VCR.use_cassette("post-receive") do
subject
end
end
it 'calls /internal/post-receive' do
VCR.use_cassette("post-receive") do
expect(subject['merge_request_urls']).to eq(merge_request_urls)
expect(subject['broadcast_message']).to eq('Message')
expect(subject['reference_counter_decreased']).to eq(true)
end
end
it 'throws a NotFound error when post-receive is not available' do
VCR.use_cassette("post-receive-not-found") do
expect { subject }.to raise_error(GitlabNet::NotFound)
end
end
end
describe '#authorized_key' do
let (:ssh_key) { "rsa-key" }
it "should return nil when the resource is not implemented" do
VCR.use_cassette("ssh-key-not-implemented") do
result = gitlab_net.authorized_key("whatever")
expect(result).to be_nil
end
end
it "should return nil when the fingerprint is not found" do
VCR.use_cassette("ssh-key-not-found") do
result = gitlab_net.authorized_key("whatever")
expect(result).to be_nil
end
end
it "should return a ssh key with a valid fingerprint" do
VCR.use_cassette("ssh-key-ok") do
result = gitlab_net.authorized_key(ssh_key)
expect(result).to eq({
"can_push" => false,
"created_at" => "2017-06-21T09:50:07.150Z",
"id" => 99,
"key" => "ssh-rsa rsa-key dummy@gitlab.com",
"title" => "untitled"
})
end
end
end
describe '#two_factor_recovery_codes' do
it 'returns two factor recovery codes' do
VCR.use_cassette('two-factor-recovery-codes') do
result = gitlab_net.two_factor_recovery_codes(key)
expect(result['success']).to be_truthy
expect(result['recovery_codes']).to eq(['f67c514de60c4953','41278385fc00c1e0'])
end
end
it 'returns false when recovery codes cannot be generated' do
VCR.use_cassette('two-factor-recovery-codes-fail') do
result = gitlab_net.two_factor_recovery_codes('key-777')
expect(result['success']).to be_falsey
expect(result['message']).to eq('Could not find the given key')
end
end
end
describe '#notify_post_receive' do
let(:gl_repository) { 'project-1' }
let(:repo_path) { '/path/to/my/repo.git' }
let(:params) do
{ gl_repository: gl_repository, project: repo_path }
end
it 'sets the arguments as form parameters' do
VCR.use_cassette('notify-post-receive') do
expect_any_instance_of(Net::HTTP::Post).to receive(:set_form_data).with(hash_including(params))
gitlab_net.notify_post_receive(gl_repository, repo_path)
end
end
it 'returns true if notification was succesful' do
VCR.use_cassette('notify-post-receive') do
expect(gitlab_net.notify_post_receive(gl_repository, repo_path)).to be_truthy
end
end
end
describe '#check_access' do
context 'ssh key with access nil, to project' do
it 'should allow push access for host' do
VCR.use_cassette("allowed-push") do
access = gitlab_net.check_access('git-receive-pack', nil, project, key, changes, 'ssh')
expect(access.allowed?).to be_truthy
expect(access.gl_project_path).to eq('gitlab-org/gitlab.test')
end
end
context 'but project not found' do
where(:desc, :cassette, :message) do
'deny push access for host' | 'allowed-push-project-not-found' | 'The project you were looking for could not be found.'
'deny push access for host (when text/html)' | 'allowed-push-project-not-found-text-html' | 'API is not accessible'
'deny push access for host (when text/plain)' | 'allowed-push-project-not-found-text-plain' | 'API is not accessible'
'deny push access for host (when 404 is returned)' | 'allowed-push-project-not-found-404' | 'The project you were looking for could not be found.'
'deny push access for host (when 404 is returned with text/html)' | 'allowed-push-project-not-found-404-text-html' | 'API is not accessible'
'deny push access for host (when 404 is returned with text/plain)' | 'allowed-push-project-not-found-404-text-plain' | 'API is not accessible'
end
with_them do
it 'should deny push access for host' do
VCR.use_cassette(cassette) do
access = gitlab_net.check_access('git-receive-pack', nil, project, key, changes, 'ssh')
expect(access.allowed?).to be_falsey
expect(access.message).to eql(message)
end
end
end
end
it 'adds the secret_token to the request' do
VCR.use_cassette("allowed-push") do
expect_any_instance_of(Net::HTTP::Post).to receive(:set_form_data).with(hash_including(secret_token: secret))
gitlab_net.check_access('git-receive-pack', nil, project, key, changes, 'ssh')
end
end
it 'should allow pull access for host' do
VCR.use_cassette("allowed-pull") do
access = gitlab_net.check_access('git-upload-pack', nil, project, key, changes, 'ssh')
expect(access.allowed?).to be_truthy
expect(access.gl_project_path).to eq('gitlab-org/gitlab.test')
end
end
end
context 'ssh access has been disabled' do
it 'should deny pull access for host' do
VCR.use_cassette('ssh-pull-disabled') do
access = gitlab_net.check_access('git-upload-pack', nil, project, key, changes, 'ssh')
expect(access.allowed?).to be_falsey
expect(access.message).to eq 'Git access over SSH is not allowed'
end
end
it 'should deny push access for host' do
VCR.use_cassette('ssh-push-disabled') do
access = gitlab_net.check_access('git-receive-pack', nil, project, key, changes, 'ssh')
expect(access.allowed?).to be_falsey
expect(access.message).to eq 'Git access over SSH is not allowed'
end
end
end
context 'http access has been disabled' do
it 'should deny pull access for host' do
VCR.use_cassette('http-pull-disabled') do
access = gitlab_net.check_access('git-upload-pack', nil, project, key, changes, 'http')
expect(access.allowed?).to be_falsey
expect(access.message).to eq 'Pulling over HTTP is not allowed.'
end
end
it 'should deny push access for host' do
VCR.use_cassette("http-push-disabled") do
access = gitlab_net.check_access('git-receive-pack', nil, project, key, changes, 'http')
expect(access.allowed?).to be_falsey
expect(access.message).to eq 'Pushing over HTTP is not allowed.'
end
end
end
context 'ssh key without access to project' do
where(:desc, :cassette, :message) do
'deny push access for host' | 'ssh-push-project-denied' | 'Git access over SSH is not allowed'
'deny push access for host (when 401 is returned)' | 'ssh-push-project-denied-401' | 'Git access over SSH is not allowed'
'deny push access for host (when 401 is returned with text/html)' | 'ssh-push-project-denied-401-text-html' | 'API is not accessible'
'deny push access for host (when 401 is returned with text/plain)' | 'ssh-push-project-denied-401-text-plain' | 'API is not accessible'
'deny pull access for host' | 'ssh-pull-project-denied' | 'Git access over SSH is not allowed'
'deny pull access for host (when 401 is returned)' | 'ssh-pull-project-denied-401' | 'Git access over SSH is not allowed'
'deny pull access for host (when 401 is returned with text/html)' | 'ssh-pull-project-denied-401-text-html' | 'API is not accessible'
'deny pull access for host (when 401 is returned with text/plain)' | 'ssh-pull-project-denied-401-text-plain' | 'API is not accessible'
end
with_them do
it 'should deny push access for host' do
VCR.use_cassette(cassette) do
access = gitlab_net.check_access('git-receive-pack', nil, project, key2, changes, 'ssh')
expect(access.allowed?).to be_falsey
expect(access.message).to eql(message)
end
end
end
it 'should deny pull access for host (with user)' do
VCR.use_cassette("ssh-pull-project-denied-with-user") do
access = gitlab_net.check_access('git-upload-pack', nil, project, 'user-2', changes, 'ssh')
expect(access.allowed?).to be_falsey
expect(access.message).to eql('Git access over SSH is not allowed')
end
end
end
it 'handles non 200 status codes' do
resp = double(:resp, code: 501)
allow(gitlab_net).to receive(:post).and_return(resp)
access = gitlab_net.check_access('git-upload-pack', nil, project, 'user-2', changes, 'ssh')
expect(access).not_to be_allowed
end
it "raises an exception if the connection fails" do
allow_any_instance_of(Net::HTTP).to receive(:request).and_raise(StandardError)
expect {
gitlab_net.check_access('git-upload-pack', nil, project, 'user-1', changes, 'ssh')
}.to raise_error(GitlabNet::ApiUnreachableError)
end
end
describe '#base_api_endpoint' do
let(:net) { described_class.new }
subject { net.send :base_api_endpoint }
it { is_expected.to include(net.send(:config).gitlab_url) }
it("uses API version 4") { is_expected.to end_with("api/v4") }
end
describe '#internal_api_endpoint' do
let(:net) { described_class.new }
subject { net.send :internal_api_endpoint }
it { is_expected.to include(net.send(:config).gitlab_url) }
it("uses API version 4") { is_expected.to end_with("api/v4/internal") }
end
describe '#http_client_for' do
subject { gitlab_net.send :http_client_for, URI('https://localhost/') }
before do
allow(gitlab_net).to receive :cert_store
allow(gitlab_net.send(:config)).to receive(:http_settings) { {'self_signed_cert' => true} }
end
it { expect(subject.verify_mode).to eq(OpenSSL::SSL::VERIFY_NONE) }
end
describe '#http_request_for' do
context 'with stub' do
let(:get) { double(Net::HTTP::Get) }
let(:user) { 'user' }
let(:password) { 'password' }
let(:url) { URI 'http://localhost/' }
let(:params) { { 'key1' => 'value1' } }
let(:headers) { { 'Content-Type' => 'application/json'} }
let(:options) { { json: { 'key2' => 'value2' } } }
context 'with no params, options or headers' do
subject { gitlab_net.send :http_request_for, :get, url }
before do
allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('user') { user }
allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('password') { password }
expect(Net::HTTP::Get).to receive(:new).with('/', {}).and_return(get)
expect(get).to receive(:basic_auth).with(user, password).once
expect(get).to receive(:set_form_data).with(hash_including(secret_token: secret)).once
end
it { is_expected.not_to be_nil }
end
context 'with params' do
subject { gitlab_net.send :http_request_for, :get, url, params: params, headers: headers }
before do
allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('user') { user }
allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('password') { password }
expect(Net::HTTP::Get).to receive(:new).with('/', headers).and_return(get)
expect(get).to receive(:basic_auth).with(user, password).once
expect(get).to receive(:set_form_data).with({ 'key1' => 'value1', secret_token: secret }).once
end
it { is_expected.not_to be_nil }
end
context 'with headers' do
subject { gitlab_net.send :http_request_for, :get, url, headers: headers }
before do
allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('user') { user }
allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('password') { password }
expect(Net::HTTP::Get).to receive(:new).with('/', headers).and_return(get)
expect(get).to receive(:basic_auth).with(user, password).once
expect(get).to receive(:set_form_data).with(hash_including(secret_token: secret)).once
end
it { is_expected.not_to be_nil }
end
context 'with options' do
context 'with json' do
subject { gitlab_net.send :http_request_for, :get, url, options: options }
before do
allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('user') { user }
allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('password') { password }
expect(Net::HTTP::Get).to receive(:new).with('/', {}).and_return(get)
expect(get).to receive(:basic_auth).with(user, password).once
expect(get).to receive(:body=).with({ 'key2' => 'value2', secret_token: secret }.to_json).once
expect(get).not_to receive(:set_form_data)
end
it { is_expected.not_to be_nil }
end
end
end
context 'Unix socket' do
it 'sets the Host header to "localhost"' do
gitlab_net = described_class.new
expect(gitlab_net).to receive(:secret_token).and_return(secret)
request = gitlab_net.send(:http_request_for, :get, URI('http+unix://%2Ffoo'))
expect(request['Host']).to eq('localhost')
end
end
end
describe '#cert_store' do
let(:store) do
double(OpenSSL::X509::Store).tap do |store|
allow(OpenSSL::X509::Store).to receive(:new) { store }
end
end
before :each do
expect(store).to receive(:set_default_paths).once
end
after do
gitlab_net.send :cert_store
end
it "calls add_file with http_settings['ca_file']" do
allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('ca_file') { 'test_file' }
allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('ca_path') { nil }
expect(store).to receive(:add_file).with('test_file')
expect(store).not_to receive(:add_path)
end
it "calls add_path with http_settings['ca_path']" do
allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('ca_file') { nil }
allow(gitlab_net.send(:config).http_settings).to receive(:[]).with('ca_path') { 'test_path' }
expect(store).not_to receive(:add_file)
expect(store).to receive(:add_path).with('test_path')
end
end
end
require_relative 'spec_helper'
require_relative '../lib/gitlab_shell'
require_relative '../lib/gitlab_access_status'
describe GitlabShell do
before do
$logger = double('logger').as_null_object
FileUtils.mkdir_p(tmp_repos_path)
end
after do
FileUtils.rm_rf(tmp_repos_path)
end
subject do
ARGV[0] = gl_id
GitlabShell.new(gl_id).tap do |shell|
allow(shell).to receive(:exec_cmd).and_return(:exec_called)
allow(shell).to receive(:api).and_return(api)
end
end
let(:git_config_options) { ['receive.MaxInputSize=10000'] }
let(:gitaly_check_access) do
GitAccessStatus.new(
true,
'200',
'ok',
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: { 'repository' => { 'relative_path' => repo_name, 'storage_name' => 'default'} , 'address' => 'unix:gitaly.socket' },
git_protocol: git_protocol,
gl_console_messages: gl_console_messages
)
end
let(:api) do
double(GitlabNet).tap do |api|
allow(api).to receive(:discover).and_return({ 'name' => 'John Doe', 'username' => 'testuser' })
allow(api).to receive(:check_access).and_return(GitAccessStatus.new(
true,
'200',
'ok',
gl_repository: gl_repository,
gl_project_path: gl_project_path,
gl_id: gl_id,
gl_username: gl_username,
git_config_options: nil,
gitaly: nil,
git_protocol: git_protocol))
allow(api).to receive(:two_factor_recovery_codes).and_return({
'success' => true,
'recovery_codes' => %w[f67c514de60c4953 41278385fc00c1e0]
})
end
end
let(:gl_id) { "key-#{rand(100) + 100}" }
let(:ssh_cmd) { nil }
let(:tmp_repos_path) { File.join(ROOT_PATH, 'tmp', 'repositories') }
let(:repo_name) { 'gitlab-ci.git' }
let(:gl_repository) { 'project-1' }
let(:gl_project_path) { 'group/subgroup/gitlab-ci' }
let(:gl_id) { 'user-1' }
let(:gl_username) { 'testuser' }
let(:git_config_options) { ['receive.MaxInputSize=10000'] }
let(:git_protocol) { 'version=2' }
let(:gl_console_messages) { nil }
before do
allow_any_instance_of(GitlabConfig).to receive(:audit_usernames).and_return(false)
end
describe '#initialize' do
let(:ssh_cmd) { 'git-receive-pack' }
it { expect(subject.gl_id).to eq gl_id }
end
describe '#parse_cmd' do
describe 'git' do
context 'w/o namespace' do
let(:ssh_args) { %w(git-upload-pack gitlab-ci.git) }
before do
subject.send :parse_cmd, ssh_args
end
it 'has the correct attributes' do
expect(subject.repo_name).to eq 'gitlab-ci.git'
expect(subject.command).to eq 'git-upload-pack'
end
end
context 'namespace' do
let(:repo_name) { 'dmitriy.zaporozhets/gitlab-ci.git' }
let(:ssh_args) { %w(git-upload-pack dmitriy.zaporozhets/gitlab-ci.git) }
before do
subject.send :parse_cmd, ssh_args
end
it 'has the correct attributes' do
expect(subject.repo_name).to eq 'dmitriy.zaporozhets/gitlab-ci.git'
expect(subject.command).to eq 'git-upload-pack'
end
end
context 'with an invalid number of arguments' do
let(:ssh_args) { %w(foobar) }
it "should raise an DisallowedCommandError" do
expect { subject.send :parse_cmd, ssh_args }.to raise_error(GitlabShell::DisallowedCommandError)
end
end
context 'with an API command' do
before do
subject.send :parse_cmd, ssh_args
end
context 'when generating recovery codes' do
let(:ssh_args) { %w(2fa_recovery_codes) }
it 'sets the correct command' do
expect(subject.command).to eq('2fa_recovery_codes')
end
it 'does not set repo name' do
expect(subject.repo_name).to be_nil
end
end
end
end
describe 'git-lfs' do
let(:repo_name) { 'dzaporozhets/gitlab.git' }
let(:ssh_args) { %w(git-lfs-authenticate dzaporozhets/gitlab.git download) }
before do
subject.send :parse_cmd, ssh_args
end
it 'has the correct attributes' do
expect(subject.repo_name).to eq 'dzaporozhets/gitlab.git'
expect(subject.command).to eq 'git-lfs-authenticate'
expect(subject.git_access).to eq 'git-upload-pack'
end
end
describe 'git-lfs old clients' do
let(:repo_name) { 'dzaporozhets/gitlab.git' }
let(:ssh_args) { %w(git-lfs-authenticate dzaporozhets/gitlab.git download long_oid) }
before do
subject.send :parse_cmd, ssh_args
end
it 'has the correct attributes' do
expect(subject.repo_name).to eq 'dzaporozhets/gitlab.git'
expect(subject.command).to eq 'git-lfs-authenticate'
expect(subject.git_access).to eq 'git-upload-pack'
end
end
end
describe '#exec' do
let(:gitaly_message) do
JSON.dump(
'repository' => { 'relative_path' => repo_name, 'storage_name' => 'default' },
'gl_repository' => gl_repository,
'gl_project_path' => gl_project_path,
'gl_id' => gl_id,
'gl_username' => gl_username,
'git_config_options' => git_config_options,
'git_protocol' => git_protocol
)
end
before do
allow(ENV).to receive(:[]).with('GIT_PROTOCOL').and_return(git_protocol)
end
shared_examples_for 'upload-pack' do |command|
let(:ssh_cmd) { "#{command} gitlab-ci.git" }
after { subject.exec(ssh_cmd) }
it "should process the command" do
expect(subject).to receive(:process_cmd).with(%w(git-upload-pack gitlab-ci.git))
end
it "should execute the command" do
expect(subject).to receive(:exec_cmd).with('git-upload-pack')
end
it "should log the command execution" do
message = "executing git command"
user_string = "user with id #{gl_id}"
expect($logger).to receive(:info).with(message, command: "git-upload-pack", user: user_string)
end
it "should use usernames if configured to do so" do
allow_any_instance_of(GitlabConfig).to receive(:audit_usernames).and_return(true)
expect($logger).to receive(:info).with("executing git command", hash_including(user: 'testuser'))
end
end
context 'gitaly-upload-pack' do
let(:ssh_cmd) { "git-upload-pack gitlab-ci.git" }
before do
allow(api).to receive(:check_access).and_return(gitaly_check_access)
end
after { subject.exec(ssh_cmd) }
it "should process the command" do
expect(subject).to receive(:process_cmd).with(%w(git-upload-pack gitlab-ci.git))
end
it "should execute the command" do
expect(subject).to receive(:exec_cmd).with(File.join(ROOT_PATH, "bin/gitaly-upload-pack"), gitaly_address: 'unix:gitaly.socket', json_args: gitaly_message, token: nil)
end
it "should log the command execution" do
message = "executing git command"
user_string = "user with id #{gl_id}"
expect($logger).to receive(:info).with(message, command: "gitaly-upload-pack unix:gitaly.socket #{gitaly_message}", user: user_string)
end
it "should use usernames if configured to do so" do
allow_any_instance_of(GitlabConfig).to receive(:audit_usernames).and_return(true)
expect($logger).to receive(:info).with("executing git command", hash_including(user: 'testuser'))
end
end
context 'git-receive-pack' do
let(:ssh_cmd) { "git-receive-pack gitlab-ci.git" }
before do
allow(api).to receive(:check_access).and_return(gitaly_check_access)
end
after { subject.exec(ssh_cmd) }
it "should process the command" do
expect(subject).to receive(:process_cmd).with(%w(git-receive-pack gitlab-ci.git))
end
it "should execute the command" do
expect(subject).to receive(:exec_cmd).with(File.join(ROOT_PATH, "bin/gitaly-receive-pack"), gitaly_address: 'unix:gitaly.socket', json_args: gitaly_message, token: nil)
end
it "should log the command execution" do
message = "executing git command"
user_string = "user with id #{gl_id}"
expect($logger).to receive(:info).with(message, command: "gitaly-receive-pack unix:gitaly.socket #{gitaly_message}", user: user_string)
end
context 'with a custom action' do
let(:fake_payload) { { 'api_endpoints' => [ '/fake/api/endpoint' ], 'data' => {} } }
let(:custom_action_gitlab_access_status) do
GitAccessStatus.new(
true,
'300',
'Multiple Choices',
payload: fake_payload
)
end
let(:action_custom) { double(Action::Custom) }
before do
allow(api).to receive(:check_access).and_return(custom_action_gitlab_access_status)
end
it "should not process the command" do
expect(subject).to_not receive(:process_cmd).with(%w(git-receive-pack gitlab-ci.git))
expect(Action::Custom).to receive(:new).with(gl_id, fake_payload).and_return(action_custom)
expect(action_custom).to receive(:execute)
end
end
end
context 'gitaly-receive-pack' do
let(:ssh_cmd) { "git-receive-pack gitlab-ci.git" }
before do
allow(api).to receive(:check_access).and_return(gitaly_check_access)
end
after { subject.exec(ssh_cmd) }
it "should process the command" do
expect(subject).to receive(:process_cmd).with(%w(git-receive-pack gitlab-ci.git))
end
it "should execute the command" do
expect(subject).to receive(:exec_cmd).with(File.join(ROOT_PATH, "bin/gitaly-receive-pack"), gitaly_address: 'unix:gitaly.socket', json_args: gitaly_message, token: nil)
end
it "should log the command execution" do
message = "executing git command"
user_string = "user with id #{gl_id}"
expect($logger).to receive(:info).with(message, command: "gitaly-receive-pack unix:gitaly.socket #{gitaly_message}", user: user_string)
end
it "should use usernames if configured to do so" do
allow_any_instance_of(GitlabConfig).to receive(:audit_usernames).and_return(true)
expect($logger).to receive(:info).with("executing git command", hash_including(user: 'testuser'))
end
end
shared_examples_for 'upload-archive' do |command|
let(:ssh_cmd) { "#{command} gitlab-ci.git" }
let(:exec_cmd_log_params) { exec_cmd_params }
after { subject.exec(ssh_cmd) }
it "should process the command" do
expect(subject).to receive(:process_cmd).with(%w(git-upload-archive gitlab-ci.git))
end
it "should execute the command" do
expect(subject).to receive(:exec_cmd).with(*exec_cmd_params)
end
it "should log the command execution" do
message = "executing git command"
user_string = "user with id #{gl_id}"
expect($logger).to receive(:info).with(message, command: exec_cmd_log_params.join(' '), user: user_string)
end
it "should use usernames if configured to do so" do
allow_any_instance_of(GitlabConfig).to receive(:audit_usernames).and_return(true)
expect($logger).to receive(:info).with("executing git command", hash_including(user: 'testuser'))
end
end
context 'gitaly-upload-archive' do
before do
allow(api).to receive(:check_access).and_return(gitaly_check_access)
end
it_behaves_like 'upload-archive', 'git-upload-archive' do
let(:gitaly_executable) { "gitaly-upload-archive" }
let(:exec_cmd_params) do
[
File.join(ROOT_PATH, "bin", gitaly_executable),
{ gitaly_address: 'unix:gitaly.socket', json_args: gitaly_message, token: nil }
]
end
let(:exec_cmd_log_params) do
[gitaly_executable, 'unix:gitaly.socket', gitaly_message]
end
end
end
context 'arbitrary command' do
let(:ssh_cmd) { 'arbitrary command' }
after { subject.exec(ssh_cmd) }
it "should not process the command" do
expect(subject).not_to receive(:process_cmd)
end
it "should not execute the command" do
expect(subject).not_to receive(:exec_cmd)
end
it "should log the attempt" do
message = 'Denied disallowed command'
user_string = "user with id #{gl_id}"
expect($logger).to receive(:warn).with(message, command: 'arbitrary command', user: user_string)
end
end
context 'no command' do
after { subject.exec(nil) }
it "should call api.discover" do
expect(api).to receive(:discover).with(gl_id)
end
end
context "failed connection" do
let(:ssh_cmd) { 'git-upload-pack gitlab-ci.git' }
before do
allow(api).to receive(:check_access).and_raise(GitlabNet::ApiUnreachableError)
end
after { subject.exec(ssh_cmd) }
it "should not process the command" do
expect(subject).not_to receive(:process_cmd)
end
it "should not execute the command" do
expect(subject).not_to receive(:exec_cmd)
end
end
context 'with an API command' do
before do
allow(subject).to receive(:continue?).and_return(true)
end
context 'when generating recovery codes' do
let(:ssh_cmd) { '2fa_recovery_codes' }
after do
subject.exec(ssh_cmd)
end
it 'does not call verify_access' do
expect(subject).not_to receive(:verify_access)
end
it 'calls the corresponding method' do
expect(subject).to receive(:api_2fa_recovery_codes)
end
it 'outputs recovery codes' do
expect($stdout).to receive(:puts)
.with(/f67c514de60c4953\n41278385fc00c1e0/)
end
context 'when the process is unsuccessful' do
it 'displays the error to the user' do
allow(api).to receive(:two_factor_recovery_codes).and_return({
'success' => false,
'message' => 'Could not find the given key'
})
expect($stdout).to receive(:puts)
.with(/Could not find the given key/)
end
end
end
end
context 'with a console message' do
let(:ssh_cmd) { "git-receive-pack gitlab-ci.git" }
let(:gl_console_messages) { 'Very important message' }
before do
allow(api).to receive(:check_access).and_return(gitaly_check_access)
end
it 'displays the message on $stderr' do
expect { subject.exec(ssh_cmd) }.to output("> GitLab: #{gl_console_messages}\n").to_stderr
end
end
end
describe '#validate_access' do
let(:ssh_cmd) { "git-upload-pack gitlab-ci.git" }
describe 'check access with api' do
before do
allow(api).to receive(:check_access).and_return(
GitAccessStatus.new(
false,
'denied',
gl_repository: nil,
gl_project_path: nil,
gl_id: nil,
gl_username: nil,
git_config_options: nil,
gitaly: nil,
git_protocol: nil))
end
after { subject.exec(ssh_cmd) }
it "should call api.check_access" do
expect(api).to receive(:check_access).with('git-upload-pack', nil, 'gitlab-ci.git', gl_id, '_any', 'ssh')
end
it "should disallow access and log the attempt if check_access returns false status" do
message = 'Access denied'
user_string = "user with id #{gl_id}"
expect($logger).to receive(:warn).with(message, command: 'git-upload-pack gitlab-ci.git', user: user_string)
end
end
end
describe '#api' do
let(:shell) { GitlabShell.new(gl_id) }
subject { shell.send :api }
it { is_expected.to be_a(GitlabNet) }
end
end
require_relative 'spec_helper'
require_relative '../lib/hooks_utils.rb'
describe :get_push_options do
context "when GIT_PUSH_OPTION_COUNT is not set" do
it { expect(HooksUtils.get_push_options).to eq([]) }
end
context "when one option is given" do
before do
ENV['GIT_PUSH_OPTION_COUNT'] = '1'
ENV['GIT_PUSH_OPTION_0'] = 'aaa'
end
it { expect(HooksUtils.get_push_options).to eq(['aaa']) }
end
context "when multiple options are given" do
before do
ENV['GIT_PUSH_OPTION_COUNT'] = '3'
ENV['GIT_PUSH_OPTION_0'] = 'aaa'
ENV['GIT_PUSH_OPTION_1'] = 'bbb'
ENV['GIT_PUSH_OPTION_2'] = 'ccc'
end
it { expect(HooksUtils.get_push_options).to eq(['aaa', 'bbb', 'ccc']) }
end
end
require_relative 'spec_helper'
require_relative '../lib/httpunix'
describe URI::HTTPUNIX do
describe :parse do
uri = URI::parse('http+unix://%2Fpath%2Fto%2Fsocket/img.jpg')
subject { uri }
it { is_expected.to be_an_instance_of(URI::HTTPUNIX) }
it 'has the correct attributes' do
expect(subject.scheme).to eq('http+unix')
expect(subject.hostname).to eq('/path/to/socket')
expect(subject.path).to eq('/img.jpg')
end
end
end
describe Net::HTTPUNIX do
def tmp_socket_path
# This has to be a relative path shorter than 100 bytes due to
# limitations in how Unix sockets work.
'tmp/test-socket'
end
before(:all) do
# "hello world" over unix socket server in background thread
FileUtils.mkdir_p(File.dirname(tmp_socket_path))
@server = HTTPUNIXServer.new(BindAddress: tmp_socket_path)
@server.mount_proc '/' do |req, resp|
resp.body = "Hello World (at #{req.path})"
end
@webrick_thread = Thread.new { @server.start }
sleep(0.1) while @webrick_thread.alive? && @server.status != :Running
raise "Couldn't start HTTPUNIXServer" unless @server.status == :Running
end
after(:all) do
@server.shutdown if @server
@webrick_thread.join if @webrick_thread
end
it "talks via HTTP ok" do
VCR.turned_off do
begin
WebMock.allow_net_connect!
http = Net::HTTPUNIX.new(tmp_socket_path)
expect(http.get('/').body).to eq('Hello World (at /)')
expect(http.get('/path').body).to eq('Hello World (at /path)')
ensure
WebMock.disable_net_connect!
end
end
end
end
require_relative 'spec_helper'
require_relative '../lib/object_dirs_helper'
describe ObjectDirsHelper do
before do
allow(Dir).to receive(:pwd).and_return('/home/git/repositories/foo/bar.git')
end
describe '.all_attributes' do
it do
expect(described_class.all_attributes.keys).to include(*%w[
GIT_OBJECT_DIRECTORY
GIT_OBJECT_DIRECTORY_RELATIVE
GIT_ALTERNATE_OBJECT_DIRECTORIES
GIT_ALTERNATE_OBJECT_DIRECTORIES_RELATIVE
])
end
end
describe '.absolute_object_dir' do
subject { described_class.absolute_object_dir }
context 'when GIT_OBJECT_DIRECTORY is set' do
let(:dir) { '/home/git/repositories/foo/bar.git/./objects' }
before do
allow(ENV).to receive(:[]).with('GIT_OBJECT_DIRECTORY').and_return(dir)
end
it { expect(subject).to eq(dir) }
end
context 'when GIT_OBJECT_DIRECTORY is not set' do
it { expect(subject).to be_nil }
end
end
describe '.absolute_alt_object_dirs' do
subject { described_class.absolute_alt_object_dirs }
context 'when GIT_ALTERNATE_OBJECT_DIRECTORIES is set' do
let(:dirs) { [
'/home/git/repositories/foo/bar.git/./incoming-UKU6Gl',
'/home/git/repositories/foo/bar.git/./incoming-AcU7Qr'
] }
before do
allow(ENV).to receive(:[]).with('GIT_ALTERNATE_OBJECT_DIRECTORIES').and_return(dirs.join(File::PATH_SEPARATOR))
end
it { expect(subject).to eq(dirs) }
end
context 'when GIT_ALTERNATE_OBJECT_DIRECTORIES is not set' do
it { expect(subject).to eq([]) }
end
end
describe '.relative_alt_object_dirs' do
subject { described_class.relative_alt_object_dirs }
context 'when GIT_ALTERNATE_OBJECT_DIRECTORIES is set' do
let(:dirs) { [
'/home/git/repositories/foo/bar.git/./objects/incoming-UKU6Gl',
'/home/git/repositories/foo/bar.git/./objects/incoming-AcU7Qr'
] }
before do
allow(ENV).to receive(:[]).with('GIT_ALTERNATE_OBJECT_DIRECTORIES').and_return(dirs.join(File::PATH_SEPARATOR))
end
it { expect(subject).to eq(['objects/incoming-UKU6Gl', 'objects/incoming-AcU7Qr']) }
end
context 'when GIT_ALTERNATE_OBJECT_DIRECTORIES is not set' do
it { expect(subject).to eq([]) }
end
end
describe '.relative_object_dir' do
subject { described_class.relative_object_dir }
context 'when GIT_OBJECT_DIRECTORY is set' do
before do
allow(ENV).to receive(:[]).with('GIT_OBJECT_DIRECTORY').and_return('/home/git/repositories/foo/bar.git/./objects')
end
it { expect(subject).to eq('objects') }
end
context 'when GIT_OBJECT_DIRECTORY is not set' do
it { expect(subject).to be_nil }
end
end
end
......@@ -3,15 +3,11 @@ require 'rspec-parameterized'
require 'simplecov'
SimpleCov.start
require 'gitlab_init'
ROOT_PATH = File.expand_path('..', __dir__)
Dir[File.expand_path('support/**/*.rb', __dir__)].each { |f| require f }
RSpec.configure do |config|
config.run_all_when_everything_filtered = true
config.filter_run :focus
config.before(:each) do
stub_const('ROOT_PATH', File.expand_path('..', __dir__))
end
end
......@@ -45,9 +45,8 @@ RSpec.shared_context 'gitlab shell', shared_context: :metadata do
raise "Couldn't start stub GitlabNet server" unless @server.status == :Running
system(original_root_path, 'bin/compile')
copy_dirs = ['bin', 'lib']
FileUtils.rm_rf(copy_dirs.map { |d| File.join(tmp_root_path, d) })
FileUtils.cp_r(copy_dirs, tmp_root_path)
FileUtils.rm_rf(File.join(tmp_root_path, 'bin'))
FileUtils.cp_r('bin', tmp_root_path)
end
after(:all) do
......
#!/bin/sh
printenv GL_ID | grep -q '^key_1$'
#!/bin/bash
#echo "fail: $0"
exit 1
#!/bin/bash
#echo "ok: $0"
exit 0
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