awskeyring 0.0.5 → 0.0.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA1:
3
- metadata.gz: f560b1fef85ec599c9debb10eace0e76746e64d7
4
- data.tar.gz: 356f577f81a1fd72ef6571e8aaf64e6bd6829a95
3
+ metadata.gz: 2aadd847341fb6a4dd7016dfca3fdd1ba60fe4c9
4
+ data.tar.gz: a8e548fb70b847b3fc7d7e1a277a4d12a054029c
5
5
  SHA512:
6
- metadata.gz: 7cc1ff83f60d58b3f5c9fd9ec104657425f5719649a6b28e88ce8fd6ccd46e6f550e64314129cfbeca4c0e78c0c5a809b30c93642a53f887ef2bcf2e6a2aef9f
7
- data.tar.gz: 70f3b813b8f6c3886bb83cc290e5b065351a16f365f00a2d40761811e18bf45a4768d732443e54c195e6069b6a4c6544e1117dade9a4fc9eb5acc25db85e728b
6
+ metadata.gz: 60a505a585398dc8e6034bbc51c4deef834f650fb76a042bc248f338dfbdb3e6354dab42d2536f404f93bee4785f21dfe5bc9fd4fb2c2a3b7cad52a297fc1414
7
+ data.tar.gz: 8a282201ceb09a170e1beeb2dd3ecc57dc4058ec669d5b3cb9cc1d9ce33a99ad6b3c09ad42e5c1146f504ee6559eb4c258857961a5e864709a8a5741d5bd15e0
data/.rubocop.yml CHANGED
@@ -1,3 +1,4 @@
1
+ ---
1
2
  Metrics/LineLength:
2
3
  Max: 120
3
4
 
@@ -6,7 +7,7 @@ Metrics/MethodLength:
6
7
 
7
8
  Metrics/BlockLength:
8
9
  Exclude:
9
- - spec/*/*
10
+ - spec/**/*
10
11
 
11
12
  Naming/FileName:
12
13
  Exclude:
data/.travis.yml CHANGED
@@ -1,8 +1,18 @@
1
+ ---
1
2
  language: ruby
2
3
  os: osx
3
4
  rvm:
4
- - 2.3.3
5
+ - 2.3.3
5
6
  before_install: gem install bundler
6
7
  notifications:
7
8
  slack:
8
- secure: okKLlQ93ogj8ut18MZazQD63XTIzCpTfneYQMlKPaLU5HnooAkUFmEpSN8igTJD0GuEQ7Jf8+BkFlhECWl1FwtzIW6z/yMadiiLwY6qA/O1ZVVYZmkS4kpcKtCMrvO0Hf3iPLXpJX0sQGfGsMsho6NZuNs0dzlrr4+HX/cEGmDXBocDDxdv2d25HzHtqt4l+4axeJ+PJdHDmYlDzhtMAXhkGoPfzws7MPvkVcqY0eZRW2WqccO52zlQrBNcphp7BI8mLTW/BwkEY9YndJf2xoBaoEOuaIJbGgHbmskGMRui3vZnd08/fiWkNsCI/EhB5BDJ41bwobsHEtuqu+Qx92kI+hzjtU4D16k1v/6HDm61T6gh6BTp0QEXJeOkiecjVGyzrMlBf5BEyIB7JfLvGPa7RUbvvvUxFGcEtnSbUZ49cXAG7cKiiSgcNOocCrMNIRAe864Tm606Mud1RbOv8tEiS9BJ96ktOpOfvrYPyYMRFTte2gRLwNDwcpXIWTI39CXD2EBxSfRK/OqmggLYMG5AKhxyS/Vl7E2p9gii6syB0LnpFBIZ6t+OSve7fZtCU/wruC5IrI/vHtWfApvADYKsXG6FEYJoJ1LsCDjIRlrkwV1dEVsp/HSSO6zRh9KhJ3b7yzL97Dyi6dCi+nM7aKGrox/8kvcn9alrdRt8sz3c=
9
+ secure: "okKLlQ93ogj8ut18MZazQD63XTIzCpTfneYQMlKPaLU5HnooAkUFmEpSN8ig\
10
+ TJD0GuEQ7Jf8+BkFlhECWl1FwtzIW6z/yMadiiLwY6qA/O1ZVVYZmkS4kpcKtCMrvO0Hf3iPL\
11
+ XpJX0sQGfGsMsho6NZuNs0dzlrr4+HX/cEGmDXBocDDxdv2d25HzHtqt4l+4axeJ+PJdHDmYl\
12
+ DzhtMAXhkGoPfzws7MPvkVcqY0eZRW2WqccO52zlQrBNcphp7BI8mLTW/BwkEY9YndJf2xoBa\
13
+ oEOuaIJbGgHbmskGMRui3vZnd08/fiWkNsCI/EhB5BDJ41bwobsHEtuqu+Qx92kI+hzjtU4D1\
14
+ 6k1v/6HDm61T6gh6BTp0QEXJeOkiecjVGyzrMlBf5BEyIB7JfLvGPa7RUbvvvUxFGcEtnSbUZ\
15
+ 49cXAG7cKiiSgcNOocCrMNIRAe864Tm606Mud1RbOv8tEiS9BJ96ktOpOfvrYPyYMRFTte2gR\
16
+ LwNDwcpXIWTI39CXD2EBxSfRK/OqmggLYMG5AKhxyS/Vl7E2p9gii6syB0LnpFBIZ6t+OSve7\
17
+ fZtCU/wruC5IrI/vHtWfApvADYKsXG6FEYJoJ1LsCDjIRlrkwV1dEVsp/HSSO6zRh9KhJ3b7y\
18
+ zL97Dyi6dCi+nM7aKGrox/8kvcn9alrdRt8sz3c="
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Change Log
2
2
 
3
+ ## [v0.0.6](https://github.com/vibrato/awskeyring/tree/v0.0.6) (2018-03-01)
4
+ [Full Changelog](https://github.com/vibrato/awskeyring/compare/v0.0.5...v0.0.6)
5
+
6
+ **Implemented enhancements:**
7
+
8
+ - Credential Rotation Feature [\#4](https://github.com/vibrato/awskeyring/issues/4)
9
+ - Rotate credentials feature. [\#11](https://github.com/vibrato/awskeyring/pull/11) ([tristanmorgan](https://github.com/tristanmorgan))
10
+
11
+ **Merged pull requests:**
12
+
13
+ - Input validation [\#10](https://github.com/vibrato/awskeyring/pull/10) ([tristanmorgan](https://github.com/tristanmorgan))
14
+ - Adding a check for incorrect file modes. [\#9](https://github.com/vibrato/awskeyring/pull/9) ([tristanmorgan](https://github.com/tristanmorgan))
15
+
3
16
  ## [v0.0.5](https://github.com/vibrato/awskeyring/tree/v0.0.5) (2018-02-15)
4
17
  [Full Changelog](https://github.com/vibrato/awskeyring/compare/v0.0.4...v0.0.5)
5
18
 
data/README.md CHANGED
@@ -56,6 +56,7 @@ The CLI is using [Thor](http://whatisthor.com) with help provided interactively.
56
56
  awskeyring remove ACCOUNT # Removes an ACCOUNT from the keyring
57
57
  awskeyring remove-role ROLE # Removes a ROLE from the keyring
58
58
  awskeyring remove-token ACCOUNT # Removes a token for ACCOUNT from the keyring
59
+ awskeyring rotate ACCOUNT # Rotate access keys for an ACCOUNT
59
60
  awskeyring token ACCOUNT [ROLE] [MFA] # Create an STS Token from a ROLE or an MFA code
60
61
 
61
62
  and autocomplete that can be installed with:
data/Rakefile CHANGED
@@ -13,4 +13,18 @@ end
13
13
 
14
14
  RSpec::Core::RakeTask.new(:spec)
15
15
 
16
- task default: %i[rubocop spec]
16
+ desc 'Check filemode bits'
17
+ task :filemode do
18
+ files = Dir.glob('**/*')
19
+ failure = false
20
+ files.each do |file|
21
+ mode = File.stat(file).mode
22
+ if (mode & 0x7) != (mode >> 3 & 0x7)
23
+ puts file
24
+ failure = true
25
+ end
26
+ end
27
+ abort 'Error: Incorrect file mode found' if failure
28
+ end
29
+
30
+ task default: %i[filemode rubocop spec]
data/lib/awskeyring.rb CHANGED
@@ -1,9 +1,10 @@
1
1
  require 'keychain'
2
2
  require 'aws-sdk-iam'
3
+ require 'awskeyring/validate'
3
4
 
4
5
  # Aws Key-ring logical object,
5
6
  # gives you an interface to access keychains and items.
6
- module Awskeyring
7
+ module Awskeyring # rubocop:disable Metrics/ModuleLength
7
8
  PREFS_FILE = (File.expand_path '~/.awskeyring').freeze
8
9
  ROLE_PREFIX = 'role '.freeze
9
10
  ACCOUNT_PREFIX = 'account '.freeze
@@ -65,6 +66,13 @@ module Awskeyring
65
66
  )
66
67
  end
67
68
 
69
+ def self.update_item(account:, key:, secret:)
70
+ item = get_item(account)
71
+ item.attributes[:account] = key
72
+ item.password = secret
73
+ item.save!
74
+ end
75
+
68
76
  def self.add_role(role:, arn:, account:)
69
77
  all_items.create(
70
78
  label: "#{ROLE_PREFIX}#{role}",
@@ -0,0 +1,32 @@
1
+ # Validation methods
2
+ module Awskeyring
3
+ def self.account_name(account_name)
4
+ raise 'Invalid Account Name' unless account_name =~ /\S+/
5
+ account_name
6
+ end
7
+
8
+ def self.access_key(aws_access_key)
9
+ raise 'Invalid Access Key' unless aws_access_key =~ /\AAKIA[A-Z0-9]{12,16}\z/
10
+ aws_access_key
11
+ end
12
+
13
+ def self.secret_access_key(aws_secret_access_key)
14
+ raise 'Secret Access Key is not 40 chars' if aws_secret_access_key.length != 40
15
+ aws_secret_access_key
16
+ end
17
+
18
+ def self.mfa_arn(mfa_arn)
19
+ raise 'Invalid MFA ARN' unless mfa_arn =~ %r(\Aarn:aws:iam::[0-9]{12}:mfa\/\S*\z)
20
+ mfa_arn
21
+ end
22
+
23
+ def self.role_name(account_name)
24
+ raise 'Invalid Role Name' unless account_name =~ /\S+/
25
+ account_name
26
+ end
27
+
28
+ def self.role_arn(role_arn)
29
+ raise 'Invalid Role ARN' unless role_arn =~ %r(\Aarn:aws:iam::[0-9]{12}:role\/\S*\z)
30
+ role_arn
31
+ end
32
+ end
@@ -1,3 +1,3 @@
1
1
  module Awskeyring
2
- VERSION = '0.0.5'.freeze
2
+ VERSION = '0.0.6'.freeze
3
3
  end
@@ -37,11 +37,13 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
37
37
  puts 'Creating a new Keychain, you will be prompted for a password for it.'
38
38
  Awskeyring.init_keychain(awskeyring: keychain)
39
39
 
40
+ exec_name = File.basename($PROGRAM_NAME)
41
+
40
42
  puts 'Your keychain has been initialised. It will auto-lock after 5 minutes'
41
43
  puts 'and when sleeping. Use Keychain Access to adjust.'
42
44
  puts
43
45
  puts "Add accounts to your #{keychain} keychain with:"
44
- puts " #{$PROGRAM_NAME} add"
46
+ puts " #{exec_name} add"
45
47
  end
46
48
 
47
49
  desc 'list', 'Prints a list of accounts in the keyring'
@@ -57,7 +59,7 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
57
59
 
58
60
  desc 'env ACCOUNT', 'Outputs bourne shell environment exports for an ACCOUNT'
59
61
  def env(account = nil)
60
- account = ask_missing(existing: account, message: 'account name')
62
+ account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name))
61
63
  cred, temp_cred = get_valid_item_pair(account: account)
62
64
  token = temp_cred.password unless temp_cred.nil?
63
65
  put_env_string(
@@ -86,11 +88,16 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
86
88
  method_option :key, type: :string, aliases: '-k', desc: 'AWS account key id.'
87
89
  method_option :secret, type: :string, aliases: '-s', desc: 'AWS account secret.'
88
90
  method_option :mfa, type: :string, aliases: '-m', desc: 'AWS virtual mfa arn.'
89
- def add(account = nil)
90
- account = ask_missing(existing: account, message: 'account name')
91
- key = ask_missing(existing: options[:key], message: 'access key id')
92
- secret = ask_missing(existing: options[:secret], message: 'secret access key', secure: true)
93
- mfa = ask_missing(existing: options[:mfa], message: 'mfa arn', optional: true)
91
+ def add(account = nil) # rubocop:disable Metrics/AbcSize
92
+ account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name))
93
+ key = ask_check(existing: options[:key], message: 'access key id', validator: Awskeyring.method(:access_key))
94
+ secret = ask_check(
95
+ existing: options[:secret], message: 'secret access key',
96
+ secure: true, validator: Awskeyring.method(:secret_access_key)
97
+ )
98
+ mfa = ask_check(
99
+ existing: options[:mfa], message: 'mfa arn', optional: true, validator: Awskeyring.method(:mfa_arn)
100
+ )
94
101
 
95
102
  Awskeyring.add_item(
96
103
  account: account,
@@ -98,33 +105,37 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
98
105
  secret: secret,
99
106
  comment: mfa
100
107
  )
108
+ puts "# Added account #{account}"
101
109
  end
102
110
 
103
111
  map 'add-role' => :add_role
104
112
  desc 'add-role ROLE', 'Adds a ROLE to the keyring'
105
113
  method_option :arn, type: :string, aliases: '-a', desc: 'AWS role arn.'
106
114
  def add_role(role = nil)
107
- role = ask_missing(existing: role, message: 'role name')
108
- arn = ask_missing(existing: options[:arn], message: 'role arn')
109
- account = ask_missing(existing: account, message: 'account', optional: true)
115
+ role = ask_check(existing: role, message: 'role name', validator: Awskeyring.method(:role_name))
116
+ arn = ask_check(existing: options[:arn], message: 'role arn', validator: Awskeyring.method(:role_arn))
117
+ account = ask_check(
118
+ existing: account, message: 'account', optional: true, validator: Awskeyring.method(:account_name)
119
+ )
110
120
 
111
121
  Awskeyring.add_role(
112
122
  role: role,
113
123
  arn: arn,
114
124
  account: account
115
125
  )
126
+ puts "# Added role #{role}"
116
127
  end
117
128
 
118
129
  desc 'remove ACCOUNT', 'Removes an ACCOUNT from the keyring'
119
130
  def remove(account = nil)
120
- account = ask_missing(existing: account, message: 'account name')
131
+ account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name))
121
132
  cred, temp_cred = get_valid_item_pair(account: account)
122
133
  Awskeyring.delete_pair(cred, temp_cred, "# Removing account #{account}")
123
134
  end
124
135
 
125
136
  desc 'remove-token ACCOUNT', 'Removes a token for ACCOUNT from the keyring'
126
137
  def remove_token(account = nil)
127
- account = ask_missing(existing: account, message: 'account name')
138
+ account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name))
128
139
  session_key, session_token = Awskeyring.get_pair(account)
129
140
  session_key, session_token = Awskeyring.delete_expired(session_key, session_token) if session_key
130
141
  Awskeyring.delete_pair(session_key, session_token, "# Removing token for account #{account}") if session_key
@@ -133,17 +144,47 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
133
144
  map 'remove-role' => :remove_role
134
145
  desc 'remove-role ROLE', 'Removes a ROLE from the keyring'
135
146
  def remove_role(role = nil)
136
- role = ask_missing(existing: role, message: 'role name')
147
+ role = ask_check(existing: role, message: 'role name', validator: Awskeyring.method(:role_name))
137
148
  item_role = Awskeyring.get_role(role)
138
149
  Awskeyring.delete_pair(item_role, nil, "# Removing role #{role}")
139
150
  end
140
151
 
152
+ desc 'rotate ACCOUNT', 'Rotate access keys for an ACCOUNT'
153
+ def rotate(account = nil) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
154
+ account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name))
155
+ item = Awskeyring.get_item(account)
156
+ iam = Aws::IAM::Client.new(access_key_id: item.attributes[:account], secret_access_key: item.password)
157
+
158
+ if iam.list_access_keys[:access_key_metadata].length > 1
159
+ warn "You have two access keys for account #{account}"
160
+ exit 1
161
+ end
162
+
163
+ new_key = iam.create_access_key
164
+ iam = Aws::IAM::Client.new(
165
+ access_key_id: new_key[:access_key][:access_key_id],
166
+ secret_access_key: new_key[:access_key][:secret_access_key]
167
+ )
168
+ retry_backoff do
169
+ iam.delete_access_key(
170
+ access_key_id: item.attributes[:account]
171
+ )
172
+ end
173
+ Awskeyring.update_item(
174
+ account: account,
175
+ key: new_key[:access_key][:access_key_id],
176
+ secret: new_key[:access_key][:secret_access_key]
177
+ )
178
+
179
+ puts "# Updated account #{account}"
180
+ end
181
+
141
182
  desc 'token ACCOUNT [ROLE] [MFA]', 'Create an STS Token from a ROLE or an MFA code'
142
183
  method_option :role, type: :string, aliases: '-r', desc: 'The ROLE to assume.'
143
184
  method_option :code, type: :string, aliases: '-c', desc: 'Virtual mfa CODE.'
144
185
  method_option :duration, type: :string, aliases: '-d', desc: 'Session DURATION in seconds.'
145
186
  def token(account = nil, role = nil, code = nil) # rubocop:disable all
146
- account = ask_missing(existing: account, message: 'account name')
187
+ account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name))
147
188
  role ||= options[:role]
148
189
  code ||= options[:code]
149
190
  duration = options[:duration]
@@ -206,7 +247,7 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
206
247
  desc 'console ACCOUNT', 'Open the AWS Console for the ACCOUNT'
207
248
  method_option :path, type: :string, aliases: '-p', desc: 'The service PATH to open.'
208
249
  def console(account = nil) # rubocop:disable all
209
- account = ask_missing(existing: account, message: 'account name')
250
+ account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name))
210
251
  cred, temp_cred = get_valid_item_pair(account: account)
211
252
  token = temp_cred.password unless temp_cred.nil?
212
253
 
@@ -334,6 +375,34 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
334
375
  puts 'unset AWS_SESSION_TOKEN' unless token
335
376
  end
336
377
 
378
+ def ask_check(existing:, message:, secure: false, optional: false, validator: nil)
379
+ retries ||= 3
380
+ begin
381
+ value = ask_missing(existing: existing, message: message, secure: secure, optional: optional)
382
+ value = validator.call(value) unless value.empty? && optional
383
+ rescue RuntimeError => e
384
+ warn e.message
385
+ retry unless (retries -= 1).zero?
386
+ exit 1
387
+ end
388
+ value
389
+ end
390
+
391
+ def retry_backoff(&block)
392
+ retries ||= 1
393
+ begin
394
+ yield block
395
+ rescue Aws::IAM::Errors::InvalidClientTokenId => e
396
+ if retries < 4
397
+ sleep 2**retries
398
+ retries += 1
399
+ retry
400
+ end
401
+ warn e.message
402
+ exit 1
403
+ end
404
+ end
405
+
337
406
  def ask_missing(existing:, message:, secure: false, optional: false)
338
407
  existing || ask(message: message, secure: secure, optional: optional)
339
408
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: awskeyring
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.0.5
4
+ version: 0.0.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Tristan Morgan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2018-02-15 00:00:00.000000000 Z
11
+ date: 2018-03-01 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-iam
@@ -157,6 +157,7 @@ files:
157
157
  - awskeyring.gemspec
158
158
  - exe/awskeyring
159
159
  - lib/awskeyring.rb
160
+ - lib/awskeyring/validate.rb
160
161
  - lib/awskeyring/version.rb
161
162
  - lib/awskeyring_command.rb
162
163
  homepage: https://github.com/vibrato/awskeyring