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
Commit 3aaf4751 authored by Robert Speicher's avatar Robert Speicher
Browse files

Merge branch 'ash.mckenzie/srp-refactor' into 'master'

Refactor that focuses on SRP improvements

See merge request gitlab-org/gitlab-shell!214
parents c6577e0d 014691e0
No related branches found
No related tags found
No related merge requests found
Showing
with 773 additions and 430 deletions
class UnknownError < StandardError; end
class AccessDeniedError < StandardError; end
class InvalidRepositoryPathError < StandardError; end
class DisallowedCommandError < StandardError; end
require 'json'
require_relative 'errors'
require_relative 'actor'
require_relative 'gitlab_init'
require_relative 'gitlab_net'
require_relative 'gitlab_access_status'
require_relative 'names_helper'
require_relative 'gitlab_metrics'
require_relative 'object_dirs_helper'
require 'json'
class GitlabAccess
class AccessDeniedError < StandardError; end
include NamesHelper
attr_reader :config, :gl_repository, :repo_path, :changes, :protocol
def initialize(gl_repository, repo_path, actor, changes, protocol)
@config = GitlabConfig.new
def initialize(gl_repository, repo_path, gl_id, changes, protocol)
@gl_repository = gl_repository
@repo_path = repo_path.strip
@actor = actor
@gl_id = gl_id
@changes = changes.lines
@protocol = protocol
end
def exec
status = GitlabMetrics.measure('check-access:git-receive-pack') do
api.check_access('git-receive-pack', @gl_repository, @repo_path, @actor, @changes, @protocol, env: ObjectDirsHelper.all_attributes.to_json)
GitlabMetrics.measure('check-access:git-receive-pack') do
api.check_access('git-receive-pack', gl_repository, repo_path, actor, changes, protocol, env: ObjectDirsHelper.all_attributes.to_json)
end
raise AccessDeniedError, status.message unless status.allowed?
true
rescue GitlabNet::ApiUnreachableError
$stderr.puts "GitLab: Failed to authorize your Git request: internal API unreachable"
Loading
Loading
@@ -38,9 +32,19 @@ class GitlabAccess
false
end
protected
private
attr_reader :gl_repository, :repo_path, :gl_id, :changes, :protocol
def api
GitlabNet.new
@api ||= GitlabNet.new
end
def config
@config ||= GitlabConfig.new
end
def actor
@actor ||= Actor.new_from(gl_id, audit_usernames: config.audit_usernames)
end
end
require 'json'
class GitAccessStatus
attr_reader :message, :gl_repository, :gl_id, :gl_username, :repository_path, :gitaly, :git_protocol
def initialize(status, message, gl_repository:, gl_id:, gl_username:, repository_path:, gitaly:, git_protocol:)
@status = status
@message = message
@gl_repository = gl_repository
@gl_id = gl_id
@gl_username = gl_username
@repository_path = repository_path
@gitaly = gitaly
@git_protocol = git_protocol
end
def self.create_from_json(json)
values = JSON.parse(json)
new(values["status"],
values["message"],
gl_repository: values["gl_repository"],
gl_id: values["gl_id"],
gl_username: values["gl_username"],
repository_path: values["repository_path"],
gitaly: values["gitaly"],
git_protocol: values["git_protocol"])
end
def allowed?
@status
end
end
Loading
Loading
@@ -5,9 +5,9 @@ require_relative 'gitlab_metrics'
class GitlabCustomHook
attr_reader :vars, :config
def initialize(repo_path, key_id)
def initialize(repo_path, gl_id)
@repo_path = repo_path
@vars = { 'GL_ID' => key_id }
@vars = { 'GL_ID' => gl_id }
@config = GitlabConfig.new
end
Loading
Loading
Loading
Loading
@@ -9,6 +9,7 @@ class GitlabKeys # rubocop:disable Metrics/ClassLength
attr_accessor :auth_file, :key
# TODO: whatever is not a great name
def self.command(whatever)
"#{ROOT_PATH}/bin/gitlab-shell #{whatever}"
end
Loading
Loading
@@ -21,6 +22,7 @@ class GitlabKeys # rubocop:disable Metrics/ClassLength
command(key_id)
end
# TODO: whatever is not a great name
def self.whatever_line(command, trailer)
"command=\"#{command}\",no-port-forwarding,no-X11-forwarding,no-agent-forwarding,no-pty #{trailer}"
end
Loading
Loading
require 'net/http'
require 'openssl'
require 'json'
require_relative 'gitlab_config'
require_relative 'errors'
require_relative 'gitlab_logger'
require_relative 'gitlab_access'
require_relative 'gitlab_lfs_authentication'
require_relative 'httpunix'
require_relative 'http_helper'
require_relative 'action'
class GitlabNet # rubocop:disable Metrics/ClassLength
class GitlabNet
include HTTPHelper
class ApiUnreachableError < StandardError; end
class NotFound < StandardError; end
CHECK_TIMEOUT = 5
GL_PROTOCOL = 'ssh'.freeze
API_INACCESSIBLE_ERROR = 'API is not accessible'.freeze
def check_access(cmd, gl_repository, repo, who, changes, protocol, env: {})
def check_access(cmd, gl_repository, repo, actor, changes, protocol = GL_PROTOCOL, env: {})
changes = changes.join("\n") unless changes.is_a?(String)
params = {
Loading
Loading
@@ -29,56 +26,27 @@ class GitlabNet # rubocop:disable Metrics/ClassLength
env: env
}
who_sym, _, who_v = self.class.parse_who(who)
params[who_sym] = who_v
params[actor.identifier_key.to_sym] = actor.id
url = "#{internal_api_endpoint}/allowed"
resp = post(url, params)
resp = post("#{internal_api_endpoint}/allowed", params)
if resp.code == '200'
GitAccessStatus.create_from_json(resp.body)
else
GitAccessStatus.new(false,
'API is not accessible',
gl_repository: nil,
gl_id: nil,
gl_username: nil,
repository_path: nil,
gitaly: nil,
git_protocol: nil)
end
determine_action(actor, resp)
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
def discover(actor)
resp = get("#{internal_api_endpoint}/discover?#{actor.identifier_key}=#{actor.id}")
JSON.parse(resp.body) if resp.code == HTTP_SUCCESS
rescue JSON::ParserError, ApiUnreachableError
nil
end
def lfs_authenticate(gl_id, repo)
id_sym, _, id = self.class.parse_who(gl_id)
if id_sym == :key_id
params = {
project: sanitize_path(repo),
key_id: id
}
elsif id_sym == :user_id
params = {
project: sanitize_path(repo),
user_id: id
}
else
raise ArgumentError, "lfs_authenticate() got unsupported GL_ID='#{gl_id}'!"
end
def lfs_authenticate(actor, repo)
params = { project: sanitize_path(repo) }
params[actor.identifier_key.to_sym] = actor.id
resp = post("#{internal_api_endpoint}/lfs_authenticate", params)
if resp.code == '200'
GitlabLfsAuthentication.build_from_json(resp.body)
end
GitlabLfsAuthentication.build_from_json(resp.body) if resp.code == HTTP_SUCCESS
end
def broadcast_message
Loading
Loading
@@ -93,11 +61,7 @@ class GitlabNet # rubocop:disable Metrics/ClassLength
url += "&gl_repository=#{URI.escape(gl_repository)}" if gl_repository
resp = get(url)
if resp.code == '200'
JSON.parse(resp.body)
else
[]
end
resp.code == HTTP_SUCCESS ? JSON.parse(resp.body) : []
rescue
[]
end
Loading
Loading
@@ -106,19 +70,17 @@ class GitlabNet # rubocop:disable Metrics/ClassLength
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"
def authorized_key(full_key)
resp = get("#{internal_api_endpoint}/authorized_keys?key=#{URI.escape(full_key, '+/=')}")
JSON.parse(resp.body) if resp.code == HTTP_SUCCESS
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'
def two_factor_recovery_codes(actor)
params = { actor.identifier_key.to_sym => actor.id }
resp = post("#{internal_api_endpoint}/two_factor_recovery_codes", params)
JSON.parse(resp.body) if resp.code == HTTP_SUCCESS
rescue
{}
end
Loading
Loading
@@ -127,51 +89,50 @@ class GitlabNet # rubocop:disable Metrics/ClassLength
params = { gl_repository: gl_repository, project: repo_path }
resp = post("#{internal_api_endpoint}/notify_post_receive", params)
resp.code == '200'
resp.code == HTTP_SUCCESS
rescue
false
end
def post_receive(gl_repository, identifier, changes)
params = {
gl_repository: gl_repository,
identifier: identifier,
changes: changes
}
def post_receive(gl_repository, actor, changes)
params = { gl_repository: gl_repository, identifier: actor.identifier, changes: changes }
resp = post("#{internal_api_endpoint}/post_receive", params)
raise NotFoundError if resp.code == HTTP_NOT_FOUND
raise NotFound if resp.code == '404'
JSON.parse(resp.body) if resp.code == '200'
JSON.parse(resp.body) if resp.code == HTTP_SUCCESS
end
def pre_receive(gl_repository)
resp = post("#{internal_api_endpoint}/pre_receive", gl_repository: gl_repository)
raise NotFoundError if resp.code == HTTP_NOT_FOUND
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
JSON.parse(resp.body) if resp.code == HTTP_SUCCESS
end
protected
private
def sanitize_path(repo)
repo.delete("'")
end
def determine_action(actor, resp)
json = JSON.parse(resp.body)
message = json['message']
case resp.code
when HTTP_SUCCESS
# TODO: This raise can be removed once internal API can respond with correct
# HTTP status codes, instead of relying upon parsing the body and
# accessing the 'status' key.
raise AccessDeniedError, message unless json['status']
Action::Gitaly.create_from_json(actor, json)
when HTTP_UNAUTHORIZED, HTTP_NOT_FOUND
raise AccessDeniedError, message
else
raise UnknownError, "#{API_INACCESSIBLE_ERROR}: #{message}"
end
rescue JSON::ParserError
raise UnknownError, API_INACCESSIBLE_ERROR
end
end
Loading
Loading
@@ -8,20 +8,18 @@ require 'securerandom'
class GitlabPostReceive
include NamesHelper
attr_reader :config, :gl_repository, :repo_path, :changes, :jid
def initialize(gl_repository, repo_path, actor, changes)
def initialize(gl_repository, repo_path, gl_id, changes)
@config = GitlabConfig.new
@gl_repository = gl_repository
@repo_path = repo_path.strip
@actor = actor
@gl_id = gl_id
@changes = changes
@jid = SecureRandom.hex(12)
end
def exec
response = GitlabMetrics.measure("post-receive") do
api.post_receive(gl_repository, @actor, changes)
api.post_receive(gl_repository, actor, changes)
end
return false unless response
Loading
Loading
@@ -35,12 +33,18 @@ class GitlabPostReceive
false
end
protected
private
attr_reader :config, :gl_repository, :repo_path, :gl_id, :changes, :jid
def api
@api ||= GitlabNet.new
end
def actor
@actor ||= Actor.new_from(gl_id, audit_usernames: config.audit_usernames)
end
def print_merge_request_links(merge_request_urls)
return if merge_request_urls.empty?
puts
Loading
Loading
@@ -100,8 +104,6 @@ class GitlabPostReceive
puts "=" * total_width
end
private
def parse_broadcast_msg(msg, text_length)
msg ||= ""
# just return msg if shorter than or equal to text length
Loading
Loading
Loading
Loading
@@ -3,299 +3,120 @@ require 'pathname'
require_relative 'gitlab_net'
require_relative 'gitlab_metrics'
require_relative 'actor'
class GitlabShell # rubocop:disable Metrics/ClassLength
class AccessDeniedError < StandardError; end
class DisallowedCommandError < StandardError; end
class InvalidRepositoryPathError < StandardError; end
class GitlabShell
API_2FA_RECOVERY_CODES_COMMAND = '2fa_recovery_codes'.freeze
GIT_COMMANDS = %w(git-upload-pack git-receive-pack git-upload-archive git-lfs-authenticate).freeze
GITALY_MIGRATED_COMMANDS = {
'git-upload-pack' => File.join(ROOT_PATH, 'bin', 'gitaly-upload-pack'),
'git-upload-archive' => File.join(ROOT_PATH, 'bin', 'gitaly-upload-archive'),
'git-receive-pack' => File.join(ROOT_PATH, 'bin', 'gitaly-receive-pack')
}.freeze
API_COMMANDS = %w(2fa_recovery_codes).freeze
GL_PROTOCOL = 'ssh'.freeze
GIT_UPLOAD_PACK_COMMAND = 'git-upload-pack'.freeze
GIT_RECEIVE_PACK_COMMAND = 'git-receive-pack'.freeze
GIT_UPLOAD_ARCHIVE_COMMAND = 'git-upload-archive'.freeze
GIT_LFS_AUTHENTICATE_COMMAND = 'git-lfs-authenticate'.freeze
attr_accessor :gl_id, :gl_repository, :repo_name, :command, :git_access, :git_protocol
attr_reader :repo_path
GIT_COMMANDS = [GIT_UPLOAD_PACK_COMMAND, GIT_RECEIVE_PACK_COMMAND,
GIT_UPLOAD_ARCHIVE_COMMAND, GIT_LFS_AUTHENTICATE_COMMAND].freeze
Struct.new('ParsedCommand', :command, :git_access_command, :repo_name, :args)
def initialize(who)
who_sym, = GitlabNet.parse_who(who)
if who_sym == :username
@who = who
else
@gl_id = who
end
@config = GitlabConfig.new
@actor = Actor.new_from(who, audit_usernames: @config.audit_usernames)
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}!"
if !origin_cmd || origin_cmd.empty?
puts "Welcome to GitLab, #{actor.username}!"
return true
end
args = Shellwords.shellwords(origin_cmd)
args = parse_cmd(args)
if GIT_COMMANDS.include?(args.first)
GitlabMetrics.measure('verify-access') { verify_access }
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
process_cmd(args)
true
parsed_command = parse_cmd(origin_cmd)
action = determine_action(parsed_command)
action.execute(parsed_command.command, parsed_command.args)
rescue GitlabNet::ApiUnreachableError
$stderr.puts "GitLab: Failed to authorize your Git request: internal API unreachable"
false
rescue AccessDeniedError => ex
$logger.warn('Access denied', command: origin_cmd, user: log_username)
rescue AccessDeniedError, UnknownError => ex
$logger.warn('Access denied', command: origin_cmd, user: actor.log_username)
$stderr.puts "GitLab: #{ex.message}"
false
rescue DisallowedCommandError
$logger.warn('Denied disallowed command', command: origin_cmd, user: log_username)
$stderr.puts "GitLab: Disallowed command"
$logger.warn('Denied disallowed command', command: origin_cmd, user: actor.log_username)
$stderr.puts 'GitLab: Disallowed command'
false
rescue InvalidRepositoryPathError
$stderr.puts "GitLab: Invalid repository path"
$stderr.puts 'GitLab: Invalid repository path'
false
end
protected
private
attr_reader :config, :actor
def parse_cmd(cmd)
args = Shellwords.shellwords(cmd)
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]
command = "git-#{args[1]}"
args = [command, args.last]
else
@command = args.first
command = args.first
end
@git_access = @command
git_access_command = command
return args if API_COMMANDS.include?(@command)
if command == API_2FA_RECOVERY_CODES_COMMAND
return Struct::ParsedCommand.new(command, git_access_command, nil, args)
end
raise DisallowedCommandError unless GIT_COMMANDS.include?(@command)
raise DisallowedCommandError unless GIT_COMMANDS.include?(command)
case @command
case command
when 'git-lfs-authenticate'
raise DisallowedCommandError unless args.count >= 2
@repo_name = args[1]
case args[2]
when 'download'
@git_access = 'git-upload-pack'
when 'upload'
@git_access = 'git-receive-pack'
else
raise DisallowedCommandError
end
repo_name = args[1]
git_access_command = case args[2]
when 'download'
GIT_UPLOAD_PACK_COMMAND
when 'upload'
GIT_RECEIVE_PACK_COMMAND
else
raise DisallowedCommandError
end
else
raise DisallowedCommandError unless args.count == 2
@repo_name = args.last
repo_name = args.last
end
args
Struct::ParsedCommand.new(command, git_access_command, repo_name, 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?
self.repo_path = status.repository_path
@gl_repository = status.gl_repository
@git_protocol = ENV['GIT_PROTOCOL']
@gitaly = status.gitaly
@username = status.gl_username
if defined?(@who)
@gl_id = status.gl_id
end
end
def determine_action(parsed_command)
return Action::API2FARecovery.new(actor) if parsed_command.command == API_2FA_RECOVERY_CODES_COMMAND
def process_cmd(args)
return send("api_#{@command}") if API_COMMANDS.include?(@command)
if @command == 'git-lfs-authenticate'
GitlabMetrics.measure('lfs-authenticate') do
$logger.info('Processing LFS authentication', user: log_username)
lfs_authenticate
end
return
end
executable = @command
args = [repo_path]
if GITALY_MIGRATED_COMMANDS.key?(executable) && @gitaly
executable = GITALY_MIGRATED_COMMANDS[executable]
gitaly_address = @gitaly['address']
# The entire gitaly_request hash should be built in gitlab-ce and passed
# on as-is. For now we build a fake one on the spot.
gitaly_request = {
'repository' => @gitaly['repository'],
'gl_repository' => @gl_repository,
'gl_id' => @gl_id,
'gl_username' => @username,
'git_protocol' => @git_protocol
}
args = [gitaly_address, JSON.dump(gitaly_request)]
end
args_string = [File.basename(executable), *args].join(' ')
$logger.info('executing git command', command: args_string, user: log_username)
exec_cmd(executable, *args)
end
# This method is not covered by Rspec because it ends the current Ruby process.
def exec_cmd(*args)
# If you want to call a command without arguments, use
# exec_cmd(['my_command', 'my_command']) . Otherwise use
# exec_cmd('my_command', 'my_argument', ...).
if args.count == 1 && !args.first.is_a?(Array)
raise DisallowedCommandError
end
env = {
'HOME' => ENV['HOME'],
'PATH' => ENV['PATH'],
'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'],
'LANG' => ENV['LANG'],
'GL_ID' => @gl_id,
'GL_PROTOCOL' => GL_PROTOCOL,
'GL_REPOSITORY' => @gl_repository,
'GL_USERNAME' => @username
}
if @gitaly && @gitaly.include?('token')
env['GITALY_TOKEN'] = @gitaly['token']
end
if git_trace_available?
env.merge!(
'GIT_TRACE' => @config.git_trace_log_file,
'GIT_TRACE_PACKET' => @config.git_trace_log_file,
'GIT_TRACE_PERFORMANCE' => @config.git_trace_log_file
GitlabMetrics.measure('verify-access') do
# GitlabNet#check_access will raise exception in the event of a problem
initial_action = api.check_access(
parsed_command.git_access_command,
nil,
parsed_command.repo_name,
actor,
'_any'
)
end
# 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']}"
case parsed_command.command
when GIT_LFS_AUTHENTICATE_COMMAND
Action::GitLFSAuthenticate.new(actor, parsed_command.repo_name)
else
@user = api.discover(@gl_id)
initial_action
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
lfs_access = api.lfs_authenticate(@gl_id, @repo_name)
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
def git_trace_available?
return false unless @config.git_trace_log_file
if Pathname(@config.git_trace_log_file).relative?
$logger.warn('git trace log path must be absolute, ignoring', git_trace_log_file: @config.git_trace_log_file)
return false
end
begin
File.open(@config.git_trace_log_file, 'a') { nil }
return true
rescue => ex
$logger.warn('Failed to open git trace log file', git_trace_log_file: @config.git_trace_log_file, error: ex.to_s)
return false
end
end
def repo_path=(repo_path)
raise ArgumentError, "Repository path not provided. Please make sure you're using GitLab v8.10 or later." unless repo_path
raise InvalidRepositoryPathError if File.absolute_path(repo_path) != repo_path
@repo_path = repo_path
def api
@api ||= GitlabNet.new
end
end
require 'net/http'
require 'openssl'
require_relative 'gitlab_config'
require_relative 'httpunix'
module HTTPHelper
READ_TIMEOUT = 300
HTTP_SUCCESS = '200'.freeze
HTTP_MULTIPLE_CHOICES = '300'.freeze
HTTP_UNAUTHORIZED = '401'.freeze
HTTP_NOT_FOUND = '404'.freeze
HTTP_SUCCESS_LIKE = [HTTP_SUCCESS, HTTP_MULTIPLE_CHOICES].freeze
class ApiUnreachableError < StandardError; end
class NotFoundError < StandardError; end
protected
Loading
Loading
@@ -76,7 +91,7 @@ module HTTPHelper
$logger.info('finished HTTP request', method: method.to_s.upcase, url: url, duration: Time.new - start_time)
end
if response.code == "200"
if HTTP_SUCCESS_LIKE.include?(response.code)
$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)
Loading
Loading
@@ -109,7 +124,7 @@ module HTTPHelper
end
def secret_token
@secret_token ||= File.read config.secret_file
@secret_token ||= File.read(config.secret_file)
end
def read_timeout
Loading
Loading
require_relative '../spec_helper'
require_relative '../../lib/action/api_2fa_recovery'
describe Action::API2FARecovery do
let(:key_id) { '1' }
let(:actor) { Actor::Key.new(key_id) }
let(:username) { 'testuser' }
let(:discover_payload) { { 'username' => username } }
let(:api) { double(GitlabNet) }
before do
allow(GitlabNet).to receive(:new).and_return(api)
allow(api).to receive(:discover).with(actor).and_return(discover_payload)
end
subject do
described_class.new(actor)
end
describe '#execute' do
context 'with an invalid repsonse' do
it 'returns nil' do
expect($stdin).to receive(:gets).and_return("meh\n")
expect do
expect(subject.execute(nil, nil)).to be_nil
end.to output(/New recovery codes have \*not\* been generated/).to_stdout
end
end
context 'with a negative response' do
before do
expect(subject).to receive(:continue?).and_return(false)
end
it 'returns nil' do
expect do
expect(subject.execute(nil, nil)).to be_nil
end.to output(/New recovery codes have \*not\* been generated/).to_stdout
end
end
context 'with an affirmative response' do
let(:recovery_codes) { %w{ 8dfe0f433208f40b289904c6072e4a72 c33cee7fd0a73edb56e61b785e49af03 } }
before do
expect(subject).to receive(:continue?).and_return(true)
expect(api).to receive(:two_factor_recovery_codes).with(actor).and_return(response)
end
context 'with a unsuccessful response' do
let(:response) { { 'success' => false } }
it 'puts error message to stdout' do
expect do
expect(subject.execute(nil, nil)).to be_falsey
end.to output(/An error occurred while trying to generate new recovery codes/).to_stdout
end
end
context 'with a successful response' do
let(:response) { { 'success' => true, 'recovery_codes' => recovery_codes } }
it 'puts information message including recovery codes to stdout' do
expect do
expect(subject.execute(nil, nil)).to be_truthy
end.to output(Regexp.new(recovery_codes.join("\n"))).to_stdout
end
end
end
end
end
require_relative '../spec_helper'
require_relative '../../lib/action/base'
describe Action::Base do
describe '.create_from_json' do
it 'raises a NotImplementedError exeption' do
expect do
described_class.create_from_json('nomatter')
end.to raise_error(NotImplementedError)
end
end
end
require_relative '../spec_helper'
require_relative '../../lib/action/git_lfs_authenticate'
describe Action::GitLFSAuthenticate do
let(:key_id) { '1' }
let(:repo_name) { 'gitlab-ci.git' }
let(:actor) { Actor::Key.new(key_id) }
let(:username) { 'testuser' }
let(:discover_payload) { { 'username' => username } }
let(:api) { double(GitlabNet) }
before do
allow(GitlabNet).to receive(:new).and_return(api)
allow(api).to receive(:discover).with(actor).and_return(discover_payload)
end
subject do
described_class.new(actor, repo_name)
end
describe '#execute' do
context 'when response from API is not a success' do
before do
expect(api).to receive(:lfs_authenticate).with(actor, repo_name).and_return(nil)
end
it 'returns nil' do
expect(subject.execute(nil, nil)).to be_nil
end
end
context 'when response from API is a success' do
let(:username) { 'testuser' }
let(:lfs_token) { '1234' }
let(:repository_http_path) { "/tmp/#{repo_name}" }
let(:gitlab_lfs_authentication) { GitlabLfsAuthentication.new(username, lfs_token, repository_http_path) }
before do
expect(api).to receive(:lfs_authenticate).with(actor, repo_name).and_return(gitlab_lfs_authentication)
end
it 'puts payload to stdout' do
expect($stdout).to receive(:puts).with('{"header":{"Authorization":"Basic dGVzdHVzZXI6MTIzNA=="},"href":"/tmp/gitlab-ci.git/info/lfs/"}')
expect(subject.execute(nil, nil)).to be_truthy
end
end
end
end
require_relative '../spec_helper'
require_relative '../../lib/action/gitaly'
describe Action::Gitaly do
let(:git_trace_log_file_valid) { '/tmp/git_trace_performance.log' }
let(:git_trace_log_file_invalid) { "/bleep-bop#{git_trace_log_file_valid}" }
let(:git_trace_log_file_relative) { "..#{git_trace_log_file_valid}" }
let(:key_id) { '1' }
let(:key_str) { 'key-1' }
let(:key) { Actor::Key.new(key_id) }
let(:gl_repository) { 'project-1' }
let(:gl_username) { 'testuser' }
let(:git_protocol) { 'version=2' }
let(:tmp_repos_path) { File.join(ROOT_PATH, 'tmp', 'repositories') }
let(:repo_name) { 'gitlab-ci.git' }
let(:repository_path) { File.join(tmp_repos_path, repo_name) }
let(:gitaly_address) { 'unix:gitaly.socket' }
let(:gitaly_token) { '123456' }
let(:gitaly) do
{
'repository' => { 'relative_path' => repo_name, 'storage_name' => 'default' },
'address' => gitaly_address,
'token' => gitaly_token
}
end
describe '.create_from_json' do
it 'returns an instance of Action::Gitaly' do
json = {
"gl_repository" => gl_repository,
"gl_username" => gl_username,
"repository_path" => repository_path,
"gitaly" => gitaly
}
expect(described_class.create_from_json(key_id, json)).to be_instance_of(Action::Gitaly)
end
end
subject do
described_class.new(key, gl_repository, gl_username, git_protocol, repository_path, gitaly)
end
describe '#execute' do
let(:args) { [ repository_path ] }
let(:base_exec_env) do
{
'HOME' => ENV['HOME'],
'PATH' => ENV['PATH'],
'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'],
'LANG' => ENV['LANG'],
'GL_ID' => key_str,
'GL_PROTOCOL' => GitlabNet::GL_PROTOCOL,
'GL_REPOSITORY' => gl_repository,
'GL_USERNAME' => gl_username,
'GITALY_TOKEN' => gitaly_token,
}
end
let(:with_trace_exec_env) do
base_exec_env.merge({
'GIT_TRACE' => git_trace_log_file,
'GIT_TRACE_PACKET' => git_trace_log_file,
'GIT_TRACE_PERFORMANCE' => git_trace_log_file
})
end
let(:gitaly_request) do
{
'repository' => gitaly['repository'],
'gl_repository' => gl_repository,
'gl_id' => key_str,
'gl_username' => gl_username,
'git_protocol' => git_protocol
}
end
context 'for migrated commands' do
context 'such as git-upload-pack' do
let(:git_trace_log_file) { nil }
let(:command) { 'git-upload-pack' }
before do
allow_any_instance_of(GitlabConfig).to receive(:git_trace_log_file).and_return(git_trace_log_file)
end
context 'with an invalid config.git_trace_log_file' do
let(:git_trace_log_file) { git_trace_log_file_invalid }
it 'returns true' do
expect(Kernel).to receive(:exec).with(
base_exec_env,
described_class::MIGRATED_COMMANDS[command],
gitaly_address,
JSON.dump(gitaly_request),
unsetenv_others: true,
chdir: ROOT_PATH
).and_return(true)
expect(subject.execute(command, args)).to be_truthy
end
end
context 'with n relative config.git_trace_log_file' do
let(:git_trace_log_file) { git_trace_log_file_relative }
it 'returns true' do
expect(Kernel).to receive(:exec).with(
base_exec_env,
described_class::MIGRATED_COMMANDS[command],
gitaly_address,
JSON.dump(gitaly_request),
unsetenv_others: true,
chdir: ROOT_PATH
).and_return(true)
expect(subject.execute(command, args)).to be_truthy
end
end
context 'with a valid config.git_trace_log_file' do
let(:git_trace_log_file) { git_trace_log_file_valid }
it 'returns true' do
expect(Kernel).to receive(:exec).with(
with_trace_exec_env,
described_class::MIGRATED_COMMANDS[command],
gitaly_address,
JSON.dump(gitaly_request),
unsetenv_others: true,
chdir: ROOT_PATH
).and_return(true)
expect(subject.execute(command, args)).to be_truthy
end
end
end
end
end
end
require_relative '../spec_helper'
require_relative '../../lib/actor/base'
describe Actor::Base do
describe '.identifier_key' do
it 'raises a NotImplementedError exception' do
expect do
described_class.identifier_key
end.to raise_error(NotImplementedError)
end
end
describe '.identifier_prefix' do
it 'raises a NotImplementedError exception' do
expect do
described_class.identifier_prefix
end.to raise_error(NotImplementedError)
end
end
describe '.id_regex' do
it 'raises a NotImplementedError exception' do
expect do
described_class.id_regex
end.to raise_error(NotImplementedError)
end
end
end
require_relative '../spec_helper'
require_relative '../../lib/actor/key'
describe Actor::Key do
let(:key_id) { '1' }
let(:username) { 'testuser' }
let(:api) { double(GitlabNet) }
let(:discover_payload) { { 'username' => username } }
let(:audit_usernames) { nil }
before do
allow(GitlabNet).to receive(:new).and_return(api)
allow(api).to receive(:discover).with(subject).and_return(discover_payload)
end
describe '.from' do
it 'returns an instance of Actor::Key' do
expect(described_class.from('key-1')).to be_a(Actor::Key)
end
it 'has a key_id == 1' do
expect(described_class.from('key-1').key_id).to eq '1'
end
end
describe '.identifier_prefix' do
it "returns 'key'" do
expect(described_class.identifier_prefix).to eql 'key'
end
end
describe '.identifier_key' do
it "returns 'key_id'" do
expect(described_class.identifier_key).to eql 'key_id'
end
end
subject { described_class.new(key_id, audit_usernames: audit_usernames) }
describe '#username' do
context 'with a valid user' do
it "returns '@testuser'" do
expect(subject.username).to eql '@testuser'
end
end
context 'without a valid user' do
let(:discover_payload) { nil }
it "returns 'Anonymous'" do
expect(subject.username).to eql 'Anonymous'
end
end
end
describe '#identifier' do
it "returns 'key-1'" do
expect(subject.identifier).to eql 'key-1'
end
end
describe '#log_username' do
context 'when audit_usernames is true' do
let(:audit_usernames) { true }
it "returns 'testuser'" do
expect(subject.log_username).to eql '@testuser'
end
end
context 'when audit_usernames is false' do
let(:audit_usernames) { false }
it "returns 'key with identifier key-1'" do
expect(subject.log_username).to eql 'key with identifier key-1'
end
end
end
end
require_relative '../spec_helper'
require_relative '../../lib/actor/user'
describe Actor::User do
let(:user_id) { '1' }
let(:username) { 'testuser' }
let(:audit_usernames) { nil }
describe '.from' do
it 'returns an instance of Actor::User' do
expect(described_class.from('user-1')).to be_a(Actor::User)
end
it 'has an id == 1' do
expect(described_class.from('user-1').id).to eq '1'
end
end
describe '.identifier_prefix' do
it "returns 'user'" do
expect(described_class.identifier_prefix).to eql 'user'
end
end
describe '.identifier_key' do
it "returns 'user_id'" do
expect(described_class.identifier_key).to eql 'user_id'
end
end
subject { described_class.new(user_id, audit_usernames: audit_usernames) }
describe '#username' do
it "returns 'user-1'" do
expect(subject.username).to eql 'user-1'
end
end
describe '#identifier' do
it "returns 'user-1'" do
expect(subject.identifier).to eql 'user-1'
end
end
describe '#log_username' do
context 'when audit_usernames is true' do
let(:audit_usernames) { true }
it "returns 'user-1'" do
expect(subject.log_username).to eql 'user-1'
end
end
context 'when audit_usernames is false' do
let(:audit_usernames) { false }
it "returns 'user with identifier user-1'" do
expect(subject.log_username).to eql 'user with identifier user-1'
end
end
end
end
require_relative '../spec_helper'
require_relative '../../lib/actor/username'
describe Actor::Username do
let(:username) { 'testuser' }
let(:api) { double(GitlabNet) }
let(:discover_payload) { { 'username' => username } }
let(:audit_usernames) { nil }
before do
allow(GitlabNet).to receive(:new).and_return(api)
allow(api).to receive(:discover).with(subject).and_return(discover_payload)
end
describe '.from' do
it 'returns an instance of Actor::Username' do
expect(described_class.from("username-#{username}")).to be_a(Actor::Username)
end
it 'has an id == 1' do
expect(described_class.from('username-1').id).to eq '1'
end
end
describe '.identifier_prefix' do
it "returns 'user'" do
expect(described_class.identifier_prefix).to eql 'username'
end
end
describe '.identifier_key' do
it "returns 'username'" do
expect(described_class.identifier_key).to eql 'username'
end
end
subject { described_class.new(username, audit_usernames: audit_usernames) }
describe '#username' do
context 'without a valid user' do
it "returns '@testuser'" do
expect(subject.username).to eql "@#{username}"
end
end
context 'without a valid user' do
let(:discover_payload) { nil }
it "returns 'Anonymous'" do
expect(subject.username).to eql 'Anonymous'
end
end
end
describe '#identifier' do
it "returns 'username-testuser'" do
expect(subject.identifier).to eql 'username-testuser'
end
end
describe '#log_username' do
context 'when audit_usernames is true' do
let(:audit_usernames) { true }
it "returns '@testuser'" do
expect(subject.log_username).to eql "@#{username}"
end
end
context 'when audit_usernames is false' do
let(:audit_usernames) { false }
it "returns 'user with identifier username-testuser'" do
expect(subject.log_username).to eql "user with identifier username-#{username}"
end
end
end
end
require_relative 'spec_helper'
require_relative '../lib/actor'
describe Actor do
describe '.new_from' do
context 'for an unsupported Actor type' do
it 'raises a NotImplementedError exception' do
expect do
described_class.new_from('unsupported-1')
end.to raise_error(Actor::UnsupportedActorError)
end
end
context 'for a supported Actor type' do
context 'of Key' do
it 'returns an instance of Key' do
expect(described_class.new_from('key-1')).to be_a(Actor::Key)
end
end
context 'of User' do
it 'returns an instance of User' do
expect(described_class.new_from('user-1')).to be_a(Actor::User)
end
end
context 'of Username' do
it 'returns an instance of Username' do
expect(described_class.new_from('username-john1')).to be_a(Actor::Username)
end
end
end
end
end
Loading
Loading
@@ -7,31 +7,27 @@ describe GitlabAccess do
let(:repo_path) { File.join(repository_path, repo_name) + ".git" }
let(:api) do
double(GitlabNet).tap do |api|
api.stub(check_access: GitAccessStatus.new(true,
'ok',
gl_repository: 'project-1',
gl_id: 'user-123',
gl_username: 'testuser',
repository_path: '/home/git/repositories',
gitaly: nil,
git_protocol: 'version=2'))
allow(api).to receive(:check_access).and_return(
Action::Gitaly.new(
'key-1',
'project-1',
'testuser',
'version=2',
'/home/git/repositories',
nil
)
)
end
end
subject do
GitlabAccess.new(nil, repo_path, 'key-123', 'wow', 'ssh').tap do |access|
access.stub(exec_cmd: :exec_called)
access.stub(api: api)
allow(access).to receive(:exec_cmd).and_return(:exec_called)
allow(access).to receive(:api).and_return(api)
end
end
before do
GitlabConfig.any_instance.stub(repos_path: repository_path)
end
describe :initialize do
it { subject.repo_path.should == repo_path }
it { subject.changes.should == ['wow'] }
it { subject.protocol.should == 'ssh' }
allow_any_instance_of(GitlabConfig).to receive(:repos_path).and_return(repository_path)
end
describe "#exec" do
Loading
Loading
@@ -43,16 +39,7 @@ describe GitlabAccess do
context "access is denied" do
before do
api.stub(check_access: GitAccessStatus.new(
false,
'denied',
gl_repository: nil,
gl_id: nil,
gl_username: nil,
repository_path: nil,
gitaly: nil,
git_protocol: nil
))
allow(api).to receive(:check_access).and_raise(AccessDeniedError)
end
it "returns false" do
Loading
Loading
@@ -62,7 +49,7 @@ describe GitlabAccess do
context "API connection fails" do
before do
api.stub(:check_access).and_raise(GitlabNet::ApiUnreachableError)
allow(api).to receive(:check_access).and_raise(GitlabNet::ApiUnreachableError)
end
it "returns false" do
Loading
Loading
Loading
Loading
@@ -3,29 +3,55 @@ require_relative '../lib/gitlab_config'
describe GitlabConfig do
let(:config) { GitlabConfig.new }
let(:config_data) do
{
# 'user' => 'git',
# 'http_settings' => {
# 'self_signed_cert' => false
# },
# 'log_level' => 'ERROR',
# 'audit_usernames' => true,
# 'log_format' => 'json', # Not sure on other values?
# 'git_trace_log_file' => nil
}
end
before do
expect(YAML).to receive(:load_file).and_return(config_data)
end
describe :gitlab_url do
describe '#gitlab_url' do
let(:url) { 'http://test.com' }
subject { config.gitlab_url }
before { config.send(:config)['gitlab_url'] = url }
before { config_data['gitlab_url'] = url }
it { should_not be_empty }
it { should eq(url) }
context 'remove trailing slashes' do
before { config.send(:config)['gitlab_url'] = url + '//' }
before { config_data['gitlab_url'] = url + '//' }
it { should eq(url) }
end
end
describe :audit_usernames do
describe '#audit_usernames' do
subject { config.audit_usernames }
it("returns false by default") { should eq(false) }
end
describe :log_format do
describe '#log_level' do
subject { config.log_level }
it 'returns "INFO" by default' do
should eq('INFO')
end
end
describe '#log_format' do
subject { config.log_format }
it 'returns "text" by default' do
Loading
Loading
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