aboutsummaryrefslogtreecommitdiffhomepage
path: root/src
diff options
context:
space:
mode:
authorGravatar Michael Lumish <mlumish@google.com>2015-02-02 10:52:43 -0800
committerGravatar Michael Lumish <mlumish@google.com>2015-02-02 10:52:43 -0800
commit26b18f09b966a110b5cba5eca325928c868298f0 (patch)
tree8ea9fab371f2fd9e20a7f34a8bf6d7faea114f35 /src
parent3ea4ed37eb8ec7e9c341bcd2e98781143aedecca (diff)
parentc799e8186393bed2a558e249a315400c9bbeee9f (diff)
Merge pull request #340 from tbetbetbe/grpc_ruby_add_signet_based_auth
Grpc ruby add signet based auth
Diffstat (limited to 'src')
-rwxr-xr-xsrc/ruby/Rakefile10
-rwxr-xr-xsrc/ruby/bin/interop/interop_client.rb180
-rw-r--r--src/ruby/bin/interop/test/cpp/interop/messages.rb5
-rwxr-xr-x[-rw-r--r--]src/ruby/bin/math.rb0
-rwxr-xr-x[-rw-r--r--]src/ruby/bin/math_services.rb0
-rwxr-xr-xsrc/ruby/grpc.gemspec10
-rw-r--r--src/ruby/lib/grpc.rb2
-rw-r--r--src/ruby/lib/grpc/auth/compute_engine.rb69
-rw-r--r--src/ruby/lib/grpc/auth/service_account.rb68
-rw-r--r--src/ruby/lib/grpc/auth/signet.rb67
-rw-r--r--src/ruby/spec/auth/apply_auth_examples.rb163
-rw-r--r--src/ruby/spec/auth/compute_engine_spec.rb108
-rw-r--r--src/ruby/spec/auth/service_account_spec.rb75
-rw-r--r--src/ruby/spec/auth/signet_spec.rb70
-rw-r--r--src/ruby/spec/channel_spec.rb4
-rw-r--r--src/ruby/spec/generic/active_call_spec.rb2
-rw-r--r--src/ruby/spec/spec_helper.rb12
17 files changed, 773 insertions, 72 deletions
diff --git a/src/ruby/Rakefile b/src/ruby/Rakefile
index 5fc325ef0e..b27305d16c 100755
--- a/src/ruby/Rakefile
+++ b/src/ruby/Rakefile
@@ -35,18 +35,20 @@ namespace :spec do
t.pattern = spec_files
t.rspec_opts = "--tag #{suite[:tag]}" if suite[:tag]
- t.rspec_opts = suite[:tags].map{ |t| "--tag #{t}" }.join(' ') if suite[:tags]
+ if suite[:tags]
+ t.rspec_opts = suite[:tags].map { |x| "--tag #{x}" }.join(' ')
+ end
end
end
end
end
-desc 'Run compiles the extension, runs all the tests'
+desc 'Compiles the extension then runs all the tests'
task :all
task default: :all
-task 'spec:suite:wrapper' => :compile
+task 'spec:suite:wrapper' => [:compile, :rubocop]
task 'spec:suite:idiomatic' => 'spec:suite:wrapper'
task 'spec:suite:bidi' => 'spec:suite:wrapper'
task 'spec:suite:server' => 'spec:suite:wrapper'
-task :all => ['spec:suite:idiomatic', 'spec:suite:bidi', 'spec:suite:server']
+task all: ['spec:suite:idiomatic', 'spec:suite:bidi', 'spec:suite:server']
diff --git a/src/ruby/bin/interop/interop_client.rb b/src/ruby/bin/interop/interop_client.rb
index 86739b7b67..e29e22b8c1 100755
--- a/src/ruby/bin/interop/interop_client.rb
+++ b/src/ruby/bin/interop/interop_client.rb
@@ -56,6 +56,8 @@ require 'test/cpp/interop/empty'
require 'signet/ssl_config'
+include Google::RPC::Auth
+
# loads the certificates used to access the test server securely.
def load_test_certs
this_dir = File.expand_path(File.dirname(__FILE__))
@@ -67,40 +69,54 @@ end
# loads the certificates used to access the test server securely.
def load_prod_cert
fail 'could not find a production cert' if ENV['SSL_CERT_FILE'].nil?
- p "loading prod certs from #{ENV['SSL_CERT_FILE']}"
+ logger.info("loading prod certs from #{ENV['SSL_CERT_FILE']}")
File.open(ENV['SSL_CERT_FILE']).read
end
-# creates a Credentials from the test certificates.
+# creates SSL Credentials from the test certificates.
def test_creds
certs = load_test_certs
GRPC::Core::Credentials.new(certs[0])
end
-RX_CERT = /-----BEGIN CERTIFICATE-----\n.*?-----END CERTIFICATE-----\n/m
-
-
-# creates a Credentials from the production certificates.
+# creates SSL Credentials from the production certificates.
def prod_creds
cert_text = load_prod_cert
GRPC::Core::Credentials.new(cert_text)
end
-# creates a test stub that accesses host:port securely.
-def create_stub(host, port, is_secure, host_override, use_test_ca)
- address = "#{host}:#{port}"
- if is_secure
- creds = nil
- if use_test_ca
- creds = test_creds
- else
- creds = prod_creds
- end
+# creates the SSL Credentials.
+def ssl_creds(use_test_ca)
+ return test_creds if use_test_ca
+ prod_creds
+end
+# creates a test stub that accesses host:port securely.
+def create_stub(opts)
+ address = "#{opts.host}:#{opts.port}"
+ if opts.secure
stub_opts = {
- :creds => creds,
- GRPC::Core::Channel::SSL_TARGET => host_override
+ :creds => ssl_creds(opts.use_test_ca),
+ GRPC::Core::Channel::SSL_TARGET => opts.host_override
}
+
+ # Add service account creds if specified
+ if %w(all service_account_creds).include?(opts.test_case)
+ unless opts.oauth_scope.nil?
+ fd = StringIO.new(File.read(opts.oauth_key_file))
+ logger.info("loading oauth certs from #{opts.oauth_key_file}")
+ auth_creds = ServiceAccountCredentials.new(opts.oauth_scope, fd)
+ stub_opts[:update_metadata] = auth_creds.updater_proc
+ end
+ end
+
+ # Add compute engine creds if specified
+ if %w(all compute_engine_creds).include?(opts.test_case)
+ unless opts.oauth_scope.nil?
+ stub_opts[:update_metadata] = GCECredentials.new.update_proc
+ end
+ end
+
logger.info("... connecting securely to #{address}")
Grpc::Testing::TestService::Stub.new(address, **stub_opts)
else
@@ -158,9 +174,10 @@ class NamedTests
include Grpc::Testing::PayloadType
attr_accessor :assertions # required by Minitest::Assertions
- def initialize(stub)
+ def initialize(stub, args)
@assertions = 0 # required by Minitest::Assertions
@stub = stub
+ @args = args
end
def empty_unary
@@ -170,21 +187,37 @@ class NamedTests
end
def large_unary
- req_size, wanted_response_size = 271_828, 314_159
- payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
- req = SimpleRequest.new(response_type: :COMPRESSABLE,
- response_size: wanted_response_size,
- payload: payload)
- resp = @stub.unary_call(req)
- assert_equal(:COMPRESSABLE, resp.payload.type,
- 'large_unary: payload had the wrong type')
- assert_equal(wanted_response_size, resp.payload.body.length,
- 'large_unary: payload had the wrong length')
- assert_equal(nulls(wanted_response_size), resp.payload.body,
- 'large_unary: payload content is invalid')
+ perform_large_unary
p 'OK: large_unary'
end
+ def service_account_creds
+ # ignore this test if the oauth options are not set
+ if @args.oauth_scope.nil? || @args.oauth_key_file.nil?
+ p 'NOT RUN: service_account_creds; no service_account settings'
+ return
+ end
+ json_key = File.read(@args.oauth_key_file)
+ wanted_email = MultiJson.load(json_key)['client_email']
+ resp = perform_large_unary(fill_username: true,
+ fill_oauth_scope: true)
+ assert_equal(wanted_email, resp.username,
+ 'service_account_creds: incorrect username')
+ assert(@args.oauth_scope.include?(resp.oauth_scope),
+ 'service_account_creds: incorrect oauth_scope')
+ p 'OK: service_account_creds'
+ end
+
+ def compute_engine_creds
+ resp = perform_large_unary(fill_username: true,
+ fill_oauth_scope: true)
+ assert(@args.oauth_scope.include?(resp.oauth_scope),
+ 'service_account_creds: incorrect oauth_scope')
+ assert_equal(@args.default_service_account, resp.username,
+ 'service_account_creds: incorrect username')
+ p 'OK: compute_engine_creds'
+ end
+
def client_streaming
msg_sizes = [27_182, 8, 1828, 45_904]
wanted_aggregate_size = 74_922
@@ -230,64 +263,89 @@ class NamedTests
method(m).call
end
end
+
+ private
+
+ def perform_large_unary(fill_username: false, fill_oauth_scope: false)
+ req_size, wanted_response_size = 271_828, 314_159
+ payload = Payload.new(type: :COMPRESSABLE, body: nulls(req_size))
+ req = SimpleRequest.new(response_type: :COMPRESSABLE,
+ response_size: wanted_response_size,
+ payload: payload)
+ req.fill_username = fill_username
+ req.fill_oauth_scope = fill_oauth_scope
+ resp = @stub.unary_call(req)
+ assert_equal(:COMPRESSABLE, resp.payload.type,
+ 'large_unary: payload had the wrong type')
+ assert_equal(wanted_response_size, resp.payload.body.length,
+ 'large_unary: payload had the wrong length')
+ assert_equal(nulls(wanted_response_size), resp.payload.body,
+ 'large_unary: payload content is invalid')
+ resp
+ end
end
+# Args is used to hold the command line info.
+Args = Struct.new(:default_service_account, :host, :host_override,
+ :oauth_scope, :oauth_key_file, :port, :secure, :test_case,
+ :use_test_ca)
+
# validates the the command line options, returning them as a Hash.
-def parse_options
- options = {
- 'secure' => false,
- 'server_host' => nil,
- 'server_host_override' => nil,
- 'server_port' => nil,
- 'test_case' => nil
- }
+def parse_args
+ args = Args.new
+ args.host_override = 'foo.test.google.com'
OptionParser.new do |opts|
- opts.banner = 'Usage: --server_host <server_host> --server_port server_port'
+ opts.on('--oauth_scope scope',
+ 'Scope for OAuth tokens') { |v| args['oauth_scope'] = v }
opts.on('--server_host SERVER_HOST', 'server hostname') do |v|
- options['server_host'] = v
+ args['host'] = v
+ end
+ opts.on('--default_service_account email_address',
+ 'email address of the default service account') do |v|
+ args['default_service_account'] = v
+ end
+ opts.on('--service_account_key_file PATH',
+ 'Path to the service account json key file') do |v|
+ args['oauth_key_file'] = v
end
opts.on('--server_host_override HOST_OVERRIDE',
'override host via a HTTP header') do |v|
- options['server_host_override'] = v
- end
- opts.on('--server_port SERVER_PORT', 'server port') do |v|
- options['server_port'] = v
+ args['host_override'] = v
end
+ opts.on('--server_port SERVER_PORT', 'server port') { |v| args['port'] = v }
# instance_methods(false) gives only the methods defined in that class
test_cases = NamedTests.instance_methods(false).map(&:to_s)
test_case_list = test_cases.join(',')
opts.on('--test_case CODE', test_cases, {}, 'select a test_case',
- " (#{test_case_list})") do |v|
- options['test_case'] = v
- end
+ " (#{test_case_list})") { |v| args['test_case'] = v }
opts.on('-s', '--use_tls', 'require a secure connection?') do |v|
- options['secure'] = v
+ args['secure'] = v
end
opts.on('-t', '--use_test_ca',
'if secure, use the test certificate?') do |v|
- options['use_test_ca'] = v
+ args['use_test_ca'] = v
end
end.parse!
- _check_options(options)
+ _check_args(args)
end
-def _check_options(opts)
- %w(server_host server_port test_case).each do |arg|
- if opts[arg].nil?
+def _check_args(args)
+ %w(host port test_case).each do |a|
+ if args[a].nil?
fail(OptionParser::MissingArgument, "please specify --#{arg}")
end
end
- if opts['server_host_override'].nil?
- opts['server_host_override'] = opts['server_host']
+ if args['oauth_key_file'].nil? ^ args['oauth_scope'].nil?
+ fail(OptionParser::MissingArgument,
+ 'please specify both of --service_account_key_file and --oauth_scope')
end
- opts
+ args
end
def main
- opts = parse_options
- stub = create_stub(opts['server_host'], opts['server_port'], opts['secure'],
- opts['server_host_override'], opts['use_test_ca'])
- NamedTests.new(stub).method(opts['test_case']).call
+ opts = parse_args
+ stub = create_stub(opts)
+ NamedTests.new(stub, opts).method(opts['test_case']).call
end
main
diff --git a/src/ruby/bin/interop/test/cpp/interop/messages.rb b/src/ruby/bin/interop/test/cpp/interop/messages.rb
index 491608bff2..b86cd396a9 100644
--- a/src/ruby/bin/interop/test/cpp/interop/messages.rb
+++ b/src/ruby/bin/interop/test/cpp/interop/messages.rb
@@ -41,10 +41,13 @@ Google::Protobuf::DescriptorPool.generated_pool.build do
optional :response_type, :enum, 1, "grpc.testing.PayloadType"
optional :response_size, :int32, 2
optional :payload, :message, 3, "grpc.testing.Payload"
+ optional :fill_username, :bool, 4
+ optional :fill_oauth_scope, :bool, 5
end
add_message "grpc.testing.SimpleResponse" do
optional :payload, :message, 1, "grpc.testing.Payload"
- optional :effective_gaia_user_id, :int64, 2
+ optional :username, :string, 2
+ optional :oauth_scope, :string, 3
end
add_message "grpc.testing.StreamingInputCallRequest" do
optional :payload, :message, 1, "grpc.testing.Payload"
diff --git a/src/ruby/bin/math.rb b/src/ruby/bin/math.rb
index 09d1e98586..09d1e98586 100644..100755
--- a/src/ruby/bin/math.rb
+++ b/src/ruby/bin/math.rb
diff --git a/src/ruby/bin/math_services.rb b/src/ruby/bin/math_services.rb
index f6ca6fe060..f6ca6fe060 100644..100755
--- a/src/ruby/bin/math_services.rb
+++ b/src/ruby/bin/math_services.rb
diff --git a/src/ruby/grpc.gemspec b/src/ruby/grpc.gemspec
index ffd084dc91..2ce242dd0b 100755
--- a/src/ruby/grpc.gemspec
+++ b/src/ruby/grpc.gemspec
@@ -1,3 +1,4 @@
+# -*- ruby -*-
# encoding: utf-8
$LOAD_PATH.push File.expand_path('../lib', __FILE__)
require 'grpc/version'
@@ -19,11 +20,14 @@ Gem::Specification.new do |s|
s.require_paths = ['lib']
s.platform = Gem::Platform::RUBY
- s.add_dependency 'xray'
- s.add_dependency 'logging', '~> 1.8'
+ s.add_dependency 'faraday', '~> 0.9'
s.add_dependency 'google-protobuf', '~> 3.0.0alpha.1.1'
- s.add_dependency 'signet', '~> 0.5.1'
+ s.add_dependency 'logging', '~> 1.8'
+ s.add_dependency 'jwt', '~> 1.2.1'
s.add_dependency 'minitest', '~> 5.4' # reqd for interop tests
+ s.add_dependency 'multijson', '1.10.1'
+ s.add_dependency 'signet', '~> 0.6.0'
+ s.add_dependency 'xray', '~> 1.1'
s.add_development_dependency 'bundler', '~> 1.7'
s.add_development_dependency 'rake', '~> 10.0'
diff --git a/src/ruby/lib/grpc.rb b/src/ruby/lib/grpc.rb
index 81c67ec859..758ac0c2d1 100644
--- a/src/ruby/lib/grpc.rb
+++ b/src/ruby/lib/grpc.rb
@@ -27,6 +27,8 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+require 'grpc/auth/compute_engine.rb'
+require 'grpc/auth/service_account.rb'
require 'grpc/errors'
require 'grpc/grpc'
require 'grpc/logconfig'
diff --git a/src/ruby/lib/grpc/auth/compute_engine.rb b/src/ruby/lib/grpc/auth/compute_engine.rb
new file mode 100644
index 0000000000..9004bef46e
--- /dev/null
+++ b/src/ruby/lib/grpc/auth/compute_engine.rb
@@ -0,0 +1,69 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+require 'faraday'
+require 'grpc/auth/signet'
+
+module Google
+ module RPC
+ # Module Auth provides classes that provide Google-specific authentication
+ # used to access Google gRPC services.
+ module Auth
+ # Extends Signet::OAuth2::Client so that the auth token is obtained from
+ # the GCE metadata server.
+ class GCECredentials < Signet::OAuth2::Client
+ COMPUTE_AUTH_TOKEN_URI = 'http://metadata/computeMetadata/v1/'\
+ 'instance/service-accounts/default/token'
+ COMPUTE_CHECK_URI = 'http://metadata.google.internal'
+
+ # Detect if this appear to be a GCE instance, by checking if metadata
+ # is available
+ def self.on_gce?(options = {})
+ c = options[:connection] || Faraday.default_connection
+ resp = c.get(COMPUTE_CHECK_URI)
+ return false unless resp.status == 200
+ return false unless resp.headers.key?('Metadata-Flavor')
+ return resp.headers['Metadata-Flavor'] == 'Google'
+ rescue Faraday::ConnectionFailed
+ return false
+ end
+
+ # Overrides the super class method to change how access tokens are
+ # fetched.
+ def fetch_access_token(options = {})
+ c = options[:connection] || Faraday.default_connection
+ c.headers = { 'Metadata-Flavor' => 'Google' }
+ resp = c.get(COMPUTE_AUTH_TOKEN_URI)
+ Signet::OAuth2.parse_credentials(resp.body,
+ resp.headers['content-type'])
+ end
+ end
+ end
+ end
+end
diff --git a/src/ruby/lib/grpc/auth/service_account.rb b/src/ruby/lib/grpc/auth/service_account.rb
new file mode 100644
index 0000000000..35b5cbfe2d
--- /dev/null
+++ b/src/ruby/lib/grpc/auth/service_account.rb
@@ -0,0 +1,68 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+require 'grpc/auth/signet'
+require 'multi_json'
+require 'openssl'
+
+# Reads the private key and client email fields from service account JSON key.
+def read_json_key(json_key_io)
+ json_key = MultiJson.load(json_key_io.read)
+ fail 'missing client_email' unless json_key.key?('client_email')
+ fail 'missing private_key' unless json_key.key?('private_key')
+ [json_key['private_key'], json_key['client_email']]
+end
+
+module Google
+ module RPC
+ # Module Auth provides classes that provide Google-specific authentication
+ # used to access Google gRPC services.
+ module Auth
+ # Authenticates requests using Google's Service Account credentials.
+ # (cf https://developers.google.com/accounts/docs/OAuth2ServiceAccount)
+ class ServiceAccountCredentials < Signet::OAuth2::Client
+ TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v3/token'
+ AUDIENCE = TOKEN_CRED_URI
+
+ # Initializes a ServiceAccountCredentials.
+ #
+ # @param scope [string|array] the scope(s) to access
+ # @param json_key_io [IO] an IO from which the JSON key can be read
+ def initialize(scope, json_key_io)
+ private_key, client_email = read_json_key(json_key_io)
+ super(token_credential_uri: TOKEN_CRED_URI,
+ audience: AUDIENCE,
+ scope: scope,
+ issuer: client_email,
+ signing_key: OpenSSL::PKey::RSA.new(private_key))
+ end
+ end
+ end
+ end
+end
diff --git a/src/ruby/lib/grpc/auth/signet.rb b/src/ruby/lib/grpc/auth/signet.rb
new file mode 100644
index 0000000000..a8bce1255c
--- /dev/null
+++ b/src/ruby/lib/grpc/auth/signet.rb
@@ -0,0 +1,67 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+require 'signet/oauth_2/client'
+
+module Signet
+ # Signet::OAuth2 supports OAuth2 authentication.
+ module OAuth2
+ AUTH_METADATA_KEY = :Authorization
+ # Signet::OAuth2::Client creates an OAuth2 client
+ #
+ # Here client is re-opened to add the #apply and #apply! methods which
+ # update a hash map with the fetched authentication token
+ #
+ # Eventually, this change may be merged into signet itself, or some other
+ # package that provides Google-specific auth via signet, and this extension
+ # will be unnecessary.
+ class Client
+ # Updates a_hash updated with the authentication token
+ def apply!(a_hash, opts = {})
+ # fetch the access token there is currently not one, or if the client
+ # has expired
+ fetch_access_token!(opts) if access_token.nil? || expired?
+ a_hash[AUTH_METADATA_KEY] = "Bearer #{access_token}"
+ end
+
+ # Returns a clone of a_hash updated with the authentication token
+ def apply(a_hash, opts = {})
+ a_copy = a_hash.clone
+ apply!(a_copy, opts)
+ a_copy
+ end
+
+ # Returns a reference to the #apply method, suitable for passing as
+ # a closure
+ def updater_proc
+ lambda(&method(:apply))
+ end
+ end
+ end
+end
diff --git a/src/ruby/spec/auth/apply_auth_examples.rb b/src/ruby/spec/auth/apply_auth_examples.rb
new file mode 100644
index 0000000000..09b393026f
--- /dev/null
+++ b/src/ruby/spec/auth/apply_auth_examples.rb
@@ -0,0 +1,163 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
+$LOAD_PATH.unshift(spec_dir)
+$LOAD_PATH.uniq!
+
+require 'faraday'
+require 'spec_helper'
+
+def build_json_response(payload)
+ [200,
+ { 'Content-Type' => 'application/json; charset=utf-8' },
+ MultiJson.dump(payload)]
+end
+
+WANTED_AUTH_KEY = :Authorization
+
+shared_examples 'apply/apply! are OK' do
+ # tests that use these examples need to define
+ #
+ # @client which should be an auth client
+ #
+ # @make_auth_stubs, which should stub out the expected http behaviour of the
+ # auth client
+ describe '#fetch_access_token' do
+ it 'should set access_token to the fetched value' do
+ token = '1/abcdef1234567890'
+ stubs = make_auth_stubs with_access_token: token
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+
+ @client.fetch_access_token!(connection: c)
+ expect(@client.access_token).to eq(token)
+ stubs.verify_stubbed_calls
+ end
+ end
+
+ describe '#apply!' do
+ it 'should update the target hash with fetched access token' do
+ token = '1/abcdef1234567890'
+ stubs = make_auth_stubs with_access_token: token
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+
+ md = { foo: 'bar' }
+ @client.apply!(md, connection: c)
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
+ expect(md).to eq(want)
+ stubs.verify_stubbed_calls
+ end
+ end
+
+ describe 'updater_proc' do
+ it 'should provide a proc that updates a hash with the access token' do
+ token = '1/abcdef1234567890'
+ stubs = make_auth_stubs with_access_token: token
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+
+ md = { foo: 'bar' }
+ the_proc = @client.updater_proc
+ got = the_proc.call(md, connection: c)
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
+ expect(got).to eq(want)
+ stubs.verify_stubbed_calls
+ end
+ end
+
+ describe '#apply' do
+ it 'should not update the original hash with the access token' do
+ token = '1/abcdef1234567890'
+ stubs = make_auth_stubs with_access_token: token
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+
+ md = { foo: 'bar' }
+ @client.apply(md, connection: c)
+ want = { foo: 'bar' }
+ expect(md).to eq(want)
+ stubs.verify_stubbed_calls
+ end
+
+ it 'should add the token to the returned hash' do
+ token = '1/abcdef1234567890'
+ stubs = make_auth_stubs with_access_token: token
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+
+ md = { foo: 'bar' }
+ got = @client.apply(md, connection: c)
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
+ expect(got).to eq(want)
+ stubs.verify_stubbed_calls
+ end
+
+ it 'should not fetch a new token if the current is not expired' do
+ token = '1/abcdef1234567890'
+ stubs = make_auth_stubs with_access_token: token
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+
+ n = 5 # arbitrary
+ n.times do |_t|
+ md = { foo: 'bar' }
+ got = @client.apply(md, connection: c)
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{token}" }
+ expect(got).to eq(want)
+ end
+ stubs.verify_stubbed_calls
+ end
+
+ it 'should fetch a new token if the current one is expired' do
+ token_1 = '1/abcdef1234567890'
+ token_2 = '2/abcdef1234567890'
+
+ [token_1, token_2].each do |t|
+ stubs = make_auth_stubs with_access_token: t
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+ md = { foo: 'bar' }
+ got = @client.apply(md, connection: c)
+ want = { :foo => 'bar', WANTED_AUTH_KEY => "Bearer #{t}" }
+ expect(got).to eq(want)
+ stubs.verify_stubbed_calls
+ @client.expires_at -= 3601 # default is to expire in 1hr
+ end
+ end
+ end
+end
diff --git a/src/ruby/spec/auth/compute_engine_spec.rb b/src/ruby/spec/auth/compute_engine_spec.rb
new file mode 100644
index 0000000000..9e0b4660fa
--- /dev/null
+++ b/src/ruby/spec/auth/compute_engine_spec.rb
@@ -0,0 +1,108 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
+$LOAD_PATH.unshift(spec_dir)
+$LOAD_PATH.uniq!
+
+require 'apply_auth_examples'
+require 'faraday'
+require 'grpc/auth/compute_engine'
+require 'spec_helper'
+
+describe Google::RPC::Auth::GCECredentials do
+ MD_URI = '/computeMetadata/v1/instance/service-accounts/default/token'
+ GCECredentials = Google::RPC::Auth::GCECredentials
+
+ before(:example) do
+ @client = GCECredentials.new
+ end
+
+ def make_auth_stubs(with_access_token: '')
+ Faraday::Adapter::Test::Stubs.new do |stub|
+ stub.get(MD_URI) do |env|
+ headers = env[:request_headers]
+ expect(headers['Metadata-Flavor']).to eq('Google')
+ build_json_response(
+ 'access_token' => with_access_token,
+ 'token_type' => 'Bearer',
+ 'expires_in' => 3600)
+ end
+ end
+ end
+
+ it_behaves_like 'apply/apply! are OK'
+
+ describe '#on_gce?' do
+ it 'should be true when Metadata-Flavor is Google' do
+ stubs = Faraday::Adapter::Test::Stubs.new do |stub|
+ stub.get('/') do |_env|
+ [200,
+ { 'Metadata-Flavor' => 'Google' },
+ '']
+ end
+ end
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+ expect(GCECredentials.on_gce?(connection: c)).to eq(true)
+ stubs.verify_stubbed_calls
+ end
+
+ it 'should be false when Metadata-Flavor is not Google' do
+ stubs = Faraday::Adapter::Test::Stubs.new do |stub|
+ stub.get('/') do |_env|
+ [200,
+ { 'Metadata-Flavor' => 'NotGoogle' },
+ '']
+ end
+ end
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+ expect(GCECredentials.on_gce?(connection: c)).to eq(false)
+ stubs.verify_stubbed_calls
+ end
+
+ it 'should be false if the response is not 200' do
+ stubs = Faraday::Adapter::Test::Stubs.new do |stub|
+ stub.get('/') do |_env|
+ [404,
+ { 'Metadata-Flavor' => 'Google' },
+ '']
+ end
+ end
+ c = Faraday.new do |b|
+ b.adapter(:test, stubs)
+ end
+ expect(GCECredentials.on_gce?(connection: c)).to eq(false)
+ stubs.verify_stubbed_calls
+ end
+ end
+end
diff --git a/src/ruby/spec/auth/service_account_spec.rb b/src/ruby/spec/auth/service_account_spec.rb
new file mode 100644
index 0000000000..cbc6a73ac2
--- /dev/null
+++ b/src/ruby/spec/auth/service_account_spec.rb
@@ -0,0 +1,75 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
+$LOAD_PATH.unshift(spec_dir)
+$LOAD_PATH.uniq!
+
+require 'apply_auth_examples'
+require 'grpc/auth/service_account'
+require 'jwt'
+require 'multi_json'
+require 'openssl'
+require 'spec_helper'
+
+describe Google::RPC::Auth::ServiceAccountCredentials do
+ before(:example) do
+ @key = OpenSSL::PKey::RSA.new(2048)
+ cred_json = {
+ private_key_id: 'a_private_key_id',
+ private_key: @key.to_pem,
+ client_email: 'app@developer.gserviceaccount.com',
+ client_id: 'app.apps.googleusercontent.com',
+ type: 'service_account'
+ }
+ cred_json_text = MultiJson.dump(cred_json)
+ @client = Google::RPC::Auth::ServiceAccountCredentials.new(
+ 'https://www.googleapis.com/auth/userinfo.profile',
+ StringIO.new(cred_json_text))
+ end
+
+ def make_auth_stubs(with_access_token: '')
+ Faraday::Adapter::Test::Stubs.new do |stub|
+ stub.post('/oauth2/v3/token') do |env|
+ params = Addressable::URI.form_unencode(env[:body])
+ _claim, _header = JWT.decode(params.assoc('assertion').last,
+ @key.public_key)
+ want = ['grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer']
+ expect(params.assoc('grant_type')).to eq(want)
+ build_json_response(
+ 'access_token' => with_access_token,
+ 'token_type' => 'Bearer',
+ 'expires_in' => 3600
+ )
+ end
+ end
+ end
+
+ it_behaves_like 'apply/apply! are OK'
+end
diff --git a/src/ruby/spec/auth/signet_spec.rb b/src/ruby/spec/auth/signet_spec.rb
new file mode 100644
index 0000000000..1712edf296
--- /dev/null
+++ b/src/ruby/spec/auth/signet_spec.rb
@@ -0,0 +1,70 @@
+# Copyright 2015, Google Inc.
+# All rights reserved.
+#
+# Redistribution and use in source and binary forms, with or without
+# modification, are permitted provided that the following conditions are
+# met:
+#
+# * Redistributions of source code must retain the above copyright
+# notice, this list of conditions and the following disclaimer.
+# * Redistributions in binary form must reproduce the above
+# copyright notice, this list of conditions and the following disclaimer
+# in the documentation and/or other materials provided with the
+# distribution.
+# * Neither the name of Google Inc. nor the names of its
+# contributors may be used to endorse or promote products derived from
+# this software without specific prior written permission.
+#
+# THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+# "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+# LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+# A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+# OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+# SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+# LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+# DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+# THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+spec_dir = File.expand_path(File.join(File.dirname(__FILE__)))
+$LOAD_PATH.unshift(spec_dir)
+$LOAD_PATH.uniq!
+
+require 'apply_auth_examples'
+require 'grpc/auth/signet'
+require 'jwt'
+require 'openssl'
+require 'spec_helper'
+
+describe Signet::OAuth2::Client do
+ before(:example) do
+ @key = OpenSSL::PKey::RSA.new(2048)
+ @client = Signet::OAuth2::Client.new(
+ token_credential_uri: 'https://accounts.google.com/o/oauth2/token',
+ scope: 'https://www.googleapis.com/auth/userinfo.profile',
+ issuer: 'app@example.com',
+ audience: 'https://accounts.google.com/o/oauth2/token',
+ signing_key: @key
+ )
+ end
+
+ def make_auth_stubs(with_access_token: '')
+ Faraday::Adapter::Test::Stubs.new do |stub|
+ stub.post('/o/oauth2/token') do |env|
+ params = Addressable::URI.form_unencode(env[:body])
+ _claim, _header = JWT.decode(params.assoc('assertion').last,
+ @key.public_key)
+ want = ['grant_type', 'urn:ietf:params:oauth:grant-type:jwt-bearer']
+ expect(params.assoc('grant_type')).to eq(want)
+ build_json_response(
+ 'access_token' => with_access_token,
+ 'token_type' => 'Bearer',
+ 'expires_in' => 3600
+ )
+ end
+ end
+ end
+
+ it_behaves_like 'apply/apply! are OK'
+end
diff --git a/src/ruby/spec/channel_spec.rb b/src/ruby/spec/channel_spec.rb
index 189d1c67ab..82c7915deb 100644
--- a/src/ruby/spec/channel_spec.rb
+++ b/src/ruby/spec/channel_spec.rb
@@ -29,8 +29,6 @@
require 'grpc'
-FAKE_HOST='localhost:0'
-
def load_test_certs
test_root = File.join(File.dirname(__FILE__), 'testdata')
files = ['ca.pem', 'server1.key', 'server1.pem']
@@ -38,6 +36,8 @@ def load_test_certs
end
describe GRPC::Core::Channel do
+ FAKE_HOST = 'localhost:0'
+
def create_test_cert
GRPC::Core::Credentials.new(load_test_certs[0])
end
diff --git a/src/ruby/spec/generic/active_call_spec.rb b/src/ruby/spec/generic/active_call_spec.rb
index e81b2168b0..599e68bef0 100644
--- a/src/ruby/spec/generic/active_call_spec.rb
+++ b/src/ruby/spec/generic/active_call_spec.rb
@@ -371,6 +371,6 @@ describe GRPC::ActiveCall do
end
def deadline
- Time.now + 0.25 # in 0.25 seconds; arbitrary
+ Time.now + 1 # in 1 second; arbitrary
end
end
diff --git a/src/ruby/spec/spec_helper.rb b/src/ruby/spec/spec_helper.rb
index 3322674e97..ea0a256713 100644
--- a/src/ruby/spec/spec_helper.rb
+++ b/src/ruby/spec/spec_helper.rb
@@ -27,10 +27,22 @@
# (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+spec_dir = File.expand_path(File.dirname(__FILE__))
+root_dir = File.expand_path(File.join(spec_dir, '..'))
+lib_dir = File.expand_path(File.join(root_dir, 'lib'))
+
+$LOAD_PATH.unshift(spec_dir)
+$LOAD_PATH.unshift(lib_dir)
+$LOAD_PATH.uniq!
+
+require 'faraday'
require 'rspec'
require 'logging'
require 'rspec/logging_helper'
+# Allow Faraday to support test stubs
+Faraday::Adapter.load_middleware(:test)
+
# Configure RSpec to capture log messages for each test. The output from the
# logs will be stored in the @log_output variable. It is a StringIO instance.
RSpec.configure do |config|