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