lib/awskeyring_command.rb in awskeyring-0.0.6 vs lib/awskeyring_command.rb in awskeyring-0.1.0

- old
+ new

@@ -1,33 +1,33 @@ -require 'aws-sdk-iam' -require 'cgi' require 'highline' -require 'json' -require 'open-uri' require 'thor' -require_relative 'awskeyring' +require 'awskeyring' +require 'awskeyring/awsapi' +require 'awskeyring/validate' require 'awskeyring/version' -# AWS Key-ring command line interface. +# AWSkeyring command line interface. class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength map %w[--version -v] => :__version map ['init'] => :initialise map ['ls'] => :list map ['lsr'] => :list_role map ['rm'] => :remove map ['rmr'] => :remove_role map ['rmt'] => :remove_token desc '--version, -v', 'Prints the version' + # print the version number def __version puts Awskeyring::VERSION end desc 'initialise', 'Initialises a new KEYCHAIN' method_option :keychain, type: :string, aliases: '-n', desc: 'Name of KEYCHAIN to initialise.' - def initialise # rubocop:disable Metrics/AbcSize + # initialise the keychain + def initialise unless Awskeyring.prefs.empty? puts "#{Awskeyring::PREFS_FILE} exists. no need to initialise." exit 1 end @@ -45,79 +45,89 @@ puts "Add accounts to your #{keychain} keychain with:" puts " #{exec_name} add" end desc 'list', 'Prints a list of accounts in the keyring' + # list the accounts def list - puts Awskeyring.list_item_names.join("\n") + puts Awskeyring.list_account_names.join("\n") end map 'list-role' => :list_role desc 'list-role', 'Prints a list of roles in the keyring' + # List roles def list_role puts Awskeyring.list_role_names.join("\n") end desc 'env ACCOUNT', 'Outputs bourne shell environment exports for an ACCOUNT' + # Print Env vars def env(account = nil) - account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name)) - cred, temp_cred = get_valid_item_pair(account: account) - token = temp_cred.password unless temp_cred.nil? + account = ask_check( + existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name) + ) + cred = Awskeyring.get_valid_creds(account: account) put_env_string( - account: cred.attributes[:label], - key: cred.attributes[:account], - secret: cred.password, - token: token + account: cred[:account], + key: cred[:key], + secret: cred[:secret], + token: cred[:token] ) end desc 'exec ACCOUNT command...', 'Execute a COMMAND with the environment set for an ACCOUNT' + # execute an external command with env set def exec(account, *command) - cred, temp_cred = get_valid_item_pair(account: account) - token = temp_cred.password unless temp_cred.nil? + cred = Awskeyring.get_valid_creds(account: account) env_vars = env_vars( - account: cred.attributes[:label], - key: cred.attributes[:account], - secret: cred.password, - token: token + account: cred[:account], + key: cred[:key], + secret: cred[:secret], + token: cred[:token] ) pid = Process.spawn(env_vars, command.join(' ')) Process.wait pid end desc 'add ACCOUNT', 'Adds an ACCOUNT to the keyring' method_option :key, type: :string, aliases: '-k', desc: 'AWS account key id.' method_option :secret, type: :string, aliases: '-s', desc: 'AWS account secret.' method_option :mfa, type: :string, aliases: '-m', desc: 'AWS virtual mfa arn.' - def add(account = nil) # rubocop:disable Metrics/AbcSize - account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name)) - key = ask_check(existing: options[:key], message: 'access key id', validator: Awskeyring.method(:access_key)) + # Add an Account + def add(account = nil) # rubocop:disable Metrics/MethodLength + account = ask_check( + existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name) + ) + key = ask_check( + existing: options[:key], message: 'access key id', validator: Awskeyring::Validate.method(:access_key) + ) secret = ask_check( existing: options[:secret], message: 'secret access key', - secure: true, validator: Awskeyring.method(:secret_access_key) + secure: true, validator: Awskeyring::Validate.method(:secret_access_key) ) mfa = ask_check( - existing: options[:mfa], message: 'mfa arn', optional: true, validator: Awskeyring.method(:mfa_arn) + existing: options[:mfa], message: 'mfa arn', optional: true, validator: Awskeyring::Validate.method(:mfa_arn) ) - Awskeyring.add_item( + Awskeyring.add_account( account: account, key: key, secret: secret, - comment: mfa + mfa: mfa ) puts "# Added account #{account}" end map 'add-role' => :add_role desc 'add-role ROLE', 'Adds a ROLE to the keyring' method_option :arn, type: :string, aliases: '-a', desc: 'AWS role arn.' + # Add a role def add_role(role = nil) - role = ask_check(existing: role, message: 'role name', validator: Awskeyring.method(:role_name)) - arn = ask_check(existing: options[:arn], message: 'role arn', validator: Awskeyring.method(:role_arn)) + role = ask_check(existing: role, message: 'role name', validator: Awskeyring::Validate.method(:role_name)) + arn = ask_check(existing: options[:arn], message: 'role arn', validator: Awskeyring::Validate.method(:role_arn)) account = ask_check( - existing: account, message: 'account', optional: true, validator: Awskeyring.method(:account_name) + existing: account, message: 'account', optional: true, validator: Awskeyring::Validate.method(:account_name) ) Awskeyring.add_role( role: role, arn: arn, @@ -125,68 +135,61 @@ ) puts "# Added role #{role}" end desc 'remove ACCOUNT', 'Removes an ACCOUNT from the keyring' + # Remove an account def remove(account = nil) - account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name)) - cred, temp_cred = get_valid_item_pair(account: account) - Awskeyring.delete_pair(cred, temp_cred, "# Removing account #{account}") + account = ask_check( + existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name) + ) + Awskeyring.delete_account(account: account, message: "# Removing account #{account}") end desc 'remove-token ACCOUNT', 'Removes a token for ACCOUNT from the keyring' + # remove a session token def remove_token(account = nil) - account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name)) - session_key, session_token = Awskeyring.get_pair(account) - session_key, session_token = Awskeyring.delete_expired(session_key, session_token) if session_key - Awskeyring.delete_pair(session_key, session_token, "# Removing token for account #{account}") if session_key + account = ask_check( + existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name) + ) + Awskeyring.delete_token(account: account, message: "# Removing token for account #{account}") end map 'remove-role' => :remove_role desc 'remove-role ROLE', 'Removes a ROLE from the keyring' + # remove a role def remove_role(role = nil) - role = ask_check(existing: role, message: 'role name', validator: Awskeyring.method(:role_name)) - item_role = Awskeyring.get_role(role) - Awskeyring.delete_pair(item_role, nil, "# Removing role #{role}") + role = ask_check(existing: role, message: 'role name', validator: Awskeyring::Validate.method(:role_name)) + Awskeyring.delete_role(role_name: role, message: "# Removing role #{role}") end desc 'rotate ACCOUNT', 'Rotate access keys for an ACCOUNT' - def rotate(account = nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength - account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name)) - item = Awskeyring.get_item(account) - iam = Aws::IAM::Client.new(access_key_id: item.attributes[:account], secret_access_key: item.password) - - if iam.list_access_keys[:access_key_metadata].length > 1 - warn "You have two access keys for account #{account}" - exit 1 - end - - new_key = iam.create_access_key - iam = Aws::IAM::Client.new( - access_key_id: new_key[:access_key][:access_key_id], - secret_access_key: new_key[:access_key][:secret_access_key] + # rotate Account keys + def rotate(account = nil) + account = ask_check( + existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name) ) - retry_backoff do - iam.delete_access_key( - access_key_id: item.attributes[:account] - ) - end - Awskeyring.update_item( + item_hash = Awskeyring.get_account_hash(account: account) + new_key = Awskeyring::Awsapi.rotate(account: item_hash[:account], key: item_hash[:key], secret: item_hash[:secret]) + Awskeyring.update_account( account: account, - key: new_key[:access_key][:access_key_id], - secret: new_key[:access_key][:secret_access_key] + key: new_key[:key], + secret: new_key[:secret] ) puts "# Updated account #{account}" end desc 'token ACCOUNT [ROLE] [MFA]', 'Create an STS Token from a ROLE or an MFA code' method_option :role, type: :string, aliases: '-r', desc: 'The ROLE to assume.' method_option :code, type: :string, aliases: '-c', desc: 'Virtual mfa CODE.' method_option :duration, type: :string, aliases: '-d', desc: 'Session DURATION in seconds.' + # generate a sessiopn token def token(account = nil, role = nil, code = nil) # rubocop:disable all - account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name)) + account = ask_check( + existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name) + ) role ||= options[:role] code ||= options[:code] duration = options[:duration] duration ||= (60 * 60 * 1).to_s if role duration ||= (60 * 60 * 12).to_s if code @@ -194,115 +197,62 @@ if !role && !code warn 'Please use either a role or a code' exit 2 end - session_key, session_token = Awskeyring.get_pair(account) - Awskeyring.delete_pair(session_key, session_token, '# Removing STS credentials') if session_key + Awskeyring.delete_token(account: account, message: '# Removing STS credentials') - item = Awskeyring.get_item(account) - item_role = Awskeyring.get_role(role) if role + item_hash = Awskeyring.get_account_hash(account: account) + role_arn = Awskeyring.get_role_arn(role_name: role) if role - sts = Aws::STS::Client.new(access_key_id: item.attributes[:account], secret_access_key: item.password) + new_creds = Awskeyring::Awsapi.get_token( + code: code, + role_arn: role_arn, + duration: duration, + mfa: item_hash[:mfa], + key: item_hash[:key], + secret: item_hash[:secret], + user: ENV['USER'] + ) - begin - response = - if code && role - sts.assume_role( - duration_seconds: duration.to_i, - role_arn: item_role.attributes[:account], - role_session_name: ENV['USER'], - serial_number: item.attributes[:comment], - token_code: code - ) - elsif role - sts.assume_role( - duration_seconds: duration.to_i, - role_arn: item_role.attributes[:account], - role_session_name: ENV['USER'] - ) - elsif code - sts.get_session_token( - duration_seconds: duration.to_i, - serial_number: item.attributes[:comment], - token_code: code - ) - end - rescue Aws::STS::Errors::AccessDenied => e - puts e.to_s - exit 1 - end - - Awskeyring.add_pair( + Awskeyring.add_token( account: account, - key: response.credentials[:access_key_id], - secret: response.credentials[:secret_access_key], - token: response.credentials[:session_token], - expiry: response.credentials[:expiration].to_i.to_s, + key: new_creds[:key], + secret: new_creds[:secret], + token: new_creds[:token], + expiry: new_creds[:expiry].to_i.to_s, role: role ) - puts "Authentication valid until #{response.credentials[:expiration]}" + puts "Authentication valid until #{new_creds[:expiry]}" end desc 'console ACCOUNT', 'Open the AWS Console for the ACCOUNT' method_option :path, type: :string, aliases: '-p', desc: 'The service PATH to open.' - def console(account = nil) # rubocop:disable all - account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name)) - cred, temp_cred = get_valid_item_pair(account: account) - token = temp_cred.password unless temp_cred.nil? + # Open the AWS Console + def console(account = nil) + account = ask_check( + existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name) + ) + cred = Awskeyring.get_valid_creds(account: account) path = options[:path] || 'console' - console_url = "https://console.aws.amazon.com/#{path}/home" - signin_url = 'https://signin.aws.amazon.com/federation' - policy_json = { - Version: '2012-10-17', - Statement: [{ - Action: '*', - Resource: '*', - Effect: 'Allow' - }] - }.to_json + login_url = Awskeyring::Awsapi.get_login_url( + key: cred[:key], + secret: cred[:secret], + token: cred[:token], + path: path, + user: ENV['USER'] + ) - if temp_cred - session_json = { - sessionId: cred.attributes[:account], - sessionKey: cred.password, - sessionToken: token - }.to_json - else - sts = Aws::STS::Client.new(access_key_id: cred.attributes[:account], - secret_access_key: cred.password) - - session = sts.get_federation_token(name: ENV['USER'], - policy: policy_json, - duration_seconds: (60 * 60 * 12)) - session_json = { - sessionId: session.credentials[:access_key_id], - sessionKey: session.credentials[:secret_access_key], - sessionToken: session.credentials[:session_token] - }.to_json - - end - get_signin_token_url = signin_url + '?Action=getSigninToken' \ - '&Session=' + CGI.escape(session_json) - - returned_content = open(get_signin_token_url).read - - signin_token = JSON.parse(returned_content)['SigninToken'] - signin_token_param = '&SigninToken=' + CGI.escape(signin_token) - destination_param = '&Destination=' + CGI.escape(console_url) - - login_url = signin_url + '?Action=login' + signin_token_param + destination_param - pid = Process.spawn("open \"#{login_url}\"") Process.wait pid end - # autocomplete desc 'awskeyring CURR PREV', 'Autocompletion for bourne shells', hide: true + # autocomplete def awskeyring(curr, prev) comp_line = ENV['COMP_LINE'] unless comp_line exec_name = File.basename($PROGRAM_NAME) warn "enable autocomplete with 'complete -C /path-to-command/#{exec_name} #{exec_name}'" @@ -316,16 +266,16 @@ print_auto_resp(curr, comp_len) end private - def print_auto_resp(curr, len) # rubocop:disable Metrics/AbcSize + def print_auto_resp(curr, len) case len when 2 puts list_commands.select { |elem| elem.start_with?(curr) }.join("\n") when 3 - puts Awskeyring.list_item_names.select { |elem| elem.start_with?(curr) }.join("\n") + puts Awskeyring.list_account_names.select { |elem| elem.start_with?(curr) }.join("\n") when 4 puts Awskeyring.list_role_names.select { |elem| elem.start_with?(curr) }.join("\n") else exit 1 end @@ -333,27 +283,10 @@ def list_commands self.class.all_commands.keys.map { |elem| elem.tr('_', '-') } end - def get_valid_item_pair(account:) - session_key, session_token = Awskeyring.get_pair(account) - session_key, session_token = Awskeyring.delete_expired(session_key, session_token) if session_key - - if session_key && session_token - puts '# Using temporary session credentials' - return session_key, session_token - end - - item = Awskeyring.get_item(account) - if item.nil? - warn "# Credential not found with name: #{account}" - exit 2 - end - [item, nil] - end - def env_vars(account:, key:, secret:, token:) env_var = {} env_var['AWS_DEFAULT_REGION'] = 'us-east-1' unless ENV['AWS_DEFAULT_REGION'] env_var['AWS_ACCOUNT_NAME'] = account env_var['AWS_ACCESS_KEY_ID'] = key @@ -384,24 +317,9 @@ warn e.message retry unless (retries -= 1).zero? exit 1 end value - end - - def retry_backoff(&block) - retries ||= 1 - begin - yield block - rescue Aws::IAM::Errors::InvalidClientTokenId => e - if retries < 4 - sleep 2**retries - retries += 1 - retry - end - warn e.message - exit 1 - end end def ask_missing(existing:, message:, secure: false, optional: false) existing || ask(message: message, secure: secure, optional: optional) end