Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
52 changes: 48 additions & 4 deletions lib/googleauth/service_account.rb
Original file line number Diff line number Diff line change
Expand Up @@ -45,6 +45,19 @@ class ServiceAccountCredentials < Signet::OAuth2::Client
# @type [::String] The type name for this credential.
CREDENTIAL_TYPE_NAME = "service_account".freeze

# @private
# @type [Array<Symbol>] Allowed option keys for make_creds.
VALID_MAKE_CREDS_OPTIONS = [
:json_key_io,
:scope,
:enable_self_signed_jwt,
:target_audience,
:audience,
:token_credential_uri,
:default_connection,
:connection_builder
].freeze

def enable_self_signed_jwt?
# Use a self-singed JWT if there's no information that can be used to
# obtain an OAuth token, OR if there are scopes but also an assertion
Expand All @@ -55,17 +68,30 @@ def enable_self_signed_jwt?

# Creates a ServiceAccountCredentials.
#
# @param json_key_io [IO] An IO object containing the JSON key
# @param scope [string|array|nil] the scope(s) to access
# @param json_key_io [IO, nil] Optional. An IO object containing the JSON key
# @param scope [String, Array<String>, nil] Optional. The scope(s) to access
# @param enable_self_signed_jwt [Boolean, nil] Optional. Whether to use self-signed JWTs
# @param target_audience [String, nil] Optional. The target audience for ID token requests
# @param audience [String, nil] Optional. Custom token endpoint audience URI
# @param token_credential_uri [String, nil] Optional. Custom token credential URI
# @param default_connection [Faraday::Connection, nil] Optional. The connection object to use
# @param connection_builder [Proc, nil] Optional. A Proc that returns a Faraday connection
# @raise [ArgumentError] If both scope and target_audience are specified
# @raise [InitializationError] If json_key_io is not an IO object, or
# if required credential fields (private_key or client_email) are missing
def self.make_creds options = {} # rubocop:disable Metrics/MethodLength
validate_make_creds_options options

json_key_io, scope, enable_self_signed_jwt, target_audience, audience, token_credential_uri =
options.values_at :json_key_io, :scope, :enable_self_signed_jwt, :target_audience,
:audience, :token_credential_uri
raise ArgumentError, "Cannot specify both scope and target_audience" if scope && target_audience

private_key, client_email, project_id, quota_project_id, universe_domain =
if json_key_io
unless json_key_io.respond_to? :read
raise InitializationError, "Expected an IO object for json_key_io"
end
json_key = JSON.parse json_key_io.read
if json_key.key? "type"
json_key_io.rewind
Expand All @@ -78,6 +104,10 @@ def self.make_creds options = {} # rubocop:disable Metrics/MethodLength
else
creds_from_env
end

raise InitializationError, "Missing required field: private_key" unless private_key
raise InitializationError, "Missing required field: client_email" unless client_email

project_id ||= CredentialsLoader.load_gcloud_project_id

new(token_credential_uri: token_credential_uri || TOKEN_CRED_URI,
Expand Down Expand Up @@ -122,8 +152,9 @@ def duplicate options = {}
# enclosing quotes.
#
# @param str [String] The string to unescape
# @return [String] The unescaped string
# @return [String, nil] The unescaped string, or nil if input is nil
def self.unescape str
return nil unless str
str = str.gsub '\n', "\n"
str = str[1..-2] if str.start_with?('"') && str.end_with?('"')
str
Expand Down Expand Up @@ -212,7 +243,20 @@ def self.creds_from_env
[private_key, client_email, project_id, nil, nil]
end

private_class_method :creds_from_env
# @private
# Validates options passed to make_creds and emits a warning for unrecognized keys.
#
# @param options [Hash] The options hash to validate.
def self.validate_make_creds_options options
return unless options.is_a? Hash

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Nit: based on the feedback from issue #482 would it make sense to add a warn or raise an error on some of the input types? There seemed to be confusion around String vs IO input for json_key and json_key_io so I'm wondering if we can clarify the typing a bit more.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There is a warn just down below on using incorrect entry names (most common mistake). I also defined the types in the docstrings.

json_key_io.respond_to? :read above is to check IO vs String, which will raise an error.

Do you think that's sufficient?


unknown_keys = options.keys - VALID_MAKE_CREDS_OPTIONS
return if unknown_keys.empty?

warn "Unrecognized option(s) for ServiceAccountCredentials.make_creds: #{unknown_keys.map(&:inspect).join ', '}"
end

private_class_method :creds_from_env, :validate_make_creds_options
end
end
end
48 changes: 45 additions & 3 deletions spec/googleauth/service_account_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -125,9 +125,51 @@ def cred_json_text_with_universe_domain
ServiceAccountCredentials.make_creds(
json_key_io: StringIO.new(JSON.generate(key_without_type))
)
end.not_to raise_error(
Google::Auth::InitializationError, /The provided credentials were not of type 'service_account'/
)
end.not_to raise_error
end

it "raises InitializationError when credentials are missing" do
expect do
ServiceAccountCredentials.make_creds(
scope: "https://www.googleapis.com/auth/userinfo.profile"
)
end.to raise_error(Google::Auth::InitializationError, /Missing required field/)
Comment thread
aandreassa marked this conversation as resolved.
end

it "raises InitializationError when json_key_io is passed a String instead of an IO object" do
expect do
ServiceAccountCredentials.make_creds(
json_key_io: cred_json_text
)
end.to raise_error(Google::Auth::InitializationError, "Expected an IO object for json_key_io")
end

it "warns when unrecognized options are provided" do
expect do
ServiceAccountCredentials.make_creds(
json_key_io: StringIO.new(cred_json_text),
scopes: ["https://www.googleapis.com/auth/userinfo.profile"]
)
end.to output(/Unrecognized option\(s\) for ServiceAccountCredentials\.make_creds: :scopes/).to_stderr
end

it "warns when string key option names are provided" do
expect do
ServiceAccountCredentials.make_creds(
json_key_io: StringIO.new(cred_json_text),
"scope" => ["https://www.googleapis.com/auth/userinfo.profile"]
)
end.to output(/Unrecognized option\(s\) for ServiceAccountCredentials\.make_creds: "scope"/).to_stderr
end

describe ".unescape" do
it "returns nil when given nil" do
expect(ServiceAccountCredentials.unescape(nil)).to be_nil
end

it "unescapes newlines and surrounding quotes" do
expect(ServiceAccountCredentials.unescape("\"line1\\nline2\"")).to eq("line1\nline2")
end
end

describe "universe_domain" do
Expand Down
Loading