Skip to content

Commit 74523a0

Browse files
committed
add aws-google CLI executable
1 parent a512d71 commit 74523a0

File tree

4 files changed

+115
-18
lines changed

4 files changed

+115
-18
lines changed

README.md

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

82+
- 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:
83+
84+
```
85+
[profile my_google]
86+
credential_process = aws-google --profile my_google
87+
aws_role = arn:aws:iam::[AccountID]:role/[Role]
88+
google_client_id = 123456789012-abcdefghijklmnopqrstuvwzyz0123456.apps.googleusercontent.com
89+
google_client_secret = 01234567890abcdefghijklmn
90+
91+
```
92+
8293
## Development
8394

8495
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: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ 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'
2929
spec.add_development_dependency 'bundler', '~> 1'

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

0 commit comments

Comments
 (0)