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

- old
+ new

@@ -35,15 +35,17 @@ keychain = 'awskeyring' if keychain.empty? puts 'Creating a new Keychain, you will be prompted for a password for it.' Awskeyring.init_keychain(awskeyring: keychain) + exec_name = File.basename($PROGRAM_NAME) + puts 'Your keychain has been initialised. It will auto-lock after 5 minutes' puts 'and when sleeping. Use Keychain Access to adjust.' puts puts "Add accounts to your #{keychain} keychain with:" - puts " #{$PROGRAM_NAME} add" + puts " #{exec_name} add" end desc 'list', 'Prints a list of accounts in the keyring' def list puts Awskeyring.list_item_names.join("\n") @@ -55,11 +57,11 @@ puts Awskeyring.list_role_names.join("\n") end desc 'env ACCOUNT', 'Outputs bourne shell environment exports for an ACCOUNT' def env(account = nil) - account = ask_missing(existing: account, message: 'account name') + 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? put_env_string( account: cred.attributes[:label], key: cred.attributes[:account], @@ -84,68 +86,107 @@ 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) - account = ask_missing(existing: account, message: 'account name') - key = ask_missing(existing: options[:key], message: 'access key id') - secret = ask_missing(existing: options[:secret], message: 'secret access key', secure: true) - mfa = ask_missing(existing: options[:mfa], message: 'mfa arn', optional: true) + 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)) + secret = ask_check( + existing: options[:secret], message: 'secret access key', + secure: true, validator: Awskeyring.method(:secret_access_key) + ) + mfa = ask_check( + existing: options[:mfa], message: 'mfa arn', optional: true, validator: Awskeyring.method(:mfa_arn) + ) Awskeyring.add_item( account: account, key: key, secret: secret, comment: 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.' def add_role(role = nil) - role = ask_missing(existing: role, message: 'role name') - arn = ask_missing(existing: options[:arn], message: 'role arn') - account = ask_missing(existing: account, message: 'account', optional: true) + 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)) + account = ask_check( + existing: account, message: 'account', optional: true, validator: Awskeyring.method(:account_name) + ) Awskeyring.add_role( role: role, arn: arn, account: account ) + puts "# Added role #{role}" end desc 'remove ACCOUNT', 'Removes an ACCOUNT from the keyring' def remove(account = nil) - account = ask_missing(existing: account, message: 'account name') + 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}") end desc 'remove-token ACCOUNT', 'Removes a token for ACCOUNT from the keyring' def remove_token(account = nil) - account = ask_missing(existing: account, message: 'account name') + 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 end map 'remove-role' => :remove_role desc 'remove-role ROLE', 'Removes a ROLE from the keyring' def remove_role(role = nil) - role = ask_missing(existing: role, message: 'role name') + 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}") 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] + ) + retry_backoff do + iam.delete_access_key( + access_key_id: item.attributes[:account] + ) + end + Awskeyring.update_item( + account: account, + key: new_key[:access_key][:access_key_id], + secret: new_key[:access_key][:secret_access_key] + ) + + 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.' def token(account = nil, role = nil, code = nil) # rubocop:disable all - account = ask_missing(existing: account, message: 'account name') + account = ask_check(existing: account, message: 'account name', validator: Awskeyring.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 @@ -204,11 +245,11 @@ 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_missing(existing: account, message: 'account name') + 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? path = options[:path] || 'console' @@ -330,9 +371,37 @@ env_var = env_vars(account: account, key: key, secret: secret, token: token) env_var.each { |var, value| puts "export #{var}=\"#{value}\"" } puts 'unset AWS_SECURITY_TOKEN' unless token puts 'unset AWS_SESSION_TOKEN' unless token + end + + def ask_check(existing:, message:, secure: false, optional: false, validator: nil) + retries ||= 3 + begin + value = ask_missing(existing: existing, message: message, secure: secure, optional: optional) + value = validator.call(value) unless value.empty? && optional + rescue RuntimeError => e + 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