awskeyring 0.0.6 → 0.1.0

Sign up to get free protection for your applications and to get access to all the features.
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