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 +4 -4
- data/.rspec +1 -0
- data/.rubocop.yml +3 -0
- data/CHANGELOG.md +8 -0
- data/awskeyring.gemspec +1 -1
- data/lib/awskeyring/awsapi.rb +167 -0
- data/lib/awskeyring/validate.rb +46 -24
- data/lib/awskeyring/version.rb +2 -1
- data/lib/awskeyring.rb +130 -31
- data/lib/awskeyring_command.rb +101 -183
- metadata +3 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA1:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e245afbb0324eea304fcc2c010617ebc3b78c53f
|
4
|
+
data.tar.gz: 7be3de5bff9b16373021863d33a5aaa677046f60
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: 04610b85c96d2da14c5d7874790fd9b33425aea0c0d1e1b087c8e17534daa2c6238bb1dee84830890f4b13256d55dd0c37521f33c90bd97f171d50c5fd488fc8
|
7
|
+
data.tar.gz: fde8d5db0fc622b19423a171e3027c222ee9f54e5263533248eaaf10e27c05ab613404cf07656c58ec427069f8e660114fc1614ee0a252f435827b426ba486a2
|
data/.rspec
CHANGED
data/.rubocop.yml
CHANGED
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
@@ -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
|
data/lib/awskeyring/validate.rb
CHANGED
@@ -1,32 +1,54 @@
|
|
1
|
-
#
|
1
|
+
# Awskeyring Module,
|
2
|
+
# gives you an interface to access keychains and items.
|
2
3
|
module Awskeyring
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
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
|
-
|
9
|
-
|
10
|
-
aws_access_key
|
11
|
-
|
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
|
-
|
14
|
-
|
15
|
-
aws_secret_access_key
|
16
|
-
|
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
|
-
|
19
|
-
|
20
|
-
mfa_arn
|
21
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
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
|
-
|
29
|
-
|
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
|
data/lib/awskeyring/version.rb
CHANGED
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
|
-
#
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
76
|
+
# Add an account item
|
77
|
+
def self.add_account(account:, key:, secret:, mfa:)
|
61
78
|
all_items.create(
|
62
|
-
label:
|
79
|
+
label: ACCOUNT_PREFIX + account,
|
63
80
|
account: key,
|
64
81
|
password: secret,
|
65
|
-
comment:
|
82
|
+
comment: mfa
|
66
83
|
)
|
67
84
|
end
|
68
85
|
|
69
|
-
|
70
|
-
|
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:
|
97
|
+
label: ROLE_PREFIX + role,
|
79
98
|
account: arn,
|
80
99
|
password: '',
|
81
100
|
comment: account
|
82
101
|
)
|
83
102
|
end
|
84
103
|
|
85
|
-
|
86
|
-
|
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:
|
90
|
-
all_items.create(label:
|
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:
|
113
|
+
comment: ROLE_PREFIX + params[:role])
|
94
114
|
end
|
95
115
|
|
96
|
-
|
97
|
-
|
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
|
-
|
101
|
-
|
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
|
-
|
105
|
-
|
106
|
-
|
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
|
-
|
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
|
-
|
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
|
-
|
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
|
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
|
data/lib/awskeyring_command.rb
CHANGED
@@ -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
|
-
|
4
|
+
require 'awskeyring'
|
5
|
+
require 'awskeyring/awsapi'
|
6
|
+
require 'awskeyring/validate'
|
9
7
|
require 'awskeyring/version'
|
10
8
|
|
11
|
-
#
|
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
|
-
|
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.
|
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(
|
63
|
-
|
64
|
-
|
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
|
67
|
-
key: cred
|
68
|
-
secret: cred
|
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
|
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
|
79
|
-
key: cred
|
80
|
-
secret: cred
|
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
|
-
|
92
|
-
|
93
|
-
|
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.
|
111
|
+
Awskeyring.add_account(
|
103
112
|
account: account,
|
104
113
|
key: key,
|
105
114
|
secret: secret,
|
106
|
-
|
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(
|
132
|
-
|
133
|
-
|
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(
|
139
|
-
|
140
|
-
|
141
|
-
Awskeyring.
|
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
|
-
|
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
|
-
|
154
|
-
|
155
|
-
|
156
|
-
|
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
|
-
|
169
|
-
|
170
|
-
|
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[:
|
176
|
-
secret: new_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(
|
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
|
-
|
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
|
-
|
204
|
+
item_hash = Awskeyring.get_account_hash(account: account)
|
205
|
+
role_arn = Awskeyring.get_role_arn(role_name: role) if role
|
206
206
|
|
207
|
-
|
208
|
-
|
209
|
-
|
210
|
-
|
211
|
-
|
212
|
-
|
213
|
-
|
214
|
-
|
215
|
-
|
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.
|
217
|
+
Awskeyring.add_token(
|
236
218
|
account: account,
|
237
|
-
key:
|
238
|
-
secret:
|
239
|
-
token:
|
240
|
-
expiry:
|
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 #{
|
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
|
-
|
250
|
-
|
251
|
-
|
252
|
-
|
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
|
-
|
257
|
-
|
258
|
-
|
259
|
-
|
260
|
-
|
261
|
-
|
262
|
-
|
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)
|
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.
|
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
|
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-
|
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
|