diff --git a/.rubocop.yml b/.rubocop.yml index e92f524..1ebabf1 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -16,5 +16,6 @@ RSpec/ExampleLength: Enabled: false AllCops: + TargetRubyVersion: 2.6 Exclude: - bin/* diff --git a/awskeyring.gemspec b/awskeyring.gemspec index 959e618..ac929e1 100644 --- a/awskeyring.gemspec +++ b/awskeyring.gemspec @@ -20,6 +20,8 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] + spec.required_ruby_version = '>= 2.6.0' + spec.metadata = { 'bug_tracker_uri' => "#{Awskeyring::HOMEPAGE}/issues", 'changelog_uri' => "#{Awskeyring::HOMEPAGE}/blob/master/CHANGELOG.md", diff --git a/lib/awskeyring.rb b/lib/awskeyring.rb index a1a2a21..6078182 100644 --- a/lib/awskeyring.rb +++ b/lib/awskeyring.rb @@ -174,26 +174,26 @@ def self.add_token(params = {}) # Return a list account item names def self.list_account_names - items = list_items.map { |elem| elem.attributes[:label][(ACCOUNT_PREFIX.length)..-1] } + items = list_items.map { |elem| elem.attributes[:label][(ACCOUNT_PREFIX.length)..] } - tokens = list_tokens.map { |elem| elem.attributes[:label][(SESSION_KEY_PREFIX.length)..-1] } + tokens = list_tokens.map { |elem| elem.attributes[:label][(SESSION_KEY_PREFIX.length)..] } (items + tokens).uniq.sort end # Return a list role item names def self.list_role_names - list_roles.map { |elem| elem.attributes[:label][(ROLE_PREFIX.length)..-1] }.sort + list_roles.map { |elem| elem.attributes[:label][(ROLE_PREFIX.length)..] }.sort end # Return a list token item names def self.list_token_names - list_tokens.map { |elem| elem.attributes[:label][(SESSION_KEY_PREFIX.length)..-1] }.sort + list_tokens.map { |elem| elem.attributes[:label][(SESSION_KEY_PREFIX.length)..] }.sort end # Return a list role item names and arns def self.list_role_names_plus - list_roles.map { |elem| "#{elem.attributes[:label][(ROLE_PREFIX.length)..-1]}\t#{elem.attributes[:account]}" } + list_roles.map { |elem| "#{elem.attributes[:label][(ROLE_PREFIX.length)..]}\t#{elem.attributes[:account]}" } end # Return a list of console paths diff --git a/lib/awskeyring/awsapi.rb b/lib/awskeyring/awsapi.rb index 779b175..2c7a8e3 100644 --- a/lib/awskeyring/awsapi.rb +++ b/lib/awskeyring/awsapi.rb @@ -198,9 +198,9 @@ def self.get_login_url(key:, secret:, token:, path:, user:) sessionToken: token }.to_json - destination_param = '&Destination=' + CGI.escape(console_url) + destination_param = "&Destination=#{CGI.escape(console_url)}" - AWS_SIGNIN_URL + '?Action=login' + token_param(session_json: session_json) + destination_param + "#{AWS_SIGNIN_URL}?Action=login#{token_param(session_json: session_json)}#{destination_param}" end # Get the signin token param @@ -214,7 +214,7 @@ def self.get_login_url(key:, secret:, token:, path:, user:) returned_content = request.get(uri).body signin_token = JSON.parse(returned_content)['SigninToken'] - '&SigninToken=' + CGI.escape(signin_token) + "&SigninToken=#{CGI.escape(signin_token)}" end # Get the current region diff --git a/lib/awskeyring_command.rb b/lib/awskeyring_command.rb index a138e6e..5ac9c18 100644 --- a/lib/awskeyring_command.rb +++ b/lib/awskeyring_command.rb @@ -539,23 +539,23 @@ def ask_missing(existing:, message:, secure: false, optional: false, limited_to: def ask(message:, secure: false, optional: false, limited_to: nil) if secure - Awskeyring::Input.read_secret(message.rjust(20) + ': ') + Awskeyring::Input.read_secret("#{message.rjust(20)}: ") elsif optional - Thor::LineEditor.readline((message + ' (optional)').rjust(20) + ': ') + Thor::LineEditor.readline("#{"#{message} (optional)".rjust(20)}: ") elsif limited_to - Thor::LineEditor.readline(message.rjust(20) + ': ', limited_to: limited_to) + Thor::LineEditor.readline("#{message.rjust(20)}: ", limited_to: limited_to) else - Thor::LineEditor.readline(message.rjust(20) + ': ') + Thor::LineEditor.readline("#{message.rjust(20)}: ") end end def unbundle to_delete = ENV.keys.select { |elem| elem.start_with?('BUNDLER_ORIG_') } - bundled_env = to_delete.map { |elem| elem[('BUNDLER_ORIG_'.length)..-1] } + bundled_env = to_delete.map { |elem| elem[('BUNDLER_ORIG_'.length)..] } to_delete << 'BUNDLE_GEMFILE' bundled_env.each do |env_name| - ENV[env_name] = ENV['BUNDLER_ORIG_' + env_name] - to_delete << env_name if ENV['BUNDLER_ORIG_' + env_name].start_with? 'BUNDLER_' + ENV[env_name] = ENV["BUNDLER_ORIG_#{env_name}"] + to_delete << env_name if ENV["BUNDLER_ORIG_#{env_name}"].start_with? 'BUNDLER_' end to_delete.each do |env_name| ENV.delete(env_name) diff --git a/spec/lib/awskeyring/input_spec.rb b/spec/lib/awskeyring/input_spec.rb index 0b14e00..0ea84f8 100644 --- a/spec/lib/awskeyring/input_spec.rb +++ b/spec/lib/awskeyring/input_spec.rb @@ -7,14 +7,27 @@ subject(:input) { described_class } context 'when inputting a secret key' do - before do - allow(STDIN).to receive(:getch).and_return('A', 'B', 'C', '1', '2', '3', "\n") - end - it 'asks for a secret' do + allow($stdin).to receive(:getch).and_return('A', 'B', 'C', '1', '2', '3', "\n") expect do input.read_secret(' secret access key: ') end.to output(" secret access key: ******\n").to_stdout end + + it 'asks for a secret and delete a few characters' do + allow($stdin).to receive(:getch) + .and_return('A', 'B', 'C', '1', 'm', 'i', 's', 's', "\b", "\b", "\b", "\b", '2', '3', "\n") + expect do + input.read_secret(' secret access key: ') + end.to output(" secret access key: ********\b\e[P\b\e[P\b\e[P\b\e[P**\n").to_stdout + end + + it 'asks for a secret and canceled the operation' do + allow($stdin).to receive(:getch) + .and_return('A', 'B', 'C', '1', "\u0003") + expect do + input.read_secret(' secret access key: ') + end.to raise_error(SystemExit).and output(' secret access key: ****').to_stdout + end end end diff --git a/spec/lib/awskeyring_command_more_spec.rb b/spec/lib/awskeyring_command_more_spec.rb index acdfcf8..48c414c 100644 --- a/spec/lib/awskeyring_command_more_spec.rb +++ b/spec/lib/awskeyring_command_more_spec.rb @@ -112,6 +112,25 @@ allow(Awskeyring).to receive(:role_exists).and_return('role') allow(Awskeyring).to receive(:list_account_names).and_return(['test']) allow(Awskeyring).to receive(:list_role_names).and_return(['role']) + allow(Awskeyring::Awsapi).to receive(:verify_cred) + .and_return(true) + allow(Awskeyring::Awsapi).to receive(:get_credentials_from_file) + .and_return({ + account: 'testaccount', + key: 'ASIAEXAMPLE', + secret: 'bigishLongSecret', + token: 'VeryveryVeryLongSecret', + expiry: Time.parse('2017-03-12T07:55:29Z'), + role: nil + }) + end + + it 'tries to import a valid token without remote tests' do + expect do + described_class.start(['import', 'testaccount', '-r']) + end.to output("# Token saved for account testaccount\n"\ + "# Authentication valid until #{Time.at(1_489_305_329)}\n").to_stdout + expect(Awskeyring::Awsapi).not_to have_received(:verify_cred) end it 'tries to receive a new token' do @@ -279,6 +298,14 @@ expect(Awskeyring::Awsapi).not_to have_received(:verify_cred) end + it 'tries to import a valid account' do + expect do + described_class.start(%w[import testaccount]) + end.to output("# Added account testaccount\n").to_stdout + expect(Awskeyring).to have_received(:add_account) + expect(Awskeyring::Awsapi).to have_received(:verify_cred) + end + it 'tries to import a valid account without remote tests' do expect do described_class.start(['import', 'testaccount', '-r']) @@ -308,38 +335,28 @@ context 'when we try to add an AWS account and test an mfa' do let(:access_key) { 'AKIA0123456789ABCDEF' } let(:secret_access_key) { 'AbCkTEsTAAAi8ni0987ASDFwer23j14FEQW3IUJV' } + let(:mfa_arn) { 'arn:aws:iam::012345678901:mfa/readonly' } let(:bad_mfa_arn) { 'arn:azure:iamnot::ABCD45678901:Administrators' } before do - allow(Thor::LineEditor).to receive(:readline).and_return(bad_mfa_arn) allow(Awskeyring).to receive(:account_not_exists).with('test').and_return('test') allow(Awskeyring).to receive(:item_by_account).and_return(nil) + allow(Awskeyring).to receive(:add_account).and_return(nil) + allow(Awskeyring::Awsapi).to receive(:verify_cred) + .and_return(true) + allow(Awskeyring).to receive(:update_account) end it 'tries to add an invalid mfa' do + allow(Thor::LineEditor).to receive(:readline).and_return(bad_mfa_arn) expect do described_class.start(['add', 'test', '-k', access_key, '-s', secret_access_key, '-m', bad_mfa_arn]) end.to raise_error.and output(/Invalid MFA ARN/).to_stderr end - end - context 'when we try to add an AWS account with white space' do - let(:access_key) { 'AKIA0123456789ABCDEF' } - let(:secret_access_key) { 'AbCkTEsTAAAi8ni0987ASDFwer23j14FEQW3IUJV' } - let(:mfa_arn) { 'arn:aws:iam::012345678901:mfa/readonly' } - - before do + it 'tries to add an account with whitespace' do allow(Thor::LineEditor).to receive(:readline).and_return(" #{access_key} \n") allow(Awskeyring::Input).to receive(:read_secret).and_return(" #{secret_access_key} \t") - allow(Awskeyring).to receive(:item_by_account).and_return(nil) - allow(Awskeyring).to receive(:account_not_exists).with('test').and_return('test') - allow(Awskeyring).to receive(:add_account).and_return(nil) - allow(Awskeyring::Awsapi).to receive(:verify_cred) - .and_return(true) - allow(Awskeyring).to receive(:update_account) - end - - it 'tries to add an account with whitespace' do expect do described_class.start(['add', 'test', '-m', mfa_arn]) end.to output("# Added account test\n").to_stdout @@ -355,7 +372,6 @@ before do allow(Awskeyring).to receive(:add_role).and_return(nil) - allow(Thor::LineEditor).to receive(:readline).and_return(bad_role_arn) allow(Awskeyring).to receive(:item_by_account).and_return(nil) allow(Awskeyring).to receive(:role_not_exists).and_return('readonly') end @@ -367,6 +383,7 @@ end it 'tries to add an invalid role arn' do + allow(Thor::LineEditor).to receive(:readline).and_return(bad_role_arn) expect do described_class.start(['add-role', 'readonly', '-a', bad_role_arn]) end.to raise_error.and output(/Invalid Role ARN/).to_stderr diff --git a/spec/lib/awskeyring_command_spec.rb b/spec/lib/awskeyring_command_spec.rb index 665db66..f914579 100644 --- a/spec/lib/awskeyring_command_spec.rb +++ b/spec/lib/awskeyring_command_spec.rb @@ -67,6 +67,7 @@ allow(Awskeyring).to receive(:list_role_names_plus) .and_return(%W[admin\tarn1 minion\tarn2 readonly\tarn3]) allow(Awskeyring).to receive(:list_console_path).and_return(%w[iam cloudformation vpc]) + allow(Awskeyring).to receive(:prefs).and_return('{"awskeyring": "awskeyringtest"}') end it 'list keychain items' do @@ -92,8 +93,8 @@ end it 'lists roles with autocomplete' do - ENV['COMP_LINE'] = 'awskeyring token servian min' - expect { described_class.start(%w[awskeyring min servian]) } + ENV['COMP_LINE'] = 'awskeyring remove-role min' + expect { described_class.start(%w[awskeyring min remove-role]) } .to output("minion\n").to_stdout ENV['COMP_LINE'] = nil end @@ -105,6 +106,13 @@ ENV['COMP_LINE'] = nil end + it 'lists commands with autocomplete for help' do + ENV['COMP_LINE'] = 'awskeyring help con' + expect { described_class.start(%w[awskeyring con help]) } + .to output("console\n").to_stdout + ENV['COMP_LINE'] = nil + end + it 'lists flags with autocomplete' do ENV['COMP_LINE'] = 'awskeyring token servian minion --dura' expect { described_class.start(%w[awskeyring --dura minion]) } @@ -125,6 +133,13 @@ .to output("servian\n").to_stdout ENV['COMP_LINE'] = nil end + + it 'doesnt try to re-initialises the keychain' do + expect do + described_class.start(%w[initialise]) + end.to raise_error(SystemExit) + .and output(%r{# .+/\.awskeyring exists\. no need to initialise\.\n}).to_stdout + end end context 'when there is an account and a role' do @@ -268,13 +283,13 @@ it 'provides JSON for use with credential_process' do expect { described_class.start(%w[json test]) } - .to output(JSON.pretty_generate( + .to output("#{JSON.pretty_generate( Version: 1, AccessKeyId: 'ASIATESTTEST', SecretAccessKey: 'bigerlongbase64', SessionToken: 'evenlongerbase64token', Expiration: Time.at(Time.parse('2011-07-11T19:55:29.611Z').to_i).iso8601 - ) + "\n").to_stdout + )}\n").to_stdout expect(Awskeyring).to have_received(:account_exists).with('test') expect(Awskeyring).to have_received(:get_valid_creds).with(account: 'test', no_token: false) end