From d2dffe592112b45006291ad9a57f56e00fb208c3 Mon Sep 17 00:00:00 2001 From: Daniel Azuma Date: Mon, 14 Dec 2020 23:45:26 +0000 Subject: [PATCH] fix: Support correct service account and user refresh behavior for custom credential env variables --- lib/googleauth/credentials.rb | 38 +++++-- spec/googleauth/credentials_spec.rb | 154 +++++++++++++++------------- 2 files changed, 115 insertions(+), 77 deletions(-) diff --git a/lib/googleauth/credentials.rb b/lib/googleauth/credentials.rb index bd4ee21c..f6a315fd 100644 --- a/lib/googleauth/credentials.rb +++ b/lib/googleauth/credentials.rb @@ -405,8 +405,15 @@ def self.from_env_vars options env_vars.each do |env_var| str = ENV[env_var] next if str.nil? - return new str, options if ::File.file? str - return new ::JSON.parse(str), options rescue nil + io = + if ::File.file? str + ::StringIO.new ::File.read str + else + json = ::JSON.parse str rescue nil + json ? ::StringIO.new(str) : nil + end + next if io.nil? + return from_io io, options end nil end @@ -414,11 +421,11 @@ def self.from_env_vars options ## # @private Lookup Credentials from default file paths. def self.from_default_paths options - paths - .select { |p| ::File.file? p } - .each do |file| - return new file, options - end + paths.each do |path| + next unless path && ::File.file?(path) + io = ::StringIO.new ::File.read path + return from_io io, options + end nil end @@ -436,9 +443,24 @@ def self.from_application_default options new client, options end + # @private Read credentials from a JSON stream. + def self.from_io io, options + creds_input = { + json_key_io: io, + scope: options[:scope] || scope, + target_audience: options[:target_audience] || target_audience, + enable_self_signed_jwt: options[:enable_self_signed_jwt] && options[:scope].nil?, + token_credential_uri: options[:token_credential_uri] || token_credential_uri, + audience: options[:audience] || audience + } + client = Google::Auth::DefaultCredentials.make_creds creds_input + new client + end + private_class_method :from_env_vars, :from_default_paths, - :from_application_default + :from_application_default, + :from_io protected diff --git a/spec/googleauth/credentials_spec.rb b/spec/googleauth/credentials_spec.rb index f7520860..0994439e 100644 --- a/spec/googleauth/credentials_spec.rb +++ b/spec/googleauth/credentials_spec.rb @@ -101,21 +101,22 @@ class TestCredentials1 < Google::Auth::Credentials 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| + mocked_signet = mock_signet + + allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) 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) + expect(options[:enable_self_signed_jwt]).to eq(true) + expect(options[:target_audience]).to be_nil + expect(options[:json_key_io].read).to eq(test_json_env_val) - mocked_signet + # 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 - creds = TestCredentials1.default + creds = TestCredentials1.default enable_self_signed_jwt: true 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"]) @@ -130,25 +131,28 @@ class TestCredentials2 < Google::Auth::Credentials DEFAULT_PATHS = ["~/default/path/to/file.txt"].freeze end + json_content = JSON.generate default_keyfile_hash + allow(::ENV).to receive(:[]).with("GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS") { "true" } 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 } + allow(::File).to receive(:read).with("/unknown/path/to/file.txt") { json_content } - 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| + mocked_signet = mock_signet + + allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) 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) + expect(options[:enable_self_signed_jwt]).to be_nil + expect(options[:target_audience]).to be_nil + expect(options[:json_key_io].read).to eq(json_content) - mocked_signet + # 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 creds = TestCredentials2.default @@ -175,18 +179,19 @@ class TestCredentials3 < Google::Auth::Credentials 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| + mocked_signet = mock_signet + + allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) 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) + expect(options[:enable_self_signed_jwt]).to be_nil + expect(options[:target_audience]).to be_nil + expect(options[:json_key_io].read).to eq(test_json_env_val) - mocked_signet + # 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 creds = TestCredentials3.default @@ -204,25 +209,28 @@ class TestCredentials4 < Google::Auth::Credentials DEFAULT_PATHS = ["~/default/path/to/file.txt"].freeze end + json_content = JSON.generate default_keyfile_hash + allow(::ENV).to receive(:[]).with("GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS") { "true" } 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 } + allow(::File).to receive(:read).with("~/default/path/to/file.txt") { json_content } - 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| + mocked_signet = mock_signet + + allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) 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) + expect(options[:enable_self_signed_jwt]).to be_nil + expect(options[:target_audience]).to be_nil + expect(options[:json_key_io].read).to eq(json_content) - mocked_signet + # 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 creds = TestCredentials4.default @@ -310,18 +318,19 @@ class TestCredentials11 < Google::Auth::Credentials 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| + mocked_signet = mock_signet + + allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) 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) + expect(options[:enable_self_signed_jwt]).to be_nil + expect(options[:target_audience]).to be_nil + expect(options[:json_key_io].read).to eq(test_json_env_val) - mocked_signet + # 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 creds = TestCredentials11.default @@ -338,25 +347,28 @@ class TestCredentials12 < Google::Auth::Credentials self.paths = ["~/default/path/to/file.txt"] end + json_content = JSON.generate default_keyfile_hash + allow(::ENV).to receive(:[]).with("GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS") { "true" } 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 } + allow(::File).to receive(:read).with("/unknown/path/to/file.txt") { json_content } - 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| + mocked_signet = mock_signet + + allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) 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) + expect(options[:enable_self_signed_jwt]).to be_nil + expect(options[:target_audience]).to be_nil + expect(options[:json_key_io].read).to eq(json_content) - mocked_signet + # 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 creds = TestCredentials12.default @@ -382,18 +394,19 @@ class TestCredentials13 < Google::Auth::Credentials 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| + mocked_signet = mock_signet + + allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) 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) + expect(options[:enable_self_signed_jwt]).to be_nil + expect(options[:target_audience]).to be_nil + expect(options[:json_key_io].read).to eq(test_json_env_val) - mocked_signet + # 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 creds = TestCredentials13.default @@ -410,25 +423,28 @@ class TestCredentials14 < Google::Auth::Credentials self.paths = ["~/default/path/to/file.txt"] end + json_content = JSON.generate default_keyfile_hash + allow(::ENV).to receive(:[]).with("GOOGLE_AUTH_SUPPRESS_CREDENTIALS_WARNINGS") { "true" } 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 } + allow(::File).to receive(:read).with("~/default/path/to/file.txt") { json_content } - 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| + mocked_signet = mock_signet + + allow(Google::Auth::ServiceAccountCredentials).to receive(:make_creds) 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) + expect(options[:enable_self_signed_jwt]).to be_nil + expect(options[:target_audience]).to be_nil + expect(options[:json_key_io].read).to eq(json_content) - mocked_signet + # 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 creds = TestCredentials14.default