aws-google 0.1.0 → 0.1.6

Sign up to get free protection for your applications and to get access to all the features.
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