Skip to content

Commit d4e0130

Browse files
aeitzmangcf-owl-bot[bot]lsirac
authored
feat: Add framework for BYOID metrics headers (#1332)
* Add framework for BYOID metrics headers * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * responding to PR comments * fix: changing try catch to if statement * Fix lint and test coverage issue * fix comment --------- Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com> Co-authored-by: Leo <39062083+lsirac@users.noreply.github.com>
1 parent 73e10d9 commit d4e0130

9 files changed

Lines changed: 253 additions & 30 deletions

File tree

packages/google-auth/google/auth/aws.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -742,6 +742,11 @@ def _should_use_metadata_server(self):
742742

743743
return False
744744

745+
def _create_default_metrics_options(self):
746+
metrics_options = super(Credentials, self)._create_default_metrics_options()
747+
metrics_options["source"] = "aws"
748+
return metrics_options
749+
745750
@classmethod
746751
def from_info(cls, info, **kwargs):
747752
"""Creates an AWS Credentials instance from parsed external account info.

packages/google-auth/google/auth/external_account.py

Lines changed: 32 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@
4040
from google.auth import credentials
4141
from google.auth import exceptions
4242
from google.auth import impersonated_credentials
43+
from google.auth import metrics
4344
from google.oauth2 import sts
4445
from google.oauth2 import utils
4546

@@ -140,6 +141,8 @@ def __init__(
140141
self._client_auth = None
141142
self._sts_client = sts.Client(self._token_url, self._client_auth)
142143

144+
self._metrics_options = self._create_default_metrics_options()
145+
143146
if self._service_account_impersonation_url:
144147
self._impersonated_credentials = self._initialize_impersonated_credentials()
145148
else:
@@ -284,7 +287,9 @@ def token_info_url(self):
284287
def with_scopes(self, scopes, default_scopes=None):
285288
kwargs = self._constructor_args()
286289
kwargs.update(scopes=scopes, default_scopes=default_scopes)
287-
return self.__class__(**kwargs)
290+
scoped = self.__class__(**kwargs)
291+
scoped._metrics_options = self._metrics_options
292+
return scoped
288293

289294
@abc.abstractmethod
290295
def retrieve_subject_token(self, request):
@@ -362,6 +367,11 @@ def refresh(self, request):
362367
# is used. The client ID is sufficient for determining the user project.
363368
if self._workforce_pool_user_project and not self._client_id:
364369
additional_options = {"userProject": self._workforce_pool_user_project}
370+
additional_headers = {
371+
metrics.API_CLIENT_HEADER: metrics.byoid_metrics_header(
372+
self._metrics_options
373+
)
374+
}
365375
response_data = self._sts_client.exchange_token(
366376
request=request,
367377
grant_type=_STS_GRANT_TYPE,
@@ -371,6 +381,7 @@ def refresh(self, request):
371381
scopes=scopes,
372382
requested_token_type=_STS_REQUESTED_TOKEN_TYPE,
373383
additional_options=additional_options,
384+
additional_headers=additional_headers,
374385
)
375386
self.token = response_data.get("access_token")
376387
lifetime = datetime.timedelta(seconds=response_data.get("expires_in"))
@@ -381,13 +392,17 @@ def with_quota_project(self, quota_project_id):
381392
# Return copy of instance with the provided quota project ID.
382393
kwargs = self._constructor_args()
383394
kwargs.update(quota_project_id=quota_project_id)
384-
return self.__class__(**kwargs)
395+
new_cred = self.__class__(**kwargs)
396+
new_cred._metrics_options = self._metrics_options
397+
return new_cred
385398

386399
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
387400
def with_token_uri(self, token_uri):
388401
kwargs = self._constructor_args()
389402
kwargs.update(token_url=token_uri)
390-
return self.__class__(**kwargs)
403+
new_cred = self.__class__(**kwargs)
404+
new_cred._metrics_options = self._metrics_options
405+
return new_cred
391406

392407
def _initialize_impersonated_credentials(self):
393408
"""Generates an impersonated credentials.
@@ -411,6 +426,7 @@ def _initialize_impersonated_credentials(self):
411426
service_account_impersonation_options={},
412427
)
413428
source_credentials = self.__class__(**kwargs)
429+
source_credentials._metrics_options = self._metrics_options
414430

415431
# Determine target_principal.
416432
target_principal = self.service_account_email
@@ -432,6 +448,19 @@ def _initialize_impersonated_credentials(self):
432448
),
433449
)
434450

451+
def _create_default_metrics_options(self):
452+
metrics_options = {}
453+
if self._service_account_impersonation_url:
454+
metrics_options["sa-impersonation"] = "true"
455+
else:
456+
metrics_options["sa-impersonation"] = "false"
457+
if self._service_account_impersonation_options.get("token_lifetime_seconds"):
458+
metrics_options["config-lifetime"] = "true"
459+
else:
460+
metrics_options["config-lifetime"] = "false"
461+
462+
return metrics_options
463+
435464
@classmethod
436465
def from_info(cls, info, **kwargs):
437466
"""Creates a Credentials instance from parsed external account info.

packages/google-auth/google/auth/identity_pool.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -216,6 +216,18 @@ def _parse_token_data(
216216
)
217217
return token
218218

219+
def _create_default_metrics_options(self):
220+
metrics_options = super(Credentials, self)._create_default_metrics_options()
221+
# Check that credential source is a dict before checking for file vs url. This check needs to be done
222+
# here because the external_account credential constructor needs to pass the metrics options to the
223+
# impersonated credential object before the identity_pool credentials are validated.
224+
if isinstance(self._credential_source, Mapping):
225+
if self._credential_source.get("file"):
226+
metrics_options["source"] = "file"
227+
else:
228+
metrics_options["source"] = "url"
229+
return metrics_options
230+
219231
@classmethod
220232
def from_info(cls, info, **kwargs):
221233
"""Creates an Identity Pool Credentials instance from parsed external account info.

packages/google-auth/google/auth/metrics.py

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,9 @@
2323

2424
API_CLIENT_HEADER = "x-goog-api-client"
2525

26+
# BYOID Specific consts
27+
BYOID_HEADER_SECTION = "google-byoid-sdk"
28+
2629
# Auth request type
2730
REQUEST_TYPE_ACCESS_TOKEN = "auth-request-type/at"
2831
REQUEST_TYPE_ID_TOKEN = "auth-request-type/it"
@@ -123,6 +126,15 @@ def reauth_continue():
123126
return "{} {}".format(python_and_auth_lib_version(), REQUEST_TYPE_REAUTH_CONTINUE)
124127

125128

129+
# x-goog-api-client header value for BYOID calls to the Security Token Service exchange token endpoint.
130+
# Example: "gl-python/3.7 auth/1.1 google-byoid-sdk source/aws sa-impersonation/true sa-impersonation/true"
131+
def byoid_metrics_header(metrics_options):
132+
header = "{} {}".format(python_and_auth_lib_version(), BYOID_HEADER_SECTION)
133+
for key, value in metrics_options.items():
134+
header = "{} {}/{}".format(header, key, value)
135+
return header
136+
137+
126138
def add_metric_header(headers, metric_header_value):
127139
"""Add x-goog-api-client header with the given value.
128140

packages/google-auth/google/auth/pluggable.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -422,3 +422,8 @@ def _validate_running_mode(self):
422422
raise exceptions.InvalidValue(
423423
"Interactive mode is only enabled for workforce pool."
424424
)
425+
426+
def _create_default_metrics_options(self):
427+
metrics_options = super(Credentials, self)._create_default_metrics_options()
428+
metrics_options["source"] = "executable"
429+
return metrics_options

packages/google-auth/tests/test_aws.py

Lines changed: 30 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,8 @@
3232
"gl-python/3.7 auth/1.1 auth-request-type/at cred-type/imp"
3333
)
3434

35+
LANG_LIBRARY_METRICS_HEADER_VALUE = "gl-python/3.7 auth/1.1"
36+
3537
CLIENT_ID = "username"
3638
CLIENT_SECRET = "password"
3739
# Base64 encoding of "username:password".
@@ -1793,8 +1795,14 @@ def test_retrieve_subject_token_error_determining_aws_security_creds(self):
17931795

17941796
assert excinfo.match(r"Unable to retrieve AWS security credentials")
17951797

1798+
@mock.patch(
1799+
"google.auth.metrics.python_and_auth_lib_version",
1800+
return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
1801+
)
17961802
@mock.patch("google.auth._helpers.utcnow")
1797-
def test_refresh_success_without_impersonation_ignore_default_scopes(self, utcnow):
1803+
def test_refresh_success_without_impersonation_ignore_default_scopes(
1804+
self, utcnow, mock_auth_lib_value
1805+
):
17981806
utcnow.return_value = datetime.datetime.strptime(
17991807
self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
18001808
)
@@ -1808,6 +1816,7 @@ def test_refresh_success_without_impersonation_ignore_default_scopes(self, utcno
18081816
token_headers = {
18091817
"Content-Type": "application/x-www-form-urlencoded",
18101818
"Authorization": "Basic " + BASIC_AUTH_ENCODING,
1819+
"x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false source/aws",
18111820
}
18121821
token_request_data = {
18131822
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
@@ -1849,8 +1858,14 @@ def test_refresh_success_without_impersonation_ignore_default_scopes(self, utcno
18491858
assert credentials.scopes == SCOPES
18501859
assert credentials.default_scopes == ["ignored"]
18511860

1861+
@mock.patch(
1862+
"google.auth.metrics.python_and_auth_lib_version",
1863+
return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
1864+
)
18521865
@mock.patch("google.auth._helpers.utcnow")
1853-
def test_refresh_success_without_impersonation_use_default_scopes(self, utcnow):
1866+
def test_refresh_success_without_impersonation_use_default_scopes(
1867+
self, utcnow, mock_auth_lib_value
1868+
):
18541869
utcnow.return_value = datetime.datetime.strptime(
18551870
self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
18561871
)
@@ -1864,6 +1879,7 @@ def test_refresh_success_without_impersonation_use_default_scopes(self, utcnow):
18641879
token_headers = {
18651880
"Content-Type": "application/x-www-form-urlencoded",
18661881
"Authorization": "Basic " + BASIC_AUTH_ENCODING,
1882+
"x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/false config-lifetime/false source/aws",
18671883
}
18681884
token_request_data = {
18691885
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
@@ -1909,9 +1925,13 @@ def test_refresh_success_without_impersonation_use_default_scopes(self, utcnow):
19091925
"google.auth.metrics.token_request_access_token_impersonate",
19101926
return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
19111927
)
1928+
@mock.patch(
1929+
"google.auth.metrics.python_and_auth_lib_version",
1930+
return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
1931+
)
19121932
@mock.patch("google.auth._helpers.utcnow")
19131933
def test_refresh_success_with_impersonation_ignore_default_scopes(
1914-
self, utcnow, mock_metrics_header_value
1934+
self, utcnow, mock_metrics_header_value, mock_auth_lib_value
19151935
):
19161936
utcnow.return_value = datetime.datetime.strptime(
19171937
self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
@@ -1929,6 +1949,7 @@ def test_refresh_success_with_impersonation_ignore_default_scopes(
19291949
token_headers = {
19301950
"Content-Type": "application/x-www-form-urlencoded",
19311951
"Authorization": "Basic " + BASIC_AUTH_ENCODING,
1952+
"x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false source/aws",
19321953
}
19331954
token_request_data = {
19341955
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",
@@ -2000,9 +2021,13 @@ def test_refresh_success_with_impersonation_ignore_default_scopes(
20002021
"google.auth.metrics.token_request_access_token_impersonate",
20012022
return_value=IMPERSONATE_ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
20022023
)
2024+
@mock.patch(
2025+
"google.auth.metrics.python_and_auth_lib_version",
2026+
return_value=LANG_LIBRARY_METRICS_HEADER_VALUE,
2027+
)
20032028
@mock.patch("google.auth._helpers.utcnow")
20042029
def test_refresh_success_with_impersonation_use_default_scopes(
2005-
self, utcnow, mock_metrics_header_value
2030+
self, utcnow, mock_metrics_header_value, mock_auth_lib_value
20062031
):
20072032
utcnow.return_value = datetime.datetime.strptime(
20082033
self.AWS_SIGNATURE_TIME, "%Y-%m-%dT%H:%M:%SZ"
@@ -2020,6 +2045,7 @@ def test_refresh_success_with_impersonation_use_default_scopes(
20202045
token_headers = {
20212046
"Content-Type": "application/x-www-form-urlencoded",
20222047
"Authorization": "Basic " + BASIC_AUTH_ENCODING,
2048+
"x-goog-api-client": "gl-python/3.7 auth/1.1 google-byoid-sdk sa-impersonation/true config-lifetime/false source/aws",
20232049
}
20242050
token_request_data = {
20252051
"grant_type": "urn:ietf:params:oauth:grant-type:token-exchange",

0 commit comments

Comments
 (0)