Skip to content

Commit 172682e

Browse files
committed
Refactor credential provider
Make it easier to use credentials in AWS config.
1 parent 34c5dbb commit 172682e

File tree

5 files changed

+87
-123
lines changed

5 files changed

+87
-123
lines changed

README.md

Lines changed: 10 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -65,28 +65,18 @@ role_credentials = Aws::Google.new(
6565
puts Aws::STS::Client.new(credentials: role_credentials).get_caller_identity
6666
```
6767

68-
- Or, set `Aws::Google.config` hash to add Google auth to the default credential provider chain:
69-
70-
```ruby
71-
Aws::Google.config = {
72-
role_arn: aws_role,
73-
google_client_id: client_id,
74-
google_client_secret: client_secret,
75-
}
76-
77-
puts Aws::STS::Client.new.get_caller_identity
68+
- Or, add the properties to your AWS config profile ([`~/.aws/config`](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-where)) to use Google as the AWS credential provider without any changes to your application code:
69+
70+
```ini
71+
[my_profile]
72+
google =
73+
role_arn = arn:aws:iam::[AccountID]:role/[Role]
74+
client_id = 123456789012-abcdefghijklmnopqrstuvwzyz0123456.apps.googleusercontent.com
75+
client_secret = 01234567890abcdefghijklmn
76+
credential_process = aws-google
7877
```
7978

80-
- Or, set `credential_process` in your AWS config profile ([`~/.aws/config`](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html#cli-configure-files-where)) to `aws-google` to [Source Credentials with an External Process](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html) without any change to your application code:
81-
82-
```
83-
[profile my_google]
84-
credential_process = aws-google --profile my_google
85-
aws_role = arn:aws:iam::[AccountID]:role/[Role]
86-
google_client_id = 123456789012-abcdefghijklmnopqrstuvwzyz0123456.apps.googleusercontent.com
87-
google_client_secret = 01234567890abcdefghijklmn
88-
89-
```
79+
The extra `credential_process` config line tells AWS to [Source Credentials with an External Process](https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html), in this case the `aws-google` script, which allows you to seamlessly use the same Google login configuration from non-Ruby SDKs (like the CLI).
9080

9181
## Development
9282

exe/aws-google

Lines changed: 1 addition & 55 deletions
Original file line numberDiff line numberDiff line change
@@ -6,61 +6,8 @@
66
require 'aws/google'
77
require 'time'
88
require 'json'
9-
require 'optparse'
109

11-
def error(msg)
12-
puts msg
13-
exit 1
14-
end
15-
16-
options = {}
17-
OptionParser.new do |opts|
18-
opts.on('-p PROFILE', '--profile PROFILE', 'Profile') do |p|
19-
options[:profile] = p
20-
end
21-
opts.on('-v', '--version', 'Version') do
22-
require 'aws/google/version'
23-
puts Aws::Google::VERSION
24-
exit 0
25-
end
26-
opts.on('-r ROLE', '--role ROLE', 'AWS Role arn') do |r|
27-
options[:role_arn] = r
28-
end
29-
opts.on('-i ID', '--client-id ID', 'Google Client ID') do |id|
30-
options[:google_client_id] = id
31-
end
32-
opts.on('-s SECRET', '--client-secret SECRET', 'Google Client Secret') do |secret|
33-
options[:google_client_secret] = secret
34-
end
35-
opts.on('-h DOMAIN', '--domain DOMAIN', 'Google Domain') do |hd|
36-
options[:domain] = hd
37-
end
38-
opts.on('-d DURATION', '--duration DURATION', 'Duration in seconds') do |d|
39-
options[:duration_seconds] = d.to_i
40-
end
41-
opts.on('--port PORT', 'Port number for local server') do |p|
42-
options[:port] = p.to_i
43-
end
44-
opts.on('--online', 'Online authentication, no refresh token') do |o|
45-
options[:online] = true
46-
end
47-
end.parse!
48-
49-
config = Aws.shared_config
50-
profile = options[:profile] || ENV['AWS_PROFILE'] || 'default'
51-
52-
options[:role_arn] ||= config.get('aws_role', profile: profile) ||
53-
error('Missing config: aws_role')
54-
options[:google_client_id] ||= config.get('google_client_id', profile: profile) ||
55-
error('Missing config: google_client_id')
56-
options[:google_client_secret] ||= config.get('google_client_secret', profile: profile) ||
57-
error('Missing config: google_client_secret')
58-
59-
# Cache temporary-session credentials in a separately-named profile.
60-
# Stored credentials take priority over credential_process,
61-
# so they would never be refreshed if stored in the same profile.
62-
options[:profile] += '_session'
63-
google = Aws::Google.new(options)
10+
google = ::Aws::CredentialProviderChain.new.resolve
6411
credentials = google.credentials
6512
output = {
6613
Version: 1,
@@ -69,5 +16,4 @@ output = {
6916
SessionToken: credentials.session_token,
7017
Expiration: Time.at(google.expiration.to_i).iso8601
7118
}
72-
require 'json'
7319
puts output.to_json

lib/aws/google.rb

Lines changed: 7 additions & 44 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
require_relative 'google/version'
22
require 'aws-sdk-core'
33
require_relative 'google/credential_provider'
4+
require_relative 'google/cached_credentials'
45

56
require 'googleauth'
67
require 'google/api_client/auth/storage'
@@ -23,7 +24,7 @@ module Aws
2324
# constructed.
2425
class Google
2526
include ::Aws::CredentialProvider
26-
include ::Aws::RefreshingCredentials
27+
include ::Aws::Google::CachedCredentials
2728

2829
class << self
2930
attr_accessor :config
@@ -34,40 +35,29 @@ class << self
3435
# @option options [Integer] :duration_seconds
3536
# @option options [String] :external_id
3637
# @option options [STS::Client] :client STS::Client to use (default: create new client)
37-
# @option options [String] :profile AWS Profile to store temporary credentials (default `default`)
3838
# @option options [String] :domain G Suite domain for account-selection hint
3939
# @option options [String] :online if `true` only a temporary access token will be provided,
4040
# a long-lived refresh token will not be created and stored on the filesystem.
4141
# @option options [String] :port port for local server to listen on to capture oauth browser redirect.
4242
# Defaults to 1234. Set to nil or 0 to use an out-of-band authentication process.
43-
# @option options [::Google::Auth::ClientId] :google_id
43+
# @option options [String] :client_id Google client ID
44+
# @option options [String] :client_secret Google client secret
4445
def initialize(options = {})
4546
@oauth_attempted = false
4647
@assume_role_params = options.slice(
4748
*Aws::STS::Client.api.operation(:assume_role_with_web_identity).
4849
input.shape.member_names
4950
)
5051

51-
@profile = options[:profile] || ENV['AWS_DEFAULT_PROFILE'] || 'default'
5252
@google_id = ::Google::Auth::ClientId.new(
53-
options[:google_client_id],
54-
options[:google_client_secret]
53+
options[:client_id],
54+
options[:client_secret]
5555
)
5656
@client = options[:client] || Aws::STS::Client.new(credentials: nil)
5757
@domain = options[:domain]
5858
@online = options[:online]
5959
@port = options[:port] || 1234
60-
61-
# Use existing AWS credentials stored in the shared config if available.
62-
# If this is `nil` or expired, #refresh will be called on the first AWS API service call
63-
# to generate AWS credentials derived from Google authentication.
64-
@expiration = Aws.shared_config.get('expiration', profile: @profile) rescue nil
65-
@mutex = Mutex.new
66-
if near_expiration?
67-
refresh!
68-
else
69-
@credentials = Aws.shared_config.credentials(profile: @profile) rescue nil
70-
end
60+
super
7161
end
7262

7363
private
@@ -199,35 +189,8 @@ def refresh
199189
c.session_token
200190
)
201191
@expiration = c.expiration.to_i
202-
write_credentials
203-
end
204-
205-
# Write credentials and expiration to AWS credentials file.
206-
def write_credentials
207-
# AWS CLI is needed because writing AWS credentials is not supported by the AWS Ruby SDK.
208-
return unless system('which aws >/dev/null 2>&1')
209-
%w[
210-
access_key_id
211-
secret_access_key
212-
session_token
213-
].map {|x| ["aws_#{x}", @credentials.send(x)]}.
214-
to_h.
215-
merge(expiration: @expiration).each do |key, value|
216-
system("aws configure set #{key} #{value} --profile #{@profile}")
217-
end
218-
end
219-
end
220-
221-
# Patch Aws::SharedConfig to allow fetching arbitrary keys from the shared config.
222-
module SharedConfigGetKey
223-
def get(key, opts = {})
224-
profile = opts.delete(:profile) || @profile_name
225-
if @parsed_config && (prof_config = @parsed_config[profile])
226-
prof_config[key]
227-
end
228192
end
229193
end
230-
Aws::SharedConfig.prepend SharedConfigGetKey
231194

232195
# Extend ::Google::APIClient::Storage to write {type: 'authorized_user'} to credentials,
233196
# as required by Google's default credentials loader.

lib/aws/google/cached_credentials.rb

Lines changed: 47 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,47 @@
1+
module Aws
2+
class Google
3+
Aws::SharedConfig.config_reader :expiration
4+
5+
# Mixin module extending `RefreshingCredentials` that caches temporary credentials
6+
# in the credentials file, so a single session can be reused across multiple processes.
7+
# The temporary credentials are saved to a separate profile with a '_session' suffix.
8+
module CachedCredentials
9+
include RefreshingCredentials
10+
11+
# @option options [String] :profile AWS Profile to store temporary credentials (default `default`)
12+
def initialize(options = {})
13+
# Use existing AWS credentials stored in the shared session config if available.
14+
# If this is `nil` or expired, #refresh will be called on the first AWS API service call
15+
# to generate AWS credentials derived from Google authentication.
16+
@mutex = Mutex.new
17+
18+
@profile = options[:profile] || ENV['AWS_PROFILE'] || ENV['AWS_DEFAULT_PROFILE'] || 'default'
19+
@session_profile = @profile + '_session'
20+
@expiration = Aws.shared_config.expiration(profile: @session_profile) rescue nil
21+
@credentials = Aws.shared_config.credentials(profile: @session_profile) rescue nil
22+
refresh_if_near_expiration
23+
end
24+
25+
def refresh_if_near_expiration
26+
if near_expiration?
27+
@mutex.synchronize do
28+
if near_expiration?
29+
refresh
30+
write_credentials
31+
end
32+
end
33+
end
34+
end
35+
36+
# Write credentials and expiration to AWS credentials file.
37+
def write_credentials
38+
# AWS CLI is needed because writing AWS credentials is not supported by the AWS Ruby SDK.
39+
return unless system('which aws >/dev/null 2>&1')
40+
Aws::SharedCredentials::KEY_MAP.transform_values(&@credentials.method(:send)).
41+
merge(expiration: @expiration).each do |key, value|
42+
system("aws configure set #{key} #{value} --profile #{@session_profile}")
43+
end
44+
end
45+
end
46+
end
47+
end

lib/aws/google/credential_provider.rb

Lines changed: 22 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -3,16 +3,34 @@ class Google
33
# Inserts GoogleCredentials into the default AWS credential provider chain.
44
# Google credentials will only be used if Aws::Google.config is set before initialization.
55
module CredentialProvider
6-
# Insert google_credentials as the second-to-last credentials provider
7-
# (in front of instance profile, which makes an http request).
6+
# Insert google_credentials as the third-to-last credentials provider
7+
# (in front of process credentials and instance_profile credentials).
88
def providers
9-
super.insert(-2, [:google_credentials, {}])
9+
super.insert(-3, [:google_credentials, {}])
1010
end
1111

1212
def google_credentials(options)
13-
(config = Google.config) && Google.new(options.merge(config))
13+
profile_name = determine_profile_name(options)
14+
if Aws.shared_config.config_enabled?
15+
Aws.shared_config.google_credentials_from_config(profile: profile_name)
16+
end
17+
rescue Errors::NoSuchProfileError
18+
nil
1419
end
1520
end
1621
::Aws::CredentialProviderChain.prepend CredentialProvider
22+
23+
module GoogleSharedCredentials
24+
def google_credentials_from_config(opts = {})
25+
p = opts[:profile] || @profile_name
26+
if @config_enabled && @parsed_config
27+
entry = @parsed_config.fetch(p, {})
28+
if (google_opts = entry['google'])
29+
Google.new(google_opts.transform_keys(&:to_sym))
30+
end
31+
end
32+
end
33+
end
34+
::Aws::SharedConfig.prepend GoogleSharedCredentials
1735
end
1836
end

0 commit comments

Comments
 (0)