Skip to content

Commit c7011b6

Browse files
authoredMay 25, 2023
feat: add metrics (part 3) (#1305)
This PR adds `x-goog-api-client` header to - access token and id token refresh requests, for compute engine credentials / user credentials / service account credentials / impersonated credentials - reauth start and continue requests - metadata server ping requests Previous PRs: Part 1: #1298 Part 2: #1303
1 parent 5f3dd94 commit c7011b6

13 files changed

+355
-56
lines changed
 

‎google/auth/compute_engine/_metadata.py

+23-7
Original file line numberDiff line numberDiff line change
@@ -29,6 +29,7 @@
2929
from google.auth import _helpers
3030
from google.auth import environment_vars
3131
from google.auth import exceptions
32+
from google.auth import metrics
3233

3334
_LOGGER = logging.getLogger(__name__)
3435

@@ -121,13 +122,13 @@ def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):
121122
# the metadata resolution was particularly slow. The latter case is
122123
# "unlikely".
123124
retries = 0
125+
headers = _METADATA_HEADERS.copy()
126+
headers[metrics.API_CLIENT_HEADER] = metrics.mds_ping()
127+
124128
while retries < retry_count:
125129
try:
126130
response = request(
127-
url=_METADATA_IP_ROOT,
128-
method="GET",
129-
headers=_METADATA_HEADERS,
130-
timeout=timeout,
131+
url=_METADATA_IP_ROOT, method="GET", headers=headers, timeout=timeout
131132
)
132133

133134
metadata_flavor = response.headers.get(_METADATA_FLAVOR_HEADER)
@@ -150,7 +151,13 @@ def ping(request, timeout=_METADATA_DEFAULT_TIMEOUT, retry_count=3):
150151

151152

152153
def get(
153-
request, path, root=_METADATA_ROOT, params=None, recursive=False, retry_count=5
154+
request,
155+
path,
156+
root=_METADATA_ROOT,
157+
params=None,
158+
recursive=False,
159+
retry_count=5,
160+
headers=None,
154161
):
155162
"""Fetch a resource from the metadata server.
156163
@@ -167,6 +174,7 @@ def get(
167174
details.
168175
retry_count (int): How many times to attempt connecting to metadata
169176
server using above timeout.
177+
headers (Optional[Mapping[str, str]]): Headers for the request.
170178
171179
Returns:
172180
Union[Mapping, str]: If the metadata server returns JSON, a mapping of
@@ -180,6 +188,10 @@ def get(
180188
base_url = urlparse.urljoin(root, path)
181189
query_params = {} if params is None else params
182190

191+
headers_to_use = _METADATA_HEADERS.copy()
192+
if headers:
193+
headers_to_use.update(headers)
194+
183195
if recursive:
184196
query_params["recursive"] = "true"
185197

@@ -188,7 +200,7 @@ def get(
188200
retries = 0
189201
while retries < retry_count:
190202
try:
191-
response = request(url=url, method="GET", headers=_METADATA_HEADERS)
203+
response = request(url=url, method="GET", headers=headers_to_use)
192204
break
193205

194206
except exceptions.TransportError as e:
@@ -300,8 +312,12 @@ def get_service_account_token(request, service_account="default", scopes=None):
300312
else:
301313
params = None
302314

315+
metrics_header = {
316+
metrics.API_CLIENT_HEADER: metrics.token_request_access_token_mds()
317+
}
318+
303319
path = "instance/service-accounts/{0}/token".format(service_account)
304-
token_json = get(request, path, params=params)
320+
token_json = get(request, path, params=params, headers=metrics_header)
305321
token_expiry = _helpers.utcnow() + datetime.timedelta(
306322
seconds=token_json["expires_in"]
307323
)

‎google/auth/compute_engine/credentials.py

+6-1
Original file line numberDiff line numberDiff line change
@@ -378,7 +378,12 @@ def _call_metadata_identity_endpoint(self, request):
378378
try:
379379
path = "instance/service-accounts/default/identity"
380380
params = {"audience": self._target_audience, "format": "full"}
381-
id_token = _metadata.get(request, path, params=params)
381+
metrics_header = {
382+
metrics.API_CLIENT_HEADER: metrics.token_request_id_token_mds()
383+
}
384+
id_token = _metadata.get(
385+
request, path, params=params, headers=metrics_header
386+
)
382387
except exceptions.TransportError as caught_exc:
383388
new_exc = exceptions.RefreshError(caught_exc)
384389
six.raise_from(new_exc, caught_exc)

‎google/auth/impersonated_credentials.py

+8-2
Original file line numberDiff line numberDiff line change
@@ -265,7 +265,10 @@ def _update_token(self, request):
265265
"lifetime": str(self._lifetime) + "s",
266266
}
267267

268-
headers = {"Content-Type": "application/json"}
268+
headers = {
269+
"Content-Type": "application/json",
270+
metrics.API_CLIENT_HEADER: metrics.token_request_access_token_impersonate(),
271+
}
269272

270273
# Apply the source credentials authentication info.
271274
self._source_credentials.apply(headers)
@@ -426,7 +429,10 @@ def refresh(self, request):
426429
"includeEmail": self._include_email,
427430
}
428431

429-
headers = {"Content-Type": "application/json"}
432+
headers = {
433+
"Content-Type": "application/json",
434+
metrics.API_CLIENT_HEADER: metrics.token_request_id_token_impersonate(),
435+
}
430436

431437
authed_session = AuthorizedSession(
432438
self._target_credentials._source_credentials, auth_request=request

‎google/oauth2/_client.py

+27-6
Original file line numberDiff line numberDiff line change
@@ -34,6 +34,7 @@
3434
from google.auth import _helpers
3535
from google.auth import exceptions
3636
from google.auth import jwt
37+
from google.auth import metrics
3738
from google.auth import transport
3839

3940
_URLENCODED_CONTENT_TYPE = "application/x-www-form-urlencoded"
@@ -146,6 +147,7 @@ def _token_endpoint_request_no_throw(
146147
access_token=None,
147148
use_json=False,
148149
can_retry=True,
150+
headers=None,
149151
**kwargs
150152
):
151153
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
@@ -161,6 +163,7 @@ def _token_endpoint_request_no_throw(
161163
use_json (Optional(bool)): Use urlencoded format or json format for the
162164
content type. The default value is False.
163165
can_retry (bool): Enable or disable request retry behavior.
166+
headers (Optional[Mapping[str, str]]): The headers for the request.
164167
kwargs: Additional arguments passed on to the request method. The
165168
kwargs will be passed to `requests.request` method, see:
166169
https://docs.python-requests.org/en/latest/api/#requests.request.
@@ -176,18 +179,21 @@ def _token_endpoint_request_no_throw(
176179
is retryable.
177180
"""
178181
if use_json:
179-
headers = {"Content-Type": _JSON_CONTENT_TYPE}
182+
headers_to_use = {"Content-Type": _JSON_CONTENT_TYPE}
180183
body = json.dumps(body).encode("utf-8")
181184
else:
182-
headers = {"Content-Type": _URLENCODED_CONTENT_TYPE}
185+
headers_to_use = {"Content-Type": _URLENCODED_CONTENT_TYPE}
183186
body = urllib.parse.urlencode(body).encode("utf-8")
184187

185188
if access_token:
186-
headers["Authorization"] = "Bearer {}".format(access_token)
189+
headers_to_use["Authorization"] = "Bearer {}".format(access_token)
190+
191+
if headers:
192+
headers_to_use.update(headers)
187193

188194
def _perform_request():
189195
response = request(
190-
method="POST", url=token_uri, headers=headers, body=body, **kwargs
196+
method="POST", url=token_uri, headers=headers_to_use, body=body, **kwargs
191197
)
192198
response_body = (
193199
response.data.decode("utf-8")
@@ -231,6 +237,7 @@ def _token_endpoint_request(
231237
access_token=None,
232238
use_json=False,
233239
can_retry=True,
240+
headers=None,
234241
**kwargs
235242
):
236243
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
@@ -245,6 +252,7 @@ def _token_endpoint_request(
245252
use_json (Optional(bool)): Use urlencoded format or json format for the
246253
content type. The default value is False.
247254
can_retry (bool): Enable or disable request retry behavior.
255+
headers (Optional[Mapping[str, str]]): The headers for the request.
248256
kwargs: Additional arguments passed on to the request method. The
249257
kwargs will be passed to `requests.request` method, see:
250258
https://docs.python-requests.org/en/latest/api/#requests.request.
@@ -268,6 +276,7 @@ def _token_endpoint_request(
268276
access_token=access_token,
269277
use_json=use_json,
270278
can_retry=can_retry,
279+
headers=headers,
271280
**kwargs
272281
)
273282
if not response_status_ok:
@@ -301,7 +310,13 @@ def jwt_grant(request, token_uri, assertion, can_retry=True):
301310
body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
302311

303312
response_data = _token_endpoint_request(
304-
request, token_uri, body, can_retry=can_retry
313+
request,
314+
token_uri,
315+
body,
316+
can_retry=can_retry,
317+
headers={
318+
metrics.API_CLIENT_HEADER: metrics.token_request_access_token_sa_assertion()
319+
},
305320
)
306321

307322
try:
@@ -384,7 +399,13 @@ def id_token_jwt_grant(request, token_uri, assertion, can_retry=True):
384399
body = {"assertion": assertion, "grant_type": _JWT_GRANT_TYPE}
385400

386401
response_data = _token_endpoint_request(
387-
request, token_uri, body, can_retry=can_retry
402+
request,
403+
token_uri,
404+
body,
405+
can_retry=can_retry,
406+
headers={
407+
metrics.API_CLIENT_HEADER: metrics.token_request_id_token_sa_assertion()
408+
},
388409
)
389410

390411
try:

‎google/oauth2/reauth.py

+15-3
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@
3737
from six.moves import range
3838

3939
from google.auth import exceptions
40+
from google.auth import metrics
4041
from google.oauth2 import _client
4142
from google.oauth2 import challenges
4243

@@ -94,9 +95,15 @@ def _get_challenges(
9495
body = {"supportedChallengeTypes": supported_challenge_types}
9596
if requested_scopes:
9697
body["oauthScopesForDomainPolicyLookup"] = requested_scopes
98+
metrics_header = {metrics.API_CLIENT_HEADER: metrics.reauth_start()}
9799

98100
return _client._token_endpoint_request(
99-
request, _REAUTH_API + ":start", body, access_token=access_token, use_json=True
101+
request,
102+
_REAUTH_API + ":start",
103+
body,
104+
access_token=access_token,
105+
use_json=True,
106+
headers=metrics_header,
100107
)
101108

102109

@@ -123,13 +130,15 @@ def _send_challenge_result(
123130
"action": "RESPOND",
124131
"proposalResponse": client_input,
125132
}
133+
metrics_header = {metrics.API_CLIENT_HEADER: metrics.reauth_continue()}
126134

127135
return _client._token_endpoint_request(
128136
request,
129137
_REAUTH_API + "/{}:continue".format(session_id),
130138
body,
131139
access_token=access_token,
132140
use_json=True,
141+
headers=metrics_header,
133142
)
134143

135144

@@ -320,9 +329,10 @@ def refresh_grant(
320329
body["scope"] = " ".join(scopes)
321330
if rapt_token:
322331
body["rapt"] = rapt_token
332+
metrics_header = {metrics.API_CLIENT_HEADER: metrics.token_request_user()}
323333

324334
response_status_ok, response_data, retryable_error = _client._token_endpoint_request_no_throw(
325-
request, token_uri, body
335+
request, token_uri, body, headers=metrics_header
326336
)
327337
if (
328338
not response_status_ok
@@ -345,7 +355,9 @@ def refresh_grant(
345355
response_status_ok,
346356
response_data,
347357
retryable_error,
348-
) = _client._token_endpoint_request_no_throw(request, token_uri, body)
358+
) = _client._token_endpoint_request_no_throw(
359+
request, token_uri, body, headers=metrics_header
360+
)
349361

350362
if not response_status_ok:
351363
_client._handle_error_response(response_data, retryable_error)

‎tests/compute_engine/test__metadata.py

+47-12
Original file line numberDiff line numberDiff line change
@@ -38,6 +38,15 @@
3838
DATA_DIR, "smbios_product_name_non_google"
3939
)
4040

41+
ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
42+
"gl-python/3.7 auth/1.1 auth-request-type/at cred-type/mds"
43+
)
44+
MDS_PING_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1 auth-request-type/mds"
45+
MDS_PING_REQUEST_HEADER = {
46+
"metadata-flavor": "Google",
47+
"x-goog-api-client": MDS_PING_METRICS_HEADER_VALUE,
48+
}
49+
4150

4251
def make_request(data, status=http_client.OK, headers=None, retry=False):
4352
response = mock.create_autospec(transport.Response, instance=True)
@@ -87,28 +96,30 @@ def test_is_on_gce_linux_success():
8796
assert _metadata.is_on_gce(request)
8897

8998

90-
def test_ping_success():
99+
@mock.patch("google.auth.metrics.mds_ping", return_value=MDS_PING_METRICS_HEADER_VALUE)
100+
def test_ping_success(mock_metrics_header_value):
91101
request = make_request("", headers=_metadata._METADATA_HEADERS)
92102

93103
assert _metadata.ping(request)
94104

95105
request.assert_called_once_with(
96106
method="GET",
97107
url=_metadata._METADATA_IP_ROOT,
98-
headers=_metadata._METADATA_HEADERS,
108+
headers=MDS_PING_REQUEST_HEADER,
99109
timeout=_metadata._METADATA_DEFAULT_TIMEOUT,
100110
)
101111

102112

103-
def test_ping_success_retry():
113+
@mock.patch("google.auth.metrics.mds_ping", return_value=MDS_PING_METRICS_HEADER_VALUE)
114+
def test_ping_success_retry(mock_metrics_header_value):
104115
request = make_request("", headers=_metadata._METADATA_HEADERS, retry=True)
105116

106117
assert _metadata.ping(request)
107118

108119
request.assert_called_with(
109120
method="GET",
110121
url=_metadata._METADATA_IP_ROOT,
111-
headers=_metadata._METADATA_HEADERS,
122+
headers=MDS_PING_REQUEST_HEADER,
112123
timeout=_metadata._METADATA_DEFAULT_TIMEOUT,
113124
)
114125
assert request.call_count == 2
@@ -127,7 +138,8 @@ def test_ping_failure_connection_failed():
127138
assert not _metadata.ping(request)
128139

129140

130-
def test_ping_success_custom_root():
141+
@mock.patch("google.auth.metrics.mds_ping", return_value=MDS_PING_METRICS_HEADER_VALUE)
142+
def test_ping_success_custom_root(mock_metrics_header_value):
131143
request = make_request("", headers=_metadata._METADATA_HEADERS)
132144

133145
fake_ip = "1.2.3.4"
@@ -143,7 +155,7 @@ def test_ping_success_custom_root():
143155
request.assert_called_once_with(
144156
method="GET",
145157
url="http://" + fake_ip,
146-
headers=_metadata._METADATA_HEADERS,
158+
headers=MDS_PING_REQUEST_HEADER,
147159
timeout=_metadata._METADATA_DEFAULT_TIMEOUT,
148160
)
149161

@@ -341,8 +353,12 @@ def test_get_project_id():
341353
assert project_id == project
342354

343355

356+
@mock.patch(
357+
"google.auth.metrics.token_request_access_token_mds",
358+
return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
359+
)
344360
@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
345-
def test_get_service_account_token(utcnow):
361+
def test_get_service_account_token(utcnow, mock_metrics_header_value):
346362
ttl = 500
347363
request = make_request(
348364
json.dumps({"access_token": "token", "expires_in": ttl}),
@@ -354,14 +370,21 @@ def test_get_service_account_token(utcnow):
354370
request.assert_called_once_with(
355371
method="GET",
356372
url=_metadata._METADATA_ROOT + PATH + "/token",
357-
headers=_metadata._METADATA_HEADERS,
373+
headers={
374+
"metadata-flavor": "Google",
375+
"x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
376+
},
358377
)
359378
assert token == "token"
360379
assert expiry == utcnow() + datetime.timedelta(seconds=ttl)
361380

362381

382+
@mock.patch(
383+
"google.auth.metrics.token_request_access_token_mds",
384+
return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
385+
)
363386
@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
364-
def test_get_service_account_token_with_scopes_list(utcnow):
387+
def test_get_service_account_token_with_scopes_list(utcnow, mock_metrics_header_value):
365388
ttl = 500
366389
request = make_request(
367390
json.dumps({"access_token": "token", "expires_in": ttl}),
@@ -373,14 +396,23 @@ def test_get_service_account_token_with_scopes_list(utcnow):
373396
request.assert_called_once_with(
374397
method="GET",
375398
url=_metadata._METADATA_ROOT + PATH + "/token" + "?scopes=foo%2Cbar",
376-
headers=_metadata._METADATA_HEADERS,
399+
headers={
400+
"metadata-flavor": "Google",
401+
"x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
402+
},
377403
)
378404
assert token == "token"
379405
assert expiry == utcnow() + datetime.timedelta(seconds=ttl)
380406

381407

408+
@mock.patch(
409+
"google.auth.metrics.token_request_access_token_mds",
410+
return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
411+
)
382412
@mock.patch("google.auth._helpers.utcnow", return_value=datetime.datetime.min)
383-
def test_get_service_account_token_with_scopes_string(utcnow):
413+
def test_get_service_account_token_with_scopes_string(
414+
utcnow, mock_metrics_header_value
415+
):
384416
ttl = 500
385417
request = make_request(
386418
json.dumps({"access_token": "token", "expires_in": ttl}),
@@ -392,7 +424,10 @@ def test_get_service_account_token_with_scopes_string(utcnow):
392424
request.assert_called_once_with(
393425
method="GET",
394426
url=_metadata._METADATA_ROOT + PATH + "/token" + "?scopes=foo%2Cbar",
395-
headers=_metadata._METADATA_HEADERS,
427+
headers={
428+
"metadata-flavor": "Google",
429+
"x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
430+
},
396431
)
397432
assert token == "token"
398433
assert expiry == utcnow() + datetime.timedelta(seconds=ttl)

‎tests/compute_engine/test_credentials.py

+27-3
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,13 @@
4343
b"bsxbLa6Fp0SYeYwO8ifEnkRvasVpc1WTQqfRB2JCj5pTBDzJpIpFCMmnQ"
4444
)
4545

46+
ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
47+
"gl-python/3.7 auth/1.1 auth-request-type/at cred-type/mds"
48+
)
49+
ID_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
50+
"gl-python/3.7 auth/1.1 auth-request-type/it cred-type/mds"
51+
)
52+
4653

4754
class TestCredentials(object):
4855
credentials = None
@@ -96,12 +103,16 @@ def test_refresh_success(self, get, utcnow):
96103
# expired)
97104
assert self.credentials.valid
98105

106+
@mock.patch(
107+
"google.auth.metrics.token_request_access_token_mds",
108+
return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
109+
)
99110
@mock.patch(
100111
"google.auth._helpers.utcnow",
101112
return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
102113
)
103114
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
104-
def test_refresh_success_with_scopes(self, get, utcnow):
115+
def test_refresh_success_with_scopes(self, get, utcnow, mock_metrics_header_value):
105116
get.side_effect = [
106117
{
107118
# First request is for sevice account info.
@@ -133,7 +144,10 @@ def test_refresh_success_with_scopes(self, get, utcnow):
133144
assert self.credentials.valid
134145

135146
kwargs = get.call_args[1]
136-
assert kwargs == {"params": {"scopes": "three,four"}}
147+
assert kwargs["params"] == {"scopes": "three,four"}
148+
assert kwargs["headers"] == {
149+
"x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE
150+
}
137151

138152
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
139153
def test_refresh_error(self, get):
@@ -732,11 +746,17 @@ def test_sign_bytes(self, sign, get):
732746
# The JWT token signature is 'signature' encoded in base 64:
733747
assert signature == b"signature"
734748

749+
@mock.patch(
750+
"google.auth.metrics.token_request_id_token_mds",
751+
return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE,
752+
)
735753
@mock.patch(
736754
"google.auth.compute_engine._metadata.get_service_account_info", autospec=True
737755
)
738756
@mock.patch("google.auth.compute_engine._metadata.get", autospec=True)
739-
def test_get_id_token_from_metadata(self, get, get_service_account_info):
757+
def test_get_id_token_from_metadata(
758+
self, get, get_service_account_info, mock_metrics_header_value
759+
):
740760
get.return_value = SAMPLE_ID_TOKEN
741761
get_service_account_info.return_value = {"email": "foo@example.com"}
742762

@@ -745,6 +765,10 @@ def test_get_id_token_from_metadata(self, get, get_service_account_info):
745765
)
746766
cred.refresh(request=mock.Mock())
747767

768+
assert get.call_args.kwargs["headers"] == {
769+
"x-goog-api-client": ID_TOKEN_REQUEST_METRICS_HEADER_VALUE
770+
}
771+
748772
assert cred.token == SAMPLE_ID_TOKEN
749773
assert cred.expiry == datetime.datetime.fromtimestamp(SAMPLE_ID_TOKEN_EXP)
750774
assert cred._use_metadata_identity_endpoint

‎tests/oauth2/test__client.py

+51-8
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,13 @@
4646
" https://www.googleapis.com/auth/logging.write"
4747
)
4848

49+
ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
50+
"gl-python/3.7 auth/1.1 auth-request-type/at cred-type/sa"
51+
)
52+
ID_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
53+
"gl-python/3.7 auth/1.1 auth-request-type/it cred-type/sa"
54+
)
55+
4956

5057
@pytest.mark.parametrize("retryable", [True, False])
5158
def test__handle_error_response(retryable):
@@ -483,47 +490,83 @@ def test_refresh_grant_no_access_token():
483490
assert not excinfo.value.retryable
484491

485492

493+
@mock.patch(
494+
"google.auth.metrics.token_request_access_token_sa_assertion",
495+
return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
496+
)
486497
@mock.patch("google.oauth2._client._parse_expiry", return_value=None)
487498
@mock.patch.object(_client, "_token_endpoint_request", autospec=True)
488-
def test_jwt_grant_retry_default(mock_token_endpoint_request, mock_expiry):
499+
def test_jwt_grant_retry_default(
500+
mock_token_endpoint_request, mock_expiry, mock_metrics_header_value
501+
):
489502
_client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock())
490503
mock_token_endpoint_request.assert_called_with(
491-
mock.ANY, mock.ANY, mock.ANY, can_retry=True
504+
mock.ANY,
505+
mock.ANY,
506+
mock.ANY,
507+
can_retry=True,
508+
headers={"x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE},
492509
)
493510

494511

495512
@pytest.mark.parametrize("can_retry", [True, False])
513+
@mock.patch(
514+
"google.auth.metrics.token_request_access_token_sa_assertion",
515+
return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
516+
)
496517
@mock.patch("google.oauth2._client._parse_expiry", return_value=None)
497518
@mock.patch.object(_client, "_token_endpoint_request", autospec=True)
498519
def test_jwt_grant_retry_with_retry(
499-
mock_token_endpoint_request, mock_expiry, can_retry
520+
mock_token_endpoint_request, mock_expiry, mock_metrics_header_value, can_retry
500521
):
501522
_client.jwt_grant(mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry)
502523
mock_token_endpoint_request.assert_called_with(
503-
mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry
524+
mock.ANY,
525+
mock.ANY,
526+
mock.ANY,
527+
can_retry=can_retry,
528+
headers={"x-goog-api-client": ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE},
504529
)
505530

506531

532+
@mock.patch(
533+
"google.auth.metrics.token_request_id_token_sa_assertion",
534+
return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE,
535+
)
507536
@mock.patch("google.auth.jwt.decode", return_value={"exp": 0})
508537
@mock.patch.object(_client, "_token_endpoint_request", autospec=True)
509-
def test_id_token_jwt_grant_retry_default(mock_token_endpoint_request, mock_jwt_decode):
538+
def test_id_token_jwt_grant_retry_default(
539+
mock_token_endpoint_request, mock_jwt_decode, mock_metrics_header_value
540+
):
510541
_client.id_token_jwt_grant(mock.Mock(), mock.Mock(), mock.Mock())
511542
mock_token_endpoint_request.assert_called_with(
512-
mock.ANY, mock.ANY, mock.ANY, can_retry=True
543+
mock.ANY,
544+
mock.ANY,
545+
mock.ANY,
546+
can_retry=True,
547+
headers={"x-goog-api-client": ID_TOKEN_REQUEST_METRICS_HEADER_VALUE},
513548
)
514549

515550

516551
@pytest.mark.parametrize("can_retry", [True, False])
552+
@mock.patch(
553+
"google.auth.metrics.token_request_id_token_sa_assertion",
554+
return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE,
555+
)
517556
@mock.patch("google.auth.jwt.decode", return_value={"exp": 0})
518557
@mock.patch.object(_client, "_token_endpoint_request", autospec=True)
519558
def test_id_token_jwt_grant_retry_with_retry(
520-
mock_token_endpoint_request, mock_jwt_decode, can_retry
559+
mock_token_endpoint_request, mock_jwt_decode, mock_metrics_header_value, can_retry
521560
):
522561
_client.id_token_jwt_grant(
523562
mock.Mock(), mock.Mock(), mock.Mock(), can_retry=can_retry
524563
)
525564
mock_token_endpoint_request.assert_called_with(
526-
mock.ANY, mock.ANY, mock.ANY, can_retry=can_retry
565+
mock.ANY,
566+
mock.ANY,
567+
mock.ANY,
568+
can_retry=can_retry,
569+
headers={"x-goog-api-client": ID_TOKEN_REQUEST_METRICS_HEADER_VALUE},
527570
)
528571

529572

‎tests/oauth2/test_reauth.py

+28-4
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,12 @@
4040
"encodedProofOfReauthToken": "new_rapt_token",
4141
}
4242

43+
REAUTH_START_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1 auth-request-type/re-start"
44+
REAUTH_CONTINUE_METRICS_HEADER_VALUE = (
45+
"gl-python/3.7 auth/1.1 auth-request-type/re-cont"
46+
)
47+
TOKEN_REQUEST_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1 cred-type/u"
48+
4349

4450
class MockChallenge(object):
4551
def __init__(self, name, locally_eligible, challenge_input):
@@ -56,7 +62,10 @@ def test_is_interactive():
5662
assert reauth.is_interactive()
5763

5864

59-
def test__get_challenges():
65+
@mock.patch(
66+
"google.auth.metrics.reauth_start", return_value=REAUTH_START_METRICS_HEADER_VALUE
67+
)
68+
def test__get_challenges(mock_metrics_header_value):
6069
with mock.patch(
6170
"google.oauth2._client._token_endpoint_request"
6271
) as mock_token_endpoint_request:
@@ -67,10 +76,14 @@ def test__get_challenges():
6776
{"supportedChallengeTypes": ["SAML"]},
6877
access_token="token",
6978
use_json=True,
79+
headers={"x-goog-api-client": REAUTH_START_METRICS_HEADER_VALUE},
7080
)
7181

7282

73-
def test__get_challenges_with_scopes():
83+
@mock.patch(
84+
"google.auth.metrics.reauth_start", return_value=REAUTH_START_METRICS_HEADER_VALUE
85+
)
86+
def test__get_challenges_with_scopes(mock_metrics_header_value):
7487
with mock.patch(
7588
"google.oauth2._client._token_endpoint_request"
7689
) as mock_token_endpoint_request:
@@ -86,10 +99,15 @@ def test__get_challenges_with_scopes():
8699
},
87100
access_token="token",
88101
use_json=True,
102+
headers={"x-goog-api-client": REAUTH_START_METRICS_HEADER_VALUE},
89103
)
90104

91105

92-
def test__send_challenge_result():
106+
@mock.patch(
107+
"google.auth.metrics.reauth_continue",
108+
return_value=REAUTH_CONTINUE_METRICS_HEADER_VALUE,
109+
)
110+
def test__send_challenge_result(mock_metrics_header_value):
93111
with mock.patch(
94112
"google.oauth2._client._token_endpoint_request"
95113
) as mock_token_endpoint_request:
@@ -107,6 +125,7 @@ def test__send_challenge_result():
107125
},
108126
access_token="token",
109127
use_json=True,
128+
headers={"x-goog-api-client": REAUTH_CONTINUE_METRICS_HEADER_VALUE},
110129
)
111130

112131

@@ -270,7 +289,11 @@ def test_get_rapt_token():
270289
)
271290

272291

273-
def test_refresh_grant_failed():
292+
@mock.patch(
293+
"google.auth.metrics.token_request_user",
294+
return_value=TOKEN_REQUEST_METRICS_HEADER_VALUE,
295+
)
296+
def test_refresh_grant_failed(mock_metrics_header_value):
274297
with mock.patch(
275298
"google.oauth2._client._token_endpoint_request_no_throw"
276299
) as mock_token_request:
@@ -299,6 +322,7 @@ def test_refresh_grant_failed():
299322
"scope": "foo bar",
300323
"rapt": "rapt_token",
301324
},
325+
headers={"x-goog-api-client": TOKEN_REQUEST_METRICS_HEADER_VALUE},
302326
)
303327

304328

‎tests/test_aws.py

+20-2
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,10 @@
2828
from google.auth import transport
2929

3030

31+
IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
32+
"gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp"
33+
)
34+
3135
CLIENT_ID = "username"
3236
CLIENT_SECRET = "password"
3337
# Base64 encoding of "username:password".
@@ -1901,8 +1905,14 @@ def test_refresh_success_without_impersonation_use_default_scopes(self, utcnow):
19011905
assert credentials.scopes is None
19021906
assert credentials.default_scopes == SCOPES
19031907

1908+
@mock.patch(
1909+
"google.auth.metrics.token_request_access_token_impersonate",
1910+
return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
1911+
)
19041912
@mock.patch("google.auth._helpers.utcnow")
1905-
def test_refresh_success_with_impersonation_ignore_default_scopes(self, utcnow):
1913+
def test_refresh_success_with_impersonation_ignore_default_scopes(
1914+
self, utcnow, mock_metrics_header_value
1915+
):
19061916
utcnow.return_value = datetime.datetime.strptime(
19071917
self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
19081918
)
@@ -1937,6 +1947,7 @@ def test_refresh_success_with_impersonation_ignore_default_scopes(self, utcnow):
19371947
"Content-Type": "application/json",
19381948
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
19391949
"x-goog-user-project": QUOTA_PROJECT_ID,
1950+
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
19401951
}
19411952
impersonation_request_data = {
19421953
"delegates": None,
@@ -1985,8 +1996,14 @@ def test_refresh_success_with_impersonation_ignore_default_scopes(self, utcnow):
19851996
assert credentials.scopes == SCOPES
19861997
assert credentials.default_scopes == ["ignored"]
19871998

1999+
@mock.patch(
2000+
"google.auth.metrics.token_request_access_token_impersonate",
2001+
return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
2002+
)
19882003
@mock.patch("google.auth._helpers.utcnow")
1989-
def test_refresh_success_with_impersonation_use_default_scopes(self, utcnow):
2004+
def test_refresh_success_with_impersonation_use_default_scopes(
2005+
self, utcnow, mock_metrics_header_value
2006+
):
19902007
utcnow.return_value = datetime.datetime.strptime(
19912008
self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
19922009
)
@@ -2021,6 +2038,7 @@ def test_refresh_success_with_impersonation_use_default_scopes(self, utcnow):
20212038
"Content-Type": "application/json",
20222039
"authorization": "Bearer {}".format(self.SUCCESS_RESPONSE["access_token"]),
20232040
"x-goog-user-project": QUOTA_PROJECT_ID,
2041+
"x-goog-api-client": IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
20242042
}
20252043
impersonation_request_data = {
20262044
"delegates": None,

‎tests/test_external_account.py

+48-6
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,8 @@
2626
from google.auth import transport
2727

2828

29+
METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp"
30+
2931
CLIENT_ID = "username"
3032
CLIENT_SECRET = "password"
3133
# Base64 encoding of "username:password"
@@ -751,7 +753,13 @@ def test_refresh_workforce_with_client_auth_and_no_workforce_project_success(
751753
assert not credentials.expired
752754
assert credentials.token == response["access_token"]
753755

754-
def test_refresh_impersonation_without_client_auth_success(self):
756+
@mock.patch(
757+
"google.auth.metrics.token_request_access_token_impersonate",
758+
return_value=METRICS_HEADER_VALUE,
759+
)
760+
def test_refresh_impersonation_without_client_auth_success(
761+
self, mock_metrics_header_value
762+
):
755763
# Simulate service account access token expires in 2800 seconds.
756764
expire_time = (
757765
_helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
@@ -776,6 +784,7 @@ def test_refresh_impersonation_without_client_auth_success(self):
776784
impersonation_headers = {
777785
"Content-Type": "application/json",
778786
"authorization": "Bearer {}".format(token_response["access_token"]),
787+
"x-goog-api-client": METRICS_HEADER_VALUE,
779788
}
780789
impersonation_request_data = {
781790
"delegates": None,
@@ -815,7 +824,13 @@ def test_refresh_impersonation_without_client_auth_success(self):
815824
assert not credentials.expired
816825
assert credentials.token == impersonation_response["accessToken"]
817826

818-
def test_refresh_workforce_impersonation_without_client_auth_success(self):
827+
@mock.patch(
828+
"google.auth.metrics.token_request_access_token_impersonate",
829+
return_value=METRICS_HEADER_VALUE,
830+
)
831+
def test_refresh_workforce_impersonation_without_client_auth_success(
832+
self, mock_metrics_header_value
833+
):
819834
# Simulate service account access token expires in 2800 seconds.
820835
expire_time = (
821836
_helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
@@ -843,6 +858,7 @@ def test_refresh_workforce_impersonation_without_client_auth_success(self):
843858
impersonation_headers = {
844859
"Content-Type": "application/json",
845860
"authorization": "Bearer {}".format(token_response["access_token"]),
861+
"x-goog-api-client": METRICS_HEADER_VALUE,
846862
}
847863
impersonation_request_data = {
848864
"delegates": None,
@@ -1000,7 +1016,13 @@ def test_refresh_with_client_auth_success(self):
10001016
assert not credentials.expired
10011017
assert credentials.token == self.SUCCESS_RESPONSE["access_token"]
10021018

1003-
def test_refresh_impersonation_with_client_auth_success_ignore_default_scopes(self):
1019+
@mock.patch(
1020+
"google.auth.metrics.token_request_access_token_impersonate",
1021+
return_value=METRICS_HEADER_VALUE,
1022+
)
1023+
def test_refresh_impersonation_with_client_auth_success_ignore_default_scopes(
1024+
self, mock_metrics_header_value
1025+
):
10041026
# Simulate service account access token expires in 2800 seconds.
10051027
expire_time = (
10061028
_helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
@@ -1028,6 +1050,7 @@ def test_refresh_impersonation_with_client_auth_success_ignore_default_scopes(se
10281050
impersonation_headers = {
10291051
"Content-Type": "application/json",
10301052
"authorization": "Bearer {}".format(token_response["access_token"]),
1053+
"x-goog-api-client": METRICS_HEADER_VALUE,
10311054
}
10321055
impersonation_request_data = {
10331056
"delegates": None,
@@ -1071,7 +1094,13 @@ def test_refresh_impersonation_with_client_auth_success_ignore_default_scopes(se
10711094
assert not credentials.expired
10721095
assert credentials.token == impersonation_response["accessToken"]
10731096

1074-
def test_refresh_impersonation_with_client_auth_success_use_default_scopes(self):
1097+
@mock.patch(
1098+
"google.auth.metrics.token_request_access_token_impersonate",
1099+
return_value=METRICS_HEADER_VALUE,
1100+
)
1101+
def test_refresh_impersonation_with_client_auth_success_use_default_scopes(
1102+
self, mock_metrics_header_value
1103+
):
10751104
# Simulate service account access token expires in 2800 seconds.
10761105
expire_time = (
10771106
_helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
@@ -1099,6 +1128,7 @@ def test_refresh_impersonation_with_client_auth_success_use_default_scopes(self)
10991128
impersonation_headers = {
11001129
"Content-Type": "application/json",
11011130
"authorization": "Bearer {}".format(token_response["access_token"]),
1131+
"x-goog-api-client": METRICS_HEADER_VALUE,
11021132
}
11031133
impersonation_request_data = {
11041134
"delegates": None,
@@ -1488,7 +1518,13 @@ def test_project_id_without_scopes(self):
14881518

14891519
assert credentials.get_project_id(None) is None
14901520

1491-
def test_get_project_id_cloud_resource_manager_success(self):
1521+
@mock.patch(
1522+
"google.auth.metrics.token_request_access_token_impersonate",
1523+
return_value=METRICS_HEADER_VALUE,
1524+
)
1525+
def test_get_project_id_cloud_resource_manager_success(
1526+
self, mock_metrics_header_value
1527+
):
14921528
# STS token exchange request/response.
14931529
token_response = self.SUCCESS_RESPONSE.copy()
14941530
token_headers = {"Content-Type": "application/x-www-form-urlencoded"}
@@ -1513,6 +1549,7 @@ def test_get_project_id_cloud_resource_manager_success(self):
15131549
"Content-Type": "application/json",
15141550
"x-goog-user-project": self.QUOTA_PROJECT_ID,
15151551
"authorization": "Bearer {}".format(token_response["access_token"]),
1552+
"x-goog-api-client": METRICS_HEADER_VALUE,
15161553
}
15171554
impersonation_request_data = {
15181555
"delegates": None,
@@ -1638,7 +1675,11 @@ def test_workforce_pool_get_project_id_cloud_resource_manager_success(self):
16381675
# No additional requests.
16391676
assert len(request.call_args_list) == 2
16401677

1641-
def test_refresh_impersonation_with_lifetime(self):
1678+
@mock.patch(
1679+
"google.auth.metrics.token_request_access_token_impersonate",
1680+
return_value=METRICS_HEADER_VALUE,
1681+
)
1682+
def test_refresh_impersonation_with_lifetime(self, mock_metrics_header_value):
16421683
# Simulate service account access token expires in 2800 seconds.
16431684
expire_time = (
16441685
_helpers.utcnow().replace(microsecond=0) + datetime.timedelta(seconds=2800)
@@ -1663,6 +1704,7 @@ def test_refresh_impersonation_with_lifetime(self):
16631704
impersonation_headers = {
16641705
"Content-Type": "application/json",
16651706
"authorization": "Bearer {}".format(token_response["access_token"]),
1707+
"x-goog-api-client": METRICS_HEADER_VALUE,
16661708
}
16671709
impersonation_request_data = {
16681710
"delegates": None,

‎tests/test_identity_pool.py

+9-1
Original file line numberDiff line numberDiff line change
@@ -286,6 +286,9 @@ def assert_underlying_credentials_refresh(
286286
json.dumps({"userProject": workforce_pool_user_project})
287287
)
288288

289+
metrics_header_value = (
290+
"gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp"
291+
)
289292
if service_account_impersonation_url:
290293
# Service account impersonation request/response.
291294
expire_time = (
@@ -299,6 +302,7 @@ def assert_underlying_credentials_refresh(
299302
impersonation_headers = {
300303
"Content-Type": "application/json",
301304
"authorization": "Bearer {}".format(token_response["access_token"]),
305+
"x-goog-api-client": metrics_header_value,
302306
}
303307
impersonation_request_data = {
304308
"delegates": None,
@@ -321,7 +325,11 @@ def assert_underlying_credentials_refresh(
321325

322326
request = cls.make_mock_request(*[el for req in requests for el in req])
323327

324-
credentials.refresh(request)
328+
with mock.patch(
329+
"google.auth.metrics.token_request_access_token_impersonate",
330+
return_value=metrics_header_value,
331+
):
332+
credentials.refresh(request)
325333

326334
assert len(request.call_args_list) == len(requests)
327335
if credential_data:

‎tests/test_impersonated_credentials.py

+46-1
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,13 @@
5555
SIGNER = crypt.RSASigner.from_string(PRIVATE_KEY_BYTES, "1")
5656
TOKEN_URI = "https://example.com/oauth2/token"
5757

58+
ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
59+
"gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp"
60+
)
61+
ID_TOKEN_REQUEST_METRICS_HEADER_VALUE = (
62+
"gl-python/3.7 auth/1.1 auth-request-type/it cred-type/imp"
63+
)
64+
5865

5966
@pytest.fixture
6067
def mock_donor_credentials():
@@ -188,10 +195,18 @@ def test_refresh_success(self, use_data_bytes, mock_donor_credentials):
188195
use_data_bytes=use_data_bytes,
189196
)
190197

191-
credentials.refresh(request)
198+
with mock.patch(
199+
"google.auth.metrics.token_request_access_token_impersonate",
200+
return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
201+
):
202+
credentials.refresh(request)
192203

193204
assert credentials.valid
194205
assert not credentials.expired
206+
assert (
207+
request.call_args.kwargs["headers"]["x-goog-api-client"]
208+
== ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE
209+
)
195210

196211
@pytest.mark.parametrize("use_data_bytes", [True, False])
197212
def test_refresh_success_iam_endpoint_override(
@@ -454,6 +469,36 @@ def test_id_token_success(
454469
assert id_creds.token == ID_TOKEN_DATA
455470
assert id_creds.expiry == datetime.datetime.fromtimestamp(ID_TOKEN_EXPIRY)
456471

472+
def test_id_token_metrics(self, mock_donor_credentials):
473+
credentials = self.make_credentials(lifetime=None)
474+
credentials.token = "token"
475+
credentials.expiry = None
476+
target_audience = "https://foo.bar"
477+
478+
id_creds = impersonated_credentials.IDTokenCredentials(
479+
credentials, target_audience=target_audience
480+
)
481+
482+
with mock.patch(
483+
"google.auth.metrics.token_request_id_token_impersonate",
484+
return_value=ID_TOKEN_REQUEST_METRICS_HEADER_VALUE,
485+
):
486+
with mock.patch(
487+
"google.auth.transport.requests.AuthorizedSession.post", autospec=True
488+
) as mock_post:
489+
data = {"token": ID_TOKEN_DATA}
490+
mock_post.return_value = MockResponse(data, http_client.OK)
491+
id_creds.refresh(None)
492+
493+
assert id_creds.token == ID_TOKEN_DATA
494+
assert id_creds.expiry == datetime.datetime.fromtimestamp(
495+
ID_TOKEN_EXPIRY
496+
)
497+
assert (
498+
mock_post.call_args.kwargs["headers"]["x-goog-api-client"]
499+
== ID_TOKEN_REQUEST_METRICS_HEADER_VALUE
500+
)
501+
457502
def test_id_token_from_credential(
458503
self, mock_donor_credentials, mock_authorizedsession_idtoken
459504
):

0 commit comments

Comments
 (0)
Please sign in to comment.