8000 feat: add experimental GDCH support (#1022) · s2t2/google-auth-library-python@5367aac · GitHub
[go: up one dir, main page]

Skip to content

Commit 5367aac

Browse files
feat: add experimental GDCH support (googleapis#1022)
* feat: add experimental GDCH support * address comments * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * remove quota project id Co-authored-by: Owl Bot <gcf-owl-bot[bot]@users.noreply.github.com>
1 parent 62daa73 commit 5367aac

File tree

7 files changed

+578
-20
lines changed

7 files changed

+578
-20
lines changed

google/auth/_default.py

Lines changed: 45 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -36,11 +36,13 @@
3636
_SERVICE_ACCOUNT_TYPE = "service_account"
3737
_EXTERNAL_ACCOUNT_TYPE = "external_account"
3838
_IMPERSONATED_SERVICE_ACCOUNT_TYPE = "impersonated_service_account"
39+
_GDCH_SERVICE_ACCOUNT_TYPE = "gdch_service_account"
3940
_VALID_TYPES = (
4041
_AUTHORIZED_USER_TYPE,
4142
_SERVICE_ACCOUNT_TYPE,
4243
_EXTERNAL_ACCOUNT_TYPE,
4344
_IMPERSONATED_SERVICE_ACCOUNT_TYPE,
45+
_GDCH_SERVICE_ACCOUNT_TYPE,
4446
)
4547

4648
# Help message when no credentials can be found.
@@ -134,6 +136,8 @@ def load_credentials_from_file(
134136
def _load_credentials_from_info(
135137
filename, info, scopes, default_scopes, quota_project_id, request
136138
):
139+
from google.auth.credentials import CredentialsWithQuotaProject
140+
137141
credential_type = info.get("type")
138142

139143
if credential_type == _AUTHORIZED_USER_TYPE:
@@ -158,14 +162,17 @@ def _load_credentials_from_info(
158162
credentials, project_id = _get_impersonated_service_account_credentials(
159163
filename, info, scopes
160164
)
165+
elif credential_type == _GDCH_SERVICE_ACCOUNT_TYPE:
166+
credentials, project_id = _get_gdch_service_account_credentials(info)
161167
else:
162168
raise exceptions.DefaultCredentialsError(
163169
"The file {file} does not have a valid type. "
164170
"Type is {type}, expected one of {valid_types}.".format(
165171
file=filename, type=credential_type, valid_types=_VALID_TYPES
166172
)
167173
)
168-
credentials = _apply_quota_project_id(credentials, quota_project_id)
174+
if isinstance(credentials, CredentialsWithQuotaProject):
175+
credentials = _apply_quota_project_id(credentials, quota_project_id)
169176
return credentials, project_id
170177

171178

@@ -430,6 +437,36 @@ def _get_impersonated_service_account_credentials(filename, info, scopes):
430437
return credentials, None
431438

432439

440+
def _get_gdch_service_account_credentials(info):
441+
from google.oauth2 import gdch_credentials
442+
443+
k8s_ca_cert_path = info.get("k8s_ca_cert_path")
444+
k8s_cert_path = info.get("k8s_cert_path")
445+
k8s_key_path = info.get("k8s_key_path")
446+
k8s_token_endpoint = info.get("k8s_token_endpoint")
447+
ais_ca_cert_path = info.get("ais_ca_cert_path")
448+
ais_token_endpoint = info.get("ais_token_endpoint")
449+
450+
format_version = info.get("format_version")
451+
if format_version != "v1":
452+
raise exceptions.DefaultCredentialsError(
453+
"format_version is not provided or unsupported. Supported version is: v1"
454+
)
455+
456+
return (
457+
gdch_credentials.ServiceAccountCredentials(
458+
k8s_ca_cert_path,
459+
k8s_cert_path,
460+
k8s_key_path,
461+
k8s_token_endpoint,
462+
ais_ca_cert_path,
463+
ais_token_endpoint,
464+
None,
465+
),
466+
None,
467+
)
468+
469+
433470
def _apply_quota_project_id(credentials, quota_project_id):
434471
if quota_project_id:
435472
credentials = credentials.with_quota_project(quota_project_id)
@@ -465,6 +502,11 @@ def default(scopes=None, request=None, quota_project_id=None, default_scopes=Non
465502
endpoint.
466503
The project ID returned in this case is the one corresponding to the
467504
underlying workload identity pool resource if determinable.
505+
506+
If the environment variable is set to the path of a valid GDCH service
507+
account JSON file (`Google Distributed Cloud Hosted`_), then a GDCH
508+
credential will be returned. The project ID returned is None unless it
509+
is set via `GOOGLE_CLOUD_PROJECT` environment variable.
468510
2. If the `Google Cloud SDK`_ is installed and has application default
469511
credentials set they are loaded and returned.
470512
@@ -499,6 +541,8 @@ def default(scopes=None, request=None, quota_project_id=None, default_scopes=Non
499541
.. _Metadata Service: https://cloud.google.com/compute/docs\
500542
/storing-retrieving-metadata
501543
.. _Cloud Run: https://cloud.google.com/run
544+
.. _Google Distributed Cloud Hosted: https://cloud.google.com/blog/topics\
545+
/hybrid-cloud/announcing-google-distributed-cloud-edge-and-hosted
502546
503547
Example::
504548

google/oauth2/_client.py

Lines changed: 66 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -44,11 +44,13 @@ def _handle_error_response(response_data):
4444
"""Translates an error response into an exception.
4545
4646
Args:
47-
response_data (Mapping): The decoded response data.
47+
response_data (Mapping | str): The decoded response data.
4848
4949
Raises:
5050
google.auth.exceptions.RefreshError: The errors contained in response_data.
5151
"""
52+
if isinstance(response_data, six.string_types):
53+
raise exceptions.RefreshError(response_data)
5254
try:
5355
error_details = "{}: {}".format(
5456
response_data["error"], response_data.get("error_description")
@@ -79,7 +81,13 @@ def _parse_expiry(response_data):
7981

8082

8183
def _token_endpoint_request_no_throw(
82-
request, token_uri, body, access_token=None, use_json=False
84+
request,
85+
token_uri,
86+
body,
87+
access_token=None,
88+
use_json=False,
89+
expected_status_code=http_client.OK,
90+
**kwargs
8391
):
8492
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
8593
This function doesn't throw on response errors.
@@ -93,6 +101,16 @@ def _token_endpoint_request_no_throw(
93101
access_token (Optional(str)): The access token needed to make the request.
94102
use_json (Optional(bool)): Use urlencoded format or json format for the
95103
content type. The default value is False.
104+
expected_status_code (Optional(int)): The expected the status code of
105+
the token response. The default value is 200. We may expect other
106+
status code like 201 for GDCH credentials.
107+
kwargs: Additional arguments passed on to the request method. The
108+
kwargs will be passed to `requests.request` method, see:
109+
https://docs.python-requests.org/en/latest/api/#requests.request.
110+
For example, you can use `cert=("cert_pem_path", "key_pem_path")`
111+
to set up client side SSL certificate, and use
112+
`verify="ca_bundle_path"` to set up the CA certificates for sever
113+
side SSL certificate verification.
96114
97115
Returns:
98116
Tuple(bool, Mapping[str, str]): A boolean indicating if the request is
@@ -112,32 +130,46 @@ def _token_endpoint_request_no_throw(
112130
# retry to fetch token for maximum of two times if any internal failure
113131
# occurs.
114132
while True:
115-
response = request(method="POST", url=token_uri, headers=headers, body=body)
133+
response = request(
134+
method="POST", url=token_uri, headers=headers, body=body, **kwargs
135+
)
116136
response_body = (
117137
response.data.decode("utf-8")
118138
if hasattr(response.data, "decode")
119139
else response.data
120140
)
121-
response_data = json.loads(response_body)
122141

123-
if response.status == http_client.OK:
142+
if response.status == expected_status_code:
143+
# response_body should be a JSON
144+
response_data = json.loads(response_body)
124145
break
125146
else:
126-
error_desc = response_data.get("error_description") or ""
127-
error_code = response_data.get("error") or ""
128-
if (
129-
any(e == "internal_failure" for e in (error_code, error_desc))
130-
and retry < 1
131-
):
132-
retry += 1
133-
continue
134-
return response.status == http_client.OK, response_data
135-
136-
return response.status == http_client.OK, response_data
147+
# For a failed response, response_body could be a string
148+
try:
149+
response_data = json.loads(response_body)
150+
error_desc = response_data.get("error_description") or ""
151+
error_code = response_data.get("error") or ""
152+
if (
153+
any(e == "internal_failure" for e in (error_code, error_desc))
154+
and retry < 1
155+
):
156+
retry += 1
157+
continue
158+
except ValueError:
159+
response_data = response_body
160+
return False, response_data
161+
162+
return response.status == expected_status_code, response_data
137163

138164

139165
def _token_endpoint_request(
140-
request, token_uri, body, access_token=None, use_json=False
166+
request,
167+
token_uri,
168+
body,
169+
access_token=None,
170+
use_json=False,
171+
expected_status_code=http_client.OK,
172+
**kwargs
141173
):
142174
"""Makes a request to the OAuth 2.0 authorization server's token endpoint.
143175
@@ -150,6 +182,16 @@ def _token_endpoint_request(
150182
access_token (Optional(str)): The access token needed to make the request.
151183
use_json (Optional(bool)): Use urlencoded format or json format for the
152184
content type. The default value is False.
185+
expected_status_code (Optional(int)): The expected the status code of
186+
the token response. The default value is 200. We may expect other
187+
status code like 201 for GDCH credentials.
188+
kwargs: Additional arguments passed on to the request method. The
189+
kwargs will be passed to `requests.request` method, see:
190+
https://docs.python-requests.org/en/latest/api/#requests.request.
191+
For example, you can use `cert=("cert_pem_path", "key_pem_path")`
192+
to set up client side SSL certificate, and use
193+
`verify="ca_bundle_path"` to set up the CA certificates for sever
194+
side SSL certificate verification.
153195
154196
Returns:
155197
Mapping[str, str]: The JSON-decoded response data.
@@ -159,7 +201,13 @@ def _token_endpoint_request(
159201
an error.
160202
"""
161203
response_status_ok, response_data = _token_endpoint_request_no_throw(
162-
request, token_uri, body, access_token=access_token, use_json=use_json
204+
request,
205+
token_uri,
206+
body,
207+
access_token=access_token,
208+
use_json=use_json,
209+
expected_status_code=expected_status_code,
210+
**kwargs
163211
)
164212
if not response_status_ok:
165213
_handle_error_response(response_data)

0 commit comments

Comments
 (0)
0