Commit aa4d2ba8 authored by Ash McKenzie's avatar Ash McKenzie

New Action classes

* Base - contains all common logic
* Gitaly - performs interactions with Gitaly
* API2FARecovery - 2FA recovery code generation
* GitLFSAuthenticate - git-lfs authentication
parent 28ff5940
require_relative 'action/base'
require_relative 'action/gitaly'
require_relative 'action/git_lfs_authenticate'
require_relative 'action/api_2fa_recovery'
module Action
end
require_relative '../action'
require_relative '../gitlab_logger'
module Action
class API2FARecovery < Base
def initialize(key_id)
@key_id = key_id
end
def execute(_, _)
recover
end
private
attr_reader :key_id
def continue?(question)
puts "#{question} (yes/no)"
STDOUT.flush # Make sure the question gets output before we wait for input
response = STDIN.gets.chomp
puts '' # Add a buffer in the output
response == 'yes'
end
def recover
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(key_id)
if resp['success']
codes = resp['recovery_codes'].join("\n")
$logger.info('API 2FA recovery success', user: user.log_username)
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."
true
else
$logger.info('API 2FA recovery error', user: user.log_username)
puts "An error occurred while trying to generate new recovery codes.\n" \
"#{resp['message']}"
end
end
end
end
require 'json'
require_relative '../gitlab_config'
require_relative '../gitlab_net'
require_relative '../gitlab_metrics'
require_relative '../user'
module Action
class Base
def self.create_from_json(_)
raise NotImplementedError
end
private
attr_reader :key_id
def config
@config ||= GitlabConfig.new
end
def api
@api ||= GitlabNet.new
end
def user
@user ||= User.new(key_id, audit_usernames: config.audit_usernames)
end
end
end
require_relative '../action'
require_relative '../gitlab_logger'
module Action
class GitLFSAuthenticate < Base
def initialize(key_id, repo_name)
@key_id = key_id
@repo_name = repo_name
end
def execute(_, _)
GitlabMetrics.measure('lfs-authenticate') do
$logger.info('Processing LFS authentication', user: user.log_username)
lfs_access = api.lfs_authenticate(key_id, repo_name)
return unless lfs_access
puts lfs_access.authentication_payload
end
true
end
private
attr_reader :key_id, :repo_name
end
end
require_relative '../action'
require_relative '../gitlab_logger'
require_relative '../gitlab_net'
module Action
class Gitaly < Base
REPOSITORY_PATH_NOT_PROVIDED = "Repository path not provided. Please make sure you're using GitLab v8.10 or later.".freeze
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
def initialize(key_id, gl_repository, gl_username, repository_path, gitaly)
@key_id = key_id
@gl_repository = gl_repository
@gl_username = gl_username
@repository_path = repository_path
@gitaly = gitaly
end
def self.create_from_json(key_id, json)
new(key_id,
json['gl_repository'],
json['gl_username'],
json['repository_path'],
json['gitaly'])
end
def execute(command, args)
raise ArgumentError, REPOSITORY_PATH_NOT_PROVIDED unless repository_path
raise InvalidRepositoryPathError unless valid_repository?
$logger.info('Performing Gitaly command', user: user.log_username)
process(command, args)
end
private
attr_reader :gl_repository, :gl_username, :repository_path, :gitaly
def process(command, args)
executable = command
args = [repository_path]
if MIGRATED_COMMANDS.key?(executable) && gitaly
executable = MIGRATED_COMMANDS[executable]
gitaly_address = gitaly['address']
args = [gitaly_address, JSON.dump(gitaly_request)]
end
args_string = [File.basename(executable), *args].join(' ')
$logger.info('executing git command', command: args_string, user: user.log_username)
exec_cmd(executable, *args)
end
def exec_cmd(*args)
env = exec_env
env['GITALY_TOKEN'] = gitaly['token'] if gitaly && gitaly.include?('token')
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
)
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 exec_env
{
'HOME' => ENV['HOME'],
'PATH' => ENV['PATH'],
'LD_LIBRARY_PATH' => ENV['LD_LIBRARY_PATH'],
'LANG' => ENV['LANG'],
'GL_ID' => key_id,
'GL_PROTOCOL' => GitlabNet::GL_PROTOCOL,
'GL_REPOSITORY' => gl_repository,
'GL_USERNAME' => gl_username
}
end
def gitaly_request
# 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.
{
'repository' => gitaly['repository'],
'gl_repository' => gl_repository,
'gl_id' => key_id,
'gl_username' => gl_username
}
end
def valid_repository?
File.absolute_path(repository_path) == repository_path
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
end
end
require_relative '../spec_helper'
require_relative '../../lib/action/api_2fa_recovery'
describe Action::API2FARecovery do
let(:key_id) { "key-#{rand(100) + 100}" }
let(:key) { 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(key_id).and_return(discover_payload)
end
subject do
described_class.new(key_id)
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(key_id).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) { "key-#{rand(100) + 100}" }
let(:repo_name) { 'gitlab-ci.git' }
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(key_id).and_return(discover_payload)
end
subject do
described_class.new(key_id, 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(key_id, 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(key_id, 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) { "key-#{rand(100) + 100}" }
let(:gl_repository) { 'project-1' }
let(:gl_username) { 'testuser' }
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_id, gl_repository, gl_username, 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_id,
'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_id,
'gl_username' => gl_username
}
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 an 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
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