Skip to content

Commit 821c1b6

Browse files
feat: universe domain support for service account (#1286)
* feat: universe domain support for service account * update * update * update * update token
1 parent b5a3929 commit 821c1b6

File tree

4 files changed

+250
-104
lines changed

4 files changed

+250
-104
lines changed

google/oauth2/service_account.py

Lines changed: 111 additions & 100 deletions
Original file line numberDiff line numberDiff line change
@@ -76,10 +76,12 @@
7676
from google.auth import _helpers
7777
from google.auth import _service_account_info
7878
from google.auth import credentials
79+
from google.auth import exceptions
7980
from google.auth import jwt
8081
from google.oauth2 import _client
8182

8283
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
84+
_DEFAULT_UNIVERSE_DOMAIN = "googleapis.com"
8385
_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token"
8486

8587

@@ -136,6 +138,7 @@ def __init__(
136138
quota_project_id=None,
137139
additional_claims=None,
138140
always_use_jwt_access=False,
141+
universe_domain=_DEFAULT_UNIVERSE_DOMAIN,
139142
):
140143
"""
141144
Args:
@@ -156,6 +159,9 @@ def __init__(
156159
the JWT assertion used in the authorization grant.
157160
always_use_jwt_access (Optional[bool]): Whether self signed JWT should
158161
be always used.
162+
universe_domain (str): The universe domain. The default
163+
universe domain is googleapis.com. For default value self
164+
signed jwt is used for token refresh.
159165
160166
.. note:: Typically one of the helper constructors
161167
:meth:`from_service_account_file` or
@@ -173,6 +179,13 @@ def __init__(
173179
self._quota_project_id = quota_project_id
174180
self._token_uri = token_uri
175181
self._always_use_jwt_access = always_use_jwt_access
182+
if not universe_domain:
183+
self._universe_domain = _DEFAULT_UNIVERSE_DOMAIN
184+
else:
185+
self._universe_domain = universe_domain
186+
187+
if universe_domain != _DEFAULT_UNIVERSE_DOMAIN:
188+
self._always_use_jwt_access = True
176189

177190
self._jwt_credentials = None
178191

@@ -202,6 +215,7 @@ def _from_signer_and_info(cls, signer, info, **kwargs):
202215
service_account_email=info["client_email"],
203216
token_uri=info["token_uri"],
204217
project_id=info.get("project_id"),
218+
universe_domain=info.get("universe_domain", _DEFAULT_UNIVERSE_DOMAIN),
205219
**kwargs
206220
)
207221

@@ -262,20 +276,28 @@ def requires_scopes(self):
262276
"""
263277
return True if not self._scopes else False
264278

265-
@_helpers.copy_docstring(credentials.Scoped)
266-
def with_scopes(self, scopes, default_scopes=None):
267-
return self.__class__(
279+
def _make_copy(self):
280+
cred = self.__class__(
268281
self._signer,
269282
service_account_email=self._service_account_email,
270-
scopes=scopes,
271-
default_scopes=default_scopes,
283+
scopes=copy.copy(self._scopes),
284+
default_scopes=copy.copy(self._default_scopes),
272285
token_uri=self._token_uri,
273286
subject=self._subject,
274287
project_id=self._project_id,
275288
quota_project_id=self._quota_project_id,
276289
additional_claims=self._additional_claims.copy(),
277290
always_use_jwt_access=self._always_use_jwt_access,
291+
universe_domain=self._universe_domain,
278292
)
293+
return cred
294+
295+
@_helpers.copy_docstring(credentials.Scoped)
296+
def with_scopes(self, scopes, default_scopes=None):
297+
cred = self._make_copy()
298+
cred._scopes = scopes
299+
cred._default_scopes = default_scopes
300+
return cred
279301

280302
def with_always_use_jwt_access(self, always_use_jwt_access):
281303
"""Create a copy of these credentials with the specified always_use_jwt_access value.
@@ -286,19 +308,20 @@ def with_always_use_jwt_access(self, always_use_jwt_access):
286308
Returns:
287309
google.auth.service_account.Credentials: A new credentials
288310
instance.
311+
Raises:
312+
google.auth.exceptions.InvalidValue: If the universe domain is not
313+
default and always_use_jwt_access is False.
289314
"""
290-
return self.__class__(
291-
self._signer,
292-
service_account_email=self._service_account_email,
293-
scopes=self._scopes,
294-
default_scopes=self._default_scopes,
295-
token_uri=self._token_uri,
296-
subject=self._subject,
297-
project_id=self._project_id,
298-
quota_project_id=self._quota_project_id,
299-
additional_claims=self._additional_claims.copy(),
300-
always_use_jwt_access=always_use_jwt_access,
301-
)
315+
cred = self._make_copy()
316+
if (
317+
cred._universe_domain != _DEFAULT_UNIVERSE_DOMAIN
318+
and not always_use_jwt_access
319+
):
320+
raise exceptions.InvalidValue(
321+
"always_use_jwt_access should be True for non-default universe domain"
322+
)
323+
cred._always_use_jwt_access = always_use_jwt_access
324+
return cred
302325

303326
def with_subject(self, subject):
304327
"""Create a copy of these credentials with the specified subject.
@@ -310,18 +333,9 @@ def with_subject(self, subject):
310333
google.auth.service_account.Credentials: A new credentials
311334
instance.
312335
"""
313-
return self.__class__(
314-
self._signer,
315-
service_account_email=self._service_account_email,
316-
scopes=self._scopes,
317-
default_scopes=self._default_scopes,
318-
token_uri=self._token_uri,
319-
subject=subject,
320-
project_id=self._project_id,
321-
quota_project_id=self._quota_project_id,
322-
additional_claims=self._additional_claims.copy(),
323-
always_use_jwt_access=self._always_use_jwt_access,
324-
)
336+
cred = self._make_copy()
337+
cred._subject = subject
338+
return cred
325339

326340
def with_claims(self, additional_claims):
327341
"""Returns a copy of these credentials with modified claims.
@@ -337,51 +351,21 @@ def with_claims(self, additional_claims):
337351
"""
338352
new_additional_claims = copy.deepcopy(self._additional_claims)
339353
new_additional_claims.update(additional_claims or {})
340-
341-
return self.__class__(
342-
self._signer,
343-
service_account_email=self._service_account_email,
344-
scopes=self._scopes,
345-
default_scopes=self._default_scopes,
346-
token_uri=self._token_uri,
347-
subject=self._subject,
348-
project_id=self._project_id,
349-
quota_project_id=self._quota_project_id,
350-
additional_claims=new_additional_claims,
351-
always_use_jwt_access=self._always_use_jwt_access,
352-
)
354+
cred = self._make_copy()
355+
cred._additional_claims = new_additional_claims
356+
return cred
353357

354358
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
355359
def with_quota_project(self, quota_project_id):
356-
357-
return self.__class__(
358-
self._signer,
359-
service_account_email=self._service_account_email,
360-
default_scopes=self._default_scopes,
361-
scopes=self._scopes,
362-
token_uri=self._token_uri,
363-
subject=self._subject,
364-
project_id=self._project_id,
365-
quota_project_id=quota_project_id,
366-
additional_claims=self._additional_claims.copy(),
367-
always_use_jwt_access=self._always_use_jwt_access,
368-
)
360+
cred = self._make_copy()
361+
cred._quota_project_id = quota_project_id
362+
return cred
369363

370364
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
371365
def with_token_uri(self, token_uri):
372-
373-
return self.__class__(
374-
self._signer,
375-
service_account_email=self._service_account_email,
376-
default_scopes=self._default_scopes,
377-
scopes=self._scopes,
378-
token_uri=token_uri,
379-
subject=self._subject,
380-
project_id=self._project_id,
381-
quota_project_id=self._quota_project_id,
382-
additional_claims=self._additional_claims.copy(),
383-
always_use_jwt_access=self._always_use_jwt_access,
384-
)
366+
cred = self._make_copy()
367+
cred._token_uri = token_uri
368+
return cred
385369

386370
def _make_authorization_grant_assertion(self):
387371
"""Create the OAuth 2.0 assertion.
@@ -418,6 +402,18 @@ def _make_authorization_grant_assertion(self):
418402

419403
@_helpers.copy_docstring(credentials.Credentials)
420404
def refresh(self, request):
405+
if (
406+
self._universe_domain != _DEFAULT_UNIVERSE_DOMAIN
407+
and not self._jwt_credentials
408+
):
409+
raise exceptions.RefreshError(
410+
"self._jwt_credentials is missing for non-default universe domain"
411+
)
412+
if self._universe_domain != _DEFAULT_UNIVERSE_DOMAIN and self._subject:
413+
raise exceptions.RefreshError(
414+
"domain wide delegation is not supported for non-default universe domain"
415+
)
416+
421417
# Since domain wide delegation doesn't work with self signed JWT. If
422418
# subject exists, then we should not use self signed JWT.
423419
if self._subject is None and self._jwt_credentials is not None:
@@ -544,6 +540,7 @@ def __init__(
544540
target_audience,
545541
additional_claims=None,
546542
quota_project_id=None,
543+
universe_domain=_DEFAULT_UNIVERSE_DOMAIN,
547544
):
548545
"""
549546
Args:
@@ -556,6 +553,11 @@ def __init__(
556553
additional_claims (Mapping[str, str]): Any additional claims for
557554
the JWT assertion used in the authorization grant.
558555
quota_project_id (Optional[str]): The project ID used for quota and billing.
556+
universe_domain (str): The universe domain. The default
557+
universe domain is googleapis.com. For default value IAM ID
558+
token endponint is used for token refresh. Note that
559+
iam.serviceAccountTokenCreator role is required to use the IAM
560+
endpoint.
559561
.. note:: Typically one of the helper constructors
560562
:meth:`from_service_account_file` or
561563
:meth:`from_service_account_info` are used instead of calling the
@@ -569,6 +571,14 @@ def __init__(
569571
self._quota_project_id = quota_project_id
570572
self._use_iam_endpoint = False
571573

574+
if not universe_domain:
575+
self._universe_domain = _DEFAULT_UNIVERSE_DOMAIN
576+
else:
577+
self._universe_domain = universe_domain
578+
579+
if universe_domain != _DEFAULT_UNIVERSE_DOMAIN:
580+
self._use_iam_endpoint = True
581+
572582
if additional_claims is not None:
573583
self._additional_claims = additional_claims
574584
else:
@@ -592,6 +602,8 @@ def _from_signer_and_info(cls, signer, info, **kwargs):
592602
"""
593603
kwargs.setdefault("service_account_email", info["client_email"])
594604
kwargs.setdefault("token_uri", info["token_uri"])
605+
if "universe_domain" in info:
606+
kwargs["universe_domain"] = info["universe_domain"]
595607
return cls(signer, **kwargs)
596608

597609
@classmethod
@@ -632,6 +644,20 @@ def from_service_account_file(cls, filename, **kwargs):
632644
)
633645
return cls._from_signer_and_info(signer, info, **kwargs)
634646

647+
def _make_copy(self):
648+
cred = self.__class__(
649+
self._signer,
650+
service_account_email=self._service_account_email,
651+
token_uri=self._token_uri,
652+
target_audience=self._target_audience,
653+
additional_claims=self._additional_claims.copy(),
654+
quota_project_id=self.quota_project_id,
655+
universe_domain=self._universe_domain,
656+
)
657+
# _use_iam_endpoint is not exposed in the constructor
658+
cred._use_iam_endpoint = self._use_iam_endpoint
659+
return cred
660+
635661
def with_target_audience(self, target_audience):
636662
"""Create a copy of these credentials with the specified target
637663
audience.
@@ -644,14 +670,9 @@ def with_target_audience(self, target_audience):
644670
google.auth.service_account.IDTokenCredentials: A new credentials
645671
instance.
646672
"""
647-
return self.__class__(
648-
self._signer,
649-
service_account_email=self._service_account_email,
650-
token_uri=self._token_uri,
651-
target_audience=target_audience,
652-
additional_claims=self._additional_claims.copy(),
653-
quota_project_id=self.quota_project_id,
654-
)
673+
cred = self._make_copy()
674+
cred._target_audience = target_audience
675+
return cred
655676

656677
def _with_use_iam_endpoint(self, use_iam_endpoint):
657678
"""Create a copy of these credentials with the use_iam_endpoint value.
@@ -666,39 +687,29 @@ def _with_use_iam_endpoint(self, use_iam_endpoint):
666687
Returns:
667688
google.auth.service_account.IDTokenCredentials: A new credentials
668689
instance.
690+
Raises:
691+
google.auth.exceptions.InvalidValue: If the universe domain is not
692+
default and use_iam_endpoint is False.
669693
"""
670-
cred = self.__class__(
671-
self._signer,
672-
service_account_email=self._service_account_email,
673-
token_uri=self._token_uri,
674-
target_audience=self._target_audience,
675-
additional_claims=self._additional_claims.copy(),
676-
quota_project_id=self.quota_project_id,
677-
)
694+
cred = self._make_copy()
695+
if cred._universe_domain != _DEFAULT_UNIVERSE_DOMAIN and not use_iam_endpoint:
696+
raise exceptions.InvalidValue(
697+
"use_iam_endpoint should be True for non-default universe domain"
698+
)
678699
cred._use_iam_endpoint = use_iam_endpoint
679700
return cred
680701

681702
@_helpers.copy_docstring(credentials.CredentialsWithQuotaProject)
682703
def with_quota_project(self, quota_project_id):
683-
return self.__class__(
684-
self._signer,
685-
service_account_email=self._service_account_email,
686-
token_uri=self._token_uri,
687-
target_audience=self._target_audience,
688-
additional_claims=self._additional_claims.copy(),
689-
quota_project_id=quota_project_id,
690-
)
704+
cred = self._make_copy()
705+
cred._quota_project_id = quota_project_id
706+
return cred
691707

692708
@_helpers.copy_docstring(credentials.CredentialsWithTokenUri)
693709
def with_token_uri(self, token_uri):
694-
return self.__class__(
695-
self._signer,
696-
service_account_email=self._service_account_email,
697-
token_uri=token_uri,
698-
target_audience=self._target_audience,
699-
additional_claims=self._additional_claims.copy(),
700-
quota_project_id=self._quota_project_id,
701-
)
710+
cred = self._make_copy()
711+
cred._token_uri = token_uri
712+
return cred
702713

703714
def _make_authorization_grant_assertion(self):
704715
"""Create the OAuth 2.0 assertion.

system_tests/secrets.tar.enc

0 Bytes
Binary file not shown.
Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
{
2+
"type": "service_account",
3+
"universe_domain": "universe.foo",
4+
"project_id": "example_project",
5+
"private_key_id": "1",
6+
"private_key": "-----BEGIN RSA PRIVATE KEY-----\nMIIEpAIBAAKCAQEA4ej0p7bQ7L/r4rVGUz9RN4VQWoej1Bg1mYWIDYslvKrk1gpj\n7wZgkdmM7oVK2OfgrSj/FCTkInKPqaCR0gD7K80q+mLBrN3PUkDrJQZpvRZIff3/\nxmVU1WeruQLFJjnFb2dqu0s/FY/2kWiJtBCakXvXEOb7zfbINuayL+MSsCGSdVYs\nSliS5qQpgyDap+8b5fpXZVJkq92hrcNtbkg7hCYUJczt8n9hcCTJCfUpApvaFQ18\npe+zpyl4+WzkP66I28hniMQyUlA1hBiskT7qiouq0m8IOodhv2fagSZKjOTTU2xk\nSBc//fy3ZpsL7WqgsZS7Q+0VRK8gKfqkxg5OYQIDAQABAoIBAQDGGHzQxGKX+ANk\nnQi53v/c6632dJKYXVJC+PDAz4+bzU800Y+n/bOYsWf/kCp94XcG4Lgsdd0Gx+Zq\nHD9CI1IcqqBRR2AFscsmmX6YzPLTuEKBGMW8twaYy3utlFxElMwoUEsrSWRcCA1y\nnHSDzTt871c7nxCXHxuZ6Nm/XCL7Bg8uidRTSC1sQrQyKgTPhtQdYrPQ4WZ1A4J9\nIisyDYmZodSNZe5P+LTJ6M1SCgH8KH9ZGIxv3diMwzNNpk3kxJc9yCnja4mjiGE2\nYCNusSycU5IhZwVeCTlhQGcNeV/skfg64xkiJE34c2y2ttFbdwBTPixStGaF09nU\nZ422D40BAoGBAPvVyRRsC3BF+qZdaSMFwI1yiXY7vQw5+JZh01tD28NuYdRFzjcJ\nvzT2n8LFpj5ZfZFvSMLMVEFVMgQvWnN0O6xdXvGov6qlRUSGaH9u+TCPNnIldjMP\nB8+xTwFMqI7uQr54wBB+Poq7dVRP+0oHb0NYAwUBXoEuvYo3c/nDoRcZAoGBAOWl\naLHjMv4CJbArzT8sPfic/8waSiLV9Ixs3Re5YREUTtnLq7LoymqB57UXJB3BNz/2\neCueuW71avlWlRtE/wXASj5jx6y5mIrlV4nZbVuyYff0QlcG+fgb6pcJQuO9DxMI\naqFGrWP3zye+LK87a6iR76dS9vRU+bHZpSVvGMKJAoGAFGt3TIKeQtJJyqeUWNSk\nklORNdcOMymYMIlqG+JatXQD1rR6ThgqOt8sgRyJqFCVT++YFMOAqXOBBLnaObZZ\nCFbh1fJ66BlSjoXff0W+SuOx5HuJJAa5+WtFHrPajwxeuRcNa8jwxUsB7n41wADu\nUqWWSRedVBg4Ijbw3nWwYDECgYB0pLew4z4bVuvdt+HgnJA9n0EuYowVdadpTEJg\nsoBjNHV4msLzdNqbjrAqgz6M/n8Ztg8D2PNHMNDNJPVHjJwcR7duSTA6w2p/4k28\nbvvk/45Ta3XmzlxZcZSOct3O31Cw0i2XDVc018IY5be8qendDYM08icNo7vQYkRH\n504kQQKBgQDjx60zpz8ozvm1XAj0wVhi7GwXe+5lTxiLi9Fxq721WDxPMiHDW2XL\nYXfFVy/9/GIMvEiGYdmarK1NW+VhWl1DC5xhDg0kvMfxplt4tynoq1uTsQTY31Mx\nBeF5CT/JuNYk3bEBF0H/Q3VGO1/ggVS+YezdFbLWIRoMnLj6XCFEGg==\n-----END RSA PRIVATE KEY-----\n",
7+
"client_email": "[email protected]",
8+
"client_id": "1234",
9+
"auth_uri": "https://accounts.google.com/o/oauth2/auth",
10+
"token_uri": "https://oauth2.universe.foo/token",
11+
"auth_provider_x509_cert_url": "https://www.universe.foo/oauth2/v1/certs",
12+
"client_x509_cert_url": "https://www.universe.foo/robot/v1/metadata/x509/foo.iam.gserviceaccount.com"
13+
}
14+
15+

0 commit comments

Comments
 (0)