awskeyring 0.0.6 → 0.1.0

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: 2aadd847341fb6a4dd7016dfca3fdd1ba60fe4c9
4
- data.tar.gz: a8e548fb70b847b3fc7d7e1a277a4d12a054029c
3
+ metadata.gz: e245afbb0324eea304fcc2c010617ebc3b78c53f
4
+ data.tar.gz: 7be3de5bff9b16373021863d33a5aaa677046f60
5
5
  SHA512:
6
- metadata.gz: 60a505a585398dc8e6034bbc51c4deef834f650fb76a042bc248f338dfbdb3e6354dab42d2536f404f93bee4785f21dfe5bc9fd4fb2c2a3b7cad52a297fc1414
7
- data.tar.gz: 8a282201ceb09a170e1beeb2dd3ecc57dc4058ec669d5b3cb9cc1d9ce33a99ad6b3c09ad42e5c1146f504ee6559eb4c258857961a5e864709a8a5741d5bd15e0
6
+ metadata.gz: 04610b85c96d2da14c5d7874790fd9b33425aea0c0d1e1b087c8e17534daa2c6238bb1dee84830890f4b13256d55dd0c37521f33c90bd97f171d50c5fd488fc8
7
+ data.tar.gz: fde8d5db0fc622b19423a171e3027c222ee9f54e5263533248eaaf10e27c05ab613404cf07656c58ec427069f8e660114fc1614ee0a252f435827b426ba486a2
data/.rspec CHANGED
@@ -1,2 +1,3 @@
1
+ --order rand
1
2
  --format documentation
2
3
  --color
data/.rubocop.yml CHANGED
@@ -9,6 +9,9 @@ Metrics/BlockLength:
9
9
  Exclude:
10
10
  - spec/**/*
11
11
 
12
+ Metrics/AbcSize:
13
+ Max: 20
14
+
12
15
  Naming/FileName:
13
16
  Exclude:
14
17
  - Gemfile
data/CHANGELOG.md CHANGED
@@ -1,5 +1,13 @@
1
1
  # Change Log
2
2
 
3
+ ## [v0.1.0](https://github.com/vibrato/awskeyring/tree/v0.1.0) (2018-03-14)
4
+ [Full Changelog](https://github.com/vibrato/awskeyring/compare/v0.0.6...v0.1.0)
5
+
6
+ **Implemented enhancements:**
7
+
8
+ - Item refactor [\#13](https://github.com/vibrato/awskeyring/pull/13) ([tristanmorgan](https://github.com/tristanmorgan))
9
+ - Aws refactor [\#12](https://github.com/vibrato/awskeyring/pull/12) ([tristanmorgan](https://github.com/tristanmorgan))
10
+
3
11
  ## [v0.0.6](https://github.com/vibrato/awskeyring/tree/v0.0.6) (2018-03-01)
4
12
  [Full Changelog](https://github.com/vibrato/awskeyring/compare/v0.0.5...v0.0.6)
5
13
 
data/awskeyring.gemspec CHANGED
@@ -1,4 +1,4 @@
1
- lib = File.expand_path('../lib', __FILE__)
1
+ lib = File.expand_path('lib', __dir__)
2
2
  $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3
3
  require 'awskeyring/version'
4
4
 
@@ -0,0 +1,167 @@
1
+ require 'aws-sdk-iam'
2
+ require 'cgi'
3
+ require 'json'
4
+
5
+ # Awskeyring Module,
6
+ # gives you an interface to access keychains and items.
7
+ module Awskeyring
8
+ # AWS API methods for Awskeyring
9
+ module Awsapi # rubocop:disable Metrics/ModuleLength
10
+ # Retrieves a temporary session token from AWS
11
+ #
12
+ # @param [Hash] params including
13
+ # key The aws_access_key_id
14
+ # secret The aws_secret_access_key
15
+ # user The local username
16
+ # mfa The users MFA arn
17
+ # code The MFA code
18
+ # duration time in seconds until expiry
19
+ # role_arn ARN of the role to assume
20
+ # @return [Hash] with the new credentials
21
+ # key The aws_access_key_id
22
+ # secret The aws_secret_access_key
23
+ # token The aws_session_token
24
+ # expiry expiry time
25
+ def self.get_token(params = {}) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
26
+ sts = Aws::STS::Client.new(access_key_id: params[:key], secret_access_key: params[:secret])
27
+
28
+ begin
29
+ response =
30
+ if params[:code] && params[:role_arn]
31
+ sts.assume_role(
32
+ duration_seconds: params[:duration].to_i,
33
+ role_arn: params[:role_arn],
34
+ role_session_name: params[:user],
35
+ serial_number: params[:mfa],
36
+ token_code: params[:code]
37
+ )
38
+ elsif params[:role_arn]
39
+ sts.assume_role(
40
+ duration_seconds: params[:duration].to_i,
41
+ role_arn: params[:role_arn],
42
+ role_session_name: params[:user]
43
+ )
44
+ elsif params[:code]
45
+ sts.get_session_token(
46
+ duration_seconds: params[:duration].to_i,
47
+ serial_number: params[:mfa],
48
+ token_code: params[:code]
49
+ )
50
+ end
51
+ rescue Aws::STS::Errors::AccessDenied => e
52
+ puts e.to_s
53
+ exit 1
54
+ end
55
+
56
+ {
57
+ key: response.credentials[:access_key_id],
58
+ secret: response.credentials[:secret_access_key],
59
+ token: response.credentials[:session_token],
60
+ expiry: response.credentials[:expiration]
61
+ }
62
+ end
63
+
64
+ # Retrieves an AWS Console login url
65
+ #
66
+ # @param [String] key The aws_access_key_id
67
+ # @param [String] secret The aws_secret_access_key
68
+ # @param [String] token The aws_session_token
69
+ # @param [String] user The local username
70
+ # @param [String] path within the Console to access
71
+ # @return [String] login_url to access
72
+ def self.get_login_url(key:, secret:, token:, path:, user:) # rubocop:disable Metrics/AbcSize, Metrics/MethodLength
73
+ console_url = "https://console.aws.amazon.com/#{path}/home"
74
+ signin_url = 'https://signin.aws.amazon.com/federation'
75
+ policy_json = {
76
+ Version: '2012-10-17',
77
+ Statement: [{
78
+ Action: '*',
79
+ Resource: '*',
80
+ Effect: 'Allow'
81
+ }]
82
+ }.to_json
83
+
84
+ if token
85
+ session_json = {
86
+ sessionId: key,
87
+ sessionKey: secret,
88
+ sessionToken: token
89
+ }.to_json
90
+ else
91
+ sts = Aws::STS::Client.new(access_key_id: key,
92
+ secret_access_key: secret)
93
+
94
+ session = sts.get_federation_token(name: user,
95
+ policy: policy_json,
96
+ duration_seconds: (60 * 60 * 12))
97
+ session_json = {
98
+ sessionId: session.credentials[:access_key_id],
99
+ sessionKey: session.credentials[:secret_access_key],
100
+ sessionToken: session.credentials[:session_token]
101
+ }.to_json
102
+ end
103
+
104
+ get_signin_token_url = signin_url + '?Action=getSigninToken' \
105
+ '&Session=' + CGI.escape(session_json)
106
+
107
+ returned_content = Net::HTTP.get(URI.parse(get_signin_token_url))
108
+
109
+ signin_token = JSON.parse(returned_content)['SigninToken']
110
+ signin_token_param = '&SigninToken=' + CGI.escape(signin_token)
111
+ destination_param = '&Destination=' + CGI.escape(console_url)
112
+
113
+ signin_url + '?Action=login' + signin_token_param + destination_param
114
+ end
115
+
116
+ # Rotates the AWS access keys
117
+ #
118
+ # @param [String] key The aws_access_key_id
119
+ # @param [String] secret The aws_secret_access_key
120
+ # @param [String] account the associated account name.
121
+ # @return [String] key The aws_access_key_id
122
+ # @return [String] secret The aws_secret_access_key
123
+ # @return [String] account the associated account name.
124
+ def self.rotate(account:, key:, secret:) # rubocop:disable Metrics/MethodLength
125
+ iam = Aws::IAM::Client.new(access_key_id: key, secret_access_key: secret)
126
+
127
+ if iam.list_access_keys[:access_key_metadata].length > 1
128
+ warn "You have two access keys for account #{account}"
129
+ exit 1
130
+ end
131
+
132
+ new_key = iam.create_access_key
133
+ iam = Aws::IAM::Client.new(
134
+ access_key_id: new_key[:access_key][:access_key_id],
135
+ secret_access_key: new_key[:access_key][:secret_access_key]
136
+ )
137
+ retry_backoff do
138
+ iam.delete_access_key(
139
+ access_key_id: key
140
+ )
141
+ end
142
+ {
143
+ account: account,
144
+ key: new_key[:access_key][:access_key_id],
145
+ secret: new_key[:access_key][:secret_access_key]
146
+ }
147
+ end
148
+
149
+ # Retry the call with backoff
150
+ #
151
+ # @param [Block] block the block to retry.
152
+ def self.retry_backoff(&block)
153
+ retries ||= 1
154
+ begin
155
+ yield block
156
+ rescue Aws::IAM::Errors::InvalidClientTokenId => e
157
+ if retries < 4
158
+ sleep 2**retries
159
+ retries += 1
160
+ retry
161
+ end
162
+ warn e.message
163
+ exit 1
164
+ end
165
+ end
166
+ end
167
+ end
@@ -1,32 +1,54 @@
1
- # Validation methods
1
+ # Awskeyring Module,
2
+ # gives you an interface to access keychains and items.
2
3
  module Awskeyring
3
- def self.account_name(account_name)
4
- raise 'Invalid Account Name' unless account_name =~ /\S+/
5
- account_name
6
- end
4
+ # Validation methods for Awskeyring
5
+ module Validate
6
+ # Validate an account name
7
+ #
8
+ # @param [String] account_name the associated account name.
9
+ def self.account_name(account_name)
10
+ raise 'Invalid Account Name' unless account_name =~ /\S+/
11
+ account_name
12
+ end
7
13
 
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
14
+ # Validate an AWS Access Key ID
15
+ #
16
+ # @param [String] aws_access_key The aws_access_key_id
17
+ def self.access_key(aws_access_key)
18
+ raise 'Invalid Access Key' unless aws_access_key =~ /\AAKIA[A-Z0-9]{12,16}\z/
19
+ aws_access_key
20
+ end
12
21
 
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
22
+ # Validate an AWS Secret Key ID
23
+ #
24
+ # @param [String] aws_secret_access_key The aws_secret_access_key
25
+ def self.secret_access_key(aws_secret_access_key)
26
+ raise 'Secret Access Key is not 40 chars' if aws_secret_access_key.length != 40
27
+ aws_secret_access_key
28
+ end
17
29
 
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
30
+ # Validate an Users mfa ARN
31
+ #
32
+ # @param [String] mfa_arn The users MFA arn
33
+ def self.mfa_arn(mfa_arn)
34
+ raise 'Invalid MFA ARN' unless mfa_arn =~ %r(\Aarn:aws:iam::[0-9]{12}:mfa\/\S*\z)
35
+ mfa_arn
36
+ end
22
37
 
23
- def self.role_name(account_name)
24
- raise 'Invalid Role Name' unless account_name =~ /\S+/
25
- account_name
26
- end
38
+ # Validate a Role name
39
+ #
40
+ # @param [String] role_name
41
+ def self.role_name(role_name)
42
+ raise 'Invalid Role Name' unless role_name =~ /\S+/
43
+ role_name
44
+ end
27
45
 
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
46
+ # Validate a Role ARN
47
+ #
48
+ # @param [String] role_arn The role arn
49
+ def self.role_arn(role_arn)
50
+ raise 'Invalid Role ARN' unless role_arn =~ %r(\Aarn:aws:iam::[0-9]{12}:role\/\S*\z)
51
+ role_arn
52
+ end
31
53
  end
32
54
  end
@@ -1,3 +1,4 @@
1
1
  module Awskeyring
2
- VERSION = '0.0.6'.freeze
2
+ # The Gems version number
3
+ VERSION = '0.1.0'.freeze
3
4
  end
data/lib/awskeyring.rb CHANGED
@@ -1,14 +1,23 @@
1
+ require 'json'
1
2
  require 'keychain'
2
- require 'aws-sdk-iam'
3
- require 'awskeyring/validate'
4
3
 
5
- # Aws Key-ring logical object,
4
+ # Awskeyring Module,
6
5
  # gives you an interface to access keychains and items.
7
6
  module Awskeyring # rubocop:disable Metrics/ModuleLength
7
+ # Default rpeferences fole path
8
8
  PREFS_FILE = (File.expand_path '~/.awskeyring').freeze
9
+ # Prefix for Roles
9
10
  ROLE_PREFIX = 'role '.freeze
11
+ # Prefix for Accounts
10
12
  ACCOUNT_PREFIX = 'account '.freeze
11
-
13
+ # Prefix for Session Keys
14
+ SESSION_KEY_PREFIX = 'session-key '.freeze
15
+ # Prefix for Session Tokens
16
+ SESSION_TOKEN_PREFIX = 'session-token '.freeze
17
+
18
+ # Retrieve the preferences
19
+ #
20
+ # @return [Hash] prefs of the gem
12
21
  def self.prefs
13
22
  if File.exist? PREFS_FILE
14
23
  JSON.parse(File.read(PREFS_FILE))
@@ -17,6 +26,7 @@ module Awskeyring # rubocop:disable Metrics/ModuleLength
17
26
  end
18
27
  end
19
28
 
29
+ # Create a new Keychain
20
30
  def self.init_keychain(awskeyring:)
21
31
  keychain = Keychain.create(awskeyring)
22
32
  keychain.lock_interval = 300
@@ -26,7 +36,10 @@ module Awskeyring # rubocop:disable Metrics/ModuleLength
26
36
  File.new(Awskeyring::PREFS_FILE, 'w').write JSON.dump(prefs)
27
37
  end
28
38
 
29
- def self.load_keychain
39
+ # Load the keychain for access
40
+ #
41
+ # @return [Keychain] keychain ready for use.
42
+ private_class_method def self.load_keychain
30
43
  unless File.exist?(Awskeyring::PREFS_FILE) && !prefs.empty?
31
44
  warn "Config missing, run `#{File.basename($PROGRAM_NAME)} initialise` to recreate."
32
45
  exit 1
@@ -39,95 +52,181 @@ module Awskeyring # rubocop:disable Metrics/ModuleLength
39
52
  keychain
40
53
  end
41
54
 
42
- def self.list_items
55
+ # Return a list of all acount items
56
+ private_class_method def self.list_items
43
57
  items = all_items.all.sort do |a, b|
44
58
  a.attributes[:label] <=> b.attributes[:label]
45
59
  end
46
60
  items.select { |elem| elem.attributes[:label].start_with?(ACCOUNT_PREFIX) }
47
61
  end
48
62
 
49
- def self.list_roles
63
+ # Return a list of all role items
64
+ private_class_method def self.list_roles
50
65
  items = all_items.all.sort do |a, b|
51
66
  a.attributes[:label] <=> b.attributes[:label]
52
67
  end
53
68
  items.select { |elem| elem.attributes[:label].start_with?(ROLE_PREFIX) }
54
69
  end
55
70
 
56
- def self.all_items
71
+ # Return all keychain items
72
+ private_class_method def self.all_items
57
73
  load_keychain.generic_passwords
58
74
  end
59
75
 
60
- def self.add_item(account:, key:, secret:, comment:)
76
+ # Add an account item
77
+ def self.add_account(account:, key:, secret:, mfa:)
61
78
  all_items.create(
62
- label: "#{ACCOUNT_PREFIX}#{account}",
79
+ label: ACCOUNT_PREFIX + account,
63
80
  account: key,
64
81
  password: secret,
65
- comment: comment
82
+ comment: mfa
66
83
  )
67
84
  end
68
85
 
69
- def self.update_item(account:, key:, secret:)
70
- item = get_item(account)
86
+ # update and account item
87
+ def self.update_account(account:, key:, secret:)
88
+ item = get_item(account: account)
71
89
  item.attributes[:account] = key
72
90
  item.password = secret
73
91
  item.save!
74
92
  end
75
93
 
94
+ # Add a Role item
76
95
  def self.add_role(role:, arn:, account:)
77
96
  all_items.create(
78
- label: "#{ROLE_PREFIX}#{role}",
97
+ label: ROLE_PREFIX + role,
79
98
  account: arn,
80
99
  password: '',
81
100
  comment: account
82
101
  )
83
102
  end
84
103
 
85
- def self.add_pair(params = {})
86
- all_items.create(label: "session-key #{params[:account]}",
104
+ # add a session token pair of items
105
+ def self.add_token(params = {})
106
+ all_items.create(label: SESSION_KEY_PREFIX + params[:account],
87
107
  account: params[:key],
88
108
  password: params[:secret],
89
- comment: "#{ROLE_PREFIX}#{params[:role]}")
90
- all_items.create(label: "session-token #{params[:account]}",
109
+ comment: ROLE_PREFIX + params[:role])
110
+ all_items.create(label: SESSION_TOKEN_PREFIX + params[:account],
91
111
  account: params[:expiry],
92
112
  password: params[:token],
93
- comment: "#{ROLE_PREFIX}#{params[:role]}")
113
+ comment: ROLE_PREFIX + params[:role])
94
114
  end
95
115
 
96
- def self.get_item(account)
97
- all_items.where(label: "#{ACCOUNT_PREFIX}#{account}").first
116
+ # Return an account item by name
117
+ private_class_method def self.get_item(account:)
118
+ all_items.where(label: ACCOUNT_PREFIX + account).first
98
119
  end
99
120
 
100
- def self.get_role(name)
101
- all_items.where(label: "#{ROLE_PREFIX}#{name}").first
121
+ # Return a role item by name
122
+ private_class_method def self.get_role(role_name:)
123
+ all_items.where(label: ROLE_PREFIX + role_name).first
102
124
  end
103
125
 
104
- def self.get_pair(account)
105
- session_key = all_items.where(label: "session-key #{account}").first
106
- session_token = all_items.where(label: "session-token #{account}").first if session_key
126
+ # Return a session token pair of items by name
127
+ private_class_method def self.get_pair(account:)
128
+ session_key = all_items.where(label: SESSION_KEY_PREFIX + account).first
129
+ session_token = all_items.where(label: SESSION_TOKEN_PREFIX + account).first if session_key
107
130
  [session_key, session_token]
108
131
  end
109
132
 
110
- def self.list_item_names
133
+ # Return a list account item names
134
+ def self.list_account_names
111
135
  list_items.map { |elem| elem.attributes[:label][(ACCOUNT_PREFIX.length)..-1] }
112
136
  end
113
137
 
138
+ # Return a list role item names
114
139
  def self.list_role_names
115
140
  list_roles.map { |elem| elem.attributes[:label][(ROLE_PREFIX.length)..-1] }
116
141
  end
117
142
 
118
- def self.delete_expired(key, token)
143
+ # Return a session token if available or a static key
144
+ private_class_method def self.get_valid_item_pair(account:)
145
+ session_key, session_token = get_pair(account: account)
146
+ session_key, session_token = delete_expired(key: session_key, token: session_token) if session_key
147
+
148
+ if session_key && session_token
149
+ puts '# Using temporary session credentials'
150
+ return session_key, session_token
151
+ end
152
+
153
+ item = get_item(account: account)
154
+ if item.nil?
155
+ warn "# Credential not found with name: #{account}"
156
+ exit 2
157
+ end
158
+ [item, nil]
159
+ end
160
+
161
+ # Return valid creds for account
162
+ def self.get_valid_creds(account:)
163
+ cred, temp_cred = get_valid_item_pair(account: account)
164
+ token = temp_cred.password unless temp_cred.nil?
165
+ {
166
+ account: account,
167
+ key: cred.attributes[:account],
168
+ secret: cred.password,
169
+ token: token
170
+ }
171
+ end
172
+
173
+ # Return a hash for account (skip tokens)
174
+ def self.get_account_hash(account:)
175
+ cred = get_item(account: account)
176
+ return unless cred
177
+ {
178
+ account: account,
179
+ key: cred.attributes[:account],
180
+ secret: cred.password,
181
+ mfa: cred.attributes[:comment]
182
+ }
183
+ end
184
+
185
+ # get the ARN for a role
186
+ def self.get_role_arn(role_name:)
187
+ role_item = get_role(role_name: role_name)
188
+ role_item.attributes[:account] if role_item
189
+ end
190
+
191
+ # Delete session token items if expired
192
+ private_class_method def self.delete_expired(key:, token:)
119
193
  expires_at = Time.at(token.attributes[:account].to_i)
120
194
  if expires_at < Time.now
121
- delete_pair(key, token, '# Removing expired session credentials')
195
+ delete_pair(key: key, token: token, message: '# Removing expired session credentials')
122
196
  key = nil
123
197
  token = nil
124
198
  end
125
199
  [key, token]
126
200
  end
127
201
 
128
- def self.delete_pair(key, token, message)
202
+ # Delete session token items
203
+ private_class_method def self.delete_pair(key:, token:, message:)
204
+ return unless key
129
205
  puts message if message
130
206
  token.delete if token
131
- key.delete if key
207
+ key.delete
208
+ end
209
+
210
+ # Delete a session token
211
+ def self.delete_token(account:, message:)
212
+ session_key, session_token = get_pair(account: account)
213
+ delete_pair(key: session_key, token: session_token, message: message)
214
+ end
215
+
216
+ # Delete an Account
217
+ def self.delete_account(account:, message:)
218
+ delete_token(account: account, message: '# Removing expired session credentials')
219
+ cred = get_item(account: account)
220
+ return unless cred
221
+ puts message if message
222
+ cred.delete
223
+ end
224
+
225
+ # Delete a role
226
+ def self.delete_role(role_name:, message:)
227
+ role = get_role(role_name: role_name)
228
+ return unless role
229
+ puts message if message
230
+ role.delete
132
231
  end
133
232
  end
@@ -1,14 +1,12 @@
1
- require 'aws-sdk-iam'
2
- require 'cgi'
3
1
  require 'highline'
4
- require 'json'
5
- require 'open-uri'
6
2
  require 'thor'
7
3
 
8
- require_relative 'awskeyring'
4
+ require 'awskeyring'
5
+ require 'awskeyring/awsapi'
6
+ require 'awskeyring/validate'
9
7
  require 'awskeyring/version'
10
8
 
11
- # AWS Key-ring command line interface.
9
+ # AWSkeyring command line interface.
12
10
  class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
13
11
  map %w[--version -v] => :__version
14
12
  map ['init'] => :initialise
@@ -19,13 +17,15 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
19
17
  map ['rmt'] => :remove_token
20
18
 
21
19
  desc '--version, -v', 'Prints the version'
20
+ # print the version number
22
21
  def __version
23
22
  puts Awskeyring::VERSION
24
23
  end
25
24
 
26
25
  desc 'initialise', 'Initialises a new KEYCHAIN'
27
26
  method_option :keychain, type: :string, aliases: '-n', desc: 'Name of KEYCHAIN to initialise.'
28
- def initialise # rubocop:disable Metrics/AbcSize
27
+ # initialise the keychain
28
+ def initialise
29
29
  unless Awskeyring.prefs.empty?
30
30
  puts "#{Awskeyring::PREFS_FILE} exists. no need to initialise."
31
31
  exit 1
@@ -47,38 +47,42 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
47
47
  end
48
48
 
49
49
  desc 'list', 'Prints a list of accounts in the keyring'
50
+ # list the accounts
50
51
  def list
51
- puts Awskeyring.list_item_names.join("\n")
52
+ puts Awskeyring.list_account_names.join("\n")
52
53
  end
53
54
 
54
55
  map 'list-role' => :list_role
55
56
  desc 'list-role', 'Prints a list of roles in the keyring'
57
+ # List roles
56
58
  def list_role
57
59
  puts Awskeyring.list_role_names.join("\n")
58
60
  end
59
61
 
60
62
  desc 'env ACCOUNT', 'Outputs bourne shell environment exports for an ACCOUNT'
63
+ # Print Env vars
61
64
  def env(account = nil)
62
- account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name))
63
- cred, temp_cred = get_valid_item_pair(account: account)
64
- token = temp_cred.password unless temp_cred.nil?
65
+ account = ask_check(
66
+ existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name)
67
+ )
68
+ cred = Awskeyring.get_valid_creds(account: account)
65
69
  put_env_string(
66
- account: cred.attributes[:label],
67
- key: cred.attributes[:account],
68
- secret: cred.password,
69
- token: token
70
+ account: cred[:account],
71
+ key: cred[:key],
72
+ secret: cred[:secret],
73
+ token: cred[:token]
70
74
  )
71
75
  end
72
76
 
73
77
  desc 'exec ACCOUNT command...', 'Execute a COMMAND with the environment set for an ACCOUNT'
78
+ # execute an external command with env set
74
79
  def exec(account, *command)
75
- cred, temp_cred = get_valid_item_pair(account: account)
76
- token = temp_cred.password unless temp_cred.nil?
80
+ cred = Awskeyring.get_valid_creds(account: account)
77
81
  env_vars = env_vars(
78
- account: cred.attributes[:label],
79
- key: cred.attributes[:account],
80
- secret: cred.password,
81
- token: token
82
+ account: cred[:account],
83
+ key: cred[:key],
84
+ secret: cred[:secret],
85
+ token: cred[:token]
82
86
  )
83
87
  pid = Process.spawn(env_vars, command.join(' '))
84
88
  Process.wait pid
@@ -88,22 +92,27 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
88
92
  method_option :key, type: :string, aliases: '-k', desc: 'AWS account key id.'
89
93
  method_option :secret, type: :string, aliases: '-s', desc: 'AWS account secret.'
90
94
  method_option :mfa, type: :string, aliases: '-m', desc: 'AWS virtual mfa arn.'
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))
95
+ # Add an Account
96
+ def add(account = nil) # rubocop:disable Metrics/MethodLength
97
+ account = ask_check(
98
+ existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name)
99
+ )
100
+ key = ask_check(
101
+ existing: options[:key], message: 'access key id', validator: Awskeyring::Validate.method(:access_key)
102
+ )
94
103
  secret = ask_check(
95
104
  existing: options[:secret], message: 'secret access key',
96
- secure: true, validator: Awskeyring.method(:secret_access_key)
105
+ secure: true, validator: Awskeyring::Validate.method(:secret_access_key)
97
106
  )
98
107
  mfa = ask_check(
99
- existing: options[:mfa], message: 'mfa arn', optional: true, validator: Awskeyring.method(:mfa_arn)
108
+ existing: options[:mfa], message: 'mfa arn', optional: true, validator: Awskeyring::Validate.method(:mfa_arn)
100
109
  )
101
110
 
102
- Awskeyring.add_item(
111
+ Awskeyring.add_account(
103
112
  account: account,
104
113
  key: key,
105
114
  secret: secret,
106
- comment: mfa
115
+ mfa: mfa
107
116
  )
108
117
  puts "# Added account #{account}"
109
118
  end
@@ -111,11 +120,12 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
111
120
  map 'add-role' => :add_role
112
121
  desc 'add-role ROLE', 'Adds a ROLE to the keyring'
113
122
  method_option :arn, type: :string, aliases: '-a', desc: 'AWS role arn.'
123
+ # Add a role
114
124
  def add_role(role = nil)
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))
125
+ role = ask_check(existing: role, message: 'role name', validator: Awskeyring::Validate.method(:role_name))
126
+ arn = ask_check(existing: options[:arn], message: 'role arn', validator: Awskeyring::Validate.method(:role_arn))
117
127
  account = ask_check(
118
- existing: account, message: 'account', optional: true, validator: Awskeyring.method(:account_name)
128
+ existing: account, message: 'account', optional: true, validator: Awskeyring::Validate.method(:account_name)
119
129
  )
120
130
 
121
131
  Awskeyring.add_role(
@@ -127,53 +137,43 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
127
137
  end
128
138
 
129
139
  desc 'remove ACCOUNT', 'Removes an ACCOUNT from the keyring'
140
+ # Remove an account
130
141
  def remove(account = nil)
131
- account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name))
132
- cred, temp_cred = get_valid_item_pair(account: account)
133
- Awskeyring.delete_pair(cred, temp_cred, "# Removing account #{account}")
142
+ account = ask_check(
143
+ existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name)
144
+ )
145
+ Awskeyring.delete_account(account: account, message: "# Removing account #{account}")
134
146
  end
135
147
 
136
148
  desc 'remove-token ACCOUNT', 'Removes a token for ACCOUNT from the keyring'
149
+ # remove a session token
137
150
  def remove_token(account = nil)
138
- account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name))
139
- session_key, session_token = Awskeyring.get_pair(account)
140
- session_key, session_token = Awskeyring.delete_expired(session_key, session_token) if session_key
141
- Awskeyring.delete_pair(session_key, session_token, "# Removing token for account #{account}") if session_key
151
+ account = ask_check(
152
+ existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name)
153
+ )
154
+ Awskeyring.delete_token(account: account, message: "# Removing token for account #{account}")
142
155
  end
143
156
 
144
157
  map 'remove-role' => :remove_role
145
158
  desc 'remove-role ROLE', 'Removes a ROLE from the keyring'
159
+ # remove a role
146
160
  def remove_role(role = nil)
147
- role = ask_check(existing: role, message: 'role name', validator: Awskeyring.method(:role_name))
148
- item_role = Awskeyring.get_role(role)
149
- Awskeyring.delete_pair(item_role, nil, "# Removing role #{role}")
161
+ role = ask_check(existing: role, message: 'role name', validator: Awskeyring::Validate.method(:role_name))
162
+ Awskeyring.delete_role(role_name: role, message: "# Removing role #{role}")
150
163
  end
151
164
 
152
165
  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]
166
+ # rotate Account keys
167
+ def rotate(account = nil)
168
+ account = ask_check(
169
+ existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name)
167
170
  )
168
- retry_backoff do
169
- iam.delete_access_key(
170
- access_key_id: item.attributes[:account]
171
- )
172
- end
173
- Awskeyring.update_item(
171
+ item_hash = Awskeyring.get_account_hash(account: account)
172
+ new_key = Awskeyring::Awsapi.rotate(account: item_hash[:account], key: item_hash[:key], secret: item_hash[:secret])
173
+ Awskeyring.update_account(
174
174
  account: account,
175
- key: new_key[:access_key][:access_key_id],
176
- secret: new_key[:access_key][:secret_access_key]
175
+ key: new_key[:key],
176
+ secret: new_key[:secret]
177
177
  )
178
178
 
179
179
  puts "# Updated account #{account}"
@@ -183,8 +183,11 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
183
183
  method_option :role, type: :string, aliases: '-r', desc: 'The ROLE to assume.'
184
184
  method_option :code, type: :string, aliases: '-c', desc: 'Virtual mfa CODE.'
185
185
  method_option :duration, type: :string, aliases: '-d', desc: 'Session DURATION in seconds.'
186
+ # generate a sessiopn token
186
187
  def token(account = nil, role = nil, code = nil) # rubocop:disable all
187
- account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name))
188
+ account = ask_check(
189
+ existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name)
190
+ )
188
191
  role ||= options[:role]
189
192
  code ||= options[:code]
190
193
  duration = options[:duration]
@@ -196,111 +199,58 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
196
199
  exit 2
197
200
  end
198
201
 
199
- session_key, session_token = Awskeyring.get_pair(account)
200
- Awskeyring.delete_pair(session_key, session_token, '# Removing STS credentials') if session_key
201
-
202
- item = Awskeyring.get_item(account)
203
- item_role = Awskeyring.get_role(role) if role
202
+ Awskeyring.delete_token(account: account, message: '# Removing STS credentials')
204
203
 
205
- sts = Aws::STS::Client.new(access_key_id: item.attributes[:account], secret_access_key: item.password)
204
+ item_hash = Awskeyring.get_account_hash(account: account)
205
+ role_arn = Awskeyring.get_role_arn(role_name: role) if role
206
206
 
207
- begin
208
- response =
209
- if code && role
210
- sts.assume_role(
211
- duration_seconds: duration.to_i,
212
- role_arn: item_role.attributes[:account],
213
- role_session_name: ENV['USER'],
214
- serial_number: item.attributes[:comment],
215
- token_code: code
216
- )
217
- elsif role
218
- sts.assume_role(
219
- duration_seconds: duration.to_i,
220
- role_arn: item_role.attributes[:account],
221
- role_session_name: ENV['USER']
222
- )
223
- elsif code
224
- sts.get_session_token(
225
- duration_seconds: duration.to_i,
226
- serial_number: item.attributes[:comment],
227
- token_code: code
228
- )
229
- end
230
- rescue Aws::STS::Errors::AccessDenied => e
231
- puts e.to_s
232
- exit 1
233
- end
207
+ new_creds = Awskeyring::Awsapi.get_token(
208
+ code: code,
209
+ role_arn: role_arn,
210
+ duration: duration,
211
+ mfa: item_hash[:mfa],
212
+ key: item_hash[:key],
213
+ secret: item_hash[:secret],
214
+ user: ENV['USER']
215
+ )
234
216
 
235
- Awskeyring.add_pair(
217
+ Awskeyring.add_token(
236
218
  account: account,
237
- key: response.credentials[:access_key_id],
238
- secret: response.credentials[:secret_access_key],
239
- token: response.credentials[:session_token],
240
- expiry: response.credentials[:expiration].to_i.to_s,
219
+ key: new_creds[:key],
220
+ secret: new_creds[:secret],
221
+ token: new_creds[:token],
222
+ expiry: new_creds[:expiry].to_i.to_s,
241
223
  role: role
242
224
  )
243
225
 
244
- puts "Authentication valid until #{response.credentials[:expiration]}"
226
+ puts "Authentication valid until #{new_creds[:expiry]}"
245
227
  end
246
228
 
247
229
  desc 'console ACCOUNT', 'Open the AWS Console for the ACCOUNT'
248
230
  method_option :path, type: :string, aliases: '-p', desc: 'The service PATH to open.'
249
- def console(account = nil) # rubocop:disable all
250
- account = ask_check(existing: account, message: 'account name', validator: Awskeyring.method(:account_name))
251
- cred, temp_cred = get_valid_item_pair(account: account)
252
- token = temp_cred.password unless temp_cred.nil?
231
+ # Open the AWS Console
232
+ def console(account = nil)
233
+ account = ask_check(
234
+ existing: account, message: 'account name', validator: Awskeyring::Validate.method(:account_name)
235
+ )
236
+ cred = Awskeyring.get_valid_creds(account: account)
253
237
 
254
238
  path = options[:path] || 'console'
255
239
 
256
- console_url = "https://console.aws.amazon.com/#{path}/home"
257
- signin_url = 'https://signin.aws.amazon.com/federation'
258
- policy_json = {
259
- Version: '2012-10-17',
260
- Statement: [{
261
- Action: '*',
262
- Resource: '*',
263
- Effect: 'Allow'
264
- }]
265
- }.to_json
266
-
267
- if temp_cred
268
- session_json = {
269
- sessionId: cred.attributes[:account],
270
- sessionKey: cred.password,
271
- sessionToken: token
272
- }.to_json
273
- else
274
- sts = Aws::STS::Client.new(access_key_id: cred.attributes[:account],
275
- secret_access_key: cred.password)
276
-
277
- session = sts.get_federation_token(name: ENV['USER'],
278
- policy: policy_json,
279
- duration_seconds: (60 * 60 * 12))
280
- session_json = {
281
- sessionId: session.credentials[:access_key_id],
282
- sessionKey: session.credentials[:secret_access_key],
283
- sessionToken: session.credentials[:session_token]
284
- }.to_json
285
-
286
- end
287
- get_signin_token_url = signin_url + '?Action=getSigninToken' \
288
- '&Session=' + CGI.escape(session_json)
289
-
290
- returned_content = open(get_signin_token_url).read
291
-
292
- signin_token = JSON.parse(returned_content)['SigninToken']
293
- signin_token_param = '&SigninToken=' + CGI.escape(signin_token)
294
- destination_param = '&Destination=' + CGI.escape(console_url)
295
-
296
- login_url = signin_url + '?Action=login' + signin_token_param + destination_param
240
+ login_url = Awskeyring::Awsapi.get_login_url(
241
+ key: cred[:key],
242
+ secret: cred[:secret],
243
+ token: cred[:token],
244
+ path: path,
245
+ user: ENV['USER']
246
+ )
297
247
 
298
248
  pid = Process.spawn("open \"#{login_url}\"")
299
249
  Process.wait pid
300
250
  end
301
251
 
302
- # autocomplete
303
252
  desc 'awskeyring CURR PREV', 'Autocompletion for bourne shells', hide: true
253
+ # autocomplete
304
254
  def awskeyring(curr, prev)
305
255
  comp_line = ENV['COMP_LINE']
306
256
  unless comp_line
@@ -318,12 +268,12 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
318
268
 
319
269
  private
320
270
 
321
- def print_auto_resp(curr, len) # rubocop:disable Metrics/AbcSize
271
+ def print_auto_resp(curr, len)
322
272
  case len
323
273
  when 2
324
274
  puts list_commands.select { |elem| elem.start_with?(curr) }.join("\n")
325
275
  when 3
326
- puts Awskeyring.list_item_names.select { |elem| elem.start_with?(curr) }.join("\n")
276
+ puts Awskeyring.list_account_names.select { |elem| elem.start_with?(curr) }.join("\n")
327
277
  when 4
328
278
  puts Awskeyring.list_role_names.select { |elem| elem.start_with?(curr) }.join("\n")
329
279
  else
@@ -335,23 +285,6 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
335
285
  self.class.all_commands.keys.map { |elem| elem.tr('_', '-') }
336
286
  end
337
287
 
338
- def get_valid_item_pair(account:)
339
- session_key, session_token = Awskeyring.get_pair(account)
340
- session_key, session_token = Awskeyring.delete_expired(session_key, session_token) if session_key
341
-
342
- if session_key && session_token
343
- puts '# Using temporary session credentials'
344
- return session_key, session_token
345
- end
346
-
347
- item = Awskeyring.get_item(account)
348
- if item.nil?
349
- warn "# Credential not found with name: #{account}"
350
- exit 2
351
- end
352
- [item, nil]
353
- end
354
-
355
288
  def env_vars(account:, key:, secret:, token:)
356
289
  env_var = {}
357
290
  env_var['AWS_DEFAULT_REGION'] = 'us-east-1' unless ENV['AWS_DEFAULT_REGION']
@@ -388,21 +321,6 @@ class AwskeyringCommand < Thor # rubocop:disable Metrics/ClassLength
388
321
  value
389
322
  end
390
323
 
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
-
406
324
  def ask_missing(existing:, message:, secure: false, optional: false)
407
325
  existing || ask(message: message, secure: secure, optional: optional)
408
326
  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.6
4
+ version: 0.1.0
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-03-01 00:00:00.000000000 Z
11
+ date: 2018-03-14 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/awsapi.rb
160
161
  - lib/awskeyring/validate.rb
161
162
  - lib/awskeyring/version.rb
162
163
  - lib/awskeyring_command.rb