Skip to content

Commit

Permalink
feat: Add typed error classes
Browse files Browse the repository at this point in the history
Use typed error classes so errors can be caught and handled
in a more granular way.
  • Loading branch information
blowmage committed Aug 13, 2024
1 parent a09df25 commit f423618
Show file tree
Hide file tree
Showing 22 changed files with 159 additions and 80 deletions.
3 changes: 2 additions & 1 deletion lib/googleauth/application_default.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

require "googleauth/compute_engine"
require "googleauth/default_credentials"
require "googleauth/errors"

module Google
# Module Auth provides classes that provide Google-specific authorization
Expand Down Expand Up @@ -55,7 +56,7 @@ def get_application_default scope = nil, options = {}
DefaultCredentials.from_well_known_path(scope, options) ||
DefaultCredentials.from_system_default_path(scope, options)
return creds unless creds.nil?
raise NOT_FOUND_ERROR unless GCECredentials.on_gce? options
raise CredentialsError, NOT_FOUND_ERROR unless GCECredentials.on_gce? options
GCECredentials.new options.merge(scope: scope)
end
end
Expand Down
11 changes: 6 additions & 5 deletions lib/googleauth/client_id.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

require "multi_json"
require "googleauth/credentials_loader"
require "googleauth/errors"

module Google
module Auth
Expand Down Expand Up @@ -64,8 +65,8 @@ class << self
# `client_secrets.json` files.
#
def initialize id, secret
raise "Client id can not be nil" if id.nil?
raise "Client secret can not be nil" if secret.nil?
raise ClientIdError, "Client id can not be nil" if id.nil?
raise ClientIdError, "Client secret can not be nil" if secret.nil?
@id = id
@secret = secret
end
Expand All @@ -79,7 +80,7 @@ def initialize id, secret
# @return [Google::Auth::ClientID]
#
def self.from_file file
raise "File can not be nil." if file.nil?
raise ClientIdError, "File can not be nil." if file.nil?
File.open file.to_s do |f|
json = f.read
config = MultiJson.load json
Expand All @@ -96,9 +97,9 @@ def self.from_file file
# @return [Google::Auth::ClientID]
#
def self.from_hash config
raise "Hash can not be nil." if config.nil?
raise ClientIdError, "Hash can not be nil." if config.nil?
raw_detail = config[INSTALLED_APP] || config[WEB_APP]
raise MISSING_TOP_LEVEL_ELEMENT_ERROR if raw_detail.nil?
raise ClientIdError, MISSING_TOP_LEVEL_ELEMENT_ERROR if raw_detail.nil?
ClientId.new raw_detail[CLIENT_ID], raw_detail[CLIENT_SECRET]
end
end
Expand Down
5 changes: 3 additions & 2 deletions lib/googleauth/credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -17,6 +17,7 @@
require "signet/oauth_2/client"

require "googleauth/credentials_loader"
require "googleauth/errors"

module Google
module Auth
Expand Down Expand Up @@ -475,13 +476,13 @@ def self.from_io io, options
# Verify that the keyfile argument is provided.
def verify_keyfile_provided! keyfile
return unless keyfile.nil?
raise "The keyfile passed to Google::Auth::Credentials.new was nil."
raise CredentialsError, "The keyfile passed to Google::Auth::Credentials.new was nil."
end

# Verify that the keyfile argument is a file.
def verify_keyfile_exists! keyfile
exists = ::File.file? keyfile
raise "The keyfile '#{keyfile}' is not a valid file." unless exists
raise CredentialsError, "The keyfile '#{keyfile}' is not a valid file." unless exists
end

# Initializes the Signet client.
Expand Down
9 changes: 5 additions & 4 deletions lib/googleauth/credentials_loader.rb
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@

require "os"
require "rbconfig"
require "googleauth/errors"

module Google
# Module Auth provides classes that provide Google-specific authorization
Expand Down Expand Up @@ -75,15 +76,15 @@ def from_env scope = nil, options = {}
options = interpret_options scope, options
if ENV.key?(ENV_VAR) && !ENV[ENV_VAR].empty?
path = ENV[ENV_VAR]
raise "file #{path} does not exist" unless File.exist? path
raise CredentialsError, "file #{path} does not exist" unless File.exist? path
File.open path do |f|
return make_creds options.merge(json_key_io: f)
end
elsif service_account_env_vars? || authorized_user_env_vars?
make_creds options
end
rescue StandardError => e
raise "#{NOT_FOUND_ERROR}: #{e}"
raise CredentialsError, "#{NOT_FOUND_ERROR}: #{e}"
end

# Creates an instance from a well known path.
Expand All @@ -109,7 +110,7 @@ def from_well_known_path scope = nil, options = {}
return make_creds options.merge(json_key_io: f)
end
rescue StandardError => e
raise "#{WELL_KNOWN_ERROR}: #{e}"
raise CredentialsError, "#{WELL_KNOWN_ERROR}: #{e}"
end

# Creates an instance from the system default path
Expand Down Expand Up @@ -137,7 +138,7 @@ def from_system_default_path scope = nil, options = {}
return make_creds options.merge(json_key_io: f)
end
rescue StandardError => e
raise "#{SYSTEM_DEFAULT_ERROR}: #{e}"
raise CredentialsError, "#{SYSTEM_DEFAULT_ERROR}: #{e}"
end

module_function
Expand Down
9 changes: 5 additions & 4 deletions lib/googleauth/default_credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,7 @@
require "googleauth/service_account"
require "googleauth/user_refresh"
require "googleauth/external_account"
require "googleauth/errors"

module Google
# Module Auth provides classes that provide Google-specific authorization
Expand Down Expand Up @@ -46,7 +47,7 @@ def self.make_creds options = {}
def self.read_creds
env_var = CredentialsLoader::ACCOUNT_TYPE_VAR
type = ENV[env_var]
raise "#{env_var} is undefined in env" unless type
raise CredentialsError, "#{env_var} is undefined in env" unless type
case type
when "service_account"
ServiceAccountCredentials
Expand All @@ -55,15 +56,15 @@ def self.read_creds
when "external_account"
ExternalAccount::Credentials
else
raise "credentials type '#{type}' is not supported"
raise CredentialsError, "credentials type '#{type}' is not supported"
end
end

# Reads the input json and determines which creds class to use.
def self.determine_creds_class json_key_io
json_key = MultiJson.load json_key_io.read
key = "type"
raise "the json is missing the '#{key}' field" unless json_key.key? key
raise CredentialsError, "the json is missing the '#{key}' field" unless json_key.key? key
type = json_key[key]
case type
when "service_account"
Expand All @@ -73,7 +74,7 @@ def self.determine_creds_class json_key_io
when "external_account"
[json_key, ExternalAccount::Credentials]
else
raise "credentials type '#{type}' is not supported"
raise CredentialsError, "credentials type '#{type}' is not supported"
end
end
end
Expand Down
45 changes: 45 additions & 0 deletions lib/googleauth/errors.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,45 @@
# frozen_string_literal: true

# Copyright 2024 Google LLC
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.


module Google
module Auth
##
# Base exception class for Google-specific authorization errors.
#
class Error < StandardError; end

##
# Failed to obtain an application's identity for user authorization flows.
#
class ClientIdError < Error; end

##
# Failed to obtain credentials.
#
class CredentialsError < Error; end

##
# Failed to request authorization.
#
class AuthorizerError < Error; end

##
# Failed to obtain a token.
#
class TokenError < Error; end
end
end
9 changes: 5 additions & 4 deletions lib/googleauth/external_account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
require "googleauth/external_account/aws_credentials"
require "googleauth/external_account/identity_pool_credentials"
require "googleauth/external_account/pluggable_credentials"
require "googleauth/errors"

module Google
# Module Auth provides classes that provide Google-specific authorization
Expand All @@ -40,13 +41,13 @@ class Credentials
def self.make_creds options = {}
json_key_io, scope = options.values_at :json_key_io, :scope

raise "A json file is required for external account credentials." unless json_key_io
raise CredentialsError, "A json file is required for external account credentials." unless json_key_io
user_creds = read_json_key json_key_io

# AWS credentials is determined by aws subject token type
return make_aws_credentials user_creds, scope if user_creds[:subject_token_type] == AWS_SUBJECT_TOKEN_TYPE

raise MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil?
raise CredentialsError, MISSING_CREDENTIAL_SOURCE if user_creds[:credential_source].nil?
user_creds[:scope] = scope
make_external_account_credentials user_creds
end
Expand All @@ -58,7 +59,7 @@ def self.read_json_key json_key_io
:audience, :subject_token_type, :token_url, :credential_source
]
wanted.each do |key|
raise "the json is missing the #{key} field" unless json_key.key? key
raise CredentialsError, "the json is missing the #{key} field" unless json_key.key? key
end
json_key
end
Expand All @@ -85,7 +86,7 @@ def make_external_account_credentials user_creds
unless user_creds[:credential_source][:executable].nil?
return Google::Auth::ExternalAccount::PluggableAuthCredentials.new user_creds
end
raise INVALID_EXTERNAL_ACCOUNT_TYPE
raise CredentialsError, INVALID_EXTERNAL_ACCOUNT_TYPE
end
end
end
Expand Down
16 changes: 10 additions & 6 deletions lib/googleauth/external_account/aws_credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
require "time"
require "googleauth/external_account/base_credentials"
require "googleauth/external_account/external_account_utils"
require "googleauth/errors"

module Google
# Module Auth provides classes that provide Google-specific authorization used to access Google APIs.
Expand Down Expand Up @@ -108,13 +109,13 @@ def retrieve_subject_token!

def imdsv2_session_token
return @imdsv2_session_token unless imdsv2_session_token_invalid?
raise "IMDSV2 token url must be provided" if @imdsv2_session_token_url.nil?
raise CredentialsError, "IMDSV2 token url must be provided" if @imdsv2_session_token_url.nil?
begin
response = connection.put @imdsv2_session_token_url do |req|
req.headers["x-aws-ec2-metadata-token-ttl-seconds"] = IMDSV2_TOKEN_EXPIRATION_IN_SECONDS.to_s
end
rescue Faraday::Error => e
raise "Fetching AWS IMDSV2 token error: #{e}"
raise CredentialsError, "Fetching AWS IMDSV2 token error: #{e}"
end
raise Faraday::Error unless response.success?
@imdsv2_session_token = response.body
Expand All @@ -140,7 +141,7 @@ def get_aws_resource url, name, data: nil, headers: {}
raise Faraday::Error unless response.success?
response
rescue Faraday::Error
raise "Failed to retrieve AWS #{name}."
raise CredentialsError, "Failed to retrieve AWS #{name}."
end
end

Expand Down Expand Up @@ -183,7 +184,7 @@ def fetch_security_credentials
# credentials needed to sign requests to AWS APIs.
def fetch_metadata_role_name
unless @credential_verification_url
raise "Unable to determine the AWS metadata server security credentials endpoint"
raise CredentialsError, "Unable to determine the AWS metadata server security credentials endpoint"
end

get_aws_resource(@credential_verification_url, "IAM Role").body
Expand All @@ -199,7 +200,10 @@ def region
@region = ENV[CredentialsLoader::AWS_REGION_VAR] || ENV[CredentialsLoader::AWS_DEFAULT_REGION_VAR]

unless @region
raise "region_url or region must be set for external account credentials" unless @region_url
unless @region_url
raise CredentialsError,
"region_url or region must be set for external account credentials"
end

@region ||= get_aws_resource(@region_url, "region").body[0..-2]
end
Expand Down Expand Up @@ -236,7 +240,7 @@ def initialize region_name
#
def generate_signed_request aws_credentials, original_request
uri = Addressable::URI.parse original_request[:url]
raise "Invalid AWS service URL" unless uri.hostname && uri.scheme == "https"
raise CredentialsError, "Invalid AWS service URL" unless uri.hostname && uri.scheme == "https"
service_name = uri.host.split(".").first

datetime = Time.now.utc.strftime "%Y%m%dT%H%M%SZ"
Expand Down
5 changes: 3 additions & 2 deletions lib/googleauth/external_account/base_credentials.rb
Original file line number Diff line number Diff line change
Expand Up @@ -15,6 +15,7 @@
require "googleauth/base_client"
require "googleauth/helpers/connection"
require "googleauth/oauth2/sts_client"
require "googleauth/errors"

module Google
# Module Auth provides classes that provide Google-specific authorization
Expand Down Expand Up @@ -121,7 +122,7 @@ def base_setup options
connection: default_connection
)
return unless @workforce_pool_user_project && !is_workforce_pool?
raise "workforce_pool_user_project should not be set for non-workforce pool credentials."
raise CredentialsError, "workforce_pool_user_project should not be set for non-workforce pool credentials."
end

def exchange_token
Expand All @@ -148,7 +149,7 @@ def get_impersonated_access_token token, _options = {}
end

if response.status != 200
raise "Service account impersonation failed with status #{response.status}"
raise CredentialsError, "Service account impersonation failed with status #{response.status}"
end

MultiJson.load response.body
Expand Down
2 changes: 1 addition & 1 deletion lib/googleauth/external_account/external_account_utils.rb
Original file line number Diff line number Diff line change
Expand Up @@ -83,7 +83,7 @@ def normalize_timestamp time
when String
Time.parse time
else
raise "Invalid time value #{time}"
raise ArgumentError, "Invalid time value #{time}"
end
end

Expand Down
Loading

0 comments on commit f423618

Please sign in to comment.