From f0b0c6f8e88928c278012a7884d14f69c0628ef4 Mon Sep 17 00:00:00 2001 From: Graham Paye Date: Wed, 24 Oct 2018 09:23:37 -0700 Subject: [PATCH] Add project_id instance variable (#167) --- CHANGELOG.md | 4 ++ lib/googleauth/credentials.rb | 8 +++ lib/googleauth/credentials_loader.rb | 28 +++++++--- lib/googleauth/json_key_reader.rb | 3 +- lib/googleauth/service_account.rb | 23 ++++++--- lib/googleauth/user_refresh.rb | 7 ++- lib/googleauth/version.rb | 2 +- spec/googleauth/credentials_spec.rb | 8 ++- .../get_application_default_spec.rb | 3 ++ spec/googleauth/service_account_spec.rb | 51 ++++++++++++++++++- spec/googleauth/user_refresh_spec.rb | 27 ++++++++++ 11 files changed, 142 insertions(+), 22 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 8ef096d..3e69b7e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,7 @@ +## 0.7.0 (2018/10/23) + +* Add project_id instance variable to UserRefreshCredentials, ServiceAccountCredentials, and Credentials. + ## 0.6.7 (2018/10/16) * Update memoist dependency to ~> 0.16. diff --git a/lib/googleauth/credentials.rb b/lib/googleauth/credentials.rb index 37bfb3e..98197c7 100644 --- a/lib/googleauth/credentials.rb +++ b/lib/googleauth/credentials.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. +# rubocop:disable Metrics/AbcSize, Metrics/PerceivedComplexity, MethodLength + require 'forwardable' require 'json' require 'signet/oauth_2/client' @@ -46,6 +48,7 @@ module Google DEFAULT_PATHS = [].freeze attr_accessor :client + attr_reader :project_id # Delegate client methods to the client object. extend Forwardable @@ -56,19 +59,24 @@ module Google def initialize(keyfile, options = {}) scope = options[:scope] verify_keyfile_provided! keyfile + @project_id = options['project_id'] || options['project'] if keyfile.is_a? Signet::OAuth2::Client @client = keyfile + @project_id ||= keyfile.project_id if keyfile.respond_to? :project_id elsif keyfile.is_a? Hash hash = stringify_hash_keys keyfile hash['scope'] ||= scope @client = init_client hash + @project_id ||= (hash['project_id'] || hash['project']) else verify_keyfile_exists! keyfile json = JSON.parse ::File.read(keyfile) json['scope'] ||= scope + @project_id ||= (json['project_id'] || json['project']) @client = init_client json end CredentialsLoader.warn_if_cloud_sdk_credentials @client.client_id + @project_id ||= CredentialsLoader.load_gcloud_project_id @client.fetch_access_token! end diff --git a/lib/googleauth/credentials_loader.rb b/lib/googleauth/credentials_loader.rb index fa91450..9d4aab8 100644 --- a/lib/googleauth/credentials_loader.rb +++ b/lib/googleauth/credentials_loader.rb @@ -39,14 +39,17 @@ module Google # credentials files on the file system. module CredentialsLoader extend Memoist - ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS'.freeze - - PRIVATE_KEY_VAR = 'GOOGLE_PRIVATE_KEY'.freeze - CLIENT_EMAIL_VAR = 'GOOGLE_CLIENT_EMAIL'.freeze - CLIENT_ID_VAR = 'GOOGLE_CLIENT_ID'.freeze - CLIENT_SECRET_VAR = 'GOOGLE_CLIENT_SECRET'.freeze - REFRESH_TOKEN_VAR = 'GOOGLE_REFRESH_TOKEN'.freeze - ACCOUNT_TYPE_VAR = 'GOOGLE_ACCOUNT_TYPE'.freeze + ENV_VAR = 'GOOGLE_APPLICATION_CREDENTIALS'.freeze + PRIVATE_KEY_VAR = 'GOOGLE_PRIVATE_KEY'.freeze + CLIENT_EMAIL_VAR = 'GOOGLE_CLIENT_EMAIL'.freeze + CLIENT_ID_VAR = 'GOOGLE_CLIENT_ID'.freeze + CLIENT_SECRET_VAR = 'GOOGLE_CLIENT_SECRET'.freeze + REFRESH_TOKEN_VAR = 'GOOGLE_REFRESH_TOKEN'.freeze + ACCOUNT_TYPE_VAR = 'GOOGLE_ACCOUNT_TYPE'.freeze + PROJECT_ID_VAR = 'GOOGLE_PROJECT_ID'.freeze + GCLOUD_POSIX_COMMAND = 'gcloud'.freeze + GCLOUD_WINDOWS_COMMAND = 'gcloud.cmd'.freeze + GCLOUD_CONFIG_COMMAND = 'config config-helper --format json'.freeze CREDENTIALS_FILE_NAME = 'application_default_credentials.json'.freeze NOT_FOUND_ERROR = @@ -136,6 +139,15 @@ module Google end module_function :warn_if_cloud_sdk_credentials + def load_gcloud_project_id + gcloud = GCLOUD_WINDOWS_COMMAND if OS.windows? + gcloud = GCLOUD_POSIX_COMMAND unless OS.windows? + config = MultiJson.load(`#{gcloud} #{GCLOUD_CONFIG_COMMAND}`) + config['configuration']['properties']['core']['project'] + rescue + warn 'Unable to determine project id.' + end + private def service_account_env_vars? diff --git a/lib/googleauth/json_key_reader.rb b/lib/googleauth/json_key_reader.rb index 8e1284f..0501355 100644 --- a/lib/googleauth/json_key_reader.rb +++ b/lib/googleauth/json_key_reader.rb @@ -38,7 +38,8 @@ module Google json_key = MultiJson.load(json_key_io.read) raise 'missing client_email' unless json_key.key?('client_email') raise 'missing private_key' unless json_key.key?('private_key') - [json_key['private_key'], json_key['client_email']] + project_id = json_key['project_id'] + [json_key['private_key'], json_key['client_email'], project_id] end end end diff --git a/lib/googleauth/service_account.rb b/lib/googleauth/service_account.rb index 8ca2ea4..3e799ce 100644 --- a/lib/googleauth/service_account.rb +++ b/lib/googleauth/service_account.rb @@ -50,6 +50,7 @@ module Google TOKEN_CRED_URI = 'https://www.googleapis.com/oauth2/v4/token'.freeze extend CredentialsLoader extend JsonKeyReader + attr_reader :project_id # Creates a ServiceAccountCredentials. # @@ -58,17 +59,20 @@ module Google def self.make_creds(options = {}) json_key_io, scope = options.values_at(:json_key_io, :scope) if json_key_io - private_key, client_email = read_json_key(json_key_io) + private_key, client_email, project_id = read_json_key(json_key_io) else private_key = unescape ENV[CredentialsLoader::PRIVATE_KEY_VAR] client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR] + project_id = ENV[CredentialsLoader::PROJECT_ID_VAR] end + project_id ||= self.class.load_gcloud_project_id new(token_credential_uri: TOKEN_CRED_URI, audience: TOKEN_CRED_URI, scope: scope, issuer: client_email, - signing_key: OpenSSL::PKey::RSA.new(private_key)) + signing_key: OpenSSL::PKey::RSA.new(private_key), + project_id: project_id) end # Handles certain escape sequences that sometimes appear in input. @@ -81,6 +85,7 @@ module Google end def initialize(options = {}) + @project_id = options[:project_id] super(options) end @@ -126,6 +131,7 @@ module Google EXPIRY = 60 extend CredentialsLoader extend JsonKeyReader + attr_reader :project_id # make_creds proxies the construction of a credentials instance # @@ -144,14 +150,15 @@ module Google def initialize(options = {}) json_key_io = options[:json_key_io] if json_key_io - private_key, client_email = self.class.read_json_key(json_key_io) + @private_key, @issuer, @project_id = + self.class.read_json_key(json_key_io) else - private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR] - client_email = ENV[CredentialsLoader::CLIENT_EMAIL_VAR] + @private_key = ENV[CredentialsLoader::PRIVATE_KEY_VAR] + @issuer = ENV[CredentialsLoader::CLIENT_EMAIL_VAR] + @project_id = ENV[CredentialsLoader::PROJECT_ID_VAR] end - @private_key = private_key - @issuer = client_email - @signing_key = OpenSSL::PKey::RSA.new(private_key) + @project_id ||= self.class.load_gcloud_project_id + @signing_key = OpenSSL::PKey::RSA.new(@private_key) end # Construct a jwt token if the JWT_AUD_URI key is present in the input diff --git a/lib/googleauth/user_refresh.rb b/lib/googleauth/user_refresh.rb index 090e43a..aeb1a4d 100644 --- a/lib/googleauth/user_refresh.rb +++ b/lib/googleauth/user_refresh.rb @@ -50,6 +50,7 @@ module Google AUTHORIZATION_URI = 'https://accounts.google.com/o/oauth2/auth'.freeze REVOKE_TOKEN_URI = 'https://oauth2.googleapis.com/revoke'.freeze extend CredentialsLoader + attr_reader :project_id # Create a UserRefreshCredentials. # @@ -61,13 +62,15 @@ module Google user_creds ||= { 'client_id' => ENV[CredentialsLoader::CLIENT_ID_VAR], 'client_secret' => ENV[CredentialsLoader::CLIENT_SECRET_VAR], - 'refresh_token' => ENV[CredentialsLoader::REFRESH_TOKEN_VAR] + 'refresh_token' => ENV[CredentialsLoader::REFRESH_TOKEN_VAR], + 'project_id' => ENV[CredentialsLoader::PROJECT_ID_VAR] } new(token_credential_uri: TOKEN_CRED_URI, client_id: user_creds['client_id'], client_secret: user_creds['client_secret'], refresh_token: user_creds['refresh_token'], + project_id: user_creds['project_id'], scope: scope) end @@ -86,6 +89,8 @@ module Google options ||= {} options[:token_credential_uri] ||= TOKEN_CRED_URI options[:authorization_uri] ||= AUTHORIZATION_URI + @project_id = options[:project_id] + @project_id ||= self.class.load_gcloud_project_id super(options) end diff --git a/lib/googleauth/version.rb b/lib/googleauth/version.rb index dfca2a0..b348104 100644 --- a/lib/googleauth/version.rb +++ b/lib/googleauth/version.rb @@ -31,6 +31,6 @@ module Google # Module Auth provides classes that provide Google-specific authorization # used to access Google APIs. module Auth - VERSION = '0.6.7'.freeze + VERSION = '0.7.0'.freeze end end diff --git a/spec/googleauth/credentials_spec.rb b/spec/googleauth/credentials_spec.rb index d1f518c..759b8b5 100644 --- a/spec/googleauth/credentials_spec.rb +++ b/spec/googleauth/credentials_spec.rb @@ -40,7 +40,8 @@ describe Google::Auth::Credentials, :private do 'private_key' => "-----BEGIN RSA PRIVATE KEY-----\nMIIBOwIBAAJBAOyi0Hy1l4Ym2m2o71Q0TF4O9E81isZEsX0bb+Bqz1SXEaSxLiXM\nUZE8wu0eEXivXuZg6QVCW/5l+f2+9UPrdNUCAwEAAQJAJkqubA/Chj3RSL92guy3\nktzeodarLyw8gF8pOmpuRGSiEo/OLTeRUMKKD1/kX4f9sxf3qDhB4e7dulXR1co/\nIQIhAPx8kMW4XTTL6lJYd2K5GrH8uBMp8qL5ya3/XHrBgw3dAiEA7+3Iw3ULTn2I\n1J34WlJ2D5fbzMzB4FAHUNEV7Ys3f1kCIQDtUahCMChrl7+H5t9QS+xrn77lRGhs\nB50pjvy95WXpgQIhAI2joW6JzTfz8fAapb+kiJ/h9Vcs1ZN3iyoRlNFb61JZAiA8\nNy5NyNrMVwtB/lfJf1dAK/p/Bwd8LZLtgM6PapRfgw==\n-----END RSA PRIVATE KEY-----\n", 'client_email' => 'credz-testabc1234567890xyz@developer.gserviceaccount.com', 'client_id' => 'credz-testabc1234567890xyz.apps.googleusercontent.com', - 'type' => 'service_account' + 'type' => 'service_account', + 'project_id' => 'a_project_id' } end @@ -110,6 +111,7 @@ describe Google::Auth::Credentials, :private do 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 PATH_ENV_VARS to get keyfile path' do @@ -142,6 +144,7 @@ describe Google::Auth::Credentials, :private do 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 @@ -173,6 +176,7 @@ describe Google::Auth::Credentials, :private do 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 DEFAULT_PATHS to get keyfile path' do @@ -205,6 +209,7 @@ describe Google::Auth::Credentials, :private do 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 @@ -243,6 +248,7 @@ describe Google::Auth::Credentials, :private do 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 diff --git a/spec/googleauth/get_application_default_spec.rb b/spec/googleauth/get_application_default_spec.rb index d1cf6ba..9ec1dfa 100644 --- a/spec/googleauth/get_application_default_spec.rb +++ b/spec/googleauth/get_application_default_spec.rb @@ -35,6 +35,7 @@ require 'faraday' require 'fakefs/safe' require 'googleauth' require 'spec_helper' +require 'os' describe '#get_application_default' do # Pass unique options each time to bypass memoization @@ -173,6 +174,7 @@ describe '#get_application_default' do ENV[CLIENT_SECRET_VAR] = cred_json[:client_secret] ENV[REFRESH_TOKEN_VAR] = cred_json[:refresh_token] ENV[ACCOUNT_TYPE_VAR] = cred_json[:type] + ENV[PROJECT_ID_VAR] = 'a_project_id' expect { Google::Auth.get_application_default @scope, options }.to output( Google::Auth::CredentialsLoader::CLOUD_SDK_CREDENTIALS_WARNING + "\n" ).to_stderr @@ -260,6 +262,7 @@ describe '#get_application_default' do end it 'fails if env vars are set' do + ENV[ENV_VAR] = nil ENV[PRIVATE_KEY_VAR] = cred_json[:private_key] ENV[CLIENT_EMAIL_VAR] = cred_json[:client_email] expect do diff --git a/spec/googleauth/service_account_spec.rb b/spec/googleauth/service_account_spec.rb index ef86aa0..1652a2b 100644 --- a/spec/googleauth/service_account_spec.rb +++ b/spec/googleauth/service_account_spec.rb @@ -116,7 +116,8 @@ describe Google::Auth::ServiceAccountCredentials do private_key: @key.to_pem, client_email: client_email, client_id: 'app.apps.googleusercontent.com', - type: 'service_account' + type: 'service_account', + project_id: 'a_project_id' } end @@ -213,6 +214,15 @@ describe Google::Auth::ServiceAccountCredentials do expect(@clz.from_env(@scope)).to_not be_nil end + it 'sets project_id when the PROJECT_ID_VAR env var is set' do + ENV[PRIVATE_KEY_VAR] = cred_json[:private_key] + ENV[CLIENT_EMAIL_VAR] = cred_json[:client_email] + ENV[PROJECT_ID_VAR] = cred_json[:project_id] + ENV[ENV_VAR] = nil + credentials = @clz.from_env(@scope) + expect(credentials.project_id).to eq(cred_json[:project_id]) + end + it 'succeeds when GOOGLE_PRIVATE_KEY is escaped' do escaped_key = cred_json[:private_key].gsub "\n", '\n' ENV[PRIVATE_KEY_VAR] = "\"#{escaped_key}\"" @@ -251,6 +261,19 @@ describe Google::Auth::ServiceAccountCredentials do expect(@clz.from_well_known_path(@scope)).to_not be_nil end end + + it 'successfully sets project_id when file is present' do + Dir.mktmpdir do |dir| + key_path = File.join(dir, '.config', @known_path) + key_path = File.join(dir, WELL_KNOWN_PATH) if OS.windows? + FileUtils.mkdir_p(File.dirname(key_path)) + File.write(key_path, cred_json_text) + ENV['HOME'] = dir + ENV['APPDATA'] = dir + credentials = @clz.from_well_known_path(@scope) + expect(credentials.project_id).to eq(cred_json[:project_id]) + end + end end describe '#from_system_default_path' do @@ -297,7 +320,8 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do private_key: @key.to_pem, client_email: client_email, client_id: 'app.apps.googleusercontent.com', - type: 'service_account' + type: 'service_account', + project_id: 'a_project_id' } end @@ -358,6 +382,16 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do ENV[CLIENT_EMAIL_VAR] = cred_json[:client_email] expect(clz.from_env(@scope)).to_not be_nil end + + it 'sets project_id when the PROJECT_ID_VAR env var is set' do + ENV[PRIVATE_KEY_VAR] = cred_json[:private_key] + ENV[CLIENT_EMAIL_VAR] = cred_json[:client_email] + ENV[PROJECT_ID_VAR] = cred_json[:project_id] + ENV[ENV_VAR] = nil + credentials = clz.from_env(@scope) + expect(credentials).to_not be_nil + expect(credentials.project_id).to eq(cred_json[:project_id]) + end end describe '#from_well_known_path' do @@ -387,5 +421,18 @@ describe Google::Auth::ServiceAccountJwtHeaderCredentials do expect(clz.from_well_known_path).to_not be_nil end end + + it 'successfully sets project_id when file is present' do + Dir.mktmpdir do |dir| + key_path = File.join(dir, '.config', WELL_KNOWN_PATH) + key_path = File.join(dir, WELL_KNOWN_PATH) if OS.windows? + FileUtils.mkdir_p(File.dirname(key_path)) + File.write(key_path, cred_json_text) + ENV['HOME'] = dir + ENV['APPDATA'] = dir + credentials = clz.from_well_known_path(@scope) + expect(credentials.project_id).to eq(cred_json[:project_id]) + end + end end end diff --git a/spec/googleauth/user_refresh_spec.rb b/spec/googleauth/user_refresh_spec.rb index 17068cf..fbb4443 100644 --- a/spec/googleauth/user_refresh_spec.rb +++ b/spec/googleauth/user_refresh_spec.rb @@ -94,6 +94,7 @@ describe Google::Auth::UserRefreshCredentials do @credential_vars.each { |var| @original_env_vals[var] = ENV[var] } @scope = 'https://www.googleapis.com/auth/userinfo.profile' @clz = UserRefreshCredentials + @project_id = 'a_project_id' end after(:example) do @@ -140,6 +141,7 @@ describe Google::Auth::UserRefreshCredentials do it 'succeeds when GOOGLE_CLIENT_ID, GOOGLE_CLIENT_SECRET, and '\ 'GOOGLE_REFRESH_TOKEN env vars are valid' do + ENV[ENV_VAR] = nil ENV[CLIENT_ID_VAR] = cred_json[:client_id] ENV[CLIENT_SECRET_VAR] = cred_json[:client_secret] ENV[REFRESH_TOKEN_VAR] = cred_json[:refresh_token] @@ -150,6 +152,17 @@ describe Google::Auth::UserRefreshCredentials do expect(creds.client_secret).to eq(cred_json[:client_secret]) expect(creds.refresh_token).to eq(cred_json[:refresh_token]) end + + it 'sets project_id when the PROJECT_ID_VAR env var is set' do + ENV[ENV_VAR] = nil + ENV[CLIENT_ID_VAR] = cred_json[: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] + ENV[PROJECT_ID_VAR] = @project_id + creds = @clz.from_env(@scope) + expect(creds.project_id).to eq(@project_id) + end end describe '#from_well_known_path' do @@ -198,6 +211,20 @@ describe Google::Auth::UserRefreshCredentials do expect(@clz.from_well_known_path(@scope)).to_not be_nil end end + + it 'checks gcloud config for project_id if none was provided' do + Dir.mktmpdir do |dir| + key_path = File.join(dir, '.config', @known_path) + key_path = File.join(dir, @known_path) if OS.windows? + FileUtils.mkdir_p(File.dirname(key_path)) + File.write(key_path, cred_json_text) + ENV['HOME'] = dir + ENV['APPDATA'] = dir + ENV[PROJECT_ID_VAR] = nil + expect(@clz).to receive(:load_gcloud_project_id).with(no_args) + @clz.from_well_known_path(@scope) + end + end end describe '#from_system_default_path' do