Skip to content

Commit e918f0d

Browse files
committed
Merge branch 'process-credentials' into main
2 parents a849eb3 + 856d43b commit e918f0d

File tree

5 files changed

+127
-29
lines changed

5 files changed

+127
-29
lines changed

README.md

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -77,6 +77,17 @@ Aws::Google.config = {
7777
puts Aws::STS::Client.new.get_caller_identity
7878
```
7979

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+
```
90+
8091
## Development
8192

8293
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.

aws-google.gemspec

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -23,11 +23,11 @@ Gem::Specification.new do |spec|
2323

2424
spec.add_dependency 'aws-sdk-core', '~> 3'
2525
spec.add_dependency 'google-api-client', '~> 0.23'
26-
spec.add_dependency 'launchy', '~> 2' # Peer dependency of Google::APIClient::InstalledAppFlow
26+
spec.add_dependency 'launchy', '~> 2'
2727

2828
spec.add_development_dependency 'activesupport', '~> 5'
29-
spec.add_development_dependency 'bundler', '~> 1'
30-
spec.add_development_dependency 'minitest', '~> 5.10'
29+
spec.add_development_dependency 'bundler'
30+
spec.add_development_dependency 'minitest', '~> 5.14.2'
3131
spec.add_development_dependency 'mocha', '~> 1.5'
3232
spec.add_development_dependency 'rake', '~> 12'
3333
spec.add_development_dependency 'timecop', '~> 0.8'

exe/aws-google

Lines changed: 73 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,73 @@
1+
#!/usr/bin/env ruby
2+
3+
# CLI to retrieve AWS credentials in credential_process format.
4+
# Ref: https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-sourcing-external.html
5+
6+
require 'aws/google'
7+
require 'time'
8+
require 'json'
9+
require 'optparse'
10+
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)
64+
credentials = google.credentials
65+
output = {
66+
Version: 1,
67+
AccessKeyId: credentials.access_key_id,
68+
SecretAccessKey: credentials.secret_access_key,
69+
SessionToken: credentials.session_token,
70+
Expiration: Time.at(google.expiration.to_i).iso8601
71+
}
72+
require 'json'
73+
puts output.to_json

lib/aws/google.rb

Lines changed: 30 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,7 +39,7 @@ class << self
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.
42-
# Defaults to an out-of-band authentication process.
42+
# Defaults to 1234. Set to nil or 0 to use an out-of-band authentication process.
4343
# @option options [::Google::Auth::ClientId] :google_id
4444
def initialize(options = {})
4545
@oauth_attempted = false
@@ -56,7 +56,7 @@ def initialize(options = {})
5656
@client = options[:client] || Aws::STS::Client.new(credentials: nil)
5757
@domain = options[:domain]
5858
@online = options[:online]
59-
@port = options[:port]
59+
@port = options[:port] || 1234
6060

6161
# Use existing AWS credentials stored in the shared config if available.
6262
# If this is `nil` or expired, #refresh will be called on the first AWS API service call
@@ -105,8 +105,18 @@ def google_oauth
105105
credentials.tap(&storage.method(:write_credentials))
106106
end
107107

108+
def silence_output
109+
outs = [$stdout, $stderr]
110+
clones = outs.map(&:clone)
111+
outs.each { |io| io.reopen '/dev/null'}
112+
yield
113+
ensure
114+
outs.each_with_index { |io, i| io.reopen(clones[i]) }
115+
end
116+
108117
def get_oauth_code(client, options)
109-
raise 'fallback' unless @port
118+
raise 'fallback' unless @port && !@port.zero?
119+
110120
require 'launchy'
111121
require 'webrick'
112122
code = nil
@@ -123,26 +133,29 @@ def get_oauth_code(client, options)
123133
end
124134
trap('INT') { server.shutdown }
125135
client.redirect_uri = "http://localhost:#{@port}"
126-
launchy = Launchy.open(client.authorization_uri(options).to_s)
127-
server_thread = Thread.new do
128-
begin
129-
server.start
130-
ensure server.shutdown
136+
silence_output do
137+
launchy = Launchy.open(client.authorization_uri(options).to_s)
138+
server_thread = Thread.new do
139+
begin
140+
server.start
141+
ensure server.shutdown
142+
end
143+
end
144+
while server_thread.alive?
145+
raise 'fallback' if !launchy.alive? && !launchy.value.success?
146+
147+
sleep 0.1
131148
end
132-
end
133-
while server_thread.alive?
134-
raise 'fallback' if !launchy.alive? && !launchy.value.success?
135-
sleep 0.1
136149
end
137150
code || raise('fallback')
138151
rescue StandardError
139152
trap('INT', 'DEFAULT')
140153
# Fallback to out-of-band authentication if browser launch failed.
141154
client.redirect_uri = 'oob'
142-
url = client.authorization_uri(options)
143-
print "\nOpen the following URL in a browser and enter the " \
144-
"resulting code after authorization:\n#{url}\n> "
145-
gets
155+
return ENV['OAUTH_CODE'] if ENV['OAUTH_CODE']
156+
157+
raise RuntimeError, 'Open the following URL in a browser to get a code,' \
158+
"export to $OAUTH_CODE and rerun:\n#{client.authorization_uri(options)}", []
146159
end
147160

148161
def refresh
@@ -176,7 +189,7 @@ def refresh
176189
raise e, "\nYour Google ID does not have access to the requested AWS Role. Ask your administrator to provide access.
177190
Role: #{@assume_role_params[:role_arn]}
178191
Email: #{token_params['email']}
179-
Google ID: #{token_params['sub']}", e.backtrace
192+
Google ID: #{token_params['sub']}", []
180193
end
181194

182195
c = assume_role.credentials

test/aws/google_test.rb

Lines changed: 10 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -17,6 +17,7 @@
1717
)
1818
# Disable instance metadata credentials.
1919
stub_request(:get, '169.254.169.254/latest/meta-data/iam/security-credentials/')
20+
stub_request(:put, '169.254.169.254/latest/api/token')
2021
# Disable environment credentials.
2122
ENV.stubs(:[]).returns(nil)
2223
end
@@ -79,9 +80,9 @@
7980
system.times(5)
8081

8182
c = Aws::STS::Client.new.config.credentials
82-
c.credentials.access_key_id.must_equal credentials[:access_key_id]
83-
c.credentials.secret_access_key.must_equal credentials[:secret_access_key]
84-
c.credentials.session_token.must_equal credentials[:session_token]
83+
_(c.credentials.access_key_id).must_equal credentials[:access_key_id]
84+
_(c.credentials.secret_access_key).must_equal credentials[:secret_access_key]
85+
_(c.credentials.session_token).must_equal credentials[:session_token]
8586
end
8687

8788
it 'refreshes expired Google auth token credentials' do
@@ -95,9 +96,9 @@
9596
system.times(5)
9697

9798
c = Aws::STS::Client.new.config.credentials
98-
c.credentials.access_key_id.must_equal credentials[:access_key_id]
99-
c.credentials.secret_access_key.must_equal credentials[:secret_access_key]
100-
c.credentials.session_token.must_equal credentials[:session_token]
99+
_(c.credentials.access_key_id).must_equal credentials[:access_key_id]
100+
_(c.credentials.secret_access_key).must_equal credentials[:secret_access_key]
101+
_(c.credentials.session_token).must_equal credentials[:session_token]
101102
end
102103

103104
it 'refreshes expired credentials' do
@@ -110,9 +111,9 @@
110111
)
111112
service = Aws::STS::Client.new
112113
expiration = service.config.credentials.expiration
113-
expiration.must_equal(service.config.credentials.expiration)
114+
_(expiration).must_equal(service.config.credentials.expiration)
114115
Timecop.travel(1.5.hours.from_now) do
115-
expiration.wont_equal(service.config.credentials.expiration)
116+
_(expiration).wont_equal(service.config.credentials.expiration)
116117
end
117118
end
118119

@@ -153,7 +154,7 @@
153154
err = assert_raises(Aws::STS::Errors::AccessDenied) do
154155
Aws::STS::Client.new.config.credentials
155156
end
156-
err.message.must_match /Your Google ID does not have access to the requested AWS Role./
157+
_(err.message).must_match 'Your Google ID does not have access to the requested AWS Role.'
157158
end
158159
end
159160

0 commit comments

Comments
 (0)