8000 feat: add quota project to base credentials class (#546) · akx/google-auth-library-python@3dda7b2 · GitHub
[go: up one dir, main page]

Skip to content

Commit 3dda7b2

Browse files
authored
feat: add quota project to base credentials class (googleapis#546)
1 parent 218a159 commit 3dda7b2

21 files changed

+482
-83
lines changed

.gitignore

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -13,6 +13,8 @@ docs/_build
1313
.tox/
1414
.cache/
1515
.pytest_cache/
16+
cert_path
17+
key_path
1618

1719
# Django test database
1820
db.sqlite3

google/auth/_default.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -69,7 +69,7 @@ def _warn_about_problematic_credentials(credentials):
6969
warnings.warn(_CLOUD_SDK_CREDENTIALS_WARNING)
7070

7171

72-
def load_credentials_from_file(filename, scopes=None):
72+
def load_credentials_from_file(filename, scopes=None, quota_project_id=None):
7373
"""Loads Google credentials from a file.
7474
7575
The credentials file must be a service account key or stored authorized
@@ -79,7 +79,9 @@ def load_credentials_from_file(filename, scopes=None):
7979
filename (str): The full path to the credentials file.
8080
scopes (Optional[Sequence[str]]): The list of scopes for the credentials. If
8181
specified, the credentials will automatically be scoped if
82-
necessary.
82+
B41A necessary
83+
quota_project_id (Optional[str]): The project ID used for
84+
quota and billing.
8385
8486
Returns:
8587
Tuple[google.auth.credentials.Credentials, Optional[str]]: Loaded
@@ -114,7 +116,7 @@ def load_credentials_from_file(filename, scopes=None):
114116
try:
115117
credentials = credentials.Credentials.from_authorized_user_info(
116118
info, scopes=scopes
117-
)
119+
).with_quota_project(quota_project_id)
118120
except ValueError as caught_exc:
119121
msg = "Failed to load authorized user credentials from {}".format(filename)
120122
new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
@@ -129,7 +131,7 @@ def load_credentials_from_file(filename, scopes=None):
129131
try:
130132
credentials = service_account.Credentials.from_service_account_info(
131133
info, scopes=scopes
132-
)
134+
).with_quota_project(quota_project_id)
133135
except ValueError as caught_exc:
134136
msg = "Failed to load service account credentials from {}".format(filename)
135137
new_exc = exceptions.DefaultCredentialsError(msg, caught_exc)
@@ -226,7 +228,7 @@ def _get_gce_credentials(request=None):
226228
return None, None
227229

228230

229-
def default(scopes=None, request=None):
231+
def default(scopes=None, request=None, quota_project_id=None):
230232
"""Gets the default credentials for the current environment.
231233
232234
`Application Default Credentials`_ provides an easy way to obtain
@@ -286,7 +288,8 @@ def default(scopes=None, request=None):
286288
HTTP requests. This is used to detect whether the application
287289
is running on Compute Engine. If not specified, then it will
288290
use the standard library http client to make requests.
289-
291+
quota_project_id (Optional[str]): The project ID used for
292+
quota and billing.
290293
Returns:
291294
Tuple[~google.auth.credentials.Credentials, Optional[str]]:
292295
the current environment's credentials and project ID. Project ID
@@ -314,7 +317,9 @@ def default(scopes=None, request=None):
314317
for checker in checkers:
315318
credentials, project_id = checker()
316319
if credentials is not None:
317-
credentials = with_scopes_if_required(credentials, scopes)
320+
credentials = with_scopes_if_required(
321+
credentials, scopes
322+
).with_quota_project(quota_project_id)
318323
effective_project_id = explicit_project_id or project_id
319324
if not effective_project_id:
320325
_LOGGER.warning(

google/auth/app_engine.py

Lines changed: 15 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,7 +84,7 @@ class Credentials(credentials.Scoped, credentials.Signing, credentials.Credentia
8484
tokens.
8585
"""
8686

87-
def __init__(self, scopes=None, service_account_id=None):
87+
def __init__(self, scopes=None, service_account_id=None, quota_project_id=None):
8888
"""
8989
Args:
9090
scopes (Sequence[str]): Scopes to request from the App Identity
@@ -93,6 +93,8 @@ def __init__(self, scopes=None, service_account_id=None):
9393
:func:`google.appengine.api.app_identity.get_access_token`.
9494
If not specified, the default application service account
9595
ID will be used.
96+
quota_project_id (Optional[str]): The project ID used for quota
97+
and billing.
9698
9799
Raises:
98100
EnvironmentError: If the App Engine APIs are unavailable.
@@ -107,6 +109,7 @@ def __init__(self, scopes=None, service_account_id=None):
107109
self._scopes = scopes
108110
self._service_account_id = service_account_id
109111
self._signer = Signer()
112+
self._quota_project_id = quota_project_id
110113

111114
@_helpers.copy_docstring(credentials.Credentials)
112115
def refresh(self, request):
@@ -137,7 +140,17 @@ def requires_scopes(self):
137140
@_helpers.copy_docstring(credentials.Scoped)
138141
def with_scopes(self, scopes):
139142
return self.__class__(
140-
scopes=scopes, service_account_id=self._service_account_id
143+
scopes=scopes,
144+
service_account_id=self._service_account_id,
145+
quota_project_id=self.quota_project_id,
146+
)
147+
148+
@_helpers.copy_docstring(credentials.Credentials)
149+
def with_quota_project(self, quota_project_id):
150+
return self.__class__(
151+
scopes=self._scopes,
152+
service_account_id=self._service_account_id,
153+
quota_project_id=quota_project_id,
141154
)
142155

143156
@_helpers.copy_docstring(credentials.Signing)

google/auth/compute_engine/credentials.py

Lines changed: 41 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -54,15 +54,18 @@ class Credentials(credentials.ReadOnlyScoped, credentials.Credentials):
5454
https://cloud.google.com/compute/docs/authentication#using
5555
"""
5656

57-
def __init__(self, service_account_email="default"):
57+
def __init__(self, service_account_email="default", quota_project_id=None):
5858
"""
5959
Args:
6060
service_account_email (str): The service account email to use, or
6161
'default'. A Compute Engine instance may have multiple service
6262
accounts.
63+
quota_project_id (Optional[str]): The project ID used for quota and
64+
billing.
6365
"""
6466
super(Credentials, self).__init__()
6567
self._service_account_email = service_account_email
68+
self._quota_project_id = quota_project_id
6669

6770
def _retrieve_info(self, request):
6871
"""Retrieve information about the service account.
@@ -115,6 +118,13 @@ def requires_scopes(self):
115118
"""False: Compute Engine credentials can not be scoped."""
116119
return False
117120

121+
@_helpers.copy_docstring(credentials.Credentials)
122+
def with_quota_project(self, quota_project_id):
123+
return self.__class__(
124+
service_account_email=self._service_account_email,
125+
quota_project_id=quota_project_id,
126+
)
127+
118128

119129
_DEFAULT_TOKEN_LIFETIME_SECS = 3600 # 1 hour in seconds
120130
_DEFAULT_TOKEN_URI = "https://www.googleapis.com/oauth2/v4/token"
@@ -143,6 +153,7 @@ def __init__(
143153
service_account_email=None,
144154
signer=None,
145155
use_metadata_identity_endpoint=False,
156+
quota_project_id=None,
146157
):
147158
"""
148159
Args:
@@ -165,6 +176,8 @@ def __init__(
165176
is False. If set to True, ``token_uri``, ``additional_claims``,
166177
``service_account_email``, ``signer`` argument should not be set;
167178
otherwise ValueError will be raised.
179+
quota_project_id (Optional[str]): The project ID used for quota and
180+
billing.
168181
169182
Raises:
170183
ValueError:
@@ -174,6 +187,7 @@ def __init__(
174187
"""
175188
super(IDTokenCredentials, self).__init__()
176189

190+
self._quota_project_id = quota_project_id
177191
self._use_metadata_identity_endpoint = use_metadata_identity_endpoint
178192
self._target_audience = target_audience
179193

@@ -226,6 +240,7 @@ def with_target_audience(self, target_audience):
226240
None,
227241
target_audience=target_audience,
228242
use_metadata_identity_endpoint=True,
243+
quota_project_id=self._quota_project_id,
229244
)
230245
else:
231246
return self.__class__(
@@ -236,6 +251,31 @@ def with_target_audience(self, target_audience):
236251
additional_claims=self._additional_claims.copy(),
237252
signer=self.signer,
238253
use_metadata_identity_endpoint=False,
254+
quota_project_id=self._quota_project_id,
255+
)
256+
257+
@_helpers.copy_docstring(credentials.Credentials)
258+
def with_quota_project(self, quota_project_id):
259+
260+
# since the signer is already instantiated,
261+
# the request is not needed
262+
if self._use_metadata_identity_endpoint:
263+
return self.__class__(
264+
None,
265+
target_audience=self._target_audience,
266+
use_metadata_identity_endpoint=True,
267+
quota_project_id=quota_project_id,
268+
)
269+
else:
270+
return self.__class__(
271+
None,
272+
service_account_email=self._service_account_email,
273+
token_uri=self._token_uri,
274+
target_audience=self._target_audience,
275+
additional_claims=self._additional_claims.copy(),
276+
signer=self.signer,
277+
use_metadata_identity_endpoint=False,
278+
quota_project_id=quota_project_id,
239279
)
240280

241281
def _make_authorization_grant_assertion(self):

google/auth/credentials.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -49,6 +49,8 @@ def __init__(self):
4949
self.expiry = None
5050
"""Optional[datetime]: When the token expires and is no longer valid.
5151
If this is None, the token is assumed to never expire."""
52+
self._quota_project_id = None
53+
"""Optional[str]: Project to use for quota and billing purposes."""
5254

5355
@property
5456
def expired(self):
@@ -75,6 +77,11 @@ def valid(self):
7577
"""
7678
return self.token is not None and not self.expired
7779

80+
@property
81+
def quota_project_id(self):
82+
"""Project to use for quota and billing purposes."""
83+
return self._quota_project_id
84+
7885
@abc.abstractmethod
7986
def refresh(self, request):
8087
"""Refreshes the access token.
@@ -102,6 +109,8 @@ def apply(self, headers, token=None):
102109
headers["authorization"] = "Bearer {}".format(
103110
_helpers.from_bytes(token or self.token)
104111
)
112+
if self.quota_project_id:
113+
headers["x-goog-user-project"] = self.quota_project_id
105114

106115
def before_request(self, request, method, url, headers):
107116
"""Performs credential-specific before request logic.
@@ -124,6 +133,18 @@ def before_request(self, request, method, url, headers):
124133
self.refresh(request)
125134
self.apply(headers)
126135

136+
def with_quota_project(self, quota_project_id):
137+
"""Returns a copy of these credentials with a modified quota project
138+
139+
Args:
140+
quota_project_id (str): The project to use for quota and
141+
billing purposes
142+
143+
Returns:
144+
google.oauth2.credentials.Credentials: A new credentials instance.
145+
"""
146+
raise NotImplementedError("This class does not support quota project.")
147+
127148

128149
class AnonymousCredentials(Credentials):
129150
"""Credentials that do not provide any authentication information.
@@ -161,6 +182,9 @@ def apply(self, headers, token=None):
161182
def before_request(self, request, method, url, headers):
162183
"""Anonymous credentials do nothing to the request."""
163184

185+
def with_quota_project(self, quota_project_id):
186+
raise ValueError("Anonymous credentials don't support quota project.")
187+
164188

165189
@six.add_metaclass(abc.ABCMeta)
166190
class ReadOnlyScoped(object):

google/auth/impersonated_credentials.py

Lines changed: 42 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -184,6 +184,7 @@ def __init__(
184184
target_scopes,
185185
delegates=None,
186186
lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
187+
quota_project_id=None,
187188
):
188189
"""
189190
Args:
@@ -205,6 +206,9 @@ def __init__(
205206
target_principal.
206207
lifetime (int): Number of seconds the delegated credential should
207208
be valid for (upto 3600).
209+
quota_project_id (Optional[str]): The project ID used for quota and billing.
210+
This project may be different from the project used to
211+
create the credentials.
208212
"""
209213

210214
super(Credentials, self).__init__()
@@ -221,6 +225,7 @@ def __init__(
221225
self._lifetime = lifetime
222226
self.token = None
223227
self.expiry = _helpers.utcnow()
228+
self._quota_project_id = quota_project_id
224229

225230
@_helpers.copy_docstring(credentials.Credentials)
226231
def refresh(self, request):
@@ -288,19 +293,38 @@ def service_account_email(self):
288293
def signer(self):
289294
return self
290295

296+
@_helpers.copy_docstring(credentials.Credentials)
297+
def with_quota_project(self, quota_project_id):
298+
return self.__class__(
299+
self._source_credentials,
300+
target_principal=self._target_principal,
301+
target_scopes=self._target_scopes,
302+
delegates=self._delegates,
303+
lifetime=self._lifetime,
304+
quota_project_id=quota_project_id,
305+
)
306+
291307

292308
class IDTokenCredentials(credentials.Credentials):
293309
"""Open ID Connect ID Token-based service account credentials.
294310
295311
"""
296312

297-
def __init__(self, target_credentials, target_audience=None, include_email=False):
313+
def __init__(
314+
self,
315+
target_credentials,
316+
target_audience=None,
317+
include_email=False,
318+
quota_project_id=None,
319+
):
298320
"""
299321
Args:
300322
target_credentials (google.auth.Credentials): The target
301323
credential used as to acquire the id tokens for.
302324
target_audience (string): Audience to issue the token for.
303325
include_email (bool): Include email in IdToken
326+
quota_project_id (Optional[str]): The project ID used for
327+
quota and billing.
304328
"""
305329
super(IDTokenCredentials, self).__init__()
306330

@@ -311,22 +335,37 @@ def __init__(self, target_credentials, target_audience=None, include_email=False
311335
self._target_credentials = target_credentials
312336
self._target_audience = target_audience
313337
self._include_email = include_email
338+
self._quota_project_id = quota_project_id
314339

315340
def from_credentials(self, target_credentials, target_audience=None):
316341
return self.__class__(
317-
target_credentials=self._target_credentials, target_audience=target_audience
342+
target_credentials=self._target_credentials,
343+
target_audience=target_audience,
344+
quota_project_id=self._quota_project_id,
318345
)
319346

320347
def with_target_audience(self, target_audience):
321348
return self.__class__(
322-
target_credentials=self._target_credentials, target_audience=target_audience
349+
target_credentials=self._target_credentials,
350+
target_audience=target_audience,
351+
quota_project_id=self._quota_project_id,
323352
)
324353

325354
def with_include_email(self, include_email):
326355
return self.__class__(
327356
target_credentials=self._target_credentials,
328357
target_audience=self._target_audience,
329358
include_email=include_email,
359+
quota_project_id=self._quota_project_id,
360+
)
361+
362+
@_helpers.copy_docstring(credentials.Credentials)
363+
def with_quota_project(self, quota_project_id):
364+
return self.__class__(
365+
target_credentials=self._target_credentials,
366+
target_audience=self._target_audience,
367+
include_email=self._include_email,
368+
quota_project_id=quota_project_id,
330369
)
331370

332371
@_helpers.copy_docstring(credentials.Credentials)

0 commit comments

Comments
 (0)
0