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 +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
|