diff --git a/.github/workflows/pr-verify.yml b/.github/workflows/pr-verify.yml new file mode 100644 index 0000000..059ef8a --- /dev/null +++ b/.github/workflows/pr-verify.yml @@ -0,0 +1,47 @@ +name: Run Tests + +on: + pull_request: + branches: + - main + +jobs: + # Test on code-dot-org Ruby version + test_3_0_5: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.0.5 + bundler-cache: true + + - name: Install gems + run: bundle install + + - name: Run tests + run: bundle exec rake test + + #Test on latest Ruby + test_3_3: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v3 + + - name: Set up Ruby + uses: ruby/setup-ruby@v1 + with: + ruby-version: 3.3 + bundler-cache: true + + - name: Install gems + run: bundle install + + - name: Run tests + run: bundle exec rake test diff --git a/.ruby-version b/.ruby-version new file mode 100644 index 0000000..eca690e --- /dev/null +++ b/.ruby-version @@ -0,0 +1 @@ +3.0.5 diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..f038c68 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,8 @@ +FROM ruby:3.0.5 + +WORKDIR /app + +# Copy bare minimum files to install gems +COPY Gemfile aws-google.gemspec /app/ +COPY lib /app/lib +RUN bundle install diff --git a/README.md b/README.md index 49b54a8..c096056 100644 --- a/README.md +++ b/README.md @@ -24,9 +24,14 @@ Or install it yourself as: Visit the [Google API Console](https://console.developers.google.com/) to create/obtain [OAuth 2.0 Client ID credentials](https://support.google.com/cloud/answer/6158849) (client ID and client secret) for an application in your Google account. ### Create an AWS IAM Role -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 +Create an AWS IAM Role with the desired IAM policies attached, and a ['trust policy'][1] ([`AssumeRolePolicyDocument`][2]) allowing the [`sts:AssumeRoleWithWebIdentity`][3] action with [Web Identity Federation condition keys][4] authorizing your Google Client ID (`accounts.google.com:aud`) and a specific set of Google Account IDs (`accounts.google.com:sub`): +[1]: https://docs.aws.amazon.com/IAM/latest/UserGuide/id_roles_terms-and-concepts.html#term_trust-policy "IAM Trust Policy" +[2]: https://docs.aws.amazon.com/IAM/latest/APIReference/API_CreateRole.html "Create Role API" +[3]: https://docs.aws.amazon.com/STS/latest/APIReference/API_AssumeRoleWithWebIdentity.html "Assume Role With Identity API" +[4]: https://docs.aws.amazon.com/IAM/latest/UserGuide/reference_policies_iam-condition-keys.html#condition-keys-wif "IAM Condition Keys" + ```json { "Version": "2012-10-17", @@ -53,6 +58,7 @@ your Google Client ID (`accounts.google.com:aud`) and a specific set of Google A ### Method 1: `Aws::Google` 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: + ```ruby require 'aws/google' @@ -87,9 +93,22 @@ The extra `credential_process` config line tells AWS to [Source Credentials with ## Development -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. +Prerequisites: + +* Ruby 3.0.5 + +You can have Ruby installed locally, or use Docker and mount this repository into a Ruby container. By using Docker you can avoid conflicts with differing Ruby versions or other installed gems. To run and 'bash' into a Ruby container, install Docker and run the following. See [docker-compose.yml](docker-compose.yml) for details. + +``` +docker compose build +docker compose run ruby +``` + +With either option, 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. + +To install this gem onto your local machine, run `bundle exec rake install`. -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). +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). ## Contributing diff --git a/aws-google.gemspec b/aws-google.gemspec index 9cb8004..a80635f 100644 --- a/aws-google.gemspec +++ b/aws-google.gemspec @@ -1,8 +1,9 @@ -lib = File.expand_path('../lib', __FILE__) +lib = File.expand_path('lib', __dir__) $LOAD_PATH.unshift(lib) unless $LOAD_PATH.include?(lib) require 'aws/google/version' Gem::Specification.new do |spec| + spec.required_ruby_version = '>= 3.0.5' spec.name = 'aws-google' spec.version = Aws::Google::VERSION spec.authors = ['Will Jordan'] @@ -21,14 +22,14 @@ Gem::Specification.new do |spec| spec.executables = spec.files.grep(%r{^exe/}) { |f| File.basename(f) } spec.require_paths = ['lib'] - spec.add_dependency 'aws-sdk-core', '~> 3.130' - spec.add_dependency 'google-apis-core' - spec.add_dependency 'launchy', '~> 2' + spec.add_dependency 'aws-sdk-core', '~> 3.211.0' + spec.add_dependency 'google-apis-core', '~> 0.15.1' + spec.add_dependency 'launchy', '~> 3.0.1' - spec.add_development_dependency 'activesupport', '~> 5' - spec.add_development_dependency 'minitest', '~> 5.14.2' - spec.add_development_dependency 'mocha', '~> 1.5' - spec.add_development_dependency 'rake', '~> 12' - spec.add_development_dependency 'timecop', '~> 0.8' - spec.add_development_dependency 'webmock', '~> 3.3' + spec.add_development_dependency 'activesupport', '~> 6.1.7.8' + spec.add_development_dependency 'minitest', '~> 5.25.1' + spec.add_development_dependency 'mocha', '~> 2.4.5' + spec.add_development_dependency 'rake', '~> 13.2.1' + spec.add_development_dependency 'timecop', '~> 0.9.10' + spec.add_development_dependency 'webmock', '3.24.0' end diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..a1b9691 --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,8 @@ +version: '3' +services: + ruby: + build: . + volumes: + - .:/app + working_dir: /app + command: bash diff --git a/lib/aws/google.rb b/lib/aws/google.rb index dd235ed..45957d7 100644 --- a/lib/aws/google.rb +++ b/lib/aws/google.rb @@ -41,7 +41,7 @@ class << self # @option options [String] :online if `true` only a temporary access token will be provided, # a long-lived refresh token will not be created and stored on the filesystem. # @option options [String] :port port for local server to listen on to capture oauth browser redirect. - # Defaults to 1234. Set to nil or 0 to use an out-of-band authentication process. + # Defaults to 1234. # @option options [String] :client_id Google client ID # @option options [String] :client_secret Google client secret def initialize(options = {}) @@ -98,18 +98,7 @@ def google_oauth credentials.tap(&storage.method(:write_credentials)) end - def silence_output - outs = [$stdout, $stderr] - clones = outs.map(&:clone) - outs.each { |io| io.reopen '/dev/null'} - yield - ensure - outs.each_with_index { |io, i| io.reopen(clones[i]) } - end - def get_oauth_code(client, options) - raise 'fallback' unless @port && !@port.zero? - require 'launchy' require 'webrick' code = nil @@ -120,35 +109,27 @@ def get_oauth_code(client, options) ) server.mount_proc '/' do |req, res| code = req.query['code'] - res.status = 202 - res.body = 'Login successful, you may close this browser window.' + if code + res.status = 202 + res.body = 'Login successful, you may close this browser window.' + else + res.status = 500 + res.body = "Authentication failed. Received a request to http://localhost:#{@port} that should complete Google OAuth flow, but no code was received." + end server.stop end - trap('INT') { server.shutdown } - client.redirect_uri = "http://localhost:#{@port}" - silence_output do - launchy = Launchy.open(client.authorization_uri(options).to_s) - server_thread = Thread.new do - begin - server.start - ensure server.shutdown - end - end - while server_thread.alive? - raise 'fallback' if !launchy.alive? && !launchy.value.success? - sleep 0.1 - end + client.redirect_uri = "http://localhost:#{@port}" + Launchy.open(client.authorization_uri(options).to_s) do |exception| + puts "Couldn't open browser, please authenticate with Google using this link:" + puts client.authorization_uri(options).to_s + puts + puts "Note: link must be opened on this computer, as Google will redirect to #{client.redirect_uri} to complete authentication." end - code || raise('fallback') - rescue StandardError - trap('INT', 'DEFAULT') - # Fallback to out-of-band authentication if browser launch failed. - client.redirect_uri = 'oob' - return ENV['OAUTH_CODE'] if ENV['OAUTH_CODE'] - - raise RuntimeError, 'Open the following URL in a browser to get a code,' \ - "export to $OAUTH_CODE and rerun:\n#{client.authorization_uri(options)}", [] + + server.start + + code or raise 'Failed to get OAuth code from Google' end def refresh diff --git a/lib/aws/google/cached_credentials.rb b/lib/aws/google/cached_credentials.rb index 61c93dd..de3b032 100644 --- a/lib/aws/google/cached_credentials.rb +++ b/lib/aws/google/cached_credentials.rb @@ -23,22 +23,31 @@ def initialize(options = {}) end def refresh_if_near_expiration - if near_expiration?(SYNC_EXPIRATION_LENGTH) - @mutex.synchronize do - if near_expiration?(SYNC_EXPIRATION_LENGTH) - refresh - write_credentials - end + return unless near_expiration?(SYNC_EXPIRATION_LENGTH) + + @mutex.synchronize do + if near_expiration?(SYNC_EXPIRATION_LENGTH) + refresh + write_credentials end end end # Write credentials and expiration to AWS credentials file. def write_credentials - # AWS CLI is needed because writing AWS credentials is not supported by the AWS Ruby SDK. + # Ensure the AWS CLI is available before attempting to write credentials. return unless system('which aws >/dev/null 2>&1') - Aws::SharedCredentials::KEY_MAP.transform_values(&@credentials.method(:send)). - merge(expiration: @expiration).each do |key, value| + + # Manually map the credentials to the keys used by AWS CLI + credentials_map = { + 'aws_access_key_id' => @credentials.access_key_id, + 'aws_secret_access_key' => @credentials.secret_access_key, + 'aws_session_token' => @credentials.session_token, + 'expiration' => @expiration + } + + # Use the AWS CLI to set the credentials in the session profile + credentials_map.each do |key, value| system("aws configure set #{key} #{value} --profile #{@session_profile}") end end diff --git a/lib/aws/google/version.rb b/lib/aws/google/version.rb index d3ff4cf..cb5cb00 100644 --- a/lib/aws/google/version.rb +++ b/lib/aws/google/version.rb @@ -1,5 +1,5 @@ module Aws class Google - VERSION = '0.2.0'.freeze + VERSION = '0.2.2'.freeze end end diff --git a/test/aws/google_test.rb b/test/aws/google_test.rb index f217585..ba2359e 100644 --- a/test/aws/google_test.rb +++ b/test/aws/google_test.rb @@ -83,9 +83,9 @@ it 'refreshes expired Google auth token credentials' do m = mock m.stubs(:refresh!) - m.stubs(:id_token). - returns(JWT.encode({ email: 'email', exp: Time.now.to_i - 1 }, '')). - then.returns(JWT.encode({ email: 'email' }, '')) + m.stubs(:id_token) + .returns(JWT.encode({ email: 'email', exp: Time.now.to_i - 1 }, '')) + .then.returns(JWT.encode({ email: 'email' }, '')) Google::Auth.stubs(:get_application_default).returns(m) system.times(5) @@ -108,6 +108,7 @@ expiration = provider.expiration _(expiration).must_equal(provider.expiration) Timecop.travel(1.5.hours.from_now) do + provider.refresh! _(expiration).wont_equal(provider.expiration) end end @@ -124,7 +125,7 @@ Aws::Google.any_instance.expects(:refresh).never Aws::Google.new(config).credentials end - + it 'uses config defaults for new AWS clients' do Aws::Google.stubs(:config).returns(config) @oauth_default.once @@ -204,7 +205,7 @@ Aws::Google.new(config).credentials end end - + describe 'no shared config' do before do Aws.shared_config.fresh(