From 4fa47206dbd62f8bbdd1b9d3721f6baee9fd1d62 Mon Sep 17 00:00:00 2001 From: Daniel Azuma Date: Fri, 22 Jan 2021 13:29:59 -0800 Subject: [PATCH] feat: Credential parameters inherit from superclasses --- lib/googleauth/credentials.rb | 121 ++++++++++++++++++++++------ spec/googleauth/credentials_spec.rb | 47 +++++++++++ 2 files changed, 142 insertions(+), 26 deletions(-) diff --git a/lib/googleauth/credentials.rb b/lib/googleauth/credentials.rb index 531f0f5c..9739882a 100644 --- a/lib/googleauth/credentials.rb +++ b/lib/googleauth/credentials.rb @@ -36,8 +36,45 @@ module Google module Auth ## - # Credentials is responsible for representing the authentication when connecting to an API. This - # class is also intended to be inherited by API-specific classes. + # Credentials is a high-level base class used by Google's API client + # libraries to represent the authentication when connecting to an API. + # In most cases, it is subclassed by API-specific credential classes that + # can be instantiated by clients. + # + # ## Options + # + # Credentials classes are configured with options that dictate default + # values for parameters such as scope and audience. These defaults are + # expressed as class attributes, and may differ from endpoint to endpoint. + # Normally, an API client will provide subclasses specific to each + # endpoint, configured with appropriate values. + # + # Note that these options inherit up the class hierarchy. If a particular + # options is not set for a subclass, its superclass is queried. + # + # Some older users of this class set options via constants. This usage is + # deprecated. For example, instead of setting the `AUDIENCE` constant on + # your subclass, call the `audience=` method. + # + # ## Example + # + # class MyCredentials < Google::Auth::Credentials + # # Set the default scope for these credentials + # self.scope = "http://example.com/my_scope" + # end + # + # # creds is a credentials object suitable for Google API clients + # creds = MyCredentials.default + # creds.scope # => ["http://example.com/my_scope"] + # + # class SubCredentials < MyCredentials + # # Override the default scope for this subclass + # self.scope = "http://example.com/sub_scope" + # end + # + # creds2 = SubCredentials.default + # creds2.scope # => ["http://example.com/sub_scope"] + # class Credentials ## # The default token credential URI to be used when none is provided during initialization. @@ -47,7 +84,7 @@ class Credentials # The default target audience ID to be used when none is provided during initialization. AUDIENCE = "https://oauth2.googleapis.com/token".freeze - @audience = @scope = @target_audience = @env_vars = @paths = nil + @audience = @scope = @target_audience = @env_vars = @paths = @token_credential_uri = nil ## # The default token credential URI to be used when none is provided during initialization. @@ -57,9 +94,9 @@ class Credentials # @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 + lookup_auth_param :token_credential_uri do + lookup_local_constant :TOKEN_CREDENTIAL_URI + end end ## @@ -79,9 +116,9 @@ def self.token_credential_uri= new_token_credential_uri # @return [String] # def self.audience - return @audience unless @audience.nil? - - const_get :AUDIENCE if const_defined? :AUDIENCE + lookup_auth_param :audience do + lookup_local_constant :AUDIENCE + end end ## @@ -106,9 +143,10 @@ def self.audience= new_audience # @return [String, Array] # def self.scope - return @scope unless @scope.nil? - - Array(const_get(:SCOPE)).flatten.uniq if const_defined? :SCOPE + lookup_auth_param :scope do + vals = lookup_local_constant :SCOPE + vals ? Array(vals).flatten.uniq : nil + end end ## @@ -137,7 +175,7 @@ def self.scope= new_scope # @return [String] # def self.target_audience - @target_audience + lookup_auth_param :target_audience end ## @@ -161,13 +199,12 @@ def self.target_audience= new_target_audience # @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 + lookup_auth_param :env_vars do + # Pull values when PATH_ENV_VARS or JSON_ENV_VARS constants exists. + path_env_vars = lookup_local_constant :PATH_ENV_VARS + json_env_vars = lookup_local_constant :JSON_ENV_VARS + (Array(path_env_vars) + Array(json_env_vars)).flatten.uniq if path_env_vars || json_env_vars + end end ## @@ -187,12 +224,11 @@ def self.env_vars= new_env_vars # @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 + lookup_auth_param :paths do + # Pull in values if the DEFAULT_PATHS constant exists. + vals = lookup_local_constant :DEFAULT_PATHS + vals ? Array(vals).flatten.uniq : nil + end end ## @@ -206,6 +242,39 @@ def self.paths= new_paths @paths = new_paths end + ## + # @private + # Return the given parameter value, defaulting up the class hierarchy. + # + # First returns the value of the instance variable, if set. + # Next, calls the given block if provided. (This is generally used to + # look up legacy constant-based values.) + # Otherwise, calls the superclass method if present. + # Returns nil if all steps fail. + # + # @param [Symbol] The parameter name + # @return [Object] The value + # + def self.lookup_auth_param name + val = instance_variable_get "@#{name}".to_sym + val = yield if val.nil? && block_given? + return val unless val.nil? + return superclass.send name if superclass.respond_to? name + nil + end + + ## + # @private + # Return the value of the given constant if it is defined directly in + # this class, or nil if not. + # + # @param [Symbol] Name of the constant + # @return [Object] The value + # + def self.lookup_local_constant name + const_defined?(name, false) ? const_get(name) : nil + end + ## # The Signet::OAuth2::Client object the Credentials instance is using. # diff --git a/spec/googleauth/credentials_spec.rb b/spec/googleauth/credentials_spec.rb index de29854b..0d5c06d0 100644 --- a/spec/googleauth/credentials_spec.rb +++ b/spec/googleauth/credentials_spec.rb @@ -273,6 +273,31 @@ class TestCredentials5 < Google::Auth::Credentials expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"]) end + + it "can be subclassed to pass in other env paths" do + class TestCredentials6 < 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 + DEFAULT_PATHS = ["~/default/path/to/file.txt"] + end + + class TestCredentials7 < TestCredentials6 + end + + expect(TestCredentials7.token_credential_uri).to eq("https://example.com/token") + expect(TestCredentials7.audience).to eq("https://example.com/audience") + expect(TestCredentials7.scope).to eq(["http://example.com/scope"]) + expect(TestCredentials7.env_vars).to eq(["TEST_PATH", "TEST_JSON_VARS"]) + expect(TestCredentials7.paths).to eq(["~/default/path/to/file.txt"]) + + TestCredentials7::TOKEN_CREDENTIAL_URI = "https://example.com/token2" + expect(TestCredentials7.token_credential_uri).to eq("https://example.com/token2") + TestCredentials7::AUDIENCE = nil + expect(TestCredentials7.audience).to eq("https://example.com/audience") + end end describe "using class methods" do @@ -461,6 +486,28 @@ class TestCredentials15 < Google::Auth::Credentials expect(creds.project_id).to eq(default_keyfile_hash["project_id"]) expect(creds.quota_project_id).to eq(default_keyfile_hash["quota_project_id"]) end + + it "subclasses delegate up the class hierarchy" do + class TestCredentials16 < Google::Auth::Credentials + self.scope = "http://example.com/scope" + self.target_audience = "https://example.com/target_audience" + self.env_vars = ["TEST_PATH", "TEST_JSON_VARS"] + self.paths = ["~/default/path/to/file.txt"] + end + + class TestCredentials17 < TestCredentials16 + end + + expect(TestCredentials17.scope).to eq(["http://example.com/scope"]) + expect(TestCredentials17.target_audience).to eq("https://example.com/target_audience") + expect(TestCredentials17.env_vars).to eq(["TEST_PATH", "TEST_JSON_VARS"]) + expect(TestCredentials17.paths).to eq(["~/default/path/to/file.txt"]) + + TestCredentials17.token_credential_uri = "https://example.com/token2" + expect(TestCredentials17.token_credential_uri).to eq("https://example.com/token2") + TestCredentials17.token_credential_uri = nil + expect(TestCredentials17.token_credential_uri).to eq("https://oauth2.googleapis.com/token") + end end it "warns when cloud sdk credentials are used" do