From a33a640e611d6a18531e8a5a27a7791a47d8799a Mon Sep 17 00:00:00 2001 From: Mike Moore Date: Tue, 7 May 2019 17:56:17 -0600 Subject: [PATCH] Credentials: Use methods instead of constants (#212) Update Credentials to use methods for values that are intended to be changed by users, replacing constants. Add Credentials documentation. --- .rubocop.yml | 2 + lib/googleauth/credentials.rb | 240 +++++++++++++- spec/googleauth/credentials_spec.rb | 489 +++++++++++++++++++--------- 3 files changed, 559 insertions(+), 172 deletions(-) diff --git a/.rubocop.yml b/.rubocop.yml index 17b67f6..3a555f6 100644 --- a/.rubocop.yml +++ b/.rubocop.yml @@ -7,6 +7,8 @@ AllCops: - "Rakefile" Metrics/ClassLength: Max: 110 + Exclude: + - "lib/googleauth/credentials.rb" Metrics/ModuleLength: Max: 110 Metrics/BlockLength: diff --git a/lib/googleauth/credentials.rb b/lib/googleauth/credentials.rb index 906bbdb..4db5954 100644 --- a/lib/googleauth/credentials.rb +++ b/lib/googleauth/credentials.rb @@ -35,26 +35,206 @@ require "googleauth/credentials_loader" module Google module Auth - # This class is intended to be inherited by API-specific classes - # which overrides the SCOPE constant. + ## + # Credentials is responsible for representing the authentication when connecting to an API. This + # class is also intended to be inherited by API-specific classes. class Credentials + ## + # The default token credential URI to be used when none is provided during initialization. TOKEN_CREDENTIAL_URI = "https://oauth2.googleapis.com/token".freeze + + ## + # The default target audience ID to be used when none is provided during initialization. AUDIENCE = "https://oauth2.googleapis.com/token".freeze - SCOPE = [].freeze - PATH_ENV_VARS = [].freeze - JSON_ENV_VARS = [].freeze - DEFAULT_PATHS = [].freeze + ## + # The default token credential URI to be used when none is provided during initialization. + # The URI is the authorization server's HTTP endpoint capable of issuing tokens and + # refreshing expired tokens. + # + # @return [String] + # + def self.token_credential_uri + return @token_credential_uri unless @token_credential_uri.nil? + + const_get :TOKEN_CREDENTIAL_URI if const_defined? :TOKEN_CREDENTIAL_URI + end + + ## + # Set the default token credential URI to be used when none is provided during initialization. + # + # @param [String] new_token_credential_uri + # @return [String] + # + def self.token_credential_uri= new_token_credential_uri + @token_credential_uri = new_token_credential_uri + end + + ## + # The default target audience ID to be used when none is provided during initialization. + # Used only by the assertion grant type. + # + # @return [String] + # + def self.audience + return @audience unless @audience.nil? + + const_get :AUDIENCE if const_defined? :AUDIENCE + end + + ## + # Sets the default target audience ID to be used when none is provided during initialization. + # + # @param [String] new_audience + # @return [String] + # + def self.audience= new_audience + @audience = new_audience + end + + ## + # The default scope to be used when none is provided during initialization. + # A scope is an access range defined by the authorization server. + # The scope can be a single value or a list of values. + # + # @return [String, Array] + # + def self.scope + return @scope unless @scope.nil? + + tmp_scope = [] + # Pull in values is the SCOPE constant exists. + tmp_scope << const_get(:SCOPE) if const_defined? :SCOPE + tmp_scope.flatten.uniq + end + + ## + # Sets the default scope to be used when none is provided during initialization. + # + # @param [String, Array] new_scope + # @return [String, Array] + # + def self.scope= new_scope + new_scope = Array new_scope unless new_scope.nil? + @scope = new_scope + end + + ## + # The environment variables to search for credentials. Values can either be a file path to the + # credentials file, or the JSON contents of the credentials file. + # + # @return [Array] + # + def self.env_vars + return @env_vars unless @env_vars.nil? + + # Pull values when PATH_ENV_VARS or JSON_ENV_VARS constants exists. + tmp_env_vars = [] + tmp_env_vars << const_get(:PATH_ENV_VARS) if const_defined? :PATH_ENV_VARS + tmp_env_vars << const_get(:JSON_ENV_VARS) if const_defined? :JSON_ENV_VARS + tmp_env_vars.flatten.uniq + end + + ## + # Sets the environment variables to search for credentials. + # + # @param [Array] new_env_vars + # @return [Array] + # + def self.env_vars= new_env_vars + new_env_vars = Array new_env_vars unless new_env_vars.nil? + @env_vars = new_env_vars + end + + ## + # The file paths to search for credentials files. + # + # @return [Array] + # + def self.paths + return @paths unless @paths.nil? + + tmp_paths = [] + # Pull in values is the DEFAULT_PATHS constant exists. + tmp_paths << const_get(:DEFAULT_PATHS) if const_defined? :DEFAULT_PATHS + tmp_paths.flatten.uniq + end + + ## + # Set the file paths to search for credentials files. + # + # @param [Array] new_paths + # @return [Array] + # + def self.paths= new_paths + new_paths = Array new_paths unless new_paths.nil? + @paths = new_paths + end + + ## + # The Signet::OAuth2::Client object the Credentials instance is using. + # + # @return [Signet::OAuth2::Client] + # attr_accessor :client - attr_reader :project_id - # Delegate client methods to the client object. + ## + # Identifier for the project the client is authenticating with. + # + # @return [String] + # + attr_reader :project_id + + # @private Delegate client methods to the client object. extend Forwardable + + ## + # @!attribute [r] token_credential_uri + # @return [String] The token credential URI. The URI is the authorization server's HTTP + # endpoint capable of issuing tokens and refreshing expired tokens. + # + # @!attribute [r] audience + # @return [String] The target audience ID when issuing assertions. Used only by the + # assertion grant type. + # + # @!attribute [r] scope + # @return [String, Array] The scope for this client. A scope is an access range + # defined by the authorization server. The scope can be a single value or a list of values. + # + # @!attribute [r] issuer + # @return [String] The issuer ID associated with this client. + # + # @!attribute [r] signing_key + # @return [String, OpenSSL::PKey] The signing key associated with this client. + # + # @!attribute [r] updater_proc + # @return [Proc] Returns a reference to the {Signet::OAuth2::Client#apply} method, + # suitable for passing as a closure. + # def_delegators :@client, :token_credential_uri, :audience, :scope, :issuer, :signing_key, :updater_proc # rubocop:disable Metrics/AbcSize + + ## + # Creates a new Credentials instance with the provided auth credentials, and with the default + # values configured on the class. + # + # @param [String, Hash, Signet::OAuth2::Client] keyfile + # The keyfile can be provided as one of the following: + # + # * The path to a JSON keyfile (as a +String+) + # * The contents of a JSON keyfile (as a +Hash+) + # * A +Signet::OAuth2::Client+ object + # @param [Hash] options + # The options for configuring the credentials instance. The following is supported: + # + # * +:scope+ - the scope for the client + # * +"project_id"+ (and optionally +"project"+) - the project identifier for the client + # * +:connection_builder+ - the connection builder to use for the client + # * +:default_connection+ - the default connection to use for the client + # def initialize keyfile, options = {} scope = options[:scope] verify_keyfile_provided! keyfile @@ -80,10 +260,27 @@ module Google end # rubocop:enable Metrics/AbcSize - # Returns the default credentials checking, in this order, the path env - # evironment variables, json environment variables, default paths. If the - # previously stated locations do not contain keyfile information, - # this method defaults to use the application default. + ## + # Creates a new Credentials instance with auth credentials acquired by searching the + # environment variables and paths configured on the class, and with the default values + # configured on the class. + # + # The auth credentials are searched for in the following order: + # + # 1. configured environment variables (see {Credentials.env_vars}) + # 2. configured default file paths (see {Credentials.paths}) + # 3. application default (see {Google::Auth.get_application_default}) + # + # @param [Hash] options + # The options for configuring the credentials instance. The following is supported: + # + # * +:scope+ - the scope for the client + # * +"project_id"+ (and optionally +"project"+) - the project identifier for the client + # * +:connection_builder+ - the connection builder to use for the client + # * +:default_connection+ - the default connection to use for the client + # + # @return [Credentials] + # def self.default options = {} # First try to find keyfile file or json from environment variables. client = from_env_vars options @@ -96,8 +293,10 @@ module Google client end + ## + # @private Lookup Credentials from environment variables. def self.from_env_vars options - (self::PATH_ENV_VARS + self::JSON_ENV_VARS).each do |env_var| + env_vars.each do |env_var| str = ENV[env_var] next if str.nil? return new str, options if ::File.file? str @@ -106,8 +305,10 @@ module Google nil end + ## + # @private Lookup Credentials from default file paths. def self.from_default_paths options - self::DEFAULT_PATHS + paths .select { |p| ::File.file? p } .each do |file| return new file, options @@ -115,11 +316,14 @@ module Google nil end + ## + # @private Lookup Credentials using Google::Auth.get_application_default. def self.from_application_default options - scope = options[:scope] || self::SCOPE + scope = options[:scope] || self.scope client = Google::Auth.get_application_default scope new client, options end + private_class_method :from_env_vars, :from_default_paths, :from_application_default @@ -152,9 +356,9 @@ module Google def client_options options # Keyfile options have higher priority over constructor defaults - options["token_credential_uri"] ||= self.class::TOKEN_CREDENTIAL_URI - options["audience"] ||= self.class::AUDIENCE - options["scope"] ||= self.class::SCOPE + options["token_credential_uri"] ||= self.class.token_credential_uri + options["audience"] ||= self.class.audience + options["scope"] ||= self.class.scope # client options for initializing signet client { token_credential_uri: options["token_credential_uri"], diff --git a/spec/googleauth/credentials_spec.rb b/spec/googleauth/credentials_spec.rb index 4f5dbbd..3cb2183 100644 --- a/spec/googleauth/credentials_spec.rb +++ b/spec/googleauth/credentials_spec.rb @@ -81,186 +81,367 @@ describe Google::Auth::Credentials, :private do Google::Auth::Credentials.new default_keyfile_hash, scope: "http://example.com/scope" end - it 'can be subclassed to pass in other env paths' do - TEST_PATH_ENV_VAR = 'TEST_PATH'.freeze - TEST_PATH_ENV_VAL = '/unknown/path/to/file.txt'.freeze - TEST_JSON_ENV_VAR = 'TEST_JSON_VARS'.freeze - TEST_JSON_ENV_VAL = JSON.generate(default_keyfile_hash) + describe "using CONSTANTS" do + it "can be subclassed to pass in other env paths" do + test_path_env_val = "/unknown/path/to/file.txt".freeze + test_json_env_val = JSON.generate default_keyfile_hash - ENV[TEST_PATH_ENV_VAR] = TEST_PATH_ENV_VAL - ENV[TEST_JSON_ENV_VAR] = TEST_JSON_ENV_VAL + ENV["TEST_PATH"] = test_path_env_val + ENV["TEST_JSON_VARS"] = test_json_env_val - class TestCredentials < Google::Auth::Credentials - SCOPE = "http://example.com/scope".freeze - PATH_ENV_VARS = [TEST_PATH_ENV_VAR].freeze - JSON_ENV_VARS = [TEST_JSON_ENV_VAR].freeze + class TestCredentials1 < Google::Auth::Credentials + TOKEN_CREDENTIAL_URI = "https://example.com/token".freeze + AUDIENCE = "https://example.com/audience".freeze + SCOPE = "http://example.com/scope".freeze + PATH_ENV_VARS = ["TEST_PATH"].freeze + JSON_ENV_VARS = ["TEST_JSON_VARS"].freeze + end + + allow(::File).to receive(:file?).with(test_path_env_val) { false } + allow(::File).to receive(:file?).with(test_json_env_val) { false } + + mocked_signet = double "Signet::OAuth2::Client" + allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) + allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) + allow(mocked_signet).to receive(:client_id) + allow(Signet::OAuth2::Client).to receive(:new) do |options| + expect(options[:token_credential_uri]).to eq("https://example.com/token") + expect(options[:audience]).to eq("https://example.com/audience") + expect(options[:scope]).to eq(["http://example.com/scope"]) + expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) + expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + + mocked_signet + end + + creds = TestCredentials1.default + expect(creds).to be_a_kind_of(TestCredentials1) + expect(creds.client).to eq(mocked_signet) + expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) end - allow(::File).to receive(:file?).with(TEST_PATH_ENV_VAL) { false } - allow(::File).to receive(:file?).with(TEST_JSON_ENV_VAL) { false } + it "subclasses can use PATH_ENV_VARS to get keyfile path" do + class TestCredentials2 < Google::Auth::Credentials + SCOPE = "http://example.com/scope".freeze + PATH_ENV_VARS = %w[PATH_ENV_DUMMY PATH_ENV_TEST].freeze + JSON_ENV_VARS = ["JSON_ENV_DUMMY"].freeze + DEFAULT_PATHS = ["~/default/path/to/file.txt"].freeze + end - mocked_signet = double "Signet::OAuth2::Client" - allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) - allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) - allow(mocked_signet).to receive(:client_id) - allow(Signet::OAuth2::Client).to receive(:new) do |options| - expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") - expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") - expect(options[:scope]).to eq(["http://example.com/scope"]) - expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) - expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" } + allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false } + allow(::ENV).to receive(:[]).with("PATH_ENV_TEST") { "/unknown/path/to/file.txt" } + allow(::File).to receive(:file?).with("/unknown/path/to/file.txt") { true } + allow(::File).to receive(:read).with("/unknown/path/to/file.txt") { JSON.generate default_keyfile_hash } - mocked_signet + mocked_signet = double "Signet::OAuth2::Client" + allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) + allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) + allow(mocked_signet).to receive(:client_id) + allow(Signet::OAuth2::Client).to receive(:new) do |options| + expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") + expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") + expect(options[:scope]).to eq(["http://example.com/scope"]) + expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) + expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + + mocked_signet + end + + creds = TestCredentials2.default + expect(creds).to be_a_kind_of(TestCredentials2) + expect(creds.client).to eq(mocked_signet) + expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) end - creds = TestCredentials.default - expect(creds).to be_a_kind_of(TestCredentials) - expect(creds.client).to eq(mocked_signet) - expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) + it "subclasses can use JSON_ENV_VARS to get keyfile contents" do + test_json_env_val = JSON.generate default_keyfile_hash + + class TestCredentials3 < Google::Auth::Credentials + SCOPE = "http://example.com/scope".freeze + PATH_ENV_VARS = ["PATH_ENV_DUMMY"].freeze + JSON_ENV_VARS = %w[JSON_ENV_DUMMY JSON_ENV_TEST].freeze + DEFAULT_PATHS = ["~/default/path/to/file.txt"].freeze + end + + allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" } + allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false } + allow(::File).to receive(:file?).with(test_json_env_val) { false } + allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil } + allow(::ENV).to receive(:[]).with("JSON_ENV_TEST") { test_json_env_val } + + mocked_signet = double "Signet::OAuth2::Client" + allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) + allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) + allow(mocked_signet).to receive(:client_id) + allow(Signet::OAuth2::Client).to receive(:new) do |options| + expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") + expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") + expect(options[:scope]).to eq(["http://example.com/scope"]) + expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) + expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + + mocked_signet + end + + creds = TestCredentials3.default + expect(creds).to be_a_kind_of(TestCredentials3) + expect(creds.client).to eq(mocked_signet) + expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) + end + + it "subclasses can use DEFAULT_PATHS to get keyfile path" do + class TestCredentials4 < Google::Auth::Credentials + SCOPE = "http://example.com/scope".freeze + PATH_ENV_VARS = ["PATH_ENV_DUMMY"].freeze + JSON_ENV_VARS = ["JSON_ENV_DUMMY"].freeze + DEFAULT_PATHS = ["~/default/path/to/file.txt"].freeze + end + + allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" } + allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false } + allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil } + allow(::File).to receive(:file?).with("~/default/path/to/file.txt") { true } + allow(::File).to receive(:read).with("~/default/path/to/file.txt") { JSON.generate default_keyfile_hash } + + mocked_signet = double "Signet::OAuth2::Client" + allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) + allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) + allow(mocked_signet).to receive(:client_id) + allow(Signet::OAuth2::Client).to receive(:new) do |options| + expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") + expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") + expect(options[:scope]).to eq(["http://example.com/scope"]) + expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) + expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + + mocked_signet + end + + creds = TestCredentials4.default + expect(creds).to be_a_kind_of(TestCredentials4) + expect(creds.client).to eq(mocked_signet) + expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) + end + + it "subclasses that find no matches default to Google::Auth.get_application_default" do + class TestCredentials5 < Google::Auth::Credentials + SCOPE = "http://example.com/scope".freeze + PATH_ENV_VARS = ["PATH_ENV_DUMMY"].freeze + JSON_ENV_VARS = ["JSON_ENV_DUMMY"].freeze + DEFAULT_PATHS = ["~/default/path/to/file.txt"].freeze + end + + allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" } + allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false } + allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil } + allow(::File).to receive(:file?).with("~/default/path/to/file.txt") { false } + + mocked_signet = double "Signet::OAuth2::Client" + allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) + allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) + allow(mocked_signet).to receive(:client_id) + allow(Google::Auth).to receive(:get_application_default) do |scope| + expect(scope).to eq([TestCredentials5::SCOPE]) + + # This should really be a Signet::OAuth2::Client object, + # but mocking is making that difficult, so return a valid hash instead. + default_keyfile_hash + end + allow(Signet::OAuth2::Client).to receive(:new) do |options| + expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") + expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") + expect(options[:scope]).to eq(["http://example.com/scope"]) + expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) + expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + + mocked_signet + end + + creds = TestCredentials5.default + expect(creds).to be_a_kind_of(TestCredentials5) + expect(creds.client).to eq(mocked_signet) + expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) + end end - it "subclasses can use PATH_ENV_VARS to get keyfile path" do - class TestCredentials < Google::Auth::Credentials - SCOPE = "http://example.com/scope".freeze - PATH_ENV_VARS = %w[PATH_ENV_DUMMY PATH_ENV_TEST].freeze - JSON_ENV_VARS = ["JSON_ENV_DUMMY"].freeze - DEFAULT_PATHS = ["~/default/path/to/file.txt"].freeze + describe "using class methods" do + it "can be subclassed to pass in other env paths" do + test_path_env_val = "/unknown/path/to/file.txt".freeze + test_json_env_val = JSON.generate default_keyfile_hash + + ENV["TEST_PATH"] = test_path_env_val + ENV["TEST_JSON_VARS"] = test_json_env_val + + class TestCredentials11 < Google::Auth::Credentials + self.token_credential_uri = "https://example.com/token" + self.audience = "https://example.com/audience" + self.scope = "http://example.com/scope" + self.env_vars = ["TEST_PATH", "TEST_JSON_VARS"] + end + + allow(::File).to receive(:file?).with(test_path_env_val) { false } + allow(::File).to receive(:file?).with(test_json_env_val) { false } + + mocked_signet = double "Signet::OAuth2::Client" + allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) + allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) + allow(mocked_signet).to receive(:client_id) + allow(Signet::OAuth2::Client).to receive(:new) do |options| + expect(options[:token_credential_uri]).to eq("https://example.com/token") + expect(options[:audience]).to eq("https://example.com/audience") + expect(options[:scope]).to eq(["http://example.com/scope"]) + expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) + expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + + mocked_signet + end + + creds = TestCredentials11.default + expect(creds).to be_a_kind_of(TestCredentials11) + expect(creds.client).to eq(mocked_signet) + expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) end - allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" } - allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false } - allow(::ENV).to receive(:[]).with("PATH_ENV_TEST") { "/unknown/path/to/file.txt" } - allow(::File).to receive(:file?).with("/unknown/path/to/file.txt") { true } - allow(::File).to receive(:read).with("/unknown/path/to/file.txt") { JSON.generate default_keyfile_hash } + it "subclasses can use PATH_ENV_VARS to get keyfile path" do + class TestCredentials12 < Google::Auth::Credentials + self.scope = "http://example.com/scope" + self.env_vars = %w[PATH_ENV_DUMMY PATH_ENV_TEST JSON_ENV_DUMMY] + self.paths = ["~/default/path/to/file.txt"] + end - mocked_signet = double "Signet::OAuth2::Client" - allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) - allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) - allow(mocked_signet).to receive(:client_id) - allow(Signet::OAuth2::Client).to receive(:new) do |options| - expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") - expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") - expect(options[:scope]).to eq(["http://example.com/scope"]) - expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) - expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" } + allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false } + allow(::ENV).to receive(:[]).with("PATH_ENV_TEST") { "/unknown/path/to/file.txt" } + allow(::File).to receive(:file?).with("/unknown/path/to/file.txt") { true } + allow(::File).to receive(:read).with("/unknown/path/to/file.txt") { JSON.generate default_keyfile_hash } - mocked_signet + mocked_signet = double "Signet::OAuth2::Client" + allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) + allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) + allow(mocked_signet).to receive(:client_id) + allow(Signet::OAuth2::Client).to receive(:new) do |options| + expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") + expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") + expect(options[:scope]).to eq(["http://example.com/scope"]) + expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) + expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + + mocked_signet + end + + creds = TestCredentials12.default + expect(creds).to be_a_kind_of(TestCredentials12) + expect(creds.client).to eq(mocked_signet) + expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) end - creds = TestCredentials.default - expect(creds).to be_a_kind_of(TestCredentials) - expect(creds.client).to eq(mocked_signet) - expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) - end + it "subclasses can use JSON_ENV_VARS to get keyfile contents" do + test_json_env_val = JSON.generate default_keyfile_hash - it 'subclasses can use JSON_ENV_VARS to get keyfile contents' do - TEST_JSON_ENV_VAL = JSON.generate(default_keyfile_hash) + class TestCredentials13 < Google::Auth::Credentials + self.scope = "http://example.com/scope" + self.env_vars = %w[PATH_ENV_DUMMY JSON_ENV_DUMMY JSON_ENV_TEST] + self.paths = ["~/default/path/to/file.txt"] + end - class TestCredentials < Google::Auth::Credentials - SCOPE = "http://example.com/scope".freeze - PATH_ENV_VARS = ["PATH_ENV_DUMMY"].freeze - JSON_ENV_VARS = %w[JSON_ENV_DUMMY JSON_ENV_TEST].freeze - DEFAULT_PATHS = ["~/default/path/to/file.txt"].freeze + allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" } + allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false } + allow(::File).to receive(:file?).with(test_json_env_val) { false } + allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil } + allow(::ENV).to receive(:[]).with("JSON_ENV_TEST") { test_json_env_val } + + mocked_signet = double "Signet::OAuth2::Client" + allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) + allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) + allow(mocked_signet).to receive(:client_id) + allow(Signet::OAuth2::Client).to receive(:new) do |options| + expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") + expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") + expect(options[:scope]).to eq(["http://example.com/scope"]) + expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) + expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + + mocked_signet + end + + creds = TestCredentials13.default + expect(creds).to be_a_kind_of(TestCredentials13) + expect(creds.client).to eq(mocked_signet) + expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) end - allow(::ENV).to receive(:[]).with('PATH_ENV_DUMMY') { '/fake/path/to/file.txt' } - allow(::File).to receive(:file?).with('/fake/path/to/file.txt') { false } - allow(::File).to receive(:file?).with(TEST_JSON_ENV_VAL) { false } - allow(::ENV).to receive(:[]).with('JSON_ENV_DUMMY') { nil } - allow(::ENV).to receive(:[]).with('JSON_ENV_TEST') { TEST_JSON_ENV_VAL } + it "subclasses can use DEFAULT_PATHS to get keyfile path" do + class TestCredentials14 < Google::Auth::Credentials + self.scope = "http://example.com/scope" + self.env_vars = %w[PATH_ENV_DUMMY JSON_ENV_DUMMY] + self.paths = ["~/default/path/to/file.txt"] + end - mocked_signet = double "Signet::OAuth2::Client" - allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) - allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) - allow(mocked_signet).to receive(:client_id) - allow(Signet::OAuth2::Client).to receive(:new) do |options| - expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") - expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") - expect(options[:scope]).to eq(["http://example.com/scope"]) - expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) - expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" } + allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false } + allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil } + allow(::File).to receive(:file?).with("~/default/path/to/file.txt") { true } + allow(::File).to receive(:read).with("~/default/path/to/file.txt") { JSON.generate default_keyfile_hash } - mocked_signet + mocked_signet = double "Signet::OAuth2::Client" + allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) + allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) + allow(mocked_signet).to receive(:client_id) + allow(Signet::OAuth2::Client).to receive(:new) do |options| + expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") + expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") + expect(options[:scope]).to eq(["http://example.com/scope"]) + expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) + expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + + mocked_signet + end + + creds = TestCredentials14.default + expect(creds).to be_a_kind_of(TestCredentials14) + expect(creds.client).to eq(mocked_signet) + expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) end - creds = TestCredentials.default - expect(creds).to be_a_kind_of(TestCredentials) - expect(creds.client).to eq(mocked_signet) - expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) - end + it "subclasses that find no matches default to Google::Auth.get_application_default" do + class TestCredentials15 < Google::Auth::Credentials + self.scope = "http://example.com/scope" + self.env_vars = %w[PATH_ENV_DUMMY JSON_ENV_DUMMY] + self.paths = ["~/default/path/to/file.txt"] + end - it "subclasses can use DEFAULT_PATHS to get keyfile path" do - class TestCredentials < Google::Auth::Credentials - SCOPE = "http://example.com/scope".freeze - PATH_ENV_VARS = ["PATH_ENV_DUMMY"].freeze - JSON_ENV_VARS = ["JSON_ENV_DUMMY"].freeze - DEFAULT_PATHS = ["~/default/path/to/file.txt"].freeze + allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" } + allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false } + allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil } + allow(::File).to receive(:file?).with("~/default/path/to/file.txt") { false } + + mocked_signet = double "Signet::OAuth2::Client" + allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) + allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) + allow(mocked_signet).to receive(:client_id) + allow(Google::Auth).to receive(:get_application_default) do |scope| + expect(scope).to eq(TestCredentials15.scope) + + # This should really be a Signet::OAuth2::Client object, + # but mocking is making that difficult, so return a valid hash instead. + default_keyfile_hash + end + allow(Signet::OAuth2::Client).to receive(:new) do |options| + expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") + expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") + expect(options[:scope]).to eq(["http://example.com/scope"]) + expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) + expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) + + mocked_signet + end + + creds = TestCredentials15.default + expect(creds).to be_a_kind_of(TestCredentials15) + expect(creds.client).to eq(mocked_signet) + expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) end - - allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" } - allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false } - allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil } - allow(::File).to receive(:file?).with("~/default/path/to/file.txt") { true } - allow(::File).to receive(:read).with("~/default/path/to/file.txt") { JSON.generate default_keyfile_hash } - - mocked_signet = double "Signet::OAuth2::Client" - allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) - allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) - allow(mocked_signet).to receive(:client_id) - allow(Signet::OAuth2::Client).to receive(:new) do |options| - expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") - expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") - expect(options[:scope]).to eq(["http://example.com/scope"]) - expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) - expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) - - mocked_signet - end - - creds = TestCredentials.default - expect(creds).to be_a_kind_of(TestCredentials) - expect(creds.client).to eq(mocked_signet) - expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) - end - - it "subclasses that find no matches default to Google::Auth.get_application_default" do - class TestCredentials < Google::Auth::Credentials - SCOPE = "http://example.com/scope".freeze - PATH_ENV_VARS = ["PATH_ENV_DUMMY"].freeze - JSON_ENV_VARS = ["JSON_ENV_DUMMY"].freeze - DEFAULT_PATHS = ["~/default/path/to/file.txt"].freeze - end - - allow(::ENV).to receive(:[]).with("PATH_ENV_DUMMY") { "/fake/path/to/file.txt" } - allow(::File).to receive(:file?).with("/fake/path/to/file.txt") { false } - allow(::ENV).to receive(:[]).with("JSON_ENV_DUMMY") { nil } - allow(::File).to receive(:file?).with("~/default/path/to/file.txt") { false } - - mocked_signet = double "Signet::OAuth2::Client" - allow(mocked_signet).to receive(:configure_connection).and_return(mocked_signet) - allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) - allow(mocked_signet).to receive(:client_id) - allow(Google::Auth).to receive(:get_application_default) do |scope| - expect(scope).to eq(TestCredentials::SCOPE) - - # This should really be a Signet::OAuth2::Client object, - # but mocking is making that difficult, so return a valid hash instead. - default_keyfile_hash - end - allow(Signet::OAuth2::Client).to receive(:new) do |options| - expect(options[:token_credential_uri]).to eq("https://oauth2.googleapis.com/token") - expect(options[:audience]).to eq("https://oauth2.googleapis.com/token") - expect(options[:scope]).to eq(["http://example.com/scope"]) - expect(options[:issuer]).to eq(default_keyfile_hash["client_email"]) - expect(options[:signing_key]).to be_a_kind_of(OpenSSL::PKey::RSA) - - mocked_signet - end - - creds = TestCredentials.default - expect(creds).to be_a_kind_of(TestCredentials) - expect(creds.client).to eq(mocked_signet) - expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) end it "warns when cloud sdk credentials are used" do