Skip to content
Snippets Groups Projects
Commit dcc20876 authored by Drew Blessing's avatar Drew Blessing
Browse files

Add option to recover 2FA via SSH

parent 3043b31c
No related branches found
No related tags found
1 merge request!74Add option to recover 2FA via SSH
v3.5.0
- Add option to recover 2FA via SSH
v3.4.0
- Redis Sentinel support
 
Loading
Loading
Loading
Loading
@@ -72,6 +72,15 @@ class GitlabNet
nil
end
 
def two_factor_recovery_codes(key)
key_id = key.gsub('key-', '')
resp = post("#{host}/two_factor_recovery_codes", key_id: key_id)
JSON.parse(resp.body) if resp.code == '200'
rescue
{}
end
def redis_client
redis_config = config.redis
database = redis_config['database'] || 0
Loading
Loading
Loading
Loading
@@ -8,9 +8,10 @@ class GitlabShell
class InvalidRepositoryPathError < StandardError; end
 
GIT_COMMANDS = %w(git-upload-pack git-receive-pack git-upload-archive git-annex-shell git-lfs-authenticate).freeze
API_COMMANDS = %w(2fa_recovery_codes)
GL_PROTOCOL = 'ssh'.freeze
 
attr_accessor :key_id, :repo_name, :git_cmd
attr_accessor :key_id, :repo_name, :command
attr_reader :repo_path
 
def initialize(key_id)
Loading
Loading
@@ -30,7 +31,7 @@ class GitlabShell
args = Shellwords.shellwords(origin_cmd)
parse_cmd(args)
 
verify_access
verify_access if GIT_COMMANDS.include?(args.first)
 
process_cmd(args)
 
Loading
Loading
@@ -58,12 +59,14 @@ class GitlabShell
protected
 
def parse_cmd(args)
@git_cmd = args.first
@git_access = @git_cmd
@command = args.first
@git_access = @command
 
raise DisallowedCommandError unless GIT_COMMANDS.include?(@git_cmd)
return if API_COMMANDS.include?(@command)
 
case @git_cmd
raise DisallowedCommandError unless GIT_COMMANDS.include?(@command)
case @command
when 'git-annex-shell'
raise DisallowedCommandError unless @config.git_annex_enabled?
 
Loading
Loading
@@ -94,7 +97,9 @@ class GitlabShell
end
 
def process_cmd(args)
if @git_cmd == 'git-annex-shell'
return self.send("api_#{@command}") if API_COMMANDS.include?(@command)
if @command == 'git-annex-shell'
raise DisallowedCommandError unless @config.git_annex_enabled?
 
# Make sure repository has git-annex enabled
Loading
Loading
@@ -113,8 +118,8 @@ class GitlabShell
$logger.info "gitlab-shell: executing git-annex command <#{parsed_args.join(' ')}> for #{log_username}."
exec_cmd(*parsed_args)
else
$logger.info "gitlab-shell: executing git command <#{@git_cmd} #{repo_path}> for #{log_username}."
exec_cmd(@git_cmd, repo_path)
$logger.info "gitlab-shell: executing git command <#{@command} #{repo_path}> for #{log_username}."
exec_cmd(@command, repo_path)
end
end
 
Loading
Loading
@@ -181,6 +186,39 @@ class GitlabShell
 
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(key_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 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
Loading
Loading
Loading
Loading
@@ -106,6 +106,24 @@ describe GitlabNet, vcr: true do
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-1')
expect(result['success']).to be_true
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-1')
expect(result['success']).to be_false
expect(result['message']).to eq('Could not find the given key')
end
end
end
describe :check_access do
context 'ssh key with access to project' do
it 'should allow pull access for dev.gitlab.org' do
Loading
Loading
Loading
Loading
@@ -23,6 +23,10 @@ describe GitlabShell do
double(GitlabNet).tap do |api|
api.stub(discover: { 'name' => 'John Doe' })
api.stub(check_access: GitAccessStatus.new(true, 'ok', repo_path))
api.stub(two_factor_recovery_codes: {
'success' => true,
'recovery_codes' => ['f67c514de60c4953', '41278385fc00c1e0']
})
end
end
 
Loading
Loading
@@ -53,7 +57,7 @@ describe GitlabShell do
end
 
its(:repo_name) { should == 'gitlab-ci.git' }
its(:git_cmd) { should == 'git-upload-pack' }
its(:command) { should == 'git-upload-pack' }
end
 
context 'namespace' do
Loading
Loading
@@ -65,7 +69,7 @@ describe GitlabShell do
end
 
its(:repo_name) { should == 'dmitriy.zaporozhets/gitlab-ci.git' }
its(:git_cmd) { should == 'git-upload-pack' }
its(:command) { should == 'git-upload-pack' }
end
 
context 'with an invalid number of arguments' do
Loading
Loading
@@ -75,6 +79,24 @@ describe GitlabShell 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-annex' do
Loading
Loading
@@ -88,7 +110,7 @@ describe GitlabShell do
end
 
its(:repo_name) { should == 'dzaporozhets/gitlab.git' }
its(:git_cmd) { should == 'git-annex-shell' }
its(:command) { should == 'git-annex-shell' }
end
end
 
Loading
Loading
@@ -233,6 +255,44 @@ describe GitlabShell do
end
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
api.stub(two_factor_recovery_codes: {
'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
end
 
describe :validate_access do
Loading
Loading
---
http_interactions:
- request:
method: post
uri: https://dev.gitlab.org/api/v3/internal/two_factor_recovery_codes
body:
encoding: US-ASCII
string: username=user-1&secret_token=a123
headers:
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
User-Agent:
- Ruby
Content-Type:
- application/x-www-form-urlencoded
response:
status:
code: 200
message: OK
headers:
Server:
- nginx
Date:
- Tue, 16 Aug 2016 22:10:11 GMT
Content-Type:
- application/json
Connection:
- keep-alive
Status:
- 200 OK
X-Request-Id:
- 4467029d-51c6-41bc-af5f-6da279dbb238
X-Runtime:
- '0.004589'
body:
encoding: UTF-8
string: '{ "success": false, "message": "Could not find the given key" }'
http_version:
recorded_at: Tue, 16 Aug 2016 22:10:11 GMT
recorded_with: VCR 2.4.0
---
http_interactions:
- request:
method: post
uri: https://dev.gitlab.org/api/v3/internal/two_factor_recovery_codes
body:
encoding: US-ASCII
string: username=user-1&secret_token=a123
headers:
Accept-Encoding:
- gzip;q=1.0,deflate;q=0.6,identity;q=0.3
Accept:
- "*/*"
User-Agent:
- Ruby
Content-Type:
- application/x-www-form-urlencoded
response:
status:
code: 200
message: OK
headers:
Server:
- nginx
Date:
- Tue, 16 Aug 2016 22:10:11 GMT
Content-Type:
- application/json
Connection:
- keep-alive
Status:
- 200 OK
X-Request-Id:
- 4467029d-51c6-41bc-af5f-6da279dbb238
X-Runtime:
- '0.004589'
body:
encoding: UTF-8
string: '{ "success": true, "recovery_codes": ["f67c514de60c4953","41278385fc00c1e0"] }'
http_version:
recorded_at: Tue, 16 Aug 2016 22:10:11 GMT
recorded_with: VCR 2.4.0
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