Skip to content

Commit 25ef361

Browse files
committed
initial version
0 parents  commit 25ef361

File tree

16 files changed

+588
-0
lines changed

16 files changed

+588
-0
lines changed

.gitignore

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
/.bundle/
2+
/.yardoc
3+
/Gemfile.lock
4+
/_yardoc/
5+
/coverage/
6+
/doc/
7+
/pkg/
8+
/spec/reports/
9+
/tmp/
10+
/.idea/

.travis.yml

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
sudo: false
2+
language: ruby
3+
rvm:
4+
- 2.4.1
5+
before_install: gem install bundler -v 1.14.6

Gemfile

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,2 @@
1+
source 'https://rubygems.org'
2+
gemspec

LICENSE.txt

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
Copyright 2019 Code.org
2+
3+
Licensed under the Apache License, Version 2.0 (the "License");
4+
you may not use this file except in compliance with the License.
5+
You may obtain a copy of the License at
6+
7+
http://www.apache.org/licenses/LICENSE-2.0
8+
9+
Unless required by applicable law or agreed to in writing, software
10+
distributed under the License is distributed on an "AS IS" BASIS,
11+
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12+
See the License for the specific language governing permissions and
13+
limitations under the License.

README.md

Lines changed: 92 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,92 @@
1+
# Aws::Google
2+
3+
Use Google OAuth as an AWS Credential Provider.
4+
5+
## Installation
6+
7+
Add this line to your application's `Gemfile`:
8+
9+
```ruby
10+
gem 'aws-google'
11+
```
12+
13+
And then execute:
14+
15+
$ bundle
16+
17+
Or install it yourself as:
18+
19+
$ gem install aws-google
20+
21+
## Usage
22+
23+
- Visit the [Google API Console](https://console.developers.google.com/) to create/obtain OAuth 2.0 Client ID credentials (client ID and client secret) for an application in your Google account.
24+
- Create an AWS IAM Role with the desired IAM policies attached, and a 'trust relationship' (`AssumeRolePolicyDocument`) allowing the `sts:AssumeRoleWithWebIdentity` action to be permitted
25+
by your Google Client ID and a specific set of Google Account IDs:
26+
27+
```json
28+
{
29+
"Version": "2012-10-17",
30+
"Statement": [
31+
{
32+
"Effect": "Allow",
33+
"Principal": {
34+
"Federated": "accounts.google.com"
35+
},
36+
"Action": "sts:AssumeRoleWithWebIdentity",
37+
"Condition": {
38+
"StringEquals": {
39+
"accounts.google.com:aud": "123456789012-abcdefghijklmnopqrstuvwzyz0123456.apps.googleusercontent.com"
40+
},
41+
"ForAnyValue:StringEquals": {
42+
"accounts.google.com:sub": [
43+
"000000000000000000000",
44+
"111111111111111111111"
45+
]
46+
}
47+
}
48+
}
49+
]
50+
}
51+
```
52+
53+
- In your Ruby code, construct an `Aws::Google` object by passing in the AWS role, client id and client secret:
54+
```ruby
55+
aws_role = 'arn:aws:iam::[AccountID]:role/[Role]'
56+
client_id = '123456789012-abcdefghijklmnopqrstuvwzyz0123456.apps.googleusercontent.com'
57+
client_secret = '01234567890abcdefghijklmn'
58+
59+
role_credentials = Aws::Google.new(
60+
role_arn: aws_role,
61+
google_client_id: client_id,
62+
google_client_secret: client_secret
63+
)
64+
65+
puts Aws::STS::Client.new(credentials: role_credentials).get_caller_identity
66+
```
67+
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
78+
```
79+
80+
## Development
81+
82+
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.
83+
84+
To install this gem onto your local machine, run `bundle exec rake install`. To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`, which will create a git tag for the version, push git commits and tags, and push the `.gem` file to [rubygems.org](https://rubygems.org).
85+
86+
## Contributing
87+
88+
Bug reports and pull requests are welcome on GitHub at https://github.com/code-dot-org/aws-google.
89+
90+
## License
91+
92+
The gem is available as open source under the terms of the [Apache 2.0 License](http://opensource.org/licenses/apache-2.0).

Rakefile

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,10 @@
1+
require "bundler/gem_tasks"
2+
require "rake/testtask"
3+
4+
Rake::TestTask.new(:test) do |t|
5+
t.libs << "test"
6+
t.libs << "lib"
7+
t.test_files = FileList['test/**/*_test.rb']
8+
end
9+
10+
task :default => :test

aws-google.gemspec

Lines changed: 43 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,43 @@
1+
lib = File.expand_path('../lib', __FILE__)
2+
$LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib)
3+
require 'aws/google/version'
4+
5+
Gem::Specification.new do |spec|
6+
spec.name = 'aws-google'
7+
spec.version = Aws::Google::VERSION
8+
spec.authors = ['Will Jordan']
9+
spec.email = ['[email protected]']
10+
11+
spec.summary = 'Use Google OAuth as an AWS credential provider.'
12+
spec.description = 'Use Google OAuth as an AWS credential provider.'
13+
spec.homepage = 'https://github.com/code-dot-org/aws-google'
14+
spec.license = 'Apache-2.0'
15+
16+
# Prevent pushing this gem to RubyGems.org. To allow pushes either set the 'allowed_push_host'
17+
# to allow pushing to a single host or delete this section to allow pushing to any host.
18+
if spec.respond_to?(:metadata)
19+
spec.metadata['allowed_push_host'] = "TODO: Set to 'http://mygemserver.com'"
20+
else
21+
raise 'RubyGems 2.0 or newer is required to protect against ' \
22+
'public gem pushes.'
23+
end
24+
25+
spec.files = `git ls-files -z`.split("\x0").reject do |f|
26+
f.match(%r{^(test|spec|features)/})
27+
end
28+
spec.bindir = 'exe'
29+
spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) }
30+
spec.require_paths = ['lib']
31+
32+
spec.add_dependency 'aws-sdk-core', '~> 3'
33+
spec.add_dependency 'google-api-client', '~> 0.23'
34+
spec.add_dependency 'launchy' # Peer dependency of Google::APIClient::InstalledAppFlow
35+
36+
spec.add_development_dependency 'activesupport'
37+
spec.add_development_dependency 'bundler'
38+
spec.add_development_dependency 'minitest'
39+
spec.add_development_dependency 'mocha'
40+
spec.add_development_dependency 'rake'
41+
spec.add_development_dependency 'timecop'
42+
spec.add_development_dependency 'webmock'
43+
end

bin/console

Lines changed: 14 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,14 @@
1+
#!/usr/bin/env ruby
2+
3+
require 'bundler/setup'
4+
require 'aws/google'
5+
6+
# You can add fixtures and/or initialization code here to make experimenting
7+
# with your gem easier. You can also use a different console, if you like.
8+
9+
# (If you use this, don't forget to add pry to your Gemfile!)
10+
# require "pry"
11+
# Pry.start
12+
13+
require 'irb'
14+
IRB.start(__FILE__)

bin/setup

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,8 @@
1+
#!/usr/bin/env bash
2+
set -euo pipefail
3+
IFS=$'\n\t'
4+
set -vx
5+
6+
bundle install
7+
8+
# Do any other automated setup that you need to do here

lib/aws/google.rb

Lines changed: 167 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,167 @@
1+
require_relative 'google/version'
2+
require 'aws-sdk-core'
3+
require_relative 'google/credential_provider'
4+
5+
require 'googleauth'
6+
require 'google/api_client/auth/storage'
7+
require 'google/api_client/auth/storages/file_store'
8+
require 'google/api_client/auth/installed_app'
9+
10+
module Aws
11+
# An auto-refreshing credential provider that works by assuming
12+
# a role via {Aws::STS::Client#assume_role_with_web_identity},
13+
# using an ID token derived from a Google refresh token.
14+
#
15+
# role_credentials = Aws::Google.new(
16+
# role_arn: aws_role,
17+
# google_client_id: client_id,
18+
# google_client_secret: client_secret
19+
# )
20+
#
21+
# ec2 = Aws::EC2::Client.new(credentials: role_credentials)
22+
#
23+
# If you omit `:client` option, a new {STS::Client} object will be
24+
# constructed.
25+
class Google
26+
include ::Aws::CredentialProvider
27+
include ::Aws::RefreshingCredentials
28+
29+
class << self
30+
attr_accessor :config
31+
end
32+
33+
# @option options [required, String] :role_arn
34+
# @option options [String] :policy
35+
# @option options [Integer] :duration_seconds
36+
# @option options [String] :external_id
37+
# @option options [STS::Client] :client STS::Client to use (default: create new client)
38+
# @option options [String] :profile AWS Profile to store temporary credentials (default `default`)
39+
# @option options [::Google::Auth::ClientId] :google_id
40+
def initialize(options = {})
41+
@oauth_attempted = false
42+
@assume_role_params = options.slice(
43+
*Aws::STS::Client.api.operation(:assume_role_with_web_identity).
44+
input.shape.member_names
45+
)
46+
47+
@profile = options[:profile] || ENV['AWS_DEFAULT_PROFILE'] || 'default'
48+
@google_id = ::Google::Auth::ClientId.new(
49+
options[:google_client_id],
50+
options[:google_client_secret]
51+
)
52+
@client = options[:client] || Aws::STS::Client.new(credentials: nil)
53+
54+
# Use existing AWS credentials stored in the shared config if available.
55+
# If this is `nil` or expired, #refresh will be called on the first AWS API service call
56+
# to generate AWS credentials derived from Google authentication.
57+
@expiration = Aws.shared_config.get('expiration', profile: @profile) rescue nil
58+
@mutex = Mutex.new
59+
if near_expiration?
60+
refresh!
61+
else
62+
@credentials = Aws.shared_config.credentials(profile: @profile) rescue nil
63+
end
64+
end
65+
66+
private
67+
68+
# Use cached Application Default Credentials if available,
69+
# otherwise fallback to creating new Google credentials through browser login.
70+
def google_client
71+
@google_client ||= (::Google::Auth.get_application_default rescue nil) || google_oauth
72+
end
73+
74+
# Create an OAuth2 Client using Google's default browser-based OAuth InstalledAppFlow.
75+
# Store cached credentials to the standard Google Application Default Credentials location.
76+
# Ref: http://goo.gl/IUuyuX
77+
def google_oauth
78+
return nil if @oauth_attempted
79+
@oauth_attempted = true
80+
require 'google/api_client/auth/installed_app'
81+
flow = ::Google::APIClient::InstalledAppFlow.new(
82+
client_id: @google_id.id,
83+
client_secret: @google_id.secret,
84+
scope: %w[email profile]
85+
)
86+
path = "#{ENV['HOME']}/.config/#{::Google::Auth::CredentialsLoader::WELL_KNOWN_PATH}"
87+
FileUtils.mkdir_p(File.dirname(path))
88+
flow.authorize(GoogleStorage.new(::Google::APIClient::FileStore.new(path)))
89+
end
90+
91+
def refresh
92+
assume_role = begin
93+
client = google_client
94+
return unless client
95+
96+
begin
97+
tries ||= 2
98+
id_token = client.id_token
99+
# Decode the JWT id_token to use the Google email as the AWS role session name.
100+
token_params = JWT.decode(id_token, nil, false).first
101+
rescue JWT::DecodeError, JWT::ExpiredSignature
102+
# Refresh and retry once if token is expired or invalid.
103+
client.refresh!
104+
(tries -= 1).zero? ? raise : retry
105+
end
106+
107+
@client.assume_role_with_web_identity(
108+
@assume_role_params.merge(
109+
web_identity_token: id_token,
110+
role_session_name: token_params['email']
111+
)
112+
)
113+
rescue Signet::AuthorizationError => e
114+
(@google_client = google_oauth) && retry || raise
115+
rescue Aws::STS::Errors::AccessDenied => e
116+
retry if (@google_client = google_oauth)
117+
raise e, "\nYour Google ID does not have access to the requested AWS Role. Ask your administrator to provide access.
118+
Role: #{@assume_role_params[:role_arn]}
119+
Email: #{token_params['email']}
120+
Google ID: #{token_params['sub']}", e.backtrace
121+
end
122+
c = assume_role.credentials
123+
@credentials = Aws::Credentials.new(
124+
c.access_key_id,
125+
c.secret_access_key,
126+
c.session_token
127+
)
128+
@expiration = c.expiration.to_i
129+
write_credentials
130+
end
131+
132+
# Use `aws configure set` to write credentials and expiration to AWS credentials file.
133+
# AWS CLI is needed because writing AWS credentials is not supported by the AWS Ruby SDK.
134+
def write_credentials
135+
%w[
136+
access_key_id
137+
secret_access_key
138+
session_token
139+
].map {|x| ["aws_#{x}", @credentials.send(x)]}.
140+
to_h.
141+
merge(expiration: @expiration).each do |key, value|
142+
system("aws configure set #{key} #{value} --profile #{@profile}")
143+
end
144+
end
145+
end
146+
147+
# Patch Aws::SharedConfig to allow fetching arbitrary keys from the shared config.
148+
module SharedConfigGetKey
149+
def get(key, opts = {})
150+
profile = opts.delete(:profile) || @profile_name
151+
if @parsed_config && (prof_config = @parsed_config[profile])
152+
prof_config[key]
153+
else
154+
nil
155+
end
156+
end
157+
end
158+
Aws::SharedConfig.prepend SharedConfigGetKey
159+
160+
# Extend ::Google::APIClient::Storage to write {type: 'authorized_user'} to credentials,
161+
# as required by Google's default credentials loader.
162+
class GoogleStorage < ::Google::APIClient::Storage
163+
def credentials_hash
164+
super.merge(type: 'authorized_user')
165+
end
166+
end
167+
end

lib/aws/google/credential_provider.rb

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
module Aws
2+
class Google
3+
# Inserts GoogleCredentials into the default AWS credential provider chain.
4+
# Google credentials will only be used if Aws::Google.config is set before initialization.
5+
module CredentialProvider
6+
# Insert google_credentials as the second-to-last credentials provider
7+
# (in front of instance profile, which makes an http request).
8+
def providers
9+
super.insert(-2, [:google_credentials, {}])
10+
end
11+
12+
def google_credentials(options)
13+
(config = Google.config) && Google.new(options.merge(config))
14+
end
15+
end
16+
::Aws::CredentialProviderChain.prepend CredentialProvider
17+
end
18+
end

lib/aws/google/version.rb

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,5 @@
1+
module Aws
2+
class Google
3+
VERSION = '0.1.0'.freeze
4+
end
5+
end

0 commit comments

Comments
 (0)