Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Improve quota awareness #4

Open
wants to merge 6 commits into
base: master
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions .ruby-version
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
2.6.6
4 changes: 2 additions & 2 deletions .travis.yml
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
sudo: false
language: ruby
rvm:
- 2.2.4
before_install: gem install bundler -v 1.13.3
- 2.6.6
before_install: gem install bundler -v 2.1.4
after_success: bundle exec codeclimate-test-reporter
env:
matrix:
Expand Down
2 changes: 1 addition & 1 deletion aws-rotate-keys.gemspec
Original file line number Diff line number Diff line change
Expand Up @@ -23,7 +23,7 @@ Gem::Specification.new do |spec|

spec.add_dependency "aws-sdk", "~> 2"

spec.add_development_dependency "bundler", "~> 1.13"
spec.add_development_dependency "bundler", "~> 2.0"
spec.add_development_dependency "rake", "~> 10.0"
spec.add_development_dependency "rspec", "~> 3.0"
spec.add_development_dependency "simplecov"
Expand Down
29 changes: 28 additions & 1 deletion exe/aws-rotate-keys
Original file line number Diff line number Diff line change
@@ -1,5 +1,32 @@
#!/usr/bin/env ruby

require "aws_rotate_keys"
require "optparse"

AwsRotateKeys::CLI.call
# Use some basic parsing to allow command-line overrides of config
class Parser
def self.parse(options)
output_options = {
delete_inactive: false
}

opt_parser = OptionParser.new do |opts|
opts.banner = "Usage: #{File.basename(__FILE__)} [options]"

opts.on("--delete-inactive", "If the key quota is full but one is inactive, delete that key to make room") do |p|
output_options[:delete_inactive] = p
end

opts.on("-h", "--help", "Prints this help") do
puts opts
exit
end
end

opt_parser.parse!(options)
output_options
end
end

cli_options = (Parser.parse ARGV)
AwsRotateKeys::CLI.call(options: cli_options)
77 changes: 48 additions & 29 deletions lib/aws_rotate_keys/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,8 @@

module AwsRotateKeys
class CLI
AWS_ENVIRONMENT_VARIABLES = ['AWS_ACCESS_KEY_ID', 'AWS_SECRET_ACCESS_KEY'].freeze

def self.call(*args)
new(*args).call
end
Expand All @@ -12,35 +14,54 @@ def self.call(*args)
def initialize(iam: Aws::IAM::Client.new,
credentials_path: "#{Dir.home}/.aws/credentials",
stdout: $stdout,
env: ENV)
env: ENV,
options: {})
@iam = iam
@credentials_path = credentials_path
@stdout = stdout
@env = env
@options = options
end

def call
log "Reading key quota..."
quota = access_key_quota

log "Reading existing keys..."
access_keys = aws_access_keys

if quota <= access_keys.size
log "Key set is already at quota limit of #{quota}:"
log_keylist(access_keys)

inactive_keys = access_keys.select { |k| k["status"] == "Inactive" }
if @options[:delete_inactive] && !inactive_keys.empty?
log "Deleting oldest inactive access key as requested..."
log_keylist(inactive_keys)
delete_oldest_access_key(inactive_keys)
else
raise "You must manually delete a key or use one of the command-line overrides"
end
end

log "Creating access key..."
new_key = create_access_key
access_keys = aws_access_keys # refresh key list

create_credentials_directory_if_needed

if credentials_file_exists?
if File.exist?(credentials_path)
log "Backing up #{credentials_path} to #{credentials_backup_path}..."
backup_aws_credentials_file
FileUtils.cp(credentials_path, credentials_backup_path)
end

log "Writing new access key to #{credentials_path}"
write_aws_credentials_file(new_key)

log "Deleting your oldest access key..."
delete_oldest_access_key
delete_oldest_access_key(access_keys)

log "You're all set!"
log aws_environment_variables_warning_message if aws_environment_variables?

if aws_environment_variables?
log aws_environment_variables_warning_message
end
log "You're all set!"
end

private
Expand All @@ -50,53 +71,51 @@ def create_access_key
create_access_key_response.access_key
end

def create_credentials_directory_if_needed
FileUtils.mkdir_p(credentials_dir)
end

def credentials_file_exists?
File.exist?(credentials_path)
end

# ex. ~/aws/credentials.bkp-2017-01-06-16-38-07--0800
def credentials_backup_path
credentials_path + ".bkp-#{Time.now.to_s.gsub(/[^\d]/, '-')}"
end

def backup_aws_credentials_file
FileUtils.cp(credentials_path, credentials_backup_path)
end

def write_aws_credentials_file(access_key)
FileUtils.mkdir_p(File.dirname(credentials_path)) # ensure credentials directory exists

File.open(credentials_path, "w") do |f|
f.puts "[default]"
f.puts "aws_access_key_id = #{access_key.access_key_id}"
f.puts "aws_secret_access_key = #{access_key.secret_access_key}"
end
end

def delete_oldest_access_key
def access_key_quota
ret = @iam.get_account_summary.summary_map["AccessKeysPerUserQuota"]
end

def aws_access_keys
list_access_keys_response = iam.list_access_keys
access_keys = list_access_keys_response.access_key_metadata
list_access_keys_response.access_key_metadata
end

oldest_access_key = access_keys.sort_by(&:create_date).first
def delete_oldest_access_key(access_key_list)
oldest_access_key = access_key_list.min_by(&:create_date)
iam.delete_access_key(access_key_id: oldest_access_key.access_key_id)
end

def credentials_dir
File.dirname(credentials_path)
def log_keylist(access_keys)
access_keys.each do |k|
log " #{k['create_date']} #{k['access_key_id']} #{k['status']}"
end
end

def log(msg)
stdout.puts msg
end

def aws_environment_variables?
env['AWS_ACCESS_KEY_ID'] || env['AWS_SECRET_ACCESS_KEY']
AWS_ENVIRONMENT_VARIABLES.any? { |v| env.key?(v) }
end

def aws_environment_variables_warning_message
"We've noticed that the environment variables AWS_ACCESS_KEY_ID and AWS_SECRET_ACCESS_KEY are set.\n" +
"We've noticed that the environment variables #{AWS_ENVIRONMENT_VARIABLES} are set.\n" +
"Please remove them so that aws cli and libraries use #{credentials_path} instead."
end
end
Expand Down
62 changes: 32 additions & 30 deletions spec/aws_rotate_keys_spec.rb
Original file line number Diff line number Diff line change
@@ -1,34 +1,50 @@
require "spec_helper"
require "myio"

describe AwsRotateKeys do
OLD_KEY_ID = "OLDKEY".freeze
NEW_KEY_ID = "KEY123".freeze
NEW_SECRET = "SECRET123".freeze

class IAMDouble
def initialize
@keys = [
Aws::IAM::Types::AccessKeyMetadata.new(
access_key_id: OLD_KEY_ID,
create_date: Time.new(2017, 1, 1)
)
]
end

def create_access_key
@keys << Aws::IAM::Types::AccessKeyMetadata.new(
access_key_id: NEW_KEY_ID,
create_date: Time.new(2017, 2, 1)
)

Aws::IAM::Types::CreateAccessKeyResponse.new(
access_key: Aws::IAM::Types::AccessKey.new(
access_key_id: "KEY123",
secret_access_key: "SECRET123"
access_key_id: NEW_KEY_ID,
secret_access_key: NEW_SECRET
)
)
end

def list_access_keys
Aws::IAM::Types::ListAccessKeysResponse.new(
access_key_metadata: [
Aws::IAM::Types::AccessKeyMetadata.new(
access_key_id: "KEY123",
create_date: Time.new(2017, 2, 1)
),
Aws::IAM::Types::AccessKeyMetadata.new(
access_key_id: "OLDKEY",
create_date: Time.new(2017, 1, 1)
)
]
access_key_metadata: @keys
)
end

def delete_access_key(access_key_id:)
raise "Expected to delete access key 'OLDKEY' but was #{access_key_id}" unless access_key_id == "OLDKEY"
def get_account_summary
Aws::IAM::Types::GetAccountSummaryResponse.new(
summary_map: {
"AccessKeysPerUserQuota" => 2
}
)
end

def delete_access_key(access_key_id:); end
end

let(:iam_double) { IAMDouble.new }
Expand All @@ -44,7 +60,7 @@ def rotate_keys(args = {})
end

before do
expect(iam_double).to receive(:delete_access_key).with(access_key_id: "OLDKEY")
expect(iam_double).to receive(:delete_access_key).with(access_key_id: OLD_KEY_ID)
end

context "when no credentials" do
Expand All @@ -57,7 +73,7 @@ def rotate_keys(args = {})

credentials_content = File.read(credentials_path)

expect(credentials_content).to eq "[default]\naws_access_key_id = KEY123\naws_secret_access_key = SECRET123\n"
expect(credentials_content).to eq "[default]\naws_access_key_id = #{NEW_KEY_ID}\naws_secret_access_key = #{NEW_SECRET}\n"
end
end

Expand Down Expand Up @@ -96,17 +112,3 @@ def rotate_keys(args = {})
end
end
end

class MyIO
def initialize
@content = ""
end

def puts(msg)
@content << msg + "\n"
end

def to_s
@content
end
end
Loading