diff --git a/README.md b/README.md index 066ca09..a29f660 100644 --- a/README.md +++ b/README.md @@ -61,7 +61,7 @@ more details on this config option. The CLI is using [Thor](http://whatisthor.com) with help provided interactively. - Commands: + Awskeyring commands: awskeyring --version, -v # Prints the version awskeyring add ACCOUNT # Adds an ACCOUNT to the keyring awskeyring add-role ROLE # Adds a ROLE to the keyring diff --git a/i18n/en.yml b/i18n/en.yml index 1fd0df0..66f9b63 100644 --- a/i18n/en.yml +++ b/i18n/en.yml @@ -1,41 +1,24 @@ --- en: - __version: - desc: Prints the version - add: - desc: Adds an ACCOUNT to the keyring - add_role: - desc: Adds a ROLE to the keyring - awskeyring: - desc: Autocompletion for bourne shells - console: - desc: Open the AWS Console for the ACCOUNT - env: - desc: Outputs bourne shell environment exports for an ACCOUNT - exec: - desc: Execute a COMMAND with the environment set for an ACCOUNT - import: - desc: Import an ACCOUNT to the keyring from ~/.aws/credentials - initialise: - desc: Initialises a new KEYCHAIN - json: - desc: Outputs AWS CLI compatible JSON for an ACCOUNT - list: - desc: Prints a list of accounts in the keyring - list_role: - desc: Prints a list of roles in the keyring - remove: - desc: Removes an ACCOUNT from the keyring - remove_role: - desc: Removes a ROLE from the keyring - remove_token: - desc: Removes a token for ACCOUNT from the keyring - rotate: - desc: Rotate access keys for an ACCOUNT - token: - desc: Create an STS Token from a ROLE or an MFA code - update: - desc: Updates an ACCOUNT in the keyring + __version_desc: Prints the version + add_desc: Adds an ACCOUNT to the keyring + add_role_desc: Adds a ROLE to the keyring + awskeyring_desc: Autocompletion for bourne shells + console_desc: Open the AWS Console for the ACCOUNT + default_desc: Run default help or initialise if needed. + env_desc: Outputs bourne shell environment exports for an ACCOUNT + exec_desc: Execute a COMMAND with the environment set for an ACCOUNT + import_desc: Import an ACCOUNT to the keyring from ~/.aws/credentials + initialise_desc: Initialises a new KEYCHAIN + json_desc: Outputs AWS CLI compatible JSON for an ACCOUNT + list_desc: Prints a list of accounts in the keyring + list_role_desc: Prints a list of roles in the keyring + remove_desc: Removes an ACCOUNT from the keyring + remove_role_desc: Removes a ROLE from the keyring + remove_token_desc: Removes a token for ACCOUNT from the keyring + rotate_desc: Rotate access keys for an ACCOUNT + token_desc: Create an STS Token from a ROLE or an MFA code + update_desc: Updates an ACCOUNT in the keyring method_option: arn: 'AWS role arn.' code: 'Virtual mfa CODE.' diff --git a/lib/awskeyring_command.rb b/lib/awskeyring_command.rb index 771ac1f..5c5c2e4 100644 --- a/lib/awskeyring_command.rb +++ b/lib/awskeyring_command.rb @@ -11,6 +11,7 @@ # AWSkeyring command line interface. class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength + package_name 'Awskeyring' I18n.load_path = Dir.glob(File.join(File.realpath(__dir__), '..', 'i18n', '*.{yml,yaml}')) I18n.backend.load_translations @@ -27,13 +28,24 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength map 'rot' => :rotate map 'tok' => :token map 'up' => :update + default_command :default # default to returning an error on failure. def self.exit_on_failure? true end - desc '--version, -v', I18n.t('__version.desc') + desc 'default', I18n.t('default_desc'), hide: true + # default command to run + def default + if Awskeyring.prefs.empty? + invoke :initialise + else + invoke :help + end + end + + desc '--version, -v', I18n.t('__version_desc') method_option 'no-remote', type: :boolean, aliases: '-r', desc: I18n.t('method_option.noremote'), default: false # print the version number def __version @@ -44,7 +56,7 @@ def __version puts "Homepage #{Awskeyring::HOMEPAGE}" end - desc 'initialise', I18n.t('initialise.desc') + desc 'initialise', I18n.t('initialise_desc') method_option :keychain, type: :string, aliases: '-n', desc: I18n.t('method_option.keychain') # initialise the keychain def initialise @@ -69,7 +81,7 @@ def initialise puts I18n.t('message.addkeychain', keychain: keychain, exec_name: exec_name) end - desc 'list', I18n.t('list.desc') + desc 'list', I18n.t('list_desc') # list the accounts def list if Awskeyring.list_account_names.empty? @@ -80,7 +92,7 @@ def list end map 'list-role' => :list_role - desc 'list-role', I18n.t('list_role.desc') + desc 'list-role', I18n.t('list_role_desc') method_option 'detail', type: :boolean, aliases: '-d', desc: I18n.t('method_option.detail'), default: false # List roles def list_role @@ -95,7 +107,7 @@ def list_role end end - desc 'env ACCOUNT', I18n.t('env.desc') + desc 'env ACCOUNT', I18n.t('env_desc') method_option 'no-token', type: :boolean, aliases: '-n', desc: I18n.t('method_option.notoken'), default: false method_option 'unset', type: :boolean, aliases: '-u', desc: I18n.t('method_option.unset'), default: false # Print Env vars @@ -113,7 +125,7 @@ def env(account = nil) end end - desc 'json ACCOUNT', I18n.t('json.desc') + desc 'json ACCOUNT', I18n.t('json_desc') method_option 'no-token', type: :boolean, aliases: '-n', desc: I18n.t('method_option.notoken'), default: false # Print JSON for use with credential_process def json(account) @@ -130,7 +142,7 @@ def json(account) ) end - desc 'import ACCOUNT', I18n.t('import.desc') + desc 'import ACCOUNT', I18n.t('import_desc') method_option 'no-remote', type: :boolean, aliases: '-r', desc: I18n.t('method_option.noremote'), default: false # Import an Account def import(account = nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize @@ -166,7 +178,7 @@ def import(account = nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSiz end end - desc 'exec ACCOUNT command...', I18n.t('exec.desc') + desc 'exec ACCOUNT command...', I18n.t('exec_desc') method_option 'no-token', type: :boolean, aliases: '-n', desc: I18n.t('method_option.notoken'), default: false method_option 'no-bundle', type: :boolean, aliases: '-b', desc: I18n.t('method_option.nobundle'), default: false # execute an external command with env set @@ -188,7 +200,7 @@ def exec(account, *command) end end - desc 'add ACCOUNT', I18n.t('add.desc') + desc 'add ACCOUNT', I18n.t('add_desc') method_option :key, type: :string, aliases: '-k', desc: I18n.t('method_option.key') method_option :secret, type: :string, aliases: '-s', desc: I18n.t('method_option.secret') method_option :mfa, type: :string, aliases: '-m', desc: I18n.t('method_option.mfa') @@ -219,7 +231,7 @@ def add(account = nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSize puts I18n.t('message.addaccount', account: account) end - desc 'update ACCOUNT', I18n.t('update.desc') + desc 'update ACCOUNT', I18n.t('update_desc') method_option :key, type: :string, aliases: '-k', desc: I18n.t('method_option.key') method_option :secret, type: :string, aliases: '-s', desc: I18n.t('method_option.secret') method_option 'no-remote', type: :boolean, aliases: '-r', desc: I18n.t('method_option.noremote'), default: false @@ -247,7 +259,7 @@ def update(account = nil) # rubocop:disable Metrics/MethodLength, Metrics/AbcSiz end map 'add-role' => :add_role - desc 'add-role ROLE', I18n.t('add_role.desc') + desc 'add-role ROLE', I18n.t('add_role_desc') method_option :arn, type: :string, aliases: '-a', desc: I18n.t('method_option.arn') # Add a role def add_role(role = nil) @@ -267,7 +279,7 @@ def add_role(role = nil) puts I18n.t('message.addrole', role: role) end - desc 'remove ACCOUNT', I18n.t('remove.desc') + desc 'remove ACCOUNT', I18n.t('remove_desc') # Remove an account def remove(account = nil) account = ask_check( @@ -277,7 +289,7 @@ def remove(account = nil) Awskeyring.delete_account(account: account, message: I18n.t('message.delaccount', account: account)) end - desc 'remove-token ACCOUNT', I18n.t('remove_token.desc') + desc 'remove-token ACCOUNT', I18n.t('remove_token_desc') # remove a session token def remove_token(account = nil) account = ask_check( @@ -288,7 +300,7 @@ def remove_token(account = nil) end map 'remove-role' => :remove_role - desc 'remove-role ROLE', I18n.t('remove_role.desc') + desc 'remove-role ROLE', I18n.t('remove_role_desc') # remove a role def remove_role(role = nil) role = ask_check( @@ -298,7 +310,7 @@ def remove_role(role = nil) Awskeyring.delete_role(role_name: role, message: I18n.t('message.delrole', role: role)) end - desc 'rotate ACCOUNT', I18n.t('rotate.desc') + desc 'rotate ACCOUNT', I18n.t('rotate_desc') # rotate Account keys def rotate(account = nil) # rubocop:disable Metrics/MethodLength account = ask_check( @@ -330,7 +342,7 @@ def rotate(account = nil) # rubocop:disable Metrics/MethodLength puts I18n.t('message.upaccount', account: account) end - desc 'token ACCOUNT [ROLE] [MFA]', I18n.t('token.desc') + desc 'token ACCOUNT [ROLE] [MFA]', I18n.t('token_desc') method_option :role, type: :string, aliases: '-r', desc: I18n.t('method_option.role') method_option :code, type: :string, aliases: '-c', desc: I18n.t('method_option.code') method_option :duration, type: :string, aliases: '-d', desc: I18n.t('method_option.duration') @@ -385,7 +397,7 @@ def token(account = nil, role = nil, code = nil) # rubocop:disable Metrics/AbcSi puts I18n.t('message.addtoken', account: account, time: Time.at(new_creds[:expiry].to_i)) end - desc 'console ACCOUNT', I18n.t('console.desc') + desc 'console ACCOUNT', I18n.t('console_desc') method_option :path, type: :string, aliases: '-p', desc: I18n.t('method_option.path') method_option :browser, type: :string, aliases: '-b', desc: I18n.t('method_option.browser') method_option 'no-token', type: :boolean, aliases: '-n', desc: I18n.t('method_option.notoken'), default: false @@ -424,7 +436,7 @@ def console(account = nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLeng end end - desc 'awskeyring CURR PREV', I18n.t('awskeyring.desc'), hide: true + desc "#{File.basename($PROGRAM_NAME)} CURR PREV", I18n.t('awskeyring_desc'), hide: true map File.basename($PROGRAM_NAME) => :autocomplete # autocomplete def autocomplete(curr, prev) @@ -507,7 +519,8 @@ def print_auto_resp(curr, len, sub_cmd) # rubocop:disable Metrics/MethodLength, end def list_commands - self.class.all_commands.keys.map { |elem| elem.tr('_', '-') }.reject! { |elem| elem == 'autocomplete' } + commands = self.class.all_commands.keys.map { |elem| elem.tr('_', '-') } + commands.reject! { |elem| %w[autocomplete default].include?(elem) } end def list_arguments(command:) diff --git a/man/awskeyring.5 b/man/awskeyring.5 index 6ecab64..0626dc2 100644 --- a/man/awskeyring.5 +++ b/man/awskeyring.5 @@ -1,7 +1,7 @@ .\" generated with Ronn/v0.7.3 .\" http://github.com/rtomayko/ronn/tree/0.7.3 . -.TH "AWSKEYRING" "5" "March 2021" "" "" +.TH "AWSKEYRING" "5" "April 2021" "" "" . .SH "NAME" \fBAwskeyring\fR \- is a small tool to manage AWS account keys in the macOS Keychain @@ -267,8 +267,34 @@ awskeyring env personal\-aws . .IP "" 0 . +.P +To open the AWS Console (web page) with your default browser simply run\.\.\. +. +.IP "" 4 +. +.nf + +awskeyring console personal\-aws +. +.fi +. +.IP "" 0 +. +.P +Autocomplete is enabled in your current shell with the following command\.\.\. +. +.IP "" 4 +. +.nf + +complete \-C /usr/local/bin/awskeyring awskeyring +. +.fi +. +.IP "" 0 +. .SH "HISTORY" -The motivation of this application is to provide a local secure store of AWS credentials using specifically in the macOS Keychain, to have them easily accessed from the Terminal, and to provide useful functions like assuming roles and opening the AWS Console from the cli\. For Enterprise environments there are better suited tools to use like HashiCorp Vault \fIhttps://vaultproject\.io/\fR\. +The motivation of this application is to provide a local secure store of AWS credentials using specifically in the macOS Keychain, to have them easily accessed from the Terminal, and to provide useful functions like assuming roles and opening the AWS Console from the cli\. It then expanded to include autocomplete and a desire to have an almost complete test coverage to prevent regressions in its functionality\. For Enterprise environments there are better suited tools to use like HashiCorp Vault \fIhttps://vaultproject\.io/\fR\. . .SH "SECURITY" If you believe you have found a security issue in Awskeyring, please responsibly disclose by contacting me at \fItristan\.morgan@servian\.com\fR\. Awskeyring is a Ruby script and as such Ruby is whitelisted to access your "awskeyring" keychain\. Use a strong password and keep the unlock time short\. diff --git a/man/awskeyring.5.ronn b/man/awskeyring.5.ronn index ca0103f..b665d0d 100644 --- a/man/awskeyring.5.ronn +++ b/man/awskeyring.5.ronn @@ -145,12 +145,21 @@ Now your keys are stored safely in the macOS keychain. To print environment vari awskeyring env personal-aws +To open the AWS Console (web page) with your default browser simply run... + + awskeyring console personal-aws + +Autocomplete is enabled in your current shell with the following command... + + complete -C /usr/local/bin/awskeyring awskeyring + ## HISTORY The motivation of this application is to provide a local secure store of AWS credentials using specifically in the macOS Keychain, to have them easily accessed from the Terminal, and to provide useful functions like assuming roles and opening -the AWS Console from the cli. +the AWS Console from the cli. It then expanded to include autocomplete and a desire +to have an almost complete test coverage to prevent regressions in its functionality. For Enterprise environments there are better suited tools to use like [HashiCorp Vault](https://vaultproject.io/). diff --git a/spec/lib/awskeyring_command_spec.rb b/spec/lib/awskeyring_command_spec.rb index 494355f..c35fa22 100644 --- a/spec/lib/awskeyring_command_spec.rb +++ b/spec/lib/awskeyring_command_spec.rb @@ -21,10 +21,8 @@ end it 'outputs help text' do - expect { described_class.start([]) } - .to output(/^ \w+ --version, -v\s+# Prints the version/).to_stdout expect { described_class.start(%w[help]) } - .to output(/Commands:/).to_stdout + .to output(/Awskeyring commands:/).to_stdout end it 'returns the version number' do @@ -57,6 +55,11 @@ expect { described_class.start(%w[initialise]) } .to output(/Add accounts to your test keychain with:/).to_stdout end + + it 'initialises the keychain by default' do + expect { described_class.start([]) } + .to output(/Add accounts to your test keychain with:/).to_stdout + end end context 'when no accounts or roles are set' do @@ -66,6 +69,11 @@ allow(Awskeyring).to receive(:prefs).and_return('{"awskeyring": "awskeyringtest"}') end + it 'outputs help text by default' do + expect { described_class.start([]) } + .to output(/^ \w+ --version, -v\s+# Prints the version/).to_stdout + end + it 'tells you that you must add accounts' do expect { described_class.start(%w[list]) }.to raise_error .and output(/No accounts added, run `\w+ add` to add./).to_stderr