prx-ruby-aws-creds 0.1.0 → 0.1.1
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/lib/prx-ruby-aws-creds.rb +146 -69
- metadata +73 -3
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: de3e7ce4a5d260687d001b85f356d8ed44f0042cccb441f0713302b9a463588a
|
|
4
|
+
data.tar.gz: 809cf2c28520f1881dddaa6c39f9a7a38fc9cce84e9ea2ec04fd0fe176c9fdf9
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
6
|
+
metadata.gz: 36c906b505621065a7239e5f6ce04b62a3d11d474cdcb2c20b87b0ed67b3fbc7b1cb3ff4ab6456e1c5db95515a1eef839045d215cf746e6448762c8bed2d7f00
|
|
7
|
+
data.tar.gz: a3819f034b34ee12f2695afbbdcef6e3fcf70574b46e8e67730a8c97cdf44c1e52289eb47f0a2e1ba24fc8fdafaef5ad6f9d4c4d4a889c3f491af0d7eeaa1e3e
|
data/lib/prx-ruby-aws-creds.rb
CHANGED
|
@@ -1,66 +1,105 @@
|
|
|
1
|
+
require "json"
|
|
2
|
+
require "digest"
|
|
3
|
+
require "time"
|
|
4
|
+
require "fileutils"
|
|
5
|
+
require "io/console"
|
|
6
|
+
require "inifile"
|
|
7
|
+
require "aws-sdk-core"
|
|
8
|
+
require "aws-sdk-sts"
|
|
9
|
+
require "aws-sdk-sso"
|
|
10
|
+
|
|
11
|
+
CACHE_DIRECTORY = "#{Dir.home}/.aws/ruby/cache"
|
|
1
12
|
AWS_CONFIG_FILE = ENV["AWS_CONFIG_FILE"] || "#{Dir.home}/.aws/config"
|
|
2
13
|
|
|
3
14
|
class PrxRubyAwsCreds
|
|
4
15
|
class << self
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
key_opts = assume_role_options.clone
|
|
16
|
+
# The cache key is based on the parameters used to request temporary
|
|
17
|
+
# credentials (using either STS AssumeRole or SSO GetRoleCredentials). The
|
|
18
|
+
# role session name is removed if it's randomly generated (which it always
|
|
19
|
+
# is for us). If the options were ever to include a policy document, that
|
|
20
|
+
# should get sorted before hashing.
|
|
21
|
+
# https://github.com/boto/botocore/blob/88d780dea1684da00689f2eef388fa4c782ced08/botocore/credentials.py#L700
|
|
22
|
+
#
|
|
23
|
+
# For any
|
|
24
|
+
def cache_key_path(role_options)
|
|
25
|
+
key_opts = role_options.clone
|
|
16
26
|
key_opts.delete(:role_session_name)
|
|
27
|
+
key_opts.delete(:access_token)
|
|
17
28
|
cache_key = Digest::SHA1.hexdigest(JSON.dump(key_opts))
|
|
18
29
|
|
|
19
|
-
"#{
|
|
30
|
+
"#{CACHE_DIRECTORY}/#{cache_key}.json"
|
|
20
31
|
end
|
|
21
32
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
def
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
Dir["#{Dir.home}/.aws/sso/cache/*.json"].each do |path|
|
|
34
|
-
data = JSON.parse(File.read(path))
|
|
35
|
-
if data["startUrl"] && data["startUrl"] == profile_start_url
|
|
36
|
-
sso_access_token = data["accessToken"]
|
|
37
|
-
break
|
|
33
|
+
# For a given SSO start URL, return a valid access token from the cache. If
|
|
34
|
+
# no valid token is found, returns nil. An access token will only be
|
|
35
|
+
# considered valid if it has not expired.
|
|
36
|
+
def sso_get_cached_access_token(start_url)
|
|
37
|
+
Dir["#{Dir.home}/.aws/sso/cache/*.json"].each do |path|
|
|
38
|
+
data = JSON.parse(File.read(path))
|
|
39
|
+
if data["startUrl"] && data["startUrl"] == start_url
|
|
40
|
+
expiration = Time.parse(data["expiresAt"])
|
|
41
|
+
|
|
42
|
+
if expiration > Time.now
|
|
43
|
+
return data["accessToken"]
|
|
38
44
|
end
|
|
39
45
|
end
|
|
46
|
+
end
|
|
40
47
|
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
puts "Press RETURN to request a token in a web browser."
|
|
44
|
-
puts "You can do this manually with: 'aws sso login --profile #{OPTS[:profile]}'"
|
|
45
|
-
inp = $stdin.gets.chomp
|
|
46
|
-
`aws sso login --profile #{OPTS[:profile]}` if inp.empty?
|
|
47
|
-
end
|
|
48
|
+
nil
|
|
49
|
+
end
|
|
48
50
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
51
|
+
# Returns the options passed to SSO#get_role_credentials. This is used when
|
|
52
|
+
# the profile uses an SSO, rather than a key/secret. If the selected
|
|
53
|
+
# profile is not configured for SSO, returns nil.
|
|
54
|
+
#
|
|
55
|
+
# `role_name` and `account_id` are values found in the config file for the
|
|
56
|
+
# given profile. `access_token` is a SSO token found in the SSO cache for
|
|
57
|
+
# the SSO start URL associated with the given profile.
|
|
58
|
+
def sso_get_role_options(profile_name)
|
|
59
|
+
aws_config_file = IniFile.load(AWS_CONFIG_FILE)
|
|
60
|
+
aws_config_file_section = aws_config_file["profile #{profile_name}"]
|
|
61
|
+
|
|
62
|
+
# The selected profile does not use SSO
|
|
63
|
+
return if !aws_config_file_section["sso_start_url"]
|
|
64
|
+
|
|
65
|
+
# Get the SSO start URL for the selected profile
|
|
66
|
+
profile_start_url = aws_config_file_section["sso_start_url"]
|
|
67
|
+
|
|
68
|
+
sso_access_token = sso_get_cached_access_token(profile_start_url)
|
|
69
|
+
|
|
70
|
+
# If a valid token wasn't found in the cache, prompt the user to fetch a
|
|
71
|
+
# new one.
|
|
72
|
+
if !sso_access_token
|
|
73
|
+
puts
|
|
74
|
+
puts "No #{"access token".yellow} was found for this SSO start URL associated with this profile (#{profile_start_url.blue})."
|
|
75
|
+
puts "Press #{"RETURN".green} to request a new token. This will open a web browser."
|
|
76
|
+
puts "You can also do this manually with: 'aws sso login --profile #{profile_name}'".gray
|
|
77
|
+
puts
|
|
78
|
+
inp = $stdin.gets.chomp
|
|
79
|
+
`aws sso login --profile #{profile_name}` if inp.empty?
|
|
80
|
+
sso_access_token = sso_get_cached_access_token(profile_start_url)
|
|
81
|
+
puts "This #{"access token".yellow} is valid for all SSO profiles using #{profile_start_url.blue} as their start URL."
|
|
82
|
+
puts
|
|
54
83
|
end
|
|
84
|
+
|
|
85
|
+
{
|
|
86
|
+
role_name: aws_config_file_section["sso_role_name"],
|
|
87
|
+
account_id: aws_config_file_section["sso_account_id"].to_s,
|
|
88
|
+
access_token: sso_access_token
|
|
89
|
+
}
|
|
55
90
|
end
|
|
56
91
|
|
|
57
|
-
# Returns the options passed to AssumeRole. This is used when the profile
|
|
58
|
-
# a key/secret. If the selected profile is not configured for
|
|
59
|
-
# returns nil.
|
|
60
|
-
def assume_role_options
|
|
92
|
+
# Returns the options passed to AssumeRole. This is used when the profile
|
|
93
|
+
# uses a key/secret. If the selected profile is not configured for
|
|
94
|
+
# key/secret, returns nil.
|
|
95
|
+
def assume_role_options(profile_name)
|
|
61
96
|
aws_config_file = IniFile.load(AWS_CONFIG_FILE)
|
|
62
|
-
aws_config_file_section = aws_config_file["profile #{
|
|
97
|
+
aws_config_file_section = aws_config_file["profile #{profile_name}"]
|
|
98
|
+
|
|
99
|
+
# Get the role ARN for the selected profile
|
|
63
100
|
role_arn = aws_config_file_section["role_arn"]
|
|
101
|
+
|
|
102
|
+
# Extract some values from the ARN
|
|
64
103
|
role_name = role_arn.split("role/")[1]
|
|
65
104
|
account_id = role_arn.split(":")[4]
|
|
66
105
|
|
|
@@ -71,49 +110,85 @@ class PrxRubyAwsCreds
|
|
|
71
110
|
}
|
|
72
111
|
end
|
|
73
112
|
|
|
74
|
-
|
|
75
|
-
|
|
113
|
+
# Makes a request to some AWS API endpoint that can generate temporary IAM
|
|
114
|
+
# credentials (e.g., AssumeRole, GetRoleCredentials, etc) based on the
|
|
115
|
+
# configuration of the selected profile.
|
|
116
|
+
#
|
|
117
|
+
# Cache the resulting credentials in the CACHE_DIRECTORY. The file is named
|
|
118
|
+
# using a hash of the options passed to the endpoint.
|
|
119
|
+
def get_and_cache_credentials(profile_name)
|
|
120
|
+
# Make sure the cache directory exists
|
|
121
|
+
FileUtils.mkdir_p CACHE_DIRECTORY
|
|
76
122
|
|
|
77
123
|
aws_config_file = IniFile.load(AWS_CONFIG_FILE)
|
|
78
|
-
aws_config_file_section = aws_config_file["profile #{
|
|
124
|
+
aws_config_file_section = aws_config_file["profile #{profile_name}"]
|
|
79
125
|
|
|
80
126
|
if aws_config_file_section["sso_role_name"]
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
127
|
+
# For SSO profiles, call GetRoleCredentials with a role, account, and
|
|
128
|
+
# access token to get back a set of temporary credentials.
|
|
129
|
+
# https://docs.aws.amazon.com/singlesignon/latest/PortalAPIReference/API_GetRoleCredentials.html
|
|
130
|
+
# https://docs.aws.amazon.com/sdk-for-ruby/v3/api/Aws/SSO/Client.html#get_role_credentials-instance_method
|
|
131
|
+
opts = sso_get_role_options(profile_name)
|
|
132
|
+
sso = Aws::SSO::Client.new(region: aws_config_file_section["sso_region"])
|
|
84
133
|
credentials = sso.get_role_credentials(opts)
|
|
85
134
|
|
|
135
|
+
# Cache the credentials. The structure of this file doesn't exactly
|
|
136
|
+
# match what native libraries (boto, etc) use. Instead, it matches the
|
|
137
|
+
# default output of assume_role. It could be anything, it just needs
|
|
138
|
+
# to be consistent across profile types, and match what
|
|
139
|
+
# load_and_verify_cached_credentials expects.
|
|
86
140
|
File.write(cache_key_path(opts), JSON.dump({"credentials" => {
|
|
87
141
|
"access_key_id" => credentials.role_credentials.access_key_id,
|
|
88
142
|
"secret_access_key" => credentials.role_credentials.secret_access_key,
|
|
89
143
|
"session_token" => credentials.role_credentials.session_token
|
|
90
144
|
}}))
|
|
91
145
|
|
|
146
|
+
# Return the temporary IAM credentials
|
|
92
147
|
Aws::Credentials.new(credentials.role_credentials.access_key_id, credentials.role_credentials.secret_access_key, credentials.role_credentials.session_token)
|
|
93
148
|
elsif aws_config_file_section["mfa_serial"]
|
|
149
|
+
# For profiles using an API key with an MFA token, get the serial
|
|
150
|
+
# number of the MFA device associated with the profile.
|
|
94
151
|
mfa_serial = aws_config_file_section["mfa_serial"]
|
|
95
152
|
|
|
153
|
+
# Prompt the user for the current TOTP code associated with the MFA
|
|
154
|
+
# device.
|
|
96
155
|
mfa_code = $stdin.getpass("Enter MFA code for #{mfa_serial}: ")
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
156
|
+
|
|
157
|
+
# Get a set of credentials for the role configured in the profile using
|
|
158
|
+
# the TOTP code. I don't remember why, but I don't think these
|
|
159
|
+
# credentials should be used for anything other than making another
|
|
160
|
+
# call to assume_role. Don't cache or return these credentials.
|
|
161
|
+
# Note: This is marked as a private API
|
|
162
|
+
credentials = Aws.shared_config.assume_role_credentials_from_config(profile: profile_name, token_code: mfa_code.chomp)
|
|
163
|
+
sts = Aws::STS::Client.new(region: "us-east-1", credentials: credentials)
|
|
164
|
+
|
|
165
|
+
# Make a call to get_caller_identity to ensure that the first set of
|
|
166
|
+
# credentials are valid?
|
|
102
167
|
_id = sts.get_caller_identity
|
|
103
168
|
|
|
169
|
+
# Make a regular assume_role call to get standard temporary IAM
|
|
170
|
+
# credentials.
|
|
104
171
|
opts = assume_role_options
|
|
105
|
-
cacheable_role = sts.assume_role(
|
|
172
|
+
cacheable_role = sts.assume_role(opts)
|
|
106
173
|
File.write(cache_key_path(opts), JSON.dump(cacheable_role.to_h))
|
|
107
174
|
|
|
175
|
+
# Return the temporary IAM credentials
|
|
108
176
|
Aws::Credentials.new(cacheable_role["credentials"]["access_key_id"], cacheable_role["credentials"]["secret_access_key"], cacheable_role["credentials"]["session_token"])
|
|
109
177
|
end
|
|
110
178
|
rescue Aws::SSO::Errors::UnauthorizedException
|
|
111
|
-
raise "The SSO access token for this profile is invalid. Run 'aws sso login --profile #{
|
|
179
|
+
raise "The SSO access token for this profile is invalid. Run 'aws sso login --profile #{profile_name}' to fetch a valid token."
|
|
112
180
|
end
|
|
113
181
|
|
|
114
|
-
|
|
182
|
+
# For the selected profile, look for a set of cached temporary IAM
|
|
183
|
+
# credentials. These are vanilla IAM credentials that look the same
|
|
184
|
+
# regardless of what type of profile is selected (SSO, MFA, etc).
|
|
185
|
+
#
|
|
186
|
+
# If no cached credential exist for the profile, or if the credentials are
|
|
187
|
+
# invalid (i.e., can't successfully call get_caller_identity), a new set
|
|
188
|
+
# of credentials will be fetched and cached.
|
|
189
|
+
def load_and_verify_cached_credentials(profile_name)
|
|
115
190
|
# Look up the cache file based on the options for the seleted profile.
|
|
116
|
-
options = sso_get_role_options || assume_role_options
|
|
191
|
+
options = sso_get_role_options(profile_name) || assume_role_options(profile_name)
|
|
117
192
|
|
|
118
193
|
cached_role_json = File.read(cache_key_path(options))
|
|
119
194
|
cached_role = JSON.parse(cached_role_json)
|
|
@@ -127,18 +202,20 @@ class PrxRubyAwsCreds
|
|
|
127
202
|
|
|
128
203
|
credentials
|
|
129
204
|
rescue Aws::STS::Errors::ExpiredToken
|
|
130
|
-
get_and_cache_credentials
|
|
205
|
+
get_and_cache_credentials(profile_name)
|
|
131
206
|
rescue Aws::STS::Errors::InvalidClientTokenId
|
|
132
|
-
get_and_cache_credentials
|
|
207
|
+
get_and_cache_credentials(profile_name)
|
|
133
208
|
rescue Errno::ENOENT
|
|
134
|
-
get_and_cache_credentials
|
|
209
|
+
get_and_cache_credentials(profile_name)
|
|
135
210
|
end
|
|
136
211
|
|
|
137
|
-
# Returns temporary client credentials for
|
|
138
|
-
#
|
|
139
|
-
def client_credentials
|
|
212
|
+
# Returns temporary IAM client (Aws::Credentials) credentials for a given
|
|
213
|
+
# profile.
|
|
214
|
+
def client_credentials(profile_name = nil)
|
|
215
|
+
profile_name ||= OPTS[:profile]
|
|
216
|
+
|
|
140
217
|
# For the selected profile, get the appropriate set of options.
|
|
141
|
-
options = sso_get_role_options || assume_role_options
|
|
218
|
+
options = sso_get_role_options(profile_name) || assume_role_options(profile_name)
|
|
142
219
|
|
|
143
220
|
return if !options
|
|
144
221
|
|
|
@@ -146,11 +223,11 @@ class PrxRubyAwsCreds
|
|
|
146
223
|
if !File.file?(cache_key_path(options))
|
|
147
224
|
# When no cache exists for these options, fetch new credentials, cache them
|
|
148
225
|
# and return them.
|
|
149
|
-
get_and_cache_credentials
|
|
226
|
+
get_and_cache_credentials(profile_name)
|
|
150
227
|
else
|
|
151
228
|
# When there is a cache for these options, return them if they are still
|
|
152
229
|
# valid, otherwise refresh them and return the new credentials.
|
|
153
|
-
load_and_verify_cached_credentials
|
|
230
|
+
load_and_verify_cached_credentials(profile_name)
|
|
154
231
|
end
|
|
155
232
|
end
|
|
156
233
|
end
|
metadata
CHANGED
|
@@ -1,15 +1,85 @@
|
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
|
2
2
|
name: prx-ruby-aws-creds
|
|
3
3
|
version: !ruby/object:Gem::Version
|
|
4
|
-
version: 0.1.
|
|
4
|
+
version: 0.1.1
|
|
5
5
|
platform: ruby
|
|
6
6
|
authors:
|
|
7
7
|
- Christopher Kalafarski
|
|
8
8
|
autorequire:
|
|
9
9
|
bindir: bin
|
|
10
10
|
cert_chain: []
|
|
11
|
-
date: 2023-04-
|
|
12
|
-
dependencies:
|
|
11
|
+
date: 2023-04-11 00:00:00.000000000 Z
|
|
12
|
+
dependencies:
|
|
13
|
+
- !ruby/object:Gem::Dependency
|
|
14
|
+
name: inifile
|
|
15
|
+
requirement: !ruby/object:Gem::Requirement
|
|
16
|
+
requirements:
|
|
17
|
+
- - ">="
|
|
18
|
+
- !ruby/object:Gem::Version
|
|
19
|
+
version: '0'
|
|
20
|
+
type: :runtime
|
|
21
|
+
prerelease: false
|
|
22
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
23
|
+
requirements:
|
|
24
|
+
- - ">="
|
|
25
|
+
- !ruby/object:Gem::Version
|
|
26
|
+
version: '0'
|
|
27
|
+
- !ruby/object:Gem::Dependency
|
|
28
|
+
name: nokogiri
|
|
29
|
+
requirement: !ruby/object:Gem::Requirement
|
|
30
|
+
requirements:
|
|
31
|
+
- - ">="
|
|
32
|
+
- !ruby/object:Gem::Version
|
|
33
|
+
version: '0'
|
|
34
|
+
type: :runtime
|
|
35
|
+
prerelease: false
|
|
36
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
37
|
+
requirements:
|
|
38
|
+
- - ">="
|
|
39
|
+
- !ruby/object:Gem::Version
|
|
40
|
+
version: '0'
|
|
41
|
+
- !ruby/object:Gem::Dependency
|
|
42
|
+
name: aws-sdk-core
|
|
43
|
+
requirement: !ruby/object:Gem::Requirement
|
|
44
|
+
requirements:
|
|
45
|
+
- - ">="
|
|
46
|
+
- !ruby/object:Gem::Version
|
|
47
|
+
version: '0'
|
|
48
|
+
type: :runtime
|
|
49
|
+
prerelease: false
|
|
50
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
51
|
+
requirements:
|
|
52
|
+
- - ">="
|
|
53
|
+
- !ruby/object:Gem::Version
|
|
54
|
+
version: '0'
|
|
55
|
+
- !ruby/object:Gem::Dependency
|
|
56
|
+
name: aws-sdk-sso
|
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
|
58
|
+
requirements:
|
|
59
|
+
- - ">="
|
|
60
|
+
- !ruby/object:Gem::Version
|
|
61
|
+
version: '0'
|
|
62
|
+
type: :runtime
|
|
63
|
+
prerelease: false
|
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
65
|
+
requirements:
|
|
66
|
+
- - ">="
|
|
67
|
+
- !ruby/object:Gem::Version
|
|
68
|
+
version: '0'
|
|
69
|
+
- !ruby/object:Gem::Dependency
|
|
70
|
+
name: aws-sdk-sts
|
|
71
|
+
requirement: !ruby/object:Gem::Requirement
|
|
72
|
+
requirements:
|
|
73
|
+
- - ">="
|
|
74
|
+
- !ruby/object:Gem::Version
|
|
75
|
+
version: '0'
|
|
76
|
+
type: :runtime
|
|
77
|
+
prerelease: false
|
|
78
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
79
|
+
requirements:
|
|
80
|
+
- - ">="
|
|
81
|
+
- !ruby/object:Gem::Version
|
|
82
|
+
version: '0'
|
|
13
83
|
description: tktk
|
|
14
84
|
email: chris.kalafarski@prx.org
|
|
15
85
|
executables: []
|