Skip to content

Commit 5f97a26

Browse files
authored
feat: provided opt-in debug logging (#490)
1 parent 075b59e commit 5f97a26

File tree

9 files changed

+372
-122
lines changed

9 files changed

+372
-122
lines changed

googleauth.gemspec

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,8 @@ Gem::Specification.new do |gem|
2323
gem.required_ruby_version = ">= 2.7"
2424

2525
gem.add_dependency "faraday", ">= 1.0", "< 3.a"
26-
gem.add_dependency "google-cloud-env", "~> 2.1"
26+
gem.add_dependency "google-cloud-env", "~> 2.2"
27+
gem.add_dependency "google-logging-utils", "~> 0.1"
2728
gem.add_dependency "jwt", ">= 1.4", "< 3.0"
2829
gem.add_dependency "multi_json", "~> 1.11"
2930
gem.add_dependency "os", ">= 0.9", "< 2.0"

lib/googleauth/base_client.rb

Lines changed: 11 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,8 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
require "google/logging/message"
16+
1517
module Google
1618
# Module Auth provides classes that provide Google-specific authorization
1719
# used to access Google APIs.
@@ -29,7 +31,12 @@ def apply! a_hash, opts = {}
2931
# fetch the access token there is currently not one, or if the client
3032
# has expired
3133
fetch_access_token! opts if needs_access_token?
32-
a_hash[AUTH_METADATA_KEY] = "Bearer #{send token_type}"
34+
token = send token_type
35+
a_hash[AUTH_METADATA_KEY] = "Bearer #{token}"
36+
logger&.debug do
37+
hash = Digest::SHA256.hexdigest token
38+
Google::Logging::Message.from message: "Sending auth token. (sha256:#{hash})"
39+
end
3340
end
3441

3542
# Returns a clone of a_hash updated with the authentication token
@@ -66,6 +73,9 @@ def expires_within?
6673
raise NoMethodError, "expires_within? not implemented"
6774
end
6875

76+
# The logger used to log operations on this client, such as token refresh.
77+
attr_accessor :logger
78+
6979
private
7080

7181
def token_type

lib/googleauth/compute_engine.rb

Lines changed: 47 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -96,35 +96,71 @@ def initialize options = {}
9696
# Overrides the super class method to change how access tokens are
9797
# fetched.
9898
def fetch_access_token _options = {}
99-
if token_type == :id_token
100-
query = { "audience" => target_audience, "format" => "full" }
101-
entry = "service-accounts/default/identity"
102-
else
103-
query = {}
104-
entry = "service-accounts/default/token"
105-
end
99+
query, entry =
100+
if token_type == :id_token
101+
[{ "audience" => target_audience, "format" => "full" }, "service-accounts/default/identity"]
102+
else
103+
[{}, "service-accounts/default/token"]
104+
end
106105
query[:scopes] = Array(scope).join "," if scope
107106
begin
107+
log_fetch_query
108108
resp = Google::Cloud.env.lookup_metadata_response "instance", entry, query: query
109+
log_fetch_resp resp
109110
case resp.status
110111
when 200
111112
build_token_hash resp.body, resp.headers["content-type"], resp.retrieval_monotonic_time
112113
when 403, 500
113-
msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
114-
raise Signet::UnexpectedStatusError, msg
114+
raise Signet::UnexpectedStatusError, "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
115115
when 404
116116
raise Signet::AuthorizationError, NO_METADATA_SERVER_ERROR
117117
else
118-
msg = "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
119-
raise Signet::AuthorizationError, msg
118+
raise Signet::AuthorizationError, "Unexpected error code #{resp.status} #{UNEXPECTED_ERROR_SUFFIX}"
120119
end
121120
rescue Google::Cloud::Env::MetadataServerNotResponding => e
121+
log_fetch_err e
122122
raise Signet::AuthorizationError, e.message
123123
end
124124
end
125125

126126
private
127127

128+
def log_fetch_query
129+
if token_type == :id_token
130+
logger&.info do
131+
Google::Logging::Message.from(
132+
message: "Requesting id token from MDS with aud=#{target_audience}",
133+
"credentialsId" => object_id
134+
)
135+
end
136+
else
137+
logger&.info do
138+
Google::Logging::Message.from(
139+
message: "Requesting access token from MDS",
140+
"credentialsId" => object_id
141+
)
142+
end
143+
end
144+
end
145+
146+
def log_fetch_resp resp
147+
logger&.info do
148+
Google::Logging::Message.from(
149+
message: "Received #{resp.status} from MDS",
150+
"credentialsId" => object_id
151+
)
152+
end
153+
end
154+
155+
def log_fetch_err _err
156+
logger&.info do
157+
Google::Logging::Message.from(
158+
message: "MDS did not respond to token request",
159+
"credentialsId" => object_id
160+
)
161+
end
162+
end
163+
128164
def build_token_hash body, content_type, retrieval_time
129165
hash =
130166
if ["text/html", "application/text"].include? content_type

lib/googleauth/credentials.rb

Lines changed: 32 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -337,10 +337,13 @@ def disable_universe_domain_check
337337
# @!attribute [rw] universe_domain
338338
# @return [String] The universe domain issuing these credentials.
339339
#
340+
# @!attribute [rw] logger
341+
# @return [Logger] The logger used to log credential operations such as token refresh.
342+
#
340343
def_delegators :@client,
341344
:token_credential_uri, :audience,
342345
:scope, :issuer, :signing_key, :updater_proc, :target_audience,
343-
:universe_domain, :universe_domain=
346+
:universe_domain, :universe_domain=, :logger, :logger=
344347

345348
##
346349
# Creates a new Credentials instance with the provided auth credentials, and with the default
@@ -349,16 +352,17 @@ def disable_universe_domain_check
349352
# @param [String, Hash, Signet::OAuth2::Client] keyfile
350353
# The keyfile can be provided as one of the following:
351354
#
352-
# * The path to a JSON keyfile (as a +String+)
353-
# * The contents of a JSON keyfile (as a +Hash+)
354-
# * A +Signet::OAuth2::Client+ object
355+
# * The path to a JSON keyfile (as a `String`)
356+
# * The contents of a JSON keyfile (as a `Hash`)
357+
# * A `Signet::OAuth2::Client` object
355358
# @param [Hash] options
356359
# The options for configuring the credentials instance. The following is supported:
357360
#
358-
# * +:scope+ - the scope for the client
359-
# * +"project_id"+ (and optionally +"project"+) - the project identifier for the client
360-
# * +:connection_builder+ - the connection builder to use for the client
361-
# * +:default_connection+ - the default connection to use for the client
361+
# * `:scope` - the scope for the client
362+
# * `project_id` (and optionally `project`) - the project identifier for the client
363+
# * `:connection_builder` - the connection builder to use for the client
364+
# * `:default_connection` - the default connection to use for the client
365+
# * `:logger` - the logger used to log credential operations such as token refresh.
362366
#
363367
def initialize keyfile, options = {}
364368
verify_keyfile_provided! keyfile
@@ -373,8 +377,8 @@ def initialize keyfile, options = {}
373377
else
374378
update_from_filepath keyfile, options
375379
end
380+
setup_logging logger: options.fetch(:logger, :default)
376381
@project_id ||= CredentialsLoader.load_gcloud_project_id
377-
@client.fetch_access_token! if @client.needs_access_token?
378382
@env_vars = nil
379383
@paths = nil
380384
@scope = nil
@@ -468,7 +472,8 @@ def self.from_io io, options
468472
audience: options[:audience] || audience
469473
}
470474
client = Google::Auth::DefaultCredentials.make_creds creds_input
471-
new client
475+
options = options.select { |k, _v| k == :logger }
476+
new client, options
472477
end
473478

474479
private_class_method :from_env_vars,
@@ -549,6 +554,23 @@ def update_from_filepath path, options
549554
@quota_project_id ||= json["quota_project_id"]
550555
@client = init_client json, options
551556
end
557+
558+
def setup_logging logger: :default
559+
return unless @client.respond_to? :logger=
560+
logging_env = ENV["GOOGLE_SDK_RUBY_LOGGING_GEMS"].to_s.downcase
561+
if ["false", "none"].include? logging_env
562+
logger = nil
563+
elsif @client.logger
564+
logger = @client.logger
565+
elsif logger == :default
566+
logger = nil
567+
if ["true", "all"].include?(logging_env) || logging_env.split(",").include?("googleauth")
568+
formatter = Google::Logging::StructuredFormatter.new if Google::Cloud::Env.get.logging_agent_expected?
569+
logger = Logger.new $stderr, progname: "googleauth", formatter: formatter
570+
end
571+
end
572+
@client.logger = logger
573+
end
552574
end
553575
end
554576
end

lib/googleauth/external_account/base_credentials.rb

Lines changed: 33 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -129,18 +129,39 @@ def exchange_token
129129
if @client_id.nil? && @workforce_pool_user_project
130130
additional_options = { userProject: @workforce_pool_user_project }
131131
end
132-
@sts_client.exchange_token(
132+
token_request = {
133133
audience: @audience,
134134
grant_type: STS_GRANT_TYPE,
135135
subject_token: retrieve_subject_token!,
136136
subject_token_type: @subject_token_type,
137137
scopes: @service_account_impersonation_url ? IAM_SCOPE : @scope,
138138
requested_token_type: STS_REQUESTED_TOKEN_TYPE,
139139
additional_options: additional_options
140-
)
140+
}
141+
log_token_request token_request
142+
@sts_client.exchange_token token_request
143+
end
144+
145+
def log_token_request token_request
146+
logger&.info do
147+
Google::Logging::Message.from(
148+
message: "Requesting access token from #{token_request[:grant_type]}",
149+
"credentialsId" => object_id
150+
)
151+
end
152+
logger&.debug do
153+
digest = Digest::SHA256.hexdigest token_request[:subject_token].to_s
154+
loggable_request = token_request.merge subject_token: "(sha256:#{digest})"
155+
Google::Logging::Message.from(
156+
message: "Request data",
157+
"request" => loggable_request,
158+
"credentialsId" => object_id
159+
)
160+
end
141161
end
142162

143163
def get_impersonated_access_token token, _options = {}
164+
log_impersonated_token_request token
144165
response = connection.post @service_account_impersonation_url do |req|
145166
req.headers["Authorization"] = "Bearer #{token}"
146167
req.headers["Content-Type"] = "application/json"
@@ -153,6 +174,16 @@ def get_impersonated_access_token token, _options = {}
153174

154175
MultiJson.load response.body
155176
end
177+
178+
def log_impersonated_token_request original_token
179+
logger&.info do
180+
digest = Digest::SHA256.hexdigest original_token
181+
Google::Logging::Message.from(
182+
message: "Requesting impersonated access token with original token (sha256:#{digest})",
183+
"credentialsId" => object_id
184+
)
185+
end
186+
end
156187
end
157188
end
158189
end

lib/googleauth/service_account.rb

Lines changed: 12 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
require "google/logging/message"
1516
require "googleauth/signet"
1617
require "googleauth/credentials_loader"
1718
require "googleauth/json_key_reader"
@@ -123,6 +124,7 @@ def apply_self_signed_jwt! a_hash
123124
}
124125
key_io = StringIO.new MultiJson.dump(cred_json)
125126
alt = ServiceAccountJwtHeaderCredentials.make_creds json_key_io: key_io, scope: scope
127+
alt.logger = logger
126128
alt.apply! a_hash
127129
end
128130
end
@@ -147,6 +149,7 @@ class ServiceAccountJwtHeaderCredentials
147149
attr_reader :project_id
148150
attr_reader :quota_project_id
149151
attr_accessor :universe_domain
152+
attr_accessor :logger
150153

151154
# Create a ServiceAccountJwtHeaderCredentials.
152155
#
@@ -187,10 +190,14 @@ def apply! a_hash, opts = {}
187190
return a_hash if jwt_aud_uri.nil? && @scope.nil?
188191
jwt_token = new_jwt_token jwt_aud_uri, opts
189192
a_hash[AUTH_METADATA_KEY] = "Bearer #{jwt_token}"
193+
logger&.debug do
194+
hash = Digest::SHA256.hexdigest jwt_token
195+
Google::Logging::Message.from message: "Sending JWT auth token. (sha256:#{hash})"
196+
end
190197
a_hash
191198
end
192199

193-
# Returns a clone of a_hash updated with the authoriation header
200+
# Returns a clone of a_hash updated with the authorization header
194201
def apply a_hash, opts = {}
195202
a_copy = a_hash.clone
196203
apply! a_copy, opts
@@ -219,6 +226,10 @@ def new_jwt_token jwt_aud_uri = nil, options = {}
219226
assertion["scope"] = Array(@scope).join " " if @scope
220227
assertion["aud"] = jwt_aud_uri if jwt_aud_uri
221228

229+
logger&.debug do
230+
Google::Logging::Message.from message: "JWT assertion: #{assertion}"
231+
end
232+
222233
JWT.encode assertion, @signing_key, SIGNING_ALGORITHM
223234
end
224235

0 commit comments

Comments
 (0)