Script 'mail_helper' called by obssrc
Hello community,

here is the log from the commit of package python-google-auth for 
openSUSE:Factory checked in at 2022-12-03 10:03:18
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-google-auth (Old)
 and      /work/SRC/openSUSE:Factory/.python-google-auth.new.1835 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-google-auth"

Sat Dec  3 10:03:18 2022 rev:25 rq:1039574 version:2.15.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-google-auth/python-google-auth.changes    
2022-11-17 17:23:32.840761627 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-google-auth.new.1835/python-google-auth.changes
  2022-12-03 10:03:25.227168258 +0100
@@ -1,0 +2,11 @@
+Fri Dec  2 10:25:44 UTC 2022 - John Paul Adrian Glaubitz 
<adrian.glaub...@suse.com>
+
+- Update to 2.15.0
+  * Add api_key credentials (#1184)
+  * Introduce a way to provide scopes granted by user (#1189)
+  * Allow mtls sts endpoint for external account token urls. (#1185)
+  * CI broken by removal of py.path (#1194)
+  * Ensure JWT segments have the right types (#1162)
+  * Updated the lower bound of interactive timeout and fix the kwarg… (#1182)
+
+-------------------------------------------------------------------

Old:
----
  google-auth-2.14.1.tar.gz

New:
----
  google-auth-2.15.0.tar.gz

++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Other differences:
------------------
++++++ python-google-auth.spec ++++++
--- /var/tmp/diff_new_pack.TFFXKe/_old  2022-12-03 10:03:25.939172215 +0100
+++ /var/tmp/diff_new_pack.TFFXKe/_new  2022-12-03 10:03:25.943172237 +0100
@@ -18,7 +18,7 @@
 
 %define skip_python2 1
 Name:           python-google-auth
-Version:        2.14.1
+Version:        2.15.0
 Release:        0
 Summary:        Google Authentication Library
 License:        Apache-2.0

++++++ google-auth-2.14.1.tar.gz -> google-auth-2.15.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/PKG-INFO 
new/google-auth-2.15.0/PKG-INFO
--- old/google-auth-2.14.1/PKG-INFO     2022-11-08 00:45:12.815617300 +0100
+++ new/google-auth-2.15.0/PKG-INFO     2022-12-01 21:44:10.579201200 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: google-auth
-Version: 2.14.1
+Version: 2.15.0
 Summary: Google Authentication Library
 Home-page: https://github.com/googleapis/google-auth-library-python
 Author: Google Cloud Platform
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/google/auth/_default.py 
new/google-auth-2.15.0/google/auth/_default.py
--- old/google-auth-2.14.1/google/auth/_default.py      2022-11-08 
00:41:53.000000000 +0100
+++ new/google-auth-2.15.0/google/auth/_default.py      2022-12-01 
21:40:45.000000000 +0100
@@ -479,6 +479,13 @@
     return credentials, info.get("project")
 
 
+def get_api_key_credentials(key):
+    """Return credentials with the given API key."""
+    from google.auth import api_key
+
+    return api_key.Credentials(key)
+
+
 def _apply_quota_project_id(credentials, quota_project_id):
     if quota_project_id:
         credentials = credentials.with_quota_project(quota_project_id)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/google/auth/api_key.py 
new/google-auth-2.15.0/google/auth/api_key.py
--- old/google-auth-2.14.1/google/auth/api_key.py       1970-01-01 
01:00:00.000000000 +0100
+++ new/google-auth-2.15.0/google/auth/api_key.py       2022-12-01 
21:40:45.000000000 +0100
@@ -0,0 +1,75 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+"""Google API key support.
+This module provides authentication using the `API key`_.
+.. _API key:
+    https://cloud.google.com/docs/authentication/api-keys/
+"""
+
+from google.auth import _helpers
+from google.auth import credentials
+
+
+class Credentials(credentials.Credentials):
+    """API key credentials.
+    These credentials use API key to provide authorization to applications.
+    """
+
+    def __init__(self, token):
+        """
+        Args:
+            token (str): API key string
+        Raises:
+            ValueError: If the provided API key is not a non-empty string.
+        """
+        super(Credentials, self).__init__()
+        if not token:
+            raise ValueError("Token must be a non-empty API key string")
+        self.token = token
+
+    @property
+    def expired(self):
+        return False
+
+    @property
+    def valid(self):
+        return True
+
+    @_helpers.copy_docstring(credentials.Credentials)
+    def refresh(self, request):
+        return
+
+    def apply(self, headers, token=None):
+        """Apply the API key token to the x-goog-api-key header.
+        Args:
+            headers (Mapping): The HTTP request headers.
+            token (Optional[str]): If specified, overrides the current access
+                token.
+        """
+        headers["x-goog-api-key"] = token or self.token
+
+    def before_request(self, request, method, url, headers):
+        """Performs credential-specific before request logic.
+        Refreshes the credentials if necessary, then calls :meth:`apply` to
+        apply the token to the x-goog-api-key header.
+        Args:
+            request (google.auth.transport.Request): The object used to make
+                HTTP requests.
+            method (str): The request's HTTP method or the RPC method being
+                invoked.
+            url (str): The request's URI or the RPC service's URI.
+            headers (Mapping): The request's headers.
+        """
+        self.apply(headers)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/google/auth/external_account.py 
new/google-auth-2.15.0/google/auth/external_account.py
--- old/google-auth-2.14.1/google/auth/external_account.py      2022-11-08 
00:41:53.000000000 +0100
+++ new/google-auth-2.15.0/google/auth/external_account.py      2022-12-01 
21:40:45.000000000 +0100
@@ -437,11 +437,11 @@
     @staticmethod
     def validate_token_url(token_url, url_type="token"):
         _TOKEN_URL_PATTERNS = [
-            "^[^\\.\\s\\/\\\\]+\\.sts\\.googleapis\\.com$",
-            "^sts\\.googleapis\\.com$",
-            "^sts\\.[^\\.\\s\\/\\\\]+\\.googleapis\\.com$",
-            "^[^\\.\\s\\/\\\\]+\\-sts\\.googleapis\\.com$",
-            "^sts\\-[^\\.\\s\\/\\\\]+\\.p\\.googleapis\\.com$",
+            "^[^\\.\\s\\/\\\\]+\\.sts(?:\\.mtls)?\\.googleapis\\.com$",
+            "^sts(?:\\.mtls)?\\.googleapis\\.com$",
+            "^sts\\.[^\\.\\s\\/\\\\]+(?:\\.mtls)?\\.googleapis\\.com$",
+            "^[^\\.\\s\\/\\\\]+\\-sts(?:\\.mtls)?\\.googleapis\\.com$",
+            "^sts\\-[^\\.\\s\\/\\\\]+\\.p(?:\\.mtls)?\\.googleapis\\.com$",
         ]
 
         if not Credentials.is_valid_url(_TOKEN_URL_PATTERNS, token_url):
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/google/auth/jwt.py 
new/google-auth-2.15.0/google/auth/jwt.py
--- old/google-auth-2.14.1/google/auth/jwt.py   2022-11-08 00:41:53.000000000 
+0100
+++ new/google-auth-2.15.0/google/auth/jwt.py   2022-12-01 21:40:45.000000000 
+0100
@@ -133,11 +133,12 @@
         token (Union[str, bytes]): The encoded JWT.
 
     Returns:
-        Tuple[str, str, str, str]: header, payload, signed_section, and
+        Tuple[Mapping, Mapping, str, str]: header, payload, signed_section, and
             signature.
 
     Raises:
-        ValueError: if there are an incorrect amount of segments in the token.
+        ValueError: if there are an incorrect amount of segments in the token 
or
+        segments of the wrong type.
     """
     token = _helpers.to_bytes(token)
 
@@ -152,6 +153,16 @@
     header = _decode_jwt_segment(encoded_header)
     payload = _decode_jwt_segment(encoded_payload)
 
+    if not isinstance(header, Mapping):
+        raise ValueError(
+            "Header segment should be a JSON object: 
{0}".format(encoded_header)
+        )
+
+    if not isinstance(payload, Mapping):
+        raise ValueError(
+            "Payload segment should be a JSON object: 
{0}".format(encoded_payload)
+        )
+
     return header, payload, signed_section, signature
 
 
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/google/auth/pluggable.py 
new/google-auth-2.15.0/google/auth/pluggable.py
--- old/google-auth-2.14.1/google/auth/pluggable.py     2022-11-08 
00:41:53.000000000 +0100
+++ new/google-auth-2.15.0/google/auth/pluggable.py     2022-12-01 
21:40:45.000000000 +0100
@@ -52,8 +52,7 @@
 EXECUTABLE_TIMEOUT_MILLIS_LOWER_BOUND = 5 * 1000  # 5 seconds
 EXECUTABLE_TIMEOUT_MILLIS_UPPER_BOUND = 120 * 1000  # 2 minutes
 
-EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_DEFAULT = 5 * 60 * 1000  # 5 minutes
-EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_LOWER_BOUND = 5 * 60 * 1000  # 5 minutes
+EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_LOWER_BOUND = 30 * 1000  # 30 seconds
 EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_UPPER_BOUND = 30 * 60 * 1000  # 30 
minutes
 
 
@@ -132,7 +131,9 @@
         self._credential_source_executable_output_file = 
self._credential_source_executable.get(
             "output_file"
         )
-        self._tokeninfo_username = kwargs.get("tokeninfo_username", "")  # 
dummy value
+
+        # Dummy value. This variable is only used via injection, not exposed 
to ctor
+        self._tokeninfo_username = ""
 
         if not self._credential_source_executable_command:
             raise ValueError(
@@ -150,17 +151,16 @@
         ):
             raise ValueError("Timeout must be between 5 and 120 seconds.")
 
-        if not self._credential_source_executable_interactive_timeout_millis:
-            self._credential_source_executable_interactive_timeout_millis = (
-                EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_DEFAULT
-            )
-        elif (
-            self._credential_source_executable_interactive_timeout_millis
-            < EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_LOWER_BOUND
-            or self._credential_source_executable_interactive_timeout_millis
-            > EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_UPPER_BOUND
-        ):
-            raise ValueError("Interactive timeout must be between 5 and 30 
minutes.")
+        if self._credential_source_executable_interactive_timeout_millis:
+            if (
+                self._credential_source_executable_interactive_timeout_millis
+                < EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_LOWER_BOUND
+                or 
self._credential_source_executable_interactive_timeout_millis
+                > EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_UPPER_BOUND
+            ):
+                raise ValueError(
+                    "Interactive timeout must be between 30 seconds and 30 
minutes."
+                )
 
     @_helpers.copy_docstring(external_account.Credentials)
     def retrieve_subject_token(self, request):
@@ -400,5 +400,13 @@
                 "An output_file must be specified in the credential 
configuration for interactive mode."
             )
 
+        if (
+            self.interactive
+            and not 
self._credential_source_executable_interactive_timeout_millis
+        ):
+            raise ValueError(
+                "Interactive mode cannot run without an interactive timeout."
+            )
+
         if self.interactive and not self.is_workforce_pool:
             raise ValueError("Interactive mode is only enabled for workforce 
pool.")
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/google/auth/version.py 
new/google-auth-2.15.0/google/auth/version.py
--- old/google-auth-2.14.1/google/auth/version.py       2022-11-08 
00:41:53.000000000 +0100
+++ new/google-auth-2.15.0/google/auth/version.py       2022-12-01 
21:40:45.000000000 +0100
@@ -12,4 +12,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-__version__ = "2.14.1"
+__version__ = "2.15.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/google/oauth2/credentials.py 
new/google-auth-2.15.0/google/oauth2/credentials.py
--- old/google-auth-2.14.1/google/oauth2/credentials.py 2022-11-08 
00:41:53.000000000 +0100
+++ new/google-auth-2.15.0/google/oauth2/credentials.py 2022-12-01 
21:40:45.000000000 +0100
@@ -34,6 +34,7 @@
 from datetime import datetime
 import io
 import json
+import logging
 
 import six
 
@@ -43,6 +44,8 @@
 from google.auth import exceptions
 from google.oauth2 import reauth
 
+_LOGGER = logging.getLogger(__name__)
+
 
 # The Google OAuth 2.0 token endpoint. Used for authorized user credentials.
 _GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
@@ -79,6 +82,7 @@
         rapt_token=None,
         refresh_handler=None,
         enable_reauth_refresh=False,
+        granted_scopes=None,
     ):
         """
         Args:
@@ -117,6 +121,9 @@
                 retrieving downscoped tokens from a token broker.
             enable_reauth_refresh (Optional[bool]): Whether reauth refresh flow
                 should be used. This flag is for gcloud to use only.
+            granted_scopes (Optional[Sequence[str]]): The scopes that were 
consented/granted by the user.
+                This could be different from the requested scopes and it could 
be empty if granted
+                and requested scopes were same.
         """
         super(Credentials, self).__init__()
         self.token = token
@@ -125,6 +132,7 @@
         self._id_token = id_token
         self._scopes = scopes
         self._default_scopes = default_scopes
+        self._granted_scopes = granted_scopes
         self._token_uri = token_uri
         self._client_id = client_id
         self._client_secret = client_secret
@@ -155,6 +163,7 @@
         self._id_token = d.get("_id_token")
         self._scopes = d.get("_scopes")
         self._default_scopes = d.get("_default_scopes")
+        self._granted_scopes = d.get("_granted_scopes")
         self._token_uri = d.get("_token_uri")
         self._client_id = d.get("_client_id")
         self._client_secret = d.get("_client_secret")
@@ -175,6 +184,11 @@
         return self._scopes
 
     @property
+    def granted_scopes(self):
+        """Optional[Sequence[str]]: The OAuth 2.0 permission scopes that were 
granted by the user."""
+        return self._granted_scopes
+
+    @property
     def token_uri(self):
         """Optional[str]: The OAuth 2.0 authorization server's token endpoint
         URI."""
@@ -249,6 +263,7 @@
             client_secret=self.client_secret,
             scopes=self.scopes,
             default_scopes=self.default_scopes,
+            granted_scopes=self.granted_scopes,
             quota_project_id=quota_project_id,
             rapt_token=self.rapt_token,
             enable_reauth_refresh=self._enable_reauth_refresh,
@@ -266,6 +281,7 @@
             client_secret=self.client_secret,
             scopes=self.scopes,
             default_scopes=self.default_scopes,
+            granted_scopes=self.granted_scopes,
             quota_project_id=self.quota_project_id,
             rapt_token=self.rapt_token,
             enable_reauth_refresh=self._enable_reauth_refresh,
@@ -335,10 +351,15 @@
 
         if scopes and "scope" in grant_response:
             requested_scopes = frozenset(scopes)
-            granted_scopes = frozenset(grant_response["scope"].split())
+            self._granted_scopes = grant_response["scope"].split()
+            granted_scopes = frozenset(self._granted_scopes)
             scopes_requested_but_not_granted = requested_scopes - 
granted_scopes
             if scopes_requested_but_not_granted:
-                raise exceptions.RefreshError(
+                # User might be presented with unbundled scopes at the time of
+                # consent. So it is a valid scenario to not have all the 
requested
+                # scopes as part of granted scopes but log a warning in case 
the
+                # developer wants to debug the scenario.
+                _LOGGER.warning(
                     "Not all requested scopes were granted by the "
                     "authorization server, missing scopes {}.".format(
                         ", ".join(scopes_requested_but_not_granted)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/google_auth.egg-info/PKG-INFO 
new/google-auth-2.15.0/google_auth.egg-info/PKG-INFO
--- old/google-auth-2.14.1/google_auth.egg-info/PKG-INFO        2022-11-08 
00:45:12.000000000 +0100
+++ new/google-auth-2.15.0/google_auth.egg-info/PKG-INFO        2022-12-01 
21:44:10.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: google-auth
-Version: 2.14.1
+Version: 2.15.0
 Summary: Google Authentication Library
 Home-page: https://github.com/googleapis/google-auth-library-python
 Author: Google Cloud Platform
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/google_auth.egg-info/SOURCES.txt 
new/google-auth-2.15.0/google_auth.egg-info/SOURCES.txt
--- old/google-auth-2.14.1/google_auth.egg-info/SOURCES.txt     2022-11-08 
00:45:12.000000000 +0100
+++ new/google-auth-2.15.0/google_auth.egg-info/SOURCES.txt     2022-12-01 
21:44:10.000000000 +0100
@@ -14,6 +14,7 @@
 google/auth/_jwt_async.py
 google/auth/_oauth2client.py
 google/auth/_service_account_info.py
+google/auth/api_key.py
 google/auth/app_engine.py
 google/auth/aws.py
 google/auth/credentials.py
@@ -76,6 +77,7 @@
 tests/test__helpers.py
 tests/test__oauth2client.py
 tests/test__service_account_info.py
+tests/test_api_key.py
 tests/test_app_engine.py
 tests/test_aws.py
 tests/test_credentials.py
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/tests/oauth2/test_credentials.py 
new/google-auth-2.15.0/tests/oauth2/test_credentials.py
--- old/google-auth-2.14.1/tests/oauth2/test_credentials.py     2022-11-08 
00:41:53.000000000 +0100
+++ new/google-auth-2.15.0/tests/oauth2/test_credentials.py     2022-12-01 
21:40:45.000000000 +0100
@@ -449,6 +449,7 @@
         assert creds.id_token == mock.sentinel.id_token
         assert creds.has_scopes(scopes)
         assert creds.rapt_token == new_rapt_token
+        assert creds.granted_scopes == scopes
 
         # Check that the credentials are valid (have a token and are not
         # expired.)
@@ -466,7 +467,7 @@
         token = "token"
         new_rapt_token = "new_rapt_token"
         expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
-        grant_response = {"id_token": mock.sentinel.id_token}
+        grant_response = {"id_token": mock.sentinel.id_token, "scope": "email 
profile"}
         refresh_grant.return_value = (
             # Access token
             token,
@@ -513,6 +514,7 @@
         assert creds.id_token == mock.sentinel.id_token
         assert creds.has_scopes(default_scopes)
         assert creds.rapt_token == new_rapt_token
+        assert creds.granted_scopes == default_scopes
 
         # Check that the credentials are valid (have a token and are not
         # expired.)
@@ -530,10 +532,7 @@
         token = "token"
         new_rapt_token = "new_rapt_token"
         expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
-        grant_response = {
-            "id_token": mock.sentinel.id_token,
-            "scopes": " ".join(scopes),
-        }
+        grant_response = {"id_token": mock.sentinel.id_token, "scope": " 
".join(scopes)}
         refresh_grant.return_value = (
             # Access token
             token,
@@ -580,6 +579,72 @@
         assert creds.id_token == mock.sentinel.id_token
         assert creds.has_scopes(scopes)
         assert creds.rapt_token == new_rapt_token
+        assert creds.granted_scopes == scopes
+
+        # Check that the credentials are valid (have a token and are not
+        # expired.)
+        assert creds.valid
+
+    @mock.patch("google.oauth2.reauth.refresh_grant", autospec=True)
+    @mock.patch(
+        "google.auth._helpers.utcnow",
+        return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
+    )
+    def 
test_credentials_with_only_default_scopes_requested_different_granted_scopes(
+        self, unused_utcnow, refresh_grant
+    ):
+        default_scopes = ["email", "profile"]
+        token = "token"
+        new_rapt_token = "new_rapt_token"
+        expiry = _helpers.utcnow() + datetime.timedelta(seconds=500)
+        grant_response = {"id_token": mock.sentinel.id_token, "scope": "email"}
+        refresh_grant.return_value = (
+            # Access token
+            token,
+            # New refresh token
+            None,
+            # Expiry,
+            expiry,
+            # Extra data
+            grant_response,
+            # rapt token
+            new_rapt_token,
+        )
+
+        request = mock.create_autospec(transport.Request)
+        creds = credentials.Credentials(
+            token=None,
+            refresh_token=self.REFRESH_TOKEN,
+            token_uri=self.TOKEN_URI,
+            client_id=self.CLIENT_ID,
+            client_secret=self.CLIENT_SECRET,
+            default_scopes=default_scopes,
+            rapt_token=self.RAPT_TOKEN,
+            enable_reauth_refresh=True,
+        )
+
+        # Refresh credentials
+        creds.refresh(request)
+
+        # Check jwt grant call.
+        refresh_grant.assert_called_with(
+            request,
+            self.TOKEN_URI,
+            self.REFRESH_TOKEN,
+            self.CLIENT_ID,
+            self.CLIENT_SECRET,
+            default_scopes,
+            self.RAPT_TOKEN,
+            True,
+        )
+
+        # Check that the credentials have the token and expiry
+        assert creds.token == token
+        assert creds.expiry == expiry
+        assert creds.id_token == mock.sentinel.id_token
+        assert creds.has_scopes(default_scopes)
+        assert creds.rapt_token == new_rapt_token
+        assert creds.granted_scopes == ["email"]
 
         # Check that the credentials are valid (have a token and are not
         # expired.)
@@ -590,7 +655,7 @@
         "google.auth._helpers.utcnow",
         return_value=datetime.datetime.min + _helpers.REFRESH_THRESHOLD,
     )
-    def test_credentials_with_scopes_refresh_failure_raises_refresh_error(
+    def test_credentials_with_scopes_refresh_different_granted_scopes(
         self, unused_utcnow, refresh_grant
     ):
         scopes = ["email", "profile"]
@@ -628,10 +693,7 @@
         )
 
         # Refresh credentials
-        with pytest.raises(
-            exceptions.RefreshError, match="Not all requested scopes were 
granted"
-        ):
-            creds.refresh(request)
+        creds.refresh(request)
 
         # Check jwt grant call.
         refresh_grant.assert_called_with(
@@ -651,6 +713,7 @@
         assert creds.id_token == mock.sentinel.id_token
         assert creds.has_scopes(scopes)
         assert creds.rapt_token == new_rapt_token
+        assert creds.granted_scopes == scopes_returned
 
         # Check that the credentials are valid (have a token and are not
         # expired.)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/tests/test__default.py 
new/google-auth-2.15.0/tests/test__default.py
--- old/google-auth-2.14.1/tests/test__default.py       2022-11-08 
00:41:53.000000000 +0100
+++ new/google-auth-2.15.0/tests/test__default.py       2022-12-01 
21:40:45.000000000 +0100
@@ -19,6 +19,7 @@
 import pytest  # type: ignore
 
 from google.auth import _default
+from google.auth import api_key
 from google.auth import app_engine
 from google.auth import aws
 from google.auth import compute_engine
@@ -683,6 +684,12 @@
     assert excinfo.match("Failed to load GDCH service account credentials")
 
 
+def test_get_api_key_credentials():
+    creds = _default.get_api_key_credentials("api_key")
+    assert isinstance(creds, api_key.Credentials)
+    assert creds.token == "api_key"
+
+
 class _AppIdentityModule(object):
     """The interface of the App Idenity app engine module.
     See https://cloud.google.com/appengine/docs/standard/python/refdocs\
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/tests/test_api_key.py 
new/google-auth-2.15.0/tests/test_api_key.py
--- old/google-auth-2.14.1/tests/test_api_key.py        1970-01-01 
01:00:00.000000000 +0100
+++ new/google-auth-2.15.0/tests/test_api_key.py        2022-12-01 
21:40:45.000000000 +0100
@@ -0,0 +1,45 @@
+# Copyright 2022 Google LLC
+#
+# Licensed under the Apache License, Version 2.0 (the "License");
+# you may not use this file except in compliance with the License.
+# You may obtain a copy of the License at
+#
+#      http://www.apache.org/licenses/LICENSE-2.0
+#
+# Unless required by applicable law or agreed to in writing, software
+# distributed under the License is distributed on an "AS IS" BASIS,
+# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+# See the License for the specific language governing permissions and
+# limitations under the License.
+
+import pytest  # type: ignore
+
+from google.auth import api_key
+
+
+def test_credentials_constructor():
+    with pytest.raises(ValueError) as excinfo:
+        api_key.Credentials("")
+
+    assert excinfo.match(r"Token must be a non-empty API key string")
+
+
+def test_expired_and_valid():
+    credentials = api_key.Credentials("api-key")
+
+    assert credentials.valid
+    assert credentials.token == "api-key"
+    assert not credentials.expired
+
+    credentials.refresh(None)
+    assert credentials.valid
+    assert credentials.token == "api-key"
+    assert not credentials.expired
+
+
+def test_before_request():
+    credentials = api_key.Credentials("api-key")
+    headers = {}
+
+    credentials.before_request(None, "http://example.com";, "GET", headers)
+    assert headers["x-goog-api-key"] == "api-key"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/tests/test_external_account.py 
new/google-auth-2.15.0/tests/test_external_account.py
--- old/google-auth-2.14.1/tests/test_external_account.py       2022-11-08 
00:41:53.000000000 +0100
+++ new/google-auth-2.15.0/tests/test_external_account.py       2022-12-01 
21:40:45.000000000 +0100
@@ -67,23 +67,31 @@
 
 VALID_TOKEN_URLS = [
     "https://sts.googleapis.com";,
+    "https://sts.mtls.googleapis.com";,
     "https://us-east-1.sts.googleapis.com";,
+    "https://us-east-1.sts.mtls.googleapis.com";,
     "https://US-EAST-1.sts.googleapis.com";,
     "https://sts.us-east-1.googleapis.com";,
     "https://sts.US-WEST-1.googleapis.com";,
     "https://us-east-1-sts.googleapis.com";,
     "https://US-WEST-1-sts.googleapis.com";,
+    "https://US-WEST-1-sts.mtls.googleapis.com";,
     "https://us-west-1-sts.googleapis.com/path?query";,
     "https://sts-us-east-1.p.googleapis.com";,
+    "https://sts-us-east-1.p.mtls.googleapis.com";,
 ]
 INVALID_TOKEN_URLS = [
     "https://iamcredentials.googleapis.com";,
+    "https://mtls.iamcredentials.googleapis.com";,
     "sts.googleapis.com",
+    "mtls.sts.googleapis.com",
+    "mtls.googleapis.com",
     "https://";,
     "http://sts.googleapis.com";,
     "https://st.s.googleapis.com";,
     "https://us-eas\t-1.sts.googleapis.com";,
     "https:/us-east-1.sts.googleapis.com",
+    "https:/us-east-1.mtls.sts.googleapis.com",
     "https://US-WE/ST-1-sts.googleapis.com";,
     "https://sts-us-east-1.googleapis.com";,
     "https://sts-US-WEST-1.googleapis.com";,
@@ -95,16 +103,20 @@
     "hhttps://us-east-1.sts.googleapis.com";,
     "https://us- -1.sts.googleapis.com",
     "https://-sts.googleapis.com";,
+    "https://-mtls.googleapis.com";,
     "https://us-east-1.sts.googleapis.com.evil.com";,
     "https://sts.pgoogleapis.com";,
     "https://p.googleapis.com";,
     "https://sts.p.com";,
+    "https://sts.p.mtls.com";,
     "http://sts.p.googleapis.com";,
     "https://xyz-sts.p.googleapis.com";,
     "https://sts-xyz.123.p.googleapis.com";,
     "https://sts-xyz.p1.googleapis.com";,
     "https://sts-xyz.p.foo.com";,
     "https://sts-xyz.p.foo.googleapis.com";,
+    "https://sts-xyz.mtls.p.foo.googleapis.com";,
+    "https://sts-xyz.p.mtls.foo.googleapis.com";,
 ]
 VALID_SERVICE_ACCOUNT_IMPERSONATION_URLS = [
     "https://iamcredentials.googleapis.com";,
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/tests/test_jwt.py 
new/google-auth-2.15.0/tests/test_jwt.py
--- old/google-auth-2.14.1/tests/test_jwt.py    2022-11-08 00:41:53.000000000 
+0100
+++ new/google-auth-2.15.0/tests/test_jwt.py    2022-12-01 21:40:45.000000000 
+0100
@@ -126,6 +126,29 @@
     assert payload["metadata"]["meta"] == "data"
 
 
+def test_decode_header_object(token_factory):
+    payload = token_factory()
+    # Create a malformed JWT token with a number as a header instead of a
+    # dictionary (3 == base64d(M7==))
+    payload = b"M7." + b".".join(payload.split(b".")[1:])
+
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(payload, certs=PUBLIC_CERT_BYTES)
+    assert excinfo.match(r"Header segment should be a JSON object: " + 
str(b"M7"))
+
+
+def test_decode_payload_object(signer):
+    # Create a malformed JWT token with a payload containing both "iat" and
+    # "exp" strings, although not as fields of a dictionary
+    payload = jwt.encode(signer, "iatexp")
+
+    with pytest.raises(ValueError) as excinfo:
+        jwt.decode(payload, certs=PUBLIC_CERT_BYTES)
+    assert excinfo.match(
+        r"Payload segment should be a JSON object: " + str(b"ImlhdGV4cCI")
+    )
+
+
 def test_decode_valid_es256(token_factory):
     payload = jwt.decode(
         token_factory(use_es256_signer=True), certs=EC_PUBLIC_CERT_BYTES
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google-auth-2.14.1/tests/test_pluggable.py 
new/google-auth-2.15.0/tests/test_pluggable.py
--- old/google-auth-2.14.1/tests/test_pluggable.py      2022-11-08 
00:41:53.000000000 +0100
+++ new/google-auth-2.15.0/tests/test_pluggable.py      2022-12-01 
21:40:45.000000000 +0100
@@ -232,6 +232,21 @@
             interactive=interactive,
         )
 
+    def test_from_constructor_and_injection(self):
+        credentials = pluggable.Credentials(
+            audience=AUDIENCE,
+            subject_token_type=SUBJECT_TOKEN_TYPE,
+            token_url=TOKEN_URL,
+            token_info_url=TOKEN_INFO_URL,
+            credential_source=self.CREDENTIAL_SOURCE,
+            interactive=True,
+        )
+        setattr(credentials, "_tokeninfo_username", "mock_external_account_id")
+
+        assert isinstance(credentials, pluggable.Credentials)
+        assert credentials.interactive
+        assert credentials.external_account_id == "mock_external_account_id"
+
     @mock.patch.object(pluggable.Credentials, "__init__", return_value=None)
     def test_from_info_full_options(self, mock_init):
         credentials = pluggable.Credentials.from_info(
@@ -1065,23 +1080,6 @@
         assert excinfo.match(r"Timeout must be between 5 and 120 seconds.")
 
     @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": 
"1"})
-    def 
test_credential_source_interactive_timeout_missing_will_use_default_interactive_timeout_value(
-        self
-    ):
-        CREDENTIAL_SOURCE = {
-            "executable": {
-                "command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND,
-                "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
-            }
-        }
-        credentials = self.make_pluggable(credential_source=CREDENTIAL_SOURCE)
-
-        assert (
-            
credentials._credential_source_executable_interactive_timeout_millis
-            == pluggable.EXECUTABLE_INTERACTIVE_TIMEOUT_MILLIS_DEFAULT
-        )
-
-    @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": 
"1"})
     def test_credential_source_interactive_timeout_small(self):
         with pytest.raises(ValueError) as excinfo:
             CREDENTIAL_SOURCE = {
@@ -1093,7 +1091,9 @@
             }
             _ = self.make_pluggable(credential_source=CREDENTIAL_SOURCE)
 
-        assert excinfo.match(r"Interactive timeout must be between 5 and 30 
minutes.")
+        assert excinfo.match(
+            r"Interactive timeout must be between 30 seconds and 30 minutes."
+        )
 
     @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": 
"1"})
     def test_credential_source_interactive_timeout_large(self):
@@ -1107,7 +1107,9 @@
             }
             _ = self.make_pluggable(credential_source=CREDENTIAL_SOURCE)
 
-        assert excinfo.match(r"Interactive timeout must be between 5 and 30 
minutes.")
+        assert excinfo.match(
+            r"Interactive timeout must be between 30 seconds and 30 minutes."
+        )
 
     @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": 
"1"})
     def test_retrieve_subject_token_executable_fail(self):
@@ -1137,6 +1139,25 @@
         assert excinfo.match(r"Interactive mode is only enabled for workforce 
pool.")
 
     @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": 
"1"})
+    def 
test_retrieve_subject_token_fail_on_validation_missing_interactive_timeout(
+        self
+    ):
+        CREDENTIAL_SOURCE_EXECUTABLE = {
+            "command": self.CREDENTIAL_SOURCE_EXECUTABLE_COMMAND,
+            "output_file": self.CREDENTIAL_SOURCE_EXECUTABLE_OUTPUT_FILE,
+        }
+        CREDENTIAL_SOURCE = {"executable": CREDENTIAL_SOURCE_EXECUTABLE}
+        credentials = self.make_pluggable(
+            credential_source=CREDENTIAL_SOURCE, interactive=True
+        )
+        with pytest.raises(ValueError) as excinfo:
+            _ = credentials.retrieve_subject_token(None)
+
+        assert excinfo.match(
+            r"Interactive mode cannot run without an interactive timeout."
+        )
+
+    @mock.patch.dict(os.environ, {"GOOGLE_EXTERNAL_ACCOUNT_ALLOW_EXECUTABLES": 
"1"})
     def test_retrieve_subject_token_executable_fail_interactive_mode(self):
         with mock.patch(
             "subprocess.run",

Reply via email to