Warn when using cloud sdk credentials (#145)

* Issue warning when cloud sdk credentials are used.
This commit is contained in:
Graham Paye 2018-07-18 13:54:03 -07:00 committed by GitHub
parent 85808dbaf6
commit 5d42d3b4be
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 96 additions and 25 deletions

View File

@ -13,7 +13,7 @@ Metrics/MethodLength:
Max: 20 Max: 20
Metrics/ClassLength: Metrics/ClassLength:
Enabled: false Enabled: false
Style/IndentHeredoc: Layout/IndentHeredoc:
Enabled: false Enabled: false
Style/FormatString: Style/FormatString:
Enabled: false Enabled: false

View File

@ -28,6 +28,7 @@
# OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. # OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
require 'multi_json' require 'multi_json'
require 'googleauth/credentials_loader'
module Google module Google
module Auth module Auth
@ -63,13 +64,14 @@ module Google
# & secrets in source. See {#from_file} to load from # & secrets in source. See {#from_file} to load from
# `client_secrets.json` files. # `client_secrets.json` files.
def initialize(id, secret) def initialize(id, secret)
CredentialsLoader.warn_if_cloud_sdk_credentials id
raise 'Client id can not be nil' if id.nil? raise 'Client id can not be nil' if id.nil?
raise 'Client secret can not be nil' if secret.nil? raise 'Client secret can not be nil' if secret.nil?
@id = id @id = id
@secret = secret @secret = secret
end end
# Constructs a Client ID from a JSON file downloaed from the # Constructs a Client ID from a JSON file downloaded from the
# Google Developers Console. # Google Developers Console.
# #
# @param [String, File] file # @param [String, File] file
@ -79,7 +81,7 @@ module Google
raise 'File can not be nil.' if file.nil? raise 'File can not be nil.' if file.nil?
File.open(file.to_s) do |f| File.open(file.to_s) do |f|
json = f.read json = f.read
config = MultiJson.load(json) config = MultiJson.load json
from_hash(config) from_hash(config)
end end
end end

View File

@ -31,7 +31,7 @@ require 'forwardable'
require 'json' require 'json'
require 'signet/oauth_2/client' require 'signet/oauth_2/client'
require 'googleauth/default_credentials' require 'googleauth/credentials_loader'
module Google module Google
module Auth module Auth
@ -68,6 +68,7 @@ module Google
json['scope'] ||= scope json['scope'] ||= scope
@client = init_client json @client = init_client json
end end
CredentialsLoader.warn_if_cloud_sdk_credentials @client.client_id
@client.fetch_access_token! @client.fetch_access_token!
end end
@ -78,16 +79,16 @@ module Google
def self.default(options = {}) def self.default(options = {})
scope = options[:scope] scope = options[:scope]
# First try to find keyfile file from environment variables. # First try to find keyfile file from environment variables.
client = from_path_vars(scope) client = from_path_vars scope
# Second try to find keyfile json from environment variables. # Second try to find keyfile json from environment variables.
client ||= from_json_vars(scope) client ||= from_json_vars scope
# Third try to find keyfile file from known file paths. # Third try to find keyfile file from known file paths.
client ||= from_default_paths(scope) client ||= from_default_paths scope
# Finally get instantiated client from Google::Auth # Finally get instantiated client from Google::Auth
client ||= from_application_default(scope) client ||= from_application_default scope
client client
end end

View File

@ -57,6 +57,17 @@ module Google
SYSTEM_DEFAULT_ERROR = SYSTEM_DEFAULT_ERROR =
'Unable to read the system default credential file'.freeze 'Unable to read the system default credential file'.freeze
CLOUD_SDK_CLIENT_ID = '764086051850-6qr4p6gpi6hn506pt8ejuq83di341hur.app'\
's.googleusercontent.com'.freeze
CLOUD_SDK_CREDENTIALS_WARNING = 'Your application has authenticated '\
'using end user credentials from Google Cloud SDK. We recommend that '\
'most server applications use service accounts instead. If your '\
'application continues to use end user credentials from Cloud SDK, '\
'you might receive a "quota exceeded" or "API not enabled" error. For'\
' more information about service accounts, see '\
'https://cloud.google.com/docs/authentication/.'.freeze
# make_creds proxies the construction of a credentials instance # make_creds proxies the construction of a credentials instance
# #
# By default, it calls #new on the current class, but this behaviour can # By default, it calls #new on the current class, but this behaviour can
@ -119,6 +130,11 @@ module Google
raise "#{SYSTEM_DEFAULT_ERROR}: #{e}" raise "#{SYSTEM_DEFAULT_ERROR}: #{e}"
end end
# Issues warning if cloud sdk client id is used
def warn_if_cloud_sdk_credentials(client_id)
warn CLOUD_SDK_CREDENTIALS_WARNING if client_id == CLOUD_SDK_CLIENT_ID
end
private private
def service_account_env_vars? def service_account_env_vars?

View File

@ -49,9 +49,11 @@ module Google
json_key_io, scope = options.values_at(:json_key_io, :scope) json_key_io, scope = options.values_at(:json_key_io, :scope)
if json_key_io if json_key_io
json_key, clz = determine_creds_class(json_key_io) json_key, clz = determine_creds_class(json_key_io)
warn_if_cloud_sdk_credentials json_key['client_id']
clz.make_creds(json_key_io: StringIO.new(MultiJson.dump(json_key)), clz.make_creds(json_key_io: StringIO.new(MultiJson.dump(json_key)),
scope: scope) scope: scope)
else else
warn_if_cloud_sdk_credentials ENV[CLIENT_ID_VAR]
clz = read_creds clz = read_creds
clz.make_creds(scope: scope) clz.make_creds(scope: scope)
end end
@ -73,7 +75,7 @@ module Google
# Reads the input json and determines which creds class to use. # Reads the input json and determines which creds class to use.
def self.determine_creds_class(json_key_io) def self.determine_creds_class(json_key_io)
json_key = MultiJson.load(json_key_io.read) json_key = MultiJson.load json_key_io.read
key = 'type' key = 'type'
raise "the json is missing the '#{key}' field" unless json_key.key?(key) raise "the json is missing the '#{key}' field" unless json_key.key?(key)
type = json_key[key] type = json_key[key]

View File

@ -48,7 +48,7 @@ describe Google::Auth::ClientId do
shared_examples 'it can successfully load client_id' do shared_examples 'it can successfully load client_id' do
context 'loaded from hash' do context 'loaded from hash' do
let(:client_id) { Google::Auth::ClientId.from_hash(config) } let(:client_id) { Google::Auth::ClientId.from_hash config }
it_behaves_like 'it has a valid config' it_behaves_like 'it has a valid config'
end end
@ -103,7 +103,7 @@ describe Google::Auth::ClientId do
end end
it 'should raise error' do it 'should raise error' do
expect { Google::Auth::ClientId.from_hash(config) }.to raise_error( expect { Google::Auth::ClientId.from_hash config }.to raise_error(
/Expected top level property/ /Expected top level property/
) )
end end
@ -119,7 +119,7 @@ describe Google::Auth::ClientId do
end end
it 'should raise error' do it 'should raise error' do
expect { Google::Auth::ClientId.from_hash(config) }.to raise_error( expect { Google::Auth::ClientId.from_hash config }.to raise_error(
/Client id can not be nil/ /Client id can not be nil/
) )
end end
@ -135,9 +135,26 @@ describe Google::Auth::ClientId do
end end
it 'should raise error' do it 'should raise error' do
expect { Google::Auth::ClientId.from_hash(config) }.to raise_error( expect { Google::Auth::ClientId.from_hash config }.to raise_error(
/Client secret can not be nil/ /Client secret can not be nil/
) )
end end
end end
context 'with cloud sdk credentials' do
let(:config) do
{
'web' => {
'client_id' => Google::Auth::CredentialsLoader::CLOUD_SDK_CLIENT_ID,
'client_secret' => 'notasecret'
}
}
end
it 'should raise warning' do
expect { Google::Auth::ClientId.from_hash config }.to output(
Google::Auth::CredentialsLoader::CLOUD_SDK_CREDENTIALS_WARNING + "\n"
).to_stderr
end
end
end end

View File

@ -29,6 +29,7 @@
require 'googleauth' require 'googleauth'
# This test is testing the private class Google::Auth::Credentials. We want to # This test is testing the private class Google::Auth::Credentials. We want to
# make sure that the passed in scope propogates to the Signet object. This means # make sure that the passed in scope propogates to the Signet object. This means
# testing the private API, which is generally frowned on. # testing the private API, which is generally frowned on.
@ -46,6 +47,7 @@ describe Google::Auth::Credentials, :private do
it 'uses a default scope' do it 'uses a default scope' do
mocked_signet = double('Signet::OAuth2::Client') mocked_signet = double('Signet::OAuth2::Client')
allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) 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| allow(Signet::OAuth2::Client).to receive(:new) do |options|
expect(options[:token_credential_uri]).to eq('https://accounts.google.com/o/oauth2/token') expect(options[:token_credential_uri]).to eq('https://accounts.google.com/o/oauth2/token')
expect(options[:audience]).to eq('https://accounts.google.com/o/oauth2/token') expect(options[:audience]).to eq('https://accounts.google.com/o/oauth2/token')
@ -62,6 +64,7 @@ describe Google::Auth::Credentials, :private do
it 'uses a custom scope' do it 'uses a custom scope' do
mocked_signet = double('Signet::OAuth2::Client') mocked_signet = double('Signet::OAuth2::Client')
allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) 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| allow(Signet::OAuth2::Client).to receive(:new) do |options|
expect(options[:token_credential_uri]).to eq('https://accounts.google.com/o/oauth2/token') expect(options[:token_credential_uri]).to eq('https://accounts.google.com/o/oauth2/token')
expect(options[:audience]).to eq('https://accounts.google.com/o/oauth2/token') expect(options[:audience]).to eq('https://accounts.google.com/o/oauth2/token')
@ -93,6 +96,7 @@ describe Google::Auth::Credentials, :private do
mocked_signet = double('Signet::OAuth2::Client') mocked_signet = double('Signet::OAuth2::Client')
allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) 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| allow(Signet::OAuth2::Client).to receive(:new) do |options|
expect(options[:token_credential_uri]).to eq('https://accounts.google.com/o/oauth2/token') expect(options[:token_credential_uri]).to eq('https://accounts.google.com/o/oauth2/token')
expect(options[:audience]).to eq('https://accounts.google.com/o/oauth2/token') expect(options[:audience]).to eq('https://accounts.google.com/o/oauth2/token')
@ -124,6 +128,7 @@ describe Google::Auth::Credentials, :private do
mocked_signet = double('Signet::OAuth2::Client') mocked_signet = double('Signet::OAuth2::Client')
allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) 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| allow(Signet::OAuth2::Client).to receive(:new) do |options|
expect(options[:token_credential_uri]).to eq('https://accounts.google.com/o/oauth2/token') expect(options[:token_credential_uri]).to eq('https://accounts.google.com/o/oauth2/token')
expect(options[:audience]).to eq('https://accounts.google.com/o/oauth2/token') expect(options[:audience]).to eq('https://accounts.google.com/o/oauth2/token')
@ -154,6 +159,7 @@ describe Google::Auth::Credentials, :private do
mocked_signet = double('Signet::OAuth2::Client') mocked_signet = double('Signet::OAuth2::Client')
allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) 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| allow(Signet::OAuth2::Client).to receive(:new) do |options|
expect(options[:token_credential_uri]).to eq('https://accounts.google.com/o/oauth2/token') expect(options[:token_credential_uri]).to eq('https://accounts.google.com/o/oauth2/token')
expect(options[:audience]).to eq('https://accounts.google.com/o/oauth2/token') expect(options[:audience]).to eq('https://accounts.google.com/o/oauth2/token')
@ -185,6 +191,7 @@ describe Google::Auth::Credentials, :private do
mocked_signet = double('Signet::OAuth2::Client') mocked_signet = double('Signet::OAuth2::Client')
allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) 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| allow(Signet::OAuth2::Client).to receive(:new) do |options|
expect(options[:token_credential_uri]).to eq('https://accounts.google.com/o/oauth2/token') expect(options[:token_credential_uri]).to eq('https://accounts.google.com/o/oauth2/token')
expect(options[:audience]).to eq('https://accounts.google.com/o/oauth2/token') expect(options[:audience]).to eq('https://accounts.google.com/o/oauth2/token')
@ -215,6 +222,7 @@ describe Google::Auth::Credentials, :private do
mocked_signet = double('Signet::OAuth2::Client') mocked_signet = double('Signet::OAuth2::Client')
allow(mocked_signet).to receive(:fetch_access_token!).and_return(true) 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| allow(Google::Auth).to receive(:get_application_default) do |scope|
expect(scope).to eq(TestCredentials::SCOPE) expect(scope).to eq(TestCredentials::SCOPE)
@ -236,4 +244,16 @@ describe Google::Auth::Credentials, :private do
expect(creds).to be_a_kind_of(TestCredentials) expect(creds).to be_a_kind_of(TestCredentials)
expect(creds.client).to eq(mocked_signet) expect(creds.client).to eq(mocked_signet)
end end
it 'warns when cloud sdk credentials are used' do
mocked_signet = double('Signet::OAuth2::Client')
allow(mocked_signet).to receive(:fetch_access_token!).and_return(true)
allow(Signet::OAuth2::Client).to receive(:new) do |options|
mocked_signet
end
allow(mocked_signet).to receive(:client_id).and_return(Google::Auth::CredentialsLoader::CLOUD_SDK_CLIENT_ID)
expect { Google::Auth::Credentials.new default_keyfile_hash }.to output(
Google::Auth::CredentialsLoader::CLOUD_SDK_CREDENTIALS_WARNING + "\n"
).to_stderr
end
end end

View File

@ -63,7 +63,7 @@ describe '#get_application_default' do
Dir.mktmpdir do |dir| Dir.mktmpdir do |dir|
key_path = File.join(dir, 'does-not-exist') key_path = File.join(dir, 'does-not-exist')
ENV[@var_name] = key_path ENV[@var_name] = key_path
expect { Google::Auth.get_application_default(@scope, options) } expect { Google::Auth.get_application_default @scope, options }
.to raise_error RuntimeError .to raise_error RuntimeError
end end
end end
@ -76,7 +76,7 @@ describe '#get_application_default' do
ENV.delete(@var_name) unless ENV[@var_name].nil? # no env var ENV.delete(@var_name) unless ENV[@var_name].nil? # no env var
ENV['HOME'] = dir # no config present in this tmp dir ENV['HOME'] = dir # no config present in this tmp dir
expect do expect do
Google::Auth.get_application_default(@scope, options) Google::Auth.get_application_default @scope, options
end.to raise_error RuntimeError end.to raise_error RuntimeError
end end
expect(stub).to have_been_requested expect(stub).to have_been_requested
@ -90,7 +90,7 @@ describe '#get_application_default' do
FileUtils.mkdir_p(File.dirname(key_path)) FileUtils.mkdir_p(File.dirname(key_path))
File.write(key_path, cred_json_text) File.write(key_path, cred_json_text)
ENV[@var_name] = key_path ENV[@var_name] = key_path
expect(Google::Auth.get_application_default(@scope, options)) expect(Google::Auth.get_application_default @scope, options)
.to_not be_nil .to_not be_nil
end end
end end
@ -102,7 +102,7 @@ describe '#get_application_default' do
FileUtils.mkdir_p(File.dirname(key_path)) FileUtils.mkdir_p(File.dirname(key_path))
File.write(key_path, cred_json_text) File.write(key_path, cred_json_text)
ENV['HOME'] = dir ENV['HOME'] = dir
expect(Google::Auth.get_application_default(@scope, options)) expect(Google::Auth.get_application_default @scope, options)
.to_not be_nil .to_not be_nil
end end
end end
@ -114,7 +114,7 @@ describe '#get_application_default' do
FileUtils.mkdir_p(File.dirname(key_path)) FileUtils.mkdir_p(File.dirname(key_path))
File.write(key_path, cred_json_text) File.write(key_path, cred_json_text)
ENV['HOME'] = dir ENV['HOME'] = dir
expect(Google::Auth.get_application_default(nil, options)).to_not be_nil expect(Google::Auth.get_application_default nil, options).to_not be_nil
end end
end end
@ -125,7 +125,7 @@ describe '#get_application_default' do
Dir.mktmpdir do |dir| Dir.mktmpdir do |dir|
ENV.delete(@var_name) unless ENV[@var_name].nil? # no env var ENV.delete(@var_name) unless ENV[@var_name].nil? # no env var
ENV['HOME'] = dir # no config present in this tmp dir ENV['HOME'] = dir # no config present in this tmp dir
creds = Google::Auth.get_application_default(@scope, options) creds = Google::Auth.get_application_default @scope, options
expect(creds).to_not be_nil expect(creds).to_not be_nil
end end
expect(stub).to have_been_requested expect(stub).to have_been_requested
@ -137,7 +137,7 @@ describe '#get_application_default' do
key_path = File.join('/etc/google/auth/', CREDENTIALS_FILE_NAME) key_path = File.join('/etc/google/auth/', CREDENTIALS_FILE_NAME)
FileUtils.mkdir_p(File.dirname(key_path)) FileUtils.mkdir_p(File.dirname(key_path))
File.write(key_path, cred_json_text) File.write(key_path, cred_json_text)
expect(Google::Auth.get_application_default(@scope, options)) expect(Google::Auth.get_application_default @scope, options)
.to_not be_nil .to_not be_nil
File.delete(key_path) File.delete(key_path)
end end
@ -151,9 +151,22 @@ describe '#get_application_default' do
ENV[CLIENT_SECRET_VAR] = cred_json[:client_secret] ENV[CLIENT_SECRET_VAR] = cred_json[:client_secret]
ENV[REFRESH_TOKEN_VAR] = cred_json[:refresh_token] ENV[REFRESH_TOKEN_VAR] = cred_json[:refresh_token]
ENV[ACCOUNT_TYPE_VAR] = cred_json[:type] ENV[ACCOUNT_TYPE_VAR] = cred_json[:type]
expect(Google::Auth.get_application_default(@scope, options)) expect(Google::Auth.get_application_default @scope, options)
.to_not be_nil .to_not be_nil
end end
it 'warns when using cloud sdk credentials' do
ENV.delete(@var_name) unless ENV[@var_name].nil? # no env var
ENV[PRIVATE_KEY_VAR] = cred_json[:private_key]
ENV[CLIENT_EMAIL_VAR] = cred_json[:client_email]
ENV[CLIENT_ID_VAR] = Google::Auth::CredentialsLoader::CLOUD_SDK_CLIENT_ID
ENV[CLIENT_SECRET_VAR] = cred_json[:client_secret]
ENV[REFRESH_TOKEN_VAR] = cred_json[:refresh_token]
ENV[ACCOUNT_TYPE_VAR] = cred_json[:type]
expect { Google::Auth.get_application_default @scope, options }.to output(
Google::Auth::CredentialsLoader::CLOUD_SDK_CREDENTIALS_WARNING + "\n"
).to_stderr
end
end end
describe 'when credential type is service account' do describe 'when credential type is service account' do
@ -216,7 +229,7 @@ describe '#get_application_default' do
File.write(key_path, cred_json_text) File.write(key_path, cred_json_text)
ENV[@var_name] = key_path ENV[@var_name] = key_path
expect do expect do
Google::Auth.get_application_default(@scope, options) Google::Auth.get_application_default @scope, options
end.to raise_error RuntimeError end.to raise_error RuntimeError
end end
end end
@ -229,7 +242,7 @@ describe '#get_application_default' do
File.write(key_path, cred_json_text) File.write(key_path, cred_json_text)
ENV['HOME'] = dir ENV['HOME'] = dir
expect do expect do
Google::Auth.get_application_default(@scope, options) Google::Auth.get_application_default @scope, options
end.to raise_error RuntimeError end.to raise_error RuntimeError
end end
end end
@ -238,7 +251,7 @@ describe '#get_application_default' do
ENV[PRIVATE_KEY_VAR] = cred_json[:private_key] ENV[PRIVATE_KEY_VAR] = cred_json[:private_key]
ENV[CLIENT_EMAIL_VAR] = cred_json[:client_email] ENV[CLIENT_EMAIL_VAR] = cred_json[:client_email]
expect do expect do
Google::Auth.get_application_default(@scope, options) Google::Auth.get_application_default @scope, options
end.to raise_error RuntimeError end.to raise_error RuntimeError
end end
end end

View File

@ -294,7 +294,7 @@ describe Google::Auth::UserRefreshCredentials do
end end
end end
describe 'when erros occurred with request' do describe 'when errors occurred with request' do
it 'should fail with Signet::AuthorizationError if request times out' do it 'should fail with Signet::AuthorizationError if request times out' do
allow_any_instance_of(Faraday::Connection).to receive(:get) allow_any_instance_of(Faraday::Connection).to receive(:get)
.and_raise(Faraday::TimeoutError) .and_raise(Faraday::TimeoutError)