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 2025-02-04 18:09:46
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++
Comparing /work/SRC/openSUSE:Factory/python-google-auth (Old)
 and      /work/SRC/openSUSE:Factory/.python-google-auth.new.2316 (New)
++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++

Package is "python-google-auth"

Tue Feb  4 18:09:46 2025 rev:52 rq:1242604 version:2.38.0

Changes:
--------
--- /work/SRC/openSUSE:Factory/python-google-auth/python-google-auth.changes    
2025-01-12 11:10:48.990188458 +0100
+++ 
/work/SRC/openSUSE:Factory/.python-google-auth.new.2316/python-google-auth.changes
  2025-02-04 18:09:59.649253163 +0100
@@ -1,0 +2,7 @@
+Thu Jan 30 13:30:46 UTC 2025 - John Paul Adrian Glaubitz 
<adrian.glaub...@suse.com>
+
+- Update to version 2.38.0
+  * Adding domain-wide delegation flow in impersonated credential (#1624) 
(34ee3fe)
+  * Add warnings regarding consuming externally sourced credentials (d049370)
+
+-------------------------------------------------------------------

Old:
----
  google_auth-2.37.0.tar.gz

New:
----
  google_auth-2.38.0.tar.gz

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

Other differences:
------------------
++++++ python-google-auth.spec ++++++
--- /var/tmp/diff_new_pack.jzD5qF/_old  2025-02-04 18:10:01.061311421 +0100
+++ /var/tmp/diff_new_pack.jzD5qF/_new  2025-02-04 18:10:01.065311586 +0100
@@ -18,7 +18,7 @@
 
 %{?sle15_python_module_pythons}
 Name:           python-google-auth
-Version:        2.37.0
+Version:        2.38.0
 Release:        0
 Summary:        Google Authentication Library
 License:        Apache-2.0

++++++ google_auth-2.37.0.tar.gz -> google_auth-2.38.0.tar.gz ++++++
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google_auth-2.37.0/PKG-INFO 
new/google_auth-2.38.0/PKG-INFO
--- old/google_auth-2.37.0/PKG-INFO     2024-12-11 21:14:31.451174700 +0100
+++ new/google_auth-2.38.0/PKG-INFO     2025-01-23 02:05:25.135887400 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: google-auth
-Version: 2.37.0
+Version: 2.38.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.37.0/google/auth/_default.py 
new/google_auth-2.38.0/google/auth/_default.py
--- old/google_auth-2.37.0/google/auth/_default.py      2024-12-11 
21:10:42.000000000 +0100
+++ new/google_auth-2.38.0/google/auth/_default.py      2025-01-23 
02:01:00.000000000 +0100
@@ -85,6 +85,17 @@
     user credentials, external account credentials, or impersonated service
     account credentials.
 
+    .. warning::
+        Important: If you accept a credential configuration (credential 
JSON/File/Stream)
+        from an external source for authentication to Google Cloud Platform, 
you must
+        validate it before providing it to any Google API or client library. 
Providing an
+        unvalidated credential configuration to Google APIs or libraries can 
compromise
+        the security of your systems and data. For more information, refer to
+        `Validate credential configurations from external sources`_.
+
+        .. _Validate credential configurations from external sources:
+            
https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
+
     Args:
         filename (str): The full path to the credentials file.
         scopes (Optional[Sequence[str]]): The list of scopes for the 
credentials. If
@@ -137,6 +148,17 @@
     user credentials, external account credentials, or impersonated service
     account credentials.
 
+    .. warning::
+        Important: If you accept a credential configuration (credential 
JSON/File/Stream)
+        from an external source for authentication to Google Cloud Platform, 
you must
+        validate it before providing it to any Google API or client library. 
Providing an
+        unvalidated credential configuration to Google APIs or libraries can 
compromise
+        the security of your systems and data. For more information, refer to
+        `Validate credential configurations from external sources`_.
+
+    .. _Validate credential configurations from external sources:
+        
https://cloud.google.com/docs/authentication/external/externally-sourced-credentials
+
     Args:
         info (Dict[str, Any]): A dict object containing the credentials
         scopes (Optional[Sequence[str]]): The list of scopes for the 
credentials. If
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/google_auth-2.37.0/google/auth/compute_engine/_metadata.py 
new/google_auth-2.38.0/google/auth/compute_engine/_metadata.py
--- old/google_auth-2.37.0/google/auth/compute_engine/_metadata.py      
2024-12-11 21:10:42.000000000 +0100
+++ new/google_auth-2.38.0/google/auth/compute_engine/_metadata.py      
2025-01-23 02:01:00.000000000 +0100
@@ -201,7 +201,7 @@
     url = _helpers.update_query(base_url, query_params)
 
     backoff = ExponentialBackoff(total_attempts=retry_count)
-
+    failure_reason = None
     for attempt in backoff:
         try:
             response = request(url=url, method="GET", headers=headers_to_use)
@@ -213,6 +213,11 @@
                     retry_count,
                     response.status,
                 )
+                failure_reason = (
+                    response.data.decode("utf-8")
+                    if hasattr(response.data, "decode")
+                    else response.data
+                )
                 continue
             else:
                 break
@@ -225,10 +230,13 @@
                 retry_count,
                 e,
             )
+            failure_reason = e
     else:
         raise exceptions.TransportError(
             "Failed to retrieve {} from the Google Compute Engine "
-            "metadata service. Compute Engine Metadata server 
unavailable".format(url)
+            "metadata service. Compute Engine Metadata server unavailable due 
to {}".format(
+                url, failure_reason
+            )
         )
 
     content = _helpers.from_bytes(response.data)
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google_auth-2.37.0/google/auth/iam.py 
new/google_auth-2.38.0/google/auth/iam.py
--- old/google_auth-2.37.0/google/auth/iam.py   2024-12-11 21:10:42.000000000 
+0100
+++ new/google_auth-2.38.0/google/auth/iam.py   2025-01-23 02:01:00.000000000 
+0100
@@ -48,6 +48,11 @@
     + "/serviceAccounts/{}:signBlob"
 )
 
+_IAM_SIGNJWT_ENDPOINT = (
+    "https://iamcredentials.googleapis.com/v1/projects/-";
+    + "/serviceAccounts/{}:signJwt"
+)
+
 _IAM_IDTOKEN_ENDPOINT = (
     "https://iamcredentials.googleapis.com/v1/";
     + "projects/-/serviceAccounts/{}:generateIdToken"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/google_auth-2.37.0/google/auth/impersonated_credentials.py 
new/google_auth-2.38.0/google/auth/impersonated_credentials.py
--- old/google_auth-2.37.0/google/auth/impersonated_credentials.py      
2024-12-11 21:10:42.000000000 +0100
+++ new/google_auth-2.38.0/google/auth/impersonated_credentials.py      
2025-01-23 02:01:00.000000000 +0100
@@ -38,12 +38,15 @@
 from google.auth import iam
 from google.auth import jwt
 from google.auth import metrics
+from google.oauth2 import _client
 
 
 _REFRESH_ERROR = "Unable to acquire impersonated credentials"
 
 _DEFAULT_TOKEN_LIFETIME_SECS = 3600  # 1 hour in seconds
 
+_GOOGLE_OAUTH2_TOKEN_ENDPOINT = "https://oauth2.googleapis.com/token";
+
 
 def _make_iam_token_request(
     request,
@@ -177,6 +180,7 @@
         target_principal,
         target_scopes,
         delegates=None,
+        subject=None,
         lifetime=_DEFAULT_TOKEN_LIFETIME_SECS,
         quota_project_id=None,
         iam_endpoint_override=None,
@@ -204,9 +208,12 @@
             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.
-            iam_endpoint_override (Optiona[str]): The full IAM endpoint 
override
+            iam_endpoint_override (Optional[str]): The full IAM endpoint 
override
                 with the target_principal embedded. This is useful when 
supporting
                 impersonation with regional endpoints.
+            subject (Optional[str]): sub field of a JWT. This field should 
only be set
+                if you wish to impersonate as a user. This feature is useful 
when
+                using domain wide delegation.
         """
 
         super(Credentials, self).__init__()
@@ -231,6 +238,7 @@
         self._target_principal = target_principal
         self._target_scopes = target_scopes
         self._delegates = delegates
+        self._subject = subject
         self._lifetime = lifetime or _DEFAULT_TOKEN_LIFETIME_SECS
         self.token = None
         self.expiry = _helpers.utcnow()
@@ -275,6 +283,39 @@
         # Apply the source credentials authentication info.
         self._source_credentials.apply(headers)
 
+        #  If a subject is specified a domain-wide delegation auth-flow is 
initiated
+        #  to impersonate as the provided subject (user).
+        if self._subject:
+            if self.universe_domain != credentials.DEFAULT_UNIVERSE_DOMAIN:
+                raise exceptions.GoogleAuthError(
+                    "Domain-wide delegation is not supported in universes 
other "
+                    + "than googleapis.com"
+                )
+
+            now = _helpers.utcnow()
+            payload = {
+                "iss": self._target_principal,
+                "scope": _helpers.scopes_to_string(self._target_scopes or ()),
+                "sub": self._subject,
+                "aud": _GOOGLE_OAUTH2_TOKEN_ENDPOINT,
+                "iat": _helpers.datetime_to_secs(now),
+                "exp": _helpers.datetime_to_secs(now) + 
_DEFAULT_TOKEN_LIFETIME_SECS,
+            }
+
+            assertion = _sign_jwt_request(
+                request=request,
+                principal=self._target_principal,
+                headers=headers,
+                payload=payload,
+                delegates=self._delegates,
+            )
+
+            self.token, self.expiry, _ = _client.jwt_grant(
+                request, _GOOGLE_OAUTH2_TOKEN_ENDPOINT, assertion
+            )
+
+            return
+
         self.token, self.expiry = _make_iam_token_request(
             request=request,
             principal=self._target_principal,
@@ -478,3 +519,61 @@
         self.expiry = datetime.utcfromtimestamp(
             jwt.decode(id_token, verify=False)["exp"]
         )
+
+
+def _sign_jwt_request(request, principal, headers, payload, delegates=[]):
+    """Makes a request to the Google Cloud IAM service to sign a JWT using a
+    service account's system-managed private key.
+    Args:
+        request (Request): The Request object to use.
+        principal (str): The principal to request an access token for.
+        headers (Mapping[str, str]): Map of headers to transmit.
+        payload (Mapping[str, str]): The JWT payload to sign. Must be a
+            serialized JSON object that contains a JWT Claims Set.
+        delegates (Sequence[str]): The chained list of delegates required
+            to grant the final access_token.  If set, the sequence of
+            identities must have "Service Account Token Creator" capability
+            granted to the prceeding identity.  For example, if set to
+            [serviceAccountB, serviceAccountC], the source_credential
+            must have the Token Creator role on serviceAccountB.
+            serviceAccountB must have the Token Creator on
+            serviceAccountC.
+            Finally, C must have Token Creator on target_principal.
+            If left unset, source_credential must have that role on
+            target_principal.
+
+    Raises:
+        google.auth.exceptions.TransportError: Raised if there is an underlying
+            HTTP connection error
+        google.auth.exceptions.RefreshError: Raised if the impersonated
+            credentials are not available.  Common reasons are
+            `iamcredentials.googleapis.com` is not enabled or the
+            `Service Account Token Creator` is not assigned
+    """
+    iam_endpoint = iam._IAM_SIGNJWT_ENDPOINT.format(principal)
+
+    body = {"delegates": delegates, "payload": json.dumps(payload)}
+    body = json.dumps(body).encode("utf-8")
+
+    response = request(url=iam_endpoint, method="POST", headers=headers, 
body=body)
+
+    # support both string and bytes type response.data
+    response_body = (
+        response.data.decode("utf-8")
+        if hasattr(response.data, "decode")
+        else response.data
+    )
+
+    if response.status != http_client.OK:
+        raise exceptions.RefreshError(_REFRESH_ERROR, response_body)
+
+    try:
+        jwt_response = json.loads(response_body)
+        signed_jwt = jwt_response["signedJwt"]
+        return signed_jwt
+
+    except (KeyError, ValueError) as caught_exc:
+        new_exc = exceptions.RefreshError(
+            "{}: No signed JWT in response.".format(_REFRESH_ERROR), 
response_body
+        )
+        raise new_exc from caught_exc
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google_auth-2.37.0/google/auth/version.py 
new/google_auth-2.38.0/google/auth/version.py
--- old/google_auth-2.37.0/google/auth/version.py       2024-12-11 
21:10:42.000000000 +0100
+++ new/google_auth-2.38.0/google/auth/version.py       2025-01-23 
02:01:00.000000000 +0100
@@ -12,4 +12,4 @@
 # See the License for the specific language governing permissions and
 # limitations under the License.
 
-__version__ = "2.37.0"
+__version__ = "2.38.0"
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' old/google_auth-2.37.0/google_auth.egg-info/PKG-INFO 
new/google_auth-2.38.0/google_auth.egg-info/PKG-INFO
--- old/google_auth-2.37.0/google_auth.egg-info/PKG-INFO        2024-12-11 
21:14:31.000000000 +0100
+++ new/google_auth-2.38.0/google_auth.egg-info/PKG-INFO        2025-01-23 
02:05:24.000000000 +0100
@@ -1,6 +1,6 @@
 Metadata-Version: 2.1
 Name: google-auth
-Version: 2.37.0
+Version: 2.38.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.37.0/tests/compute_engine/test__metadata.py 
new/google_auth-2.38.0/tests/compute_engine/test__metadata.py
--- old/google_auth-2.37.0/tests/compute_engine/test__metadata.py       
2024-12-11 21:10:42.000000000 +0100
+++ new/google_auth-2.38.0/tests/compute_engine/test__metadata.py       
2025-01-23 02:01:00.000000000 +0100
@@ -344,12 +344,32 @@
 @mock.patch("time.sleep", return_value=None)
 def test_get_failure_connection_failed(mock_sleep):
     request = make_request("")
-    request.side_effect = exceptions.TransportError()
+    request.side_effect = exceptions.TransportError("failure message")
 
     with pytest.raises(exceptions.TransportError) as excinfo:
         _metadata.get(request, PATH)
 
-    assert excinfo.match(r"Compute Engine Metadata server unavailable")
+    assert excinfo.match(
+        r"Compute Engine Metadata server unavailable due to failure message"
+    )
+
+    request.assert_called_with(
+        method="GET",
+        url=_metadata._METADATA_ROOT + PATH,
+        headers=_metadata._METADATA_HEADERS,
+    )
+    assert request.call_count == 5
+
+
+def test_get_too_many_requests_retryable_error_failure():
+    request = make_request("too many requests", 
status=http_client.TOO_MANY_REQUESTS)
+
+    with pytest.raises(exceptions.TransportError) as excinfo:
+        _metadata.get(request, PATH)
+
+    assert excinfo.match(
+        r"Compute Engine Metadata server unavailable due to too many requests"
+    )
 
     request.assert_called_with(
         method="GET",
diff -urN '--exclude=CVS' '--exclude=.cvsignore' '--exclude=.svn' 
'--exclude=.svnignore' 
old/google_auth-2.37.0/tests/test_impersonated_credentials.py 
new/google_auth-2.38.0/tests/test_impersonated_credentials.py
--- old/google_auth-2.37.0/tests/test_impersonated_credentials.py       
2024-12-11 21:10:42.000000000 +0100
+++ new/google_auth-2.38.0/tests/test_impersonated_credentials.py       
2025-01-23 02:01:00.000000000 +0100
@@ -71,6 +71,17 @@
         yield grant
 
 
+@pytest.fixture
+def mock_dwd_credentials():
+    with mock.patch("google.oauth2._client.jwt_grant", autospec=True) as grant:
+        grant.return_value = (
+            "1/fFAGRNJasdfz70BzhT3Zg",
+            _helpers.utcnow() + datetime.timedelta(seconds=500),
+            {},
+        )
+        yield grant
+
+
 class MockResponse:
     def __init__(self, json_data, status_code):
         self.json_data = json_data
@@ -123,6 +134,7 @@
         source_credentials=SOURCE_CREDENTIALS,
         lifetime=LIFETIME,
         target_principal=TARGET_PRINCIPAL,
+        subject=None,
         iam_endpoint_override=None,
     ):
 
@@ -132,6 +144,7 @@
             target_scopes=self.TARGET_SCOPES,
             delegates=self.DELEGATES,
             lifetime=lifetime,
+            subject=subject,
             iam_endpoint_override=iam_endpoint_override,
         )
 
@@ -239,6 +252,28 @@
         )
 
     @pytest.mark.parametrize("use_data_bytes", [True, False])
+    def test_refresh_with_subject_success(self, use_data_bytes, 
mock_dwd_credentials):
+        credentials = self.make_credentials(subject="t...@email.com", 
lifetime=None)
+
+        response_body = {"signedJwt": "example_signed_jwt"}
+
+        request = self.make_request(
+            data=json.dumps(response_body),
+            status=http_client.OK,
+            use_data_bytes=use_data_bytes,
+        )
+
+        with mock.patch(
+            "google.auth.metrics.token_request_access_token_impersonate",
+            return_value=ACCESS_TOKEN_REQUEST_METRICS_HEADER_VALUE,
+        ):
+            credentials.refresh(request)
+
+        assert credentials.valid
+        assert not credentials.expired
+        assert credentials.token == "1/fFAGRNJasdfz70BzhT3Zg"
+
+    @pytest.mark.parametrize("use_data_bytes", [True, False])
     def test_refresh_success_nonGdu(self, use_data_bytes, 
mock_donor_credentials):
         source_credentials = service_account.Credentials(
             SIGNER, "s...@email.com", TOKEN_URI, universe_domain="foo.bar"
@@ -418,6 +453,33 @@
         assert not credentials.valid
         assert credentials.expired
 
+    def test_refresh_failure_subject_with_nondefault_domain(
+        self, mock_donor_credentials
+    ):
+        source_credentials = service_account.Credentials(
+            SIGNER, "s...@email.com", TOKEN_URI, universe_domain="foo.bar"
+        )
+        credentials = self.make_credentials(
+            source_credentials=source_credentials, subject="t...@email.com"
+        )
+
+        expire_time = 
(_helpers.utcnow().replace(microsecond=0)).isoformat("T") + "Z"
+        response_body = {"accessToken": "token", "expireTime": expire_time}
+        request = self.make_request(
+            data=json.dumps(response_body), status=http_client.OK
+        )
+
+        with pytest.raises(exceptions.GoogleAuthError) as excinfo:
+            credentials.refresh(request)
+
+        assert excinfo.match(
+            "Domain-wide delegation is not supported in universes other "
+            + "than googleapis.com"
+        )
+
+        assert not credentials.valid
+        assert credentials.expired
+
     def test_expired(self):
         credentials = self.make_credentials(lifetime=None)
         assert credentials.expired
@@ -810,3 +872,61 @@
         id_creds.refresh(request)
 
         assert id_creds.quota_project_id == "project-foo"
+
+    def test_sign_jwt_request_success(self):
+        principal = "f...@example.com"
+        expected_signed_jwt = "correct_signed_jwt"
+
+        response_body = {"keyId": "1", "signedJwt": expected_signed_jwt}
+        request = self.make_request(
+            data=json.dumps(response_body), status=http_client.OK
+        )
+
+        signed_jwt = impersonated_credentials._sign_jwt_request(
+            request=request, principal=principal, headers={}, payload={}
+        )
+
+        assert signed_jwt == expected_signed_jwt
+        request.assert_called_once_with(
+            
url="https://iamcredentials.googleapis.com/v1/projects/-/serviceAccounts/f...@example.com:signJwt";,
+            method="POST",
+            headers={},
+            body=json.dumps({"delegates": [], "payload": 
json.dumps({})}).encode(
+                "utf-8"
+            ),
+        )
+
+    def test_sign_jwt_request_http_error(self):
+        principal = "f...@example.com"
+
+        request = self.make_request(
+            data="error_message", status=http_client.BAD_REQUEST
+        )
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            _ = impersonated_credentials._sign_jwt_request(
+                request=request, principal=principal, headers={}, payload={}
+            )
+
+        assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
+
+        assert excinfo.value.args[0] == "Unable to acquire impersonated 
credentials"
+        assert excinfo.value.args[1] == "error_message"
+
+    def test_sign_jwt_request_invalid_response_error(self):
+        principal = "f...@example.com"
+
+        request = self.make_request(data="invalid_data", status=http_client.OK)
+
+        with pytest.raises(exceptions.RefreshError) as excinfo:
+            _ = impersonated_credentials._sign_jwt_request(
+                request=request, principal=principal, headers={}, payload={}
+            )
+
+        assert excinfo.match(impersonated_credentials._REFRESH_ERROR)
+
+        assert (
+            excinfo.value.args[0]
+            == "Unable to acquire impersonated credentials: No signed JWT in 
response."
+        )
+        assert excinfo.value.args[1] == "invalid_data"

Reply via email to