aws-google 0.1.0 → 0.1.6

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 CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 76723a29a160f6d7326a3329e08e6316da4e8e224013ef400321cd7780e46340
4
- data.tar.gz: 2cac963af78ade133bccdafa257bb8bc691a794afa0f1f0172f920ecd5c335a9
3
+ metadata.gz: 00e6b5cf021f6b08503bc04f4359fa58028ebd227d0e12e66308af77f5613ba4
4
+ data.tar.gz: 971a4987281b9e2971574b590edba32a8522f061997cc20098ffa83e42216dac
5
5
  SHA512:
6
- metadata.gz: 4a2aa252e1108cd54bc562ab4cf2f5c54732f9a66ce86b57fbfd96c43a799c9792d9adb38661488cf00667959a47b091def2a47fc72af55fa2e5d66bd2b9ebf3
7
- data.tar.gz: 457f3dadbf5bb0b00059576ebc19daeb93a4ddd55867f49d6dab121a17a7041dc87c86fb46b8cc65d77115dbe27cb1a387340344204be7c804c01a82008f9459
6
+ metadata.gz: 71812e0486feddadacbff23840630334a9fb29466427542c0da9d2fd36184e3694bea4681ac708c2822fcf43c2ce1829adc530311ede86523359a96c78519205
7
+ data.tar.gz: d14acf9b9493d55e4ef4b10465a17f6e9cda6fd0bdaef5886a757e078a434ccac9f3faef51689bdc037cb8b5828a947788f0f24640e18039f4365597d1a2805a
data/README.md CHANGED
@@ -21,8 +21,8 @@ Or install it yourself as:
21
21
  ## Usage
22
22
 
23
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:
24
+ - Create an AWS IAM Role with the desired IAM policies attached, and a ['trust policy'](https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#term_trust-policy) ([`AssumeRolePolicyDocument`](https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateRole.html)) allowing the [`sts:AssumeRoleWithWebIdentity`](https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html) action with [Web Identity Federation condition keys](https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-wif) authorizing
25
+ your Google Client ID (`accounts.google.com:aud`) and a specific set of Google Account IDs (`accounts.google.com:sub`):
26
26
 
27
27
  ```json
28
28
  {
@@ -36,9 +36,7 @@ by your Google Client ID and a specific set of Google Account IDs:
36
36
  "Action": "sts:AssumeRoleWithWebIdentity",
37
37
  "Condition": {
38
38
  "StringEquals": {
39
- "accounts.google.com:aud": "123456789012-abcdefghijklmnopqrstuvwzyz0123456.apps.googleusercontent.com"
40
- },
41
- "ForAnyValue:StringEquals": {
39
+ "accounts.google.com:aud": "123456789012-abcdefghijklmnopqrstuvwzyz0123456.apps.googleusercontent.com",
42
40
  "accounts.google.com:sub": [
43
41
  "000000000000000000000",
44
42
  "111111111111111111111"
@@ -50,33 +48,38 @@ by your Google Client ID and a specific set of Google Account IDs:
50
48
  }
51
49
  ```
52
50
 
53
- - In your Ruby code, construct an `Aws::Google` object by passing in the AWS role, client id and client secret:
51
+ - In your Ruby code, construct an `Aws::Google` object by passing the AWS `role_arn`, Google `client_id` and `client_secret`, either as constructor arguments or via the `Aws::Google.config` global defaults:
54
52
  ```ruby
55
- aws_role = 'arn:aws:iam::[AccountID]:role/[Role]'
56
- client_id = '123456789012-abcdefghijklmnopqrstuvwzyz0123456.apps.googleusercontent.com'
57
- client_secret = '01234567890abcdefghijklmn'
53
+ require 'aws/google'
58
54
 
59
- role_credentials = Aws::Google.new(
60
- role_arn: aws_role,
61
- google_client_id: client_id,
62
- google_client_secret: client_secret
63
- )
55
+ options = {
56
+ aws_role: 'arn:aws:iam::[AccountID]:role/[Role]',
57
+ client_id: '123456789012-abcdefghijklmnopqrstuvwzyz0123456.apps.googleusercontent.com',
58
+ client_secret: '01234567890abcdefghijklmn'
59
+ }
64
60
 
65
- puts Aws::STS::Client.new(credentials: role_credentials).get_caller_identity
66
- ```
61
+ # Pass constructor arguments:
62
+ credentials = Aws::Google.new(options)
63
+ puts Aws::STS::Client.new(credentials: credentials).get_caller_identity
67
64
 
68
- - Or, set `Aws::Google.config` hash to add Google auth to the default credential provider chain:
65
+ # Set global defaults:
66
+ Aws::Google.config = options
67
+ puts Aws::STS::Client.new.get_caller_identity
68
+ ```
69
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
- }
70
+ - 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:
76
71
 
77
- puts Aws::STS::Client.new.get_caller_identity
72
+ ```ini
73
+ [my_profile]
74
+ google =
75
+ role_arn = arn:aws:iam::[AccountID]:role/[Role]
76
+ client_id = 123456789012-abcdefghijklmnopqrstuvwzyz0123456.apps.googleusercontent.com
77
+ client_secret = 01234567890abcdefghijklmn
78
+ credential_process = aws-google
78
79
  ```
79
80
 
81
+ 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).
82
+
80
83
  ## Development
81
84
 
82
85
  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.
@@ -23,11 +23,10 @@ Gem::Specification.new do |spec|
23
23
 
24
24
  spec.add_dependency 'aws-sdk-core', '~> 3'
25
25
  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'
27
27
 
28
28
  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 'minitest', '~> 5.14.2'
31
30
  spec.add_development_dependency 'mocha', '~> 1.5'
32
31
  spec.add_development_dependency 'rake', '~> 12'
33
32
  spec.add_development_dependency 'timecop', '~> 0.8'
@@ -0,0 +1,19 @@
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
+
10
+ google = ::Aws::STS::Client.new.config.credentials
11
+ credentials = google.credentials
12
+ output = {
13
+ Version: 1,
14
+ AccessKeyId: credentials.access_key_id,
15
+ SecretAccessKey: credentials.secret_access_key,
16
+ SessionToken: credentials.session_token,
17
+ Expiration: Time.at(google.expiration.to_i).iso8601
18
+ }
19
+ puts output.to_json
@@ -1,6 +1,7 @@
1
1
  require_relative 'google/version'
2
2
  require 'aws-sdk-core'
3
3
  require_relative 'google/credential_provider'
4
+ require_relative 'google/cached_credentials'
4
5
 
5
6
  require 'googleauth'
6
7
  require 'google/api_client/auth/storage'
@@ -23,48 +24,43 @@ module Aws
23
24
  # constructed.
24
25
  class Google
25
26
  include ::Aws::CredentialProvider
26
- include ::Aws::RefreshingCredentials
27
+ include ::Aws::Google::CachedCredentials
27
28
 
28
29
  class << self
30
+ # Use `Aws::Google.config` to set default options for any instance of this provider.
29
31
  attr_accessor :config
30
32
  end
33
+ self.config = {}
31
34
 
32
35
  # @option options [required, String] :role_arn
33
36
  # @option options [String] :policy
34
37
  # @option options [Integer] :duration_seconds
35
38
  # @option options [String] :external_id
36
39
  # @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`)
38
40
  # @option options [String] :domain G Suite domain for account-selection hint
39
41
  # @option options [String] :online if `true` only a temporary access token will be provided,
40
42
  # a long-lived refresh token will not be created and stored on the filesystem.
41
- # @option options [::Google::Auth::ClientId] :google_id
43
+ # @option options [String] :port port for local server to listen on to capture oauth browser redirect.
44
+ # Defaults to 1234. Set to nil or 0 to use an out-of-band authentication process.
45
+ # @option options [String] :client_id Google client ID
46
+ # @option options [String] :client_secret Google client secret
42
47
  def initialize(options = {})
48
+ options = options.merge(self.class.config)
43
49
  @oauth_attempted = false
44
50
  @assume_role_params = options.slice(
45
51
  *Aws::STS::Client.api.operation(:assume_role_with_web_identity).
46
52
  input.shape.member_names
47
53
  )
48
54
 
49
- @profile = options[:profile] || ENV['AWS_DEFAULT_PROFILE'] || 'default'
50
55
  @google_id = ::Google::Auth::ClientId.new(
51
- options[:google_client_id],
52
- options[:google_client_secret]
56
+ options[:client_id],
57
+ options[:client_secret]
53
58
  )
54
59
  @client = options[:client] || Aws::STS::Client.new(credentials: nil)
55
60
  @domain = options[:domain]
56
61
  @online = options[:online]
57
-
58
- # Use existing AWS credentials stored in the shared config if available.
59
- # If this is `nil` or expired, #refresh will be called on the first AWS API service call
60
- # to generate AWS credentials derived from Google authentication.
61
- @expiration = Aws.shared_config.get('expiration', profile: @profile) rescue nil
62
- @mutex = Mutex.new
63
- if near_expiration?
64
- refresh!
65
- else
66
- @credentials = Aws.shared_config.credentials(profile: @profile) rescue nil
67
- end
62
+ @port = options[:port] || 1234
63
+ super
68
64
  end
69
65
 
70
66
  private
@@ -96,20 +92,63 @@ module Aws
96
92
  uri_options[:hd] = @domain if @domain
97
93
  uri_options[:access_type] = 'online' if @online
98
94
 
99
- require 'google/api_client/auth/installed_app'
100
- if defined?(Launchy) && Launchy::Application::Browser.new.app_list.any?
101
- ::Google::APIClient::InstalledAppFlow.new(options).authorize(storage, uri_options)
102
- else
103
- credentials = ::Google::Auth::UserRefreshCredentials.new(
104
- options.merge(redirect_uri: 'urn:ietf:wg:oauth:2.0:oob')
105
- )
106
- url = credentials.authorization_uri(uri_options)
107
- print 'Open the following URL in the browser and enter the ' \
108
- "resulting code after authorization:\n#{url}\n> "
109
- credentials.code = gets
110
- credentials.fetch_access_token!
111
- credentials.tap(&storage.method(:write_credentials))
95
+ credentials = ::Google::Auth::UserRefreshCredentials.new(options)
96
+ credentials.code = get_oauth_code(credentials, uri_options)
97
+ credentials.fetch_access_token!
98
+ credentials.tap(&storage.method(:write_credentials))
99
+ end
100
+
101
+ def silence_output
102
+ outs = [$stdout, $stderr]
103
+ clones = outs.map(&:clone)
104
+ outs.each { |io| io.reopen '/dev/null'}
105
+ yield
106
+ ensure
107
+ outs.each_with_index { |io, i| io.reopen(clones[i]) }
108
+ end
109
+
110
+ def get_oauth_code(client, options)
111
+ raise 'fallback' unless @port && !@port.zero?
112
+
113
+ require 'launchy'
114
+ require 'webrick'
115
+ code = nil
116
+ server = WEBrick::HTTPServer.new(
117
+ Port: @port,
118
+ Logger: WEBrick::Log.new(STDOUT, 0),
119
+ AccessLog: []
120
+ )
121
+ server.mount_proc '/' do |req, res|
122
+ code = req.query['code']
123
+ res.status = 202
124
+ res.body = 'Login successful, you may close this browser window.'
125
+ server.stop
126
+ end
127
+ trap('INT') { server.shutdown }
128
+ client.redirect_uri = "http://localhost:#{@port}"
129
+ silence_output do
130
+ launchy = Launchy.open(client.authorization_uri(options).to_s)
131
+ server_thread = Thread.new do
132
+ begin
133
+ server.start
134
+ ensure server.shutdown
135
+ end
136
+ end
137
+ while server_thread.alive?
138
+ raise 'fallback' if !launchy.alive? && !launchy.value.success?
139
+
140
+ sleep 0.1
141
+ end
112
142
  end
143
+ code || raise('fallback')
144
+ rescue StandardError
145
+ trap('INT', 'DEFAULT')
146
+ # Fallback to out-of-band authentication if browser launch failed.
147
+ client.redirect_uri = 'oob'
148
+ return ENV['OAUTH_CODE'] if ENV['OAUTH_CODE']
149
+
150
+ raise RuntimeError, 'Open the following URL in a browser to get a code,' \
151
+ "export to $OAUTH_CODE and rerun:\n#{client.authorization_uri(options)}", []
113
152
  end
114
153
 
115
154
  def refresh
@@ -135,7 +174,7 @@ module Aws
135
174
  role_session_name: token_params['email']
136
175
  )
137
176
  )
138
- rescue Signet::AuthorizationError => e
177
+ rescue Signet::AuthorizationError, Aws::STS::Errors::ExpiredTokenException
139
178
  retry if (@google_client = google_oauth)
140
179
  raise
141
180
  rescue Aws::STS::Errors::AccessDenied => e
@@ -143,7 +182,7 @@ module Aws
143
182
  raise e, "\nYour Google ID does not have access to the requested AWS Role. Ask your administrator to provide access.
144
183
  Role: #{@assume_role_params[:role_arn]}
145
184
  Email: #{token_params['email']}
146
- Google ID: #{token_params['sub']}", e.backtrace
185
+ Google ID: #{token_params['sub']}", []
147
186
  end
148
187
 
149
188
  c = assume_role.credentials
@@ -153,35 +192,8 @@ Google ID: #{token_params['sub']}", e.backtrace
153
192
  c.session_token
154
193
  )
155
194
  @expiration = c.expiration.to_i
156
- write_credentials
157
- end
158
-
159
- # Write credentials and expiration to AWS credentials file.
160
- def write_credentials
161
- # AWS CLI is needed because writing AWS credentials is not supported by the AWS Ruby SDK.
162
- return unless system('which aws >/dev/null 2>&1')
163
- %w[
164
- access_key_id
165
- secret_access_key
166
- session_token
167
- ].map {|x| ["aws_#{x}", @credentials.send(x)]}.
168
- to_h.
169
- merge(expiration: @expiration).each do |key, value|
170
- system("aws configure set #{key} #{value} --profile #{@profile}")
171
- end
172
- end
173
- end
174
-
175
- # Patch Aws::SharedConfig to allow fetching arbitrary keys from the shared config.
176
- module SharedConfigGetKey
177
- def get(key, opts = {})
178
- profile = opts.delete(:profile) || @profile_name
179
- if @parsed_config && (prof_config = @parsed_config[profile])
180
- prof_config[key]
181
- end
182
195
  end
183
196
  end
184
- Aws::SharedConfig.prepend SharedConfigGetKey
185
197
 
186
198
  # Extend ::Google::APIClient::Storage to write {type: 'authorized_user'} to credentials,
187
199
  # as required by Google's default credentials loader.
@@ -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
@@ -3,16 +3,36 @@ module Aws
3
3
  # Inserts GoogleCredentials into the default AWS credential provider chain.
4
4
  # Google credentials will only be used if Aws::Google.config is set before initialization.
5
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).
6
+ # Insert google_credentials as the third-to-last credentials provider
7
+ # (in front of process credentials and instance_profile credentials).
8
8
  def providers
9
- super.insert(-2, [:google_credentials, {}])
9
+ super.insert(-3, [:google_credentials, {}])
10
10
  end
11
11
 
12
12
  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
14
19
  end
15
20
  end
16
21
  ::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
+ google_opts = @parsed_config.
28
+ fetch(p, {}).fetch('google', {}).
29
+ transform_keys(&:to_sym)
30
+ if google_opts.merge(::Aws::Google.config).has_key?(:role_arn)
31
+ Google.new(google_opts)
32
+ end
33
+ end
34
+ end
35
+ end
36
+ ::Aws::SharedConfig.prepend GoogleSharedCredentials
17
37
  end
18
38
  end
@@ -1,5 +1,5 @@
1
1
  module Aws
2
2
  class Google
3
- VERSION = '0.1.0'.freeze
3
+ VERSION = '0.1.6'.freeze
4
4
  end
5
5
  end
metadata CHANGED
@@ -1,14 +1,14 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: aws-google
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.1.0
4
+ version: 0.1.6
5
5
  platform: ruby
6
6
  authors:
7
7
  - Will Jordan
8
8
  autorequire:
9
9
  bindir: exe
10
10
  cert_chain: []
11
- date: 2019-02-07 00:00:00.000000000 Z
11
+ date: 2020-10-08 00:00:00.000000000 Z
12
12
  dependencies:
13
13
  - !ruby/object:Gem::Dependency
14
14
  name: aws-sdk-core
@@ -66,34 +66,20 @@ dependencies:
66
66
  - - "~>"
67
67
  - !ruby/object:Gem::Version
68
68
  version: '5'
69
- - !ruby/object:Gem::Dependency
70
- name: bundler
71
- requirement: !ruby/object:Gem::Requirement
72
- requirements:
73
- - - "~>"
74
- - !ruby/object:Gem::Version
75
- version: '1'
76
- type: :development
77
- prerelease: false
78
- version_requirements: !ruby/object:Gem::Requirement
79
- requirements:
80
- - - "~>"
81
- - !ruby/object:Gem::Version
82
- version: '1'
83
69
  - !ruby/object:Gem::Dependency
84
70
  name: minitest
85
71
  requirement: !ruby/object:Gem::Requirement
86
72
  requirements:
87
73
  - - "~>"
88
74
  - !ruby/object:Gem::Version
89
- version: '5.10'
75
+ version: 5.14.2
90
76
  type: :development
91
77
  prerelease: false
92
78
  version_requirements: !ruby/object:Gem::Requirement
93
79
  requirements:
94
80
  - - "~>"
95
81
  - !ruby/object:Gem::Version
96
- version: '5.10'
82
+ version: 5.14.2
97
83
  - !ruby/object:Gem::Dependency
98
84
  name: mocha
99
85
  requirement: !ruby/object:Gem::Requirement
@@ -153,7 +139,8 @@ dependencies:
153
139
  description: Use Google OAuth as an AWS credential provider.
154
140
  email:
155
141
  - will@code.org
156
- executables: []
142
+ executables:
143
+ - aws-google
157
144
  extensions: []
158
145
  extra_rdoc_files: []
159
146
  files:
@@ -166,7 +153,9 @@ files:
166
153
  - aws-google.gemspec
167
154
  - bin/console
168
155
  - bin/setup
156
+ - exe/aws-google
169
157
  - lib/aws/google.rb
158
+ - lib/aws/google/cached_credentials.rb
170
159
  - lib/aws/google/credential_provider.rb
171
160
  - lib/aws/google/version.rb
172
161
  homepage: https://github.com/code-dot-org/aws-google
@@ -189,8 +178,7 @@ required_rubygems_version: !ruby/object:Gem::Requirement
189
178
  - !ruby/object:Gem::Version
190
179
  version: '0'
191
180
  requirements: []
192
- rubyforge_project:
193
- rubygems_version: 2.7.4
181
+ rubygems_version: 3.1.2
194
182
  signing_key:
195
183
  specification_version: 4
196
184
  summary: Use Google OAuth as an AWS credential provider