feat: send quota project id in x-goog-user-project header for OAuth2 credentials (#400)
When the 3LO credentials are used, the quota project ("quota_project_id") is sent on every outgoing request in the x-goog-user-project HTTP header/grpc metadata. The quota project is used for billing and quota purposes.
diff --git a/docs/reference/google.auth.crypt.base.rst b/docs/reference/google.auth.crypt.base.rst
new file mode 100644
index 0000000..a899650
--- /dev/null
+++ b/docs/reference/google.auth.crypt.base.rst
@@ -0,0 +1,7 @@
+google.auth.crypt.base module
+=============================
+
+.. automodule:: google.auth.crypt.base
+ :members:
+ :inherited-members:
+ :show-inheritance:
diff --git a/docs/reference/google.auth.crypt.rsa.rst b/docs/reference/google.auth.crypt.rsa.rst
new file mode 100644
index 0000000..7060b03
--- /dev/null
+++ b/docs/reference/google.auth.crypt.rsa.rst
@@ -0,0 +1,7 @@
+google.auth.crypt.rsa module
+============================
+
+.. automodule:: google.auth.crypt.rsa
+ :members:
+ :inherited-members:
+ :show-inheritance:
diff --git a/google/oauth2/credentials.py b/google/oauth2/credentials.py
index 3a32c06..422c8ab 100644
--- a/google/oauth2/credentials.py
+++ b/google/oauth2/credentials.py
@@ -58,6 +58,7 @@
client_id=None,
client_secret=None,
scopes=None,
+ quota_project_id=None,
):
"""
Args:
@@ -81,6 +82,9 @@
token if refresh information is provided (e.g. The refresh
token scopes are a superset of this or contain a wild card
scope like 'https://www.googleapis.com/auth/any-api').
+ quota_project_id (Optional[str]): The project ID used for quota and billing.
+ This project may be different from the project used to
+ create the credentials.
"""
super(Credentials, self).__init__()
self.token = token
@@ -90,6 +94,7 @@
self._token_uri = token_uri
self._client_id = client_id
self._client_secret = client_secret
+ self._quota_project_id = quota_project_id
@property
def refresh_token(self):
@@ -124,6 +129,11 @@
return self._client_secret
@property
+ def quota_project_id(self):
+ """Optional[str]: The project to use for quota and billing purposes."""
+ return self._quota_project_id
+
+ @property
def requires_scopes(self):
"""False: OAuth 2.0 credentials have their scopes set when
the initial token is requested and can not be changed."""
@@ -169,6 +179,12 @@
)
)
+ @_helpers.copy_docstring(credentials.Credentials)
+ def apply(self, headers, token=None):
+ super(Credentials, self).apply(headers, token=token)
+ if self.quota_project_id is not None:
+ headers["x-goog-user-project"] = self.quota_project_id
+
@classmethod
def from_authorized_user_info(cls, info, scopes=None):
"""Creates a Credentials instance from parsed authorized user info.
@@ -202,6 +218,9 @@
scopes=scopes,
client_id=info["client_id"],
client_secret=info["client_secret"],
+ quota_project_id=info.get(
+ "quota_project_id"
+ ), # quota project may not exist
)
@classmethod
diff --git a/tests/oauth2/test_credentials.py b/tests/oauth2/test_credentials.py
index bb70f15..59031d7 100644
--- a/tests/oauth2/test_credentials.py
+++ b/tests/oauth2/test_credentials.py
@@ -294,6 +294,33 @@
# expired.)
assert creds.valid
+ def test_apply_with_quota_project_id(self):
+ creds = credentials.Credentials(
+ token="token",
+ refresh_token=self.REFRESH_TOKEN,
+ token_uri=self.TOKEN_URI,
+ client_id=self.CLIENT_ID,
+ client_secret=self.CLIENT_SECRET,
+ quota_project_id="quota-project-123",
+ )
+
+ headers = {}
+ creds.apply(headers)
+ assert headers["x-goog-user-project"] == "quota-project-123"
+
+ def test_apply_with_no_quota_project_id(self):
+ creds = credentials.Credentials(
+ token="token",
+ refresh_token=self.REFRESH_TOKEN,
+ token_uri=self.TOKEN_URI,
+ client_id=self.CLIENT_ID,
+ client_secret=self.CLIENT_SECRET,
+ )
+
+ headers = {}
+ creds.apply(headers)
+ assert "x-goog-user-project" not in headers
+
def test_from_authorized_user_info(self):
info = AUTH_USER_INFO.copy()