summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorrobot-piglet <[email protected]>2025-05-20 09:26:45 +0300
committerrobot-piglet <[email protected]>2025-05-20 09:42:10 +0300
commit68cf18d5ea6913da54fcd36e31e2b6a178900729 (patch)
tree08d557f92be11f90b786840c6757719ccb080aaf
parent9de99a9451af6f728edc321e802add05499b4bc9 (diff)
Intermediate changes
commit_hash:4518e1fc835bd128306da321c9c8a7578cfde09c
-rw-r--r--contrib/python/google-auth/py3/.dist-info/METADATA2
-rw-r--r--contrib/python/google-auth/py3/google/auth/_helpers.py240
-rw-r--r--contrib/python/google-auth/py3/google/auth/aio/_helpers.py57
-rw-r--r--contrib/python/google-auth/py3/google/auth/aio/transport/aiohttp.py6
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py9
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/_http_client.py4
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/requests.py6
-rw-r--r--contrib/python/google-auth/py3/google/auth/transport/urllib3.py4
-rw-r--r--contrib/python/google-auth/py3/google/auth/version.py2
-rw-r--r--contrib/python/google-auth/py3/google/oauth2/webauthn_types.py2
-rw-r--r--contrib/python/google-auth/py3/tests/aio/test__helpers.py110
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py2
-rw-r--r--contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py2
-rw-r--r--contrib/python/google-auth/py3/tests/test__helpers.py434
-rw-r--r--contrib/python/google-auth/py3/ya.make3
15 files changed, 870 insertions, 13 deletions
diff --git a/contrib/python/google-auth/py3/.dist-info/METADATA b/contrib/python/google-auth/py3/.dist-info/METADATA
index 53da88bdec8..ddba2bddaed 100644
--- a/contrib/python/google-auth/py3/.dist-info/METADATA
+++ b/contrib/python/google-auth/py3/.dist-info/METADATA
@@ -1,6 +1,6 @@
Metadata-Version: 2.1
Name: google-auth
-Version: 2.39.0
+Version: 2.40.0
Summary: Google Authentication Library
Home-page: https://github.com/googleapis/google-auth-library-python
Author: Google Cloud Platform
diff --git a/contrib/python/google-auth/py3/google/auth/_helpers.py b/contrib/python/google-auth/py3/google/auth/_helpers.py
index a6c07f7d829..78fe22f7264 100644
--- a/contrib/python/google-auth/py3/google/auth/_helpers.py
+++ b/contrib/python/google-auth/py3/google/auth/_helpers.py
@@ -18,15 +18,38 @@ import base64
import calendar
import datetime
from email.message import Message
+import hashlib
+import json
+import logging
import sys
+from typing import Any, Dict, Mapping, Optional, Union
import urllib
from google.auth import exceptions
+
+# _BASE_LOGGER_NAME is the base logger for all google-based loggers.
+_BASE_LOGGER_NAME = "google"
+
+# _LOGGING_INITIALIZED ensures that base logger is only configured once
+# (unless already configured by the end-user).
+_LOGGING_INITIALIZED = False
+
+
# The smallest MDS cache used by this library stores tokens until 4 minutes from
# expiry.
REFRESH_THRESHOLD = datetime.timedelta(minutes=3, seconds=45)
+# TODO(https://github.com/googleapis/google-auth-library-python/issues/1684): Audit and update the list below.
+_SENSITIVE_FIELDS = {
+ "accessToken",
+ "access_token",
+ "id_token",
+ "client_id",
+ "refresh_token",
+ "client_secret",
+}
+
def copy_docstring(source_class):
"""Decorator that copies a method's docstring from another class.
@@ -271,3 +294,220 @@ def is_python_3():
bool: True if the Python interpreter is Python 3 and False otherwise.
"""
return sys.version_info > (3, 0)
+
+
+def _hash_sensitive_info(data: Union[dict, list]) -> Union[dict, list, str]:
+ """
+ Hashes sensitive information within a dictionary.
+
+ Args:
+ data: The dictionary containing data to be processed.
+
+ Returns:
+ A new dictionary with sensitive values replaced by their SHA512 hashes.
+ If the input is a list, returns a list with each element recursively processed.
+ If the input is neither a dict nor a list, returns the type of the input as a string.
+
+ """
+ if isinstance(data, dict):
+ hashed_data: Dict[Any, Union[Optional[str], dict, list]] = {}
+ for key, value in data.items():
+ if key in _SENSITIVE_FIELDS and not isinstance(value, (dict, list)):
+ hashed_data[key] = _hash_value(value, key)
+ elif isinstance(value, (dict, list)):
+ hashed_data[key] = _hash_sensitive_info(value)
+ else:
+ hashed_data[key] = value
+ return hashed_data
+ elif isinstance(data, list):
+ hashed_list = []
+ for val in data:
+ hashed_list.append(_hash_sensitive_info(val))
+ return hashed_list
+ else:
+ # TODO(https://github.com/googleapis/google-auth-library-python/issues/1701):
+ # Investigate and hash sensitive info before logging when the data type is
+ # not a dict or a list.
+ return str(type(data))
+
+
+def _hash_value(value, field_name: str) -> Optional[str]:
+ """Hashes a value and returns a formatted hash string."""
+ if value is None:
+ return None
+ encoded_value = str(value).encode("utf-8")
+ hash_object = hashlib.sha512()
+ hash_object.update(encoded_value)
+ hex_digest = hash_object.hexdigest()
+ return f"hashed_{field_name}-{hex_digest}"
+
+
+def _logger_configured(logger: logging.Logger) -> bool:
+ """Determines whether `logger` has non-default configuration
+
+ Args:
+ logger: The logger to check.
+
+ Returns:
+ bool: Whether the logger has any non-default configuration.
+ """
+ return (
+ logger.handlers != [] or logger.level != logging.NOTSET or not logger.propagate
+ )
+
+
+def is_logging_enabled(logger: logging.Logger) -> bool:
+ """
+ Checks if debug logging is enabled for the given logger.
+
+ Args:
+ logger: The logging.Logger instance to check.
+
+ Returns:
+ True if debug logging is enabled, False otherwise.
+ """
+ # NOTE: Log propagation to the root logger is disabled unless
+ # the base logger i.e. logging.getLogger("google") is
+ # explicitly configured by the end user. Ideally this
+ # needs to happen in the client layer (already does for GAPICs).
+ # However, this is implemented here to avoid logging
+ # (if a root logger is configured) when a version of google-auth
+ # which supports logging is used with:
+ # - an older version of a GAPIC which does not support logging.
+ # - Apiary client which does not support logging.
+ global _LOGGING_INITIALIZED
+ if not _LOGGING_INITIALIZED:
+ base_logger = logging.getLogger(_BASE_LOGGER_NAME)
+ if not _logger_configured(base_logger):
+ base_logger.propagate = False
+ _LOGGING_INITIALIZED = True
+
+ return logger.isEnabledFor(logging.DEBUG)
+
+
+def request_log(
+ logger: logging.Logger,
+ method: str,
+ url: str,
+ body: Optional[bytes],
+ headers: Optional[Mapping[str, str]],
+) -> None:
+ """
+ Logs an HTTP request at the DEBUG level if logging is enabled.
+
+ Args:
+ logger: The logging.Logger instance to use.
+ method: The HTTP method (e.g., "GET", "POST").
+ url: The URL of the request.
+ body: The request body (can be None).
+ headers: The request headers (can be None).
+ """
+ if is_logging_enabled(logger):
+ content_type = (
+ headers["Content-Type"] if headers and "Content-Type" in headers else ""
+ )
+ json_body = _parse_request_body(body, content_type=content_type)
+ logged_body = _hash_sensitive_info(json_body)
+ logger.debug(
+ "Making request...",
+ extra={
+ "httpRequest": {
+ "method": method,
+ "url": url,
+ "body": logged_body,
+ "headers": headers,
+ }
+ },
+ )
+
+
+def _parse_request_body(body: Optional[bytes], content_type: str = "") -> Any:
+ """
+ Parses a request body, handling bytes and string types, and different content types.
+
+ Args:
+ body (Optional[bytes]): The request body.
+ content_type (str): The content type of the request body, e.g., "application/json",
+ "application/x-www-form-urlencoded", or "text/plain". If empty, attempts
+ to parse as JSON.
+
+ Returns:
+ Parsed body (dict, str, or None).
+ - JSON: Decodes if content_type is "application/json" or None (fallback).
+ - URL-encoded: Parses if content_type is "application/x-www-form-urlencoded".
+ - Plain text: Returns string if content_type is "text/plain".
+ - None: Returns if body is None, UTF-8 decode fails, or content_type is unknown.
+ """
+ if body is None:
+ return None
+ try:
+ body_str = body.decode("utf-8")
+ except (UnicodeDecodeError, AttributeError):
+ return None
+ content_type = content_type.lower()
+ if not content_type or "application/json" in content_type:
+ try:
+ return json.loads(body_str)
+ except (json.JSONDecodeError, TypeError):
+ return body_str
+ if "application/x-www-form-urlencoded" in content_type:
+ parsed_query = urllib.parse.parse_qs(body_str)
+ result = {k: v[0] for k, v in parsed_query.items()}
+ return result
+ if "text/plain" in content_type:
+ return body_str
+ return None
+
+
+def _parse_response(response: Any) -> Any:
+ """
+ Parses a response, attempting to decode JSON.
+
+ Args:
+ response: The response object to parse. This can be any type, but
+ it is expected to have a `json()` method if it contains JSON.
+
+ Returns:
+ The parsed response. If the response contains valid JSON, the
+ decoded JSON object (e.g., a dictionary or list) is returned.
+ If the response does not have a `json()` method or if the JSON
+ decoding fails, None is returned.
+ """
+ try:
+ json_response = response.json()
+ return json_response
+ except Exception:
+ # TODO(https://github.com/googleapis/google-auth-library-python/issues/1744):
+ # Parse and return response payload as json based on different content types.
+ return None
+
+
+def _response_log_base(logger: logging.Logger, parsed_response: Any) -> None:
+ """
+ Logs a parsed HTTP response at the DEBUG level.
+
+ This internal helper function takes a parsed response and logs it
+ using the provided logger. It also applies a hashing function to
+ potentially sensitive information before logging.
+
+ Args:
+ logger: The logging.Logger instance to use for logging.
+ parsed_response: The parsed HTTP response object (e.g., a dictionary,
+ list, or the original response if parsing failed).
+ """
+
+ logged_response = _hash_sensitive_info(parsed_response)
+ logger.debug("Response received...", extra={"httpResponse": logged_response})
+
+
+def response_log(logger: logging.Logger, response: Any) -> None:
+ """
+ Logs an HTTP response at the DEBUG level if logging is enabled.
+
+ Args:
+ logger: The logging.Logger instance to use.
+ response: The HTTP response object to log.
+ """
+ if is_logging_enabled(logger):
+ json_response = _parse_response(response)
+ _response_log_base(logger, json_response)
diff --git a/contrib/python/google-auth/py3/google/auth/aio/_helpers.py b/contrib/python/google-auth/py3/google/auth/aio/_helpers.py
new file mode 100644
index 00000000000..fd7d37a2f7b
--- /dev/null
+++ b/contrib/python/google-auth/py3/google/auth/aio/_helpers.py
@@ -0,0 +1,57 @@
+# Copyright 2025 Google Inc.
+#
+# 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.
+
+"""Helper functions for commonly used utilities."""
+
+
+import logging
+from typing import Any
+
+from google.auth import _helpers
+
+
+async def _parse_response_async(response: Any) -> Any:
+ """
+ Parses an async response, attempting to decode JSON.
+
+ Args:
+ response: The response object to parse. This can be any type, but
+ it is expected to have a `json()` method if it contains JSON.
+
+ Returns:
+ The parsed response. If the response contains valid JSON, the
+ decoded JSON object (e.g., a dictionary) is returned.
+ If the response does not have a `json()` method or if the JSON
+ decoding fails, None is returned.
+ """
+ try:
+ json_response = await response.json()
+ return json_response
+ except Exception:
+ # TODO(https://github.com/googleapis/google-auth-library-python/issues/1745):
+ # Parse and return response payload as json based on different content types.
+ return None
+
+
+async def response_log_async(logger: logging.Logger, response: Any) -> None:
+ """
+ Logs an Async HTTP response at the DEBUG level if logging is enabled.
+
+ Args:
+ logger: The logging.Logger instance to use.
+ response: The HTTP response object to log.
+ """
+ if _helpers.is_logging_enabled(logger):
+ json_response = await _parse_response_async(response)
+ _helpers._response_log_base(logger, json_response)
diff --git a/contrib/python/google-auth/py3/google/auth/aio/transport/aiohttp.py b/contrib/python/google-auth/py3/google/auth/aio/transport/aiohttp.py
index 074d1491c70..67a19f952d2 100644
--- a/contrib/python/google-auth/py3/google/auth/aio/transport/aiohttp.py
+++ b/contrib/python/google-auth/py3/google/auth/aio/transport/aiohttp.py
@@ -16,6 +16,7 @@
"""
import asyncio
+import logging
from typing import AsyncGenerator, Mapping, Optional
try:
@@ -27,8 +28,11 @@ except ImportError as caught_exc: # pragma: NO COVER
from google.auth import _helpers
from google.auth import exceptions
+from google.auth.aio import _helpers as _helpers_async
from google.auth.aio import transport
+_LOGGER = logging.getLogger(__name__)
+
class Response(transport.Response):
"""
@@ -155,6 +159,7 @@ class Request(transport.Request):
self._session = aiohttp.ClientSession()
client_timeout = aiohttp.ClientTimeout(total=timeout)
+ _helpers.request_log(_LOGGER, method, url, body, headers)
response = await self._session.request(
method,
url,
@@ -163,6 +168,7 @@ class Request(transport.Request):
timeout=client_timeout,
**kwargs,
)
+ await _helpers_async.response_log_async(_LOGGER, response)
return Response(response)
except aiohttp.ClientError as caught_exc:
diff --git a/contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py b/contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py
index bc4d9dc69af..36366be5108 100644
--- a/contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py
+++ b/contrib/python/google-auth/py3/google/auth/transport/_aiohttp_requests.py
@@ -22,14 +22,20 @@ from __future__ import absolute_import
import asyncio
import functools
+import logging
import aiohttp # type: ignore
import urllib3 # type: ignore
+from google.auth import _helpers
from google.auth import exceptions
from google.auth import transport
+from google.auth.aio import _helpers as _helpers_async
from google.auth.transport import requests
+
+_LOGGER = logging.getLogger(__name__)
+
# Timeout can be re-defined depending on async requirement. Currently made 60s more than
# sync timeout.
_DEFAULT_TIMEOUT = 180 # in seconds
@@ -182,10 +188,11 @@ class Request(transport.Request):
self.session = aiohttp.ClientSession(
auto_decompress=False
) # pragma: NO COVER
- requests._LOGGER.debug("Making request: %s %s", method, url)
+ _helpers.request_log(_LOGGER, method, url, body, headers)
response = await self.session.request(
method, url, data=body, headers=headers, timeout=timeout, **kwargs
)
+ await _helpers_async.response_log_async(_LOGGER, response)
return _CombinedResponse(response)
except aiohttp.ClientError as caught_exc:
diff --git a/contrib/python/google-auth/py3/google/auth/transport/_http_client.py b/contrib/python/google-auth/py3/google/auth/transport/_http_client.py
index cec0ab73fb3..898a86519b7 100644
--- a/contrib/python/google-auth/py3/google/auth/transport/_http_client.py
+++ b/contrib/python/google-auth/py3/google/auth/transport/_http_client.py
@@ -19,6 +19,7 @@ import logging
import socket
import urllib
+from google.auth import _helpers
from google.auth import exceptions
from google.auth import transport
@@ -99,10 +100,11 @@ class Request(transport.Request):
connection = http_client.HTTPConnection(parts.netloc, timeout=timeout)
try:
- _LOGGER.debug("Making request: %s %s", method, url)
+ _helpers.request_log(_LOGGER, method, url, body, headers)
connection.request(method, path, body=body, headers=headers, **kwargs)
response = connection.getresponse()
+ _helpers.response_log(_LOGGER, response)
return Response(response)
except (http_client.HTTPException, socket.error) as caught_exc:
diff --git a/contrib/python/google-auth/py3/google/auth/transport/requests.py b/contrib/python/google-auth/py3/google/auth/transport/requests.py
index 23a69783dc3..0540746f894 100644
--- a/contrib/python/google-auth/py3/google/auth/transport/requests.py
+++ b/contrib/python/google-auth/py3/google/auth/transport/requests.py
@@ -34,6 +34,7 @@ from requests.packages.urllib3.util.ssl_ import ( # type: ignore
create_urllib3_context,
) # pylint: disable=ungrouped-imports
+from google.auth import _helpers
from google.auth import environment_vars
from google.auth import exceptions
from google.auth import transport
@@ -182,10 +183,11 @@ class Request(transport.Request):
google.auth.exceptions.TransportError: If any exception occurred.
"""
try:
- _LOGGER.debug("Making request: %s %s", method, url)
+ _helpers.request_log(_LOGGER, method, url, body, headers)
response = self.session.request(
method, url, data=body, headers=headers, timeout=timeout, **kwargs
)
+ _helpers.response_log(_LOGGER, response)
return _Response(response)
except requests.exceptions.RequestException as caught_exc:
new_exc = exceptions.TransportError(caught_exc)
@@ -534,6 +536,7 @@ class AuthorizedSession(requests.Session):
remaining_time = guard.remaining_timeout
with TimeoutGuard(remaining_time) as guard:
+ _helpers.request_log(_LOGGER, method, url, data, headers)
response = super(AuthorizedSession, self).request(
method,
url,
@@ -542,6 +545,7 @@ class AuthorizedSession(requests.Session):
timeout=timeout,
**kwargs
)
+ _helpers.response_log(_LOGGER, response)
remaining_time = guard.remaining_timeout
# If the response indicated that the credentials needed to be
diff --git a/contrib/python/google-auth/py3/google/auth/transport/urllib3.py b/contrib/python/google-auth/py3/google/auth/transport/urllib3.py
index db4fa93ff11..03ed75aa2f5 100644
--- a/contrib/python/google-auth/py3/google/auth/transport/urllib3.py
+++ b/contrib/python/google-auth/py3/google/auth/transport/urllib3.py
@@ -50,6 +50,7 @@ except ImportError as caught_exc: # pragma: NO COVER
) from caught_exc
+from google.auth import _helpers
from google.auth import environment_vars
from google.auth import exceptions
from google.auth import transport
@@ -144,10 +145,11 @@ class Request(transport.Request):
kwargs["timeout"] = timeout
try:
- _LOGGER.debug("Making request: %s %s", method, url)
+ _helpers.request_log(_LOGGER, method, url, body, headers)
response = self.http.request(
method, url, body=body, headers=headers, **kwargs
)
+ _helpers.response_log(_LOGGER, response)
return _Response(response)
except urllib3.exceptions.HTTPError as caught_exc:
new_exc = exceptions.TransportError(caught_exc)
diff --git a/contrib/python/google-auth/py3/google/auth/version.py b/contrib/python/google-auth/py3/google/auth/version.py
index 393caa8ad44..d1363c1ef0e 100644
--- a/contrib/python/google-auth/py3/google/auth/version.py
+++ b/contrib/python/google-auth/py3/google/auth/version.py
@@ -12,4 +12,4 @@
# See the License for the specific language governing permissions and
# limitations under the License.
-__version__ = "2.39.0"
+__version__ = "2.40.0"
diff --git a/contrib/python/google-auth/py3/google/oauth2/webauthn_types.py b/contrib/python/google-auth/py3/google/oauth2/webauthn_types.py
index 7784e83d0b9..24e984f3d33 100644
--- a/contrib/python/google-auth/py3/google/oauth2/webauthn_types.py
+++ b/contrib/python/google-auth/py3/google/oauth2/webauthn_types.py
@@ -67,7 +67,7 @@ class GetRequest:
extensions: Optional[AuthenticationExtensionsClientInputs] = None
def to_json(self) -> str:
- req_options: Dict[str, Any] = {"rpid": self.rpid, "challenge": self.challenge}
+ req_options: Dict[str, Any] = {"rpId": self.rpid, "challenge": self.challenge}
if self.timeout_ms:
req_options["timeout"] = self.timeout_ms
if self.allow_credentials:
diff --git a/contrib/python/google-auth/py3/tests/aio/test__helpers.py b/contrib/python/google-auth/py3/tests/aio/test__helpers.py
new file mode 100644
index 00000000000..7642431caec
--- /dev/null
+++ b/contrib/python/google-auth/py3/tests/aio/test__helpers.py
@@ -0,0 +1,110 @@
+# Copyright 2025 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 json
+import logging
+
+import pytest # type: ignore
+
+from google.auth.aio import _helpers
+
+# _MOCK_BASE_LOGGER_NAME is the base logger namespace used for testing.
+_MOCK_BASE_LOGGER_NAME = "foogle"
+
+# _MOCK_CHILD_LOGGER_NAME is the child logger namespace used for testing.
+_MOCK_CHILD_LOGGER_NAME = "foogle.bar"
+
+
+def logger():
+ """Returns a child logger for testing."""
+ logger = logging.getLogger(_MOCK_CHILD_LOGGER_NAME)
+ logger.level = logging.NOTSET
+ logger.handlers = []
+ logger.propagate = True
+ return logger
+
+
+def base_logger():
+ """Returns a child logger for testing."""
+ logger = logging.getLogger(_MOCK_BASE_LOGGER_NAME)
+ logger.level = logging.NOTSET
+ logger.handlers = []
+ logger.propagate = True
+ return logger
+
+
+async def test_response_log_debug_enabled(logger, caplog, base_logger):
+ caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME)
+ await _helpers.response_log_async(logger, {"payload": None})
+ assert len(caplog.records) == 1
+ record = caplog.records[0]
+ assert record.message == "Response received..."
+ assert record.httpResponse == "<class 'NoneType'>"
+
+
+async def test_response_log_debug_disabled(logger, caplog, base_logger):
+ caplog.set_level(logging.INFO, logger=_MOCK_CHILD_LOGGER_NAME)
+ await _helpers.response_log_async(logger, "another_response")
+ assert "Response received..." not in caplog.text
+
+
+async def test_response_log_debug_enabled_response_json(logger, caplog, base_logger):
+ class MockResponse:
+ async def json(self):
+ return {"key1": "value1", "key2": "value2", "key3": "value3"}
+
+ response = MockResponse()
+ caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME)
+ await _helpers.response_log_async(logger, response)
+ assert len(caplog.records) == 1
+ record = caplog.records[0]
+ assert record.message == "Response received..."
+ assert record.httpResponse == {"key1": "value1", "key2": "value2", "key3": "value3"}
+
+
+async def test_parse_response_async_json_valid():
+ class MockResponse:
+ async def json(self):
+ return {"data": "test"}
+
+ response = MockResponse()
+ expected = {"data": "test"}
+ assert await _helpers._parse_response_async(response) == expected
+
+
+async def test_parse_response_async_json_invalid():
+ class MockResponse:
+ def json(self):
+ raise json.JSONDecodeError("msg", "doc", 0)
+
+ response = MockResponse()
+ assert await _helpers._parse_response_async(response) is None
+
+
+async def test_parse_response_async_no_json_method():
+ response = "plain text"
+ assert await _helpers._parse_response_async(response) is None
+
+
+async def test_parse_response_async_none():
+ assert await _helpers._parse_response_async(None) is None
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py
index 454e97cb61d..9fba266da9f 100644
--- a/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_handler.py
@@ -118,7 +118,7 @@ def test_success_get_assertion(os_get_stub, subprocess_run_stub):
"type": "get",
"origin": "fake_origin",
"requestData": {
- "rpid": "fake_rpid",
+ "rpId": "fake_rpid",
"challenge": "fake_challenge",
"allowCredentials": [{"type": "public-key", "id": "fake_id_1"}],
},
diff --git a/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py
index 5231d21896a..bafe5b05060 100644
--- a/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py
+++ b/contrib/python/google-auth/py3/tests/oauth2/test_webauthn_types.py
@@ -82,7 +82,7 @@ def test_GetRequest(has_allow_credentials):
"type": "get",
"origin": "fake_origin",
"requestData": {
- "rpid": "fake_rpid",
+ "rpId": "fake_rpid",
"timeout": 123,
"challenge": "fake_challenge",
"userVerification": "preferred",
diff --git a/contrib/python/google-auth/py3/tests/test__helpers.py b/contrib/python/google-auth/py3/tests/test__helpers.py
index c9a3847ac48..a4337c01608 100644
--- a/contrib/python/google-auth/py3/tests/test__helpers.py
+++ b/contrib/python/google-auth/py3/tests/test__helpers.py
@@ -13,12 +13,50 @@
# limitations under the License.
import datetime
+import json
+import logging
+from unittest import mock
import urllib
import pytest # type: ignore
from google.auth import _helpers
+# _MOCK_BASE_LOGGER_NAME is the base logger namespace used for testing.
+_MOCK_BASE_LOGGER_NAME = "foogle"
+
+# _MOCK_CHILD_LOGGER_NAME is the child logger namespace used for testing.
+_MOCK_CHILD_LOGGER_NAME = "foogle.bar"
+
+
+def logger():
+ """Returns a child logger for testing."""
+ logger = logging.getLogger(_MOCK_CHILD_LOGGER_NAME)
+ logger.level = logging.NOTSET
+ logger.handlers = []
+ logger.propagate = True
+ return logger
+
+
+def base_logger():
+ """Returns a child logger for testing."""
+ logger = logging.getLogger(_MOCK_BASE_LOGGER_NAME)
+ logger.level = logging.NOTSET
+ logger.handlers = []
+ logger.propagate = True
+ return logger
+
+
[email protected](autouse=True)
+def reset_logging_initialized():
+ """Resets the global _LOGGING_INITIALIZED variable before each test."""
+ original_state = _helpers._LOGGING_INITIALIZED
+ _helpers._LOGGING_INITIALIZED = False
+ yield
+ _helpers._LOGGING_INITIALIZED = original_state
+
class SourceClass(object):
def func(self): # pragma: NO COVER
@@ -92,7 +130,7 @@ def test_to_bytes_with_bytes():
def test_to_bytes_with_unicode():
- value = u"string-val"
+ value = "string-val"
encoded_value = b"string-val"
assert _helpers.to_bytes(value) == encoded_value
@@ -103,13 +141,13 @@ def test_to_bytes_with_nonstring_type():
def test_from_bytes_with_unicode():
- value = u"bytes-val"
+ value = "bytes-val"
assert _helpers.from_bytes(value) == value
def test_from_bytes_with_bytes():
value = b"string-val"
- decoded_value = u"string-val"
+ decoded_value = "string-val"
assert _helpers.from_bytes(value) == decoded_value
@@ -194,3 +232,393 @@ def test_unpadded_urlsafe_b64encode():
for case, expected in cases:
assert _helpers.unpadded_urlsafe_b64encode(case) == expected
+
+
+def test_hash_sensitive_info_basic():
+ test_data = {
+ "expires_in": 3599,
+ "access_token": "access-123",
+ "scope": "https://www.googleapis.com/auth/test-api",
+ "token_type": "Bearer",
+ }
+ hashed_data = _helpers._hash_sensitive_info(test_data)
+ assert hashed_data["expires_in"] == 3599
+ assert hashed_data["scope"] == "https://www.googleapis.com/auth/test-api"
+ assert hashed_data["access_token"].startswith("hashed_access_token-")
+ assert hashed_data["token_type"] == "Bearer"
+
+
+def test_hash_sensitive_info_multiple_sensitive():
+ test_data = {
+ "access_token": "some_long_token",
+ "id_token": "1234-5678-9012-3456",
+ "expires_in": 3599,
+ "token_type": "Bearer",
+ }
+ hashed_data = _helpers._hash_sensitive_info(test_data)
+ assert hashed_data["expires_in"] == 3599
+ assert hashed_data["token_type"] == "Bearer"
+ assert hashed_data["access_token"].startswith("hashed_access_token-")
+ assert hashed_data["id_token"].startswith("hashed_id_token-")
+
+
+def test_hash_sensitive_info_none_value():
+ test_data = {"username": "user3", "secret": None, "normal_data": "abc"}
+ hashed_data = _helpers._hash_sensitive_info(test_data)
+ assert hashed_data["secret"] is None
+ assert hashed_data["normal_data"] == "abc"
+
+
+def test_hash_sensitive_info_non_string_value():
+ test_data = {"username": "user4", "access_token": 12345, "normal_data": "def"}
+ hashed_data = _helpers._hash_sensitive_info(test_data)
+ assert hashed_data["access_token"].startswith("hashed_access_token-")
+ assert hashed_data["normal_data"] == "def"
+
+
+def test_hash_sensitive_info_list_value():
+ test_data = [
+ {"name": "Alice", "access_token": "12345"},
+ {"name": "Bob", "client_id": "1141"},
+ ]
+ hashed_data = _helpers._hash_sensitive_info(test_data)
+ assert hashed_data[0]["access_token"].startswith("hashed_access_token-")
+ assert hashed_data[1]["client_id"].startswith("hashed_client_id-")
+
+
+def test_hash_sensitive_info_nested_list_value():
+ test_data = [{"names": ["Alice", "Bob"], "tokens": [{"access_token": "1234"}]}]
+ hashed_data = _helpers._hash_sensitive_info(test_data)
+ assert hashed_data[0]["tokens"][0]["access_token"].startswith(
+ "hashed_access_token-"
+ )
+
+
+def test_hash_sensitive_info_int_value():
+ test_data = 123
+ hashed_data = _helpers._hash_sensitive_info(test_data)
+ assert hashed_data == "<class 'int'>"
+
+
+def test_hash_sensitive_info_bool_value():
+ test_data = True
+ hashed_data = _helpers._hash_sensitive_info(test_data)
+ assert hashed_data == "<class 'bool'>"
+
+
+def test_hash_sensitive_info_byte_value():
+ test_data = b"1243"
+ hashed_data = _helpers._hash_sensitive_info(test_data)
+ assert hashed_data == "<class 'bytes'>"
+
+
+def test_hash_sensitive_info_empty_dict():
+ test_data = {}
+ hashed_data = _helpers._hash_sensitive_info(test_data)
+ assert hashed_data == {}
+
+
+def test_hash_value_consistent_hashing():
+ value = "test_value"
+ field_name = "test_field"
+ hash1 = _helpers._hash_value(value, field_name)
+ hash2 = _helpers._hash_value(value, field_name)
+ assert hash1 == hash2
+
+
+def test_hash_value_different_hashing():
+ value1 = "test_value1"
+ value2 = "test_value2"
+ field_name = "test_field"
+ hash1 = _helpers._hash_value(value1, field_name)
+ hash2 = _helpers._hash_value(value2, field_name)
+ assert hash1 != hash2
+
+
+def test_hash_value_none():
+ assert _helpers._hash_value(None, "test") is None
+
+
+def test_logger_configured_default(logger):
+ assert not _helpers._logger_configured(logger)
+
+
+def test_logger_configured_with_handler(logger):
+ mock_handler = logging.NullHandler()
+ logger.addHandler(mock_handler)
+ assert _helpers._logger_configured(logger)
+
+ # Cleanup
+ logger.removeHandler(mock_handler)
+
+
+def test_logger_configured_with_custom_level(logger):
+ original_level = logger.level
+ logger.level = logging.INFO
+ assert _helpers._logger_configured(logger)
+
+ # Cleanup
+ logging.level = original_level
+
+
+def test_logger_configured_with_propagate(logger):
+ original_propagate = logger.propagate
+ logger.propagate = False
+ assert _helpers._logger_configured(logger)
+
+ # Cleanup
+ logger.propagate = original_propagate
+
+
+def test_is_logging_enabled_with_no_level_set(logger, base_logger):
+ with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", "foogle"):
+ assert _helpers.is_logging_enabled(logger) is False
+
+
+def test_is_logging_enabled_with_debug_disabled(caplog, logger, base_logger):
+ with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", _MOCK_BASE_LOGGER_NAME):
+ caplog.set_level(logging.INFO, logger=_MOCK_CHILD_LOGGER_NAME)
+ assert _helpers.is_logging_enabled(logger) is False
+
+
+def test_is_logging_enabled_with_debug_enabled(caplog, logger, base_logger):
+ with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", _MOCK_BASE_LOGGER_NAME):
+ caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME)
+ assert _helpers.is_logging_enabled(logger)
+
+
+def test_is_logging_enabled_with_base_logger_configured_with_info(
+ caplog, logger, base_logger
+):
+ with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", _MOCK_BASE_LOGGER_NAME):
+ caplog.set_level(logging.INFO, logger=_MOCK_BASE_LOGGER_NAME)
+
+ base_logger = logging.getLogger(_MOCK_BASE_LOGGER_NAME)
+ assert not _helpers.is_logging_enabled(base_logger)
+ assert not _helpers.is_logging_enabled(logger)
+
+
+def test_is_logging_enabled_with_base_logger_configured_with_debug(
+ caplog, logger, base_logger
+):
+ with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", _MOCK_BASE_LOGGER_NAME):
+ caplog.set_level(logging.DEBUG, logger=_MOCK_BASE_LOGGER_NAME)
+
+ assert _helpers.is_logging_enabled(base_logger)
+ assert _helpers.is_logging_enabled(logger)
+
+
+def test_is_logging_enabled_with_base_logger_info_child_logger_debug(
+ caplog, logger, base_logger
+):
+ with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", _MOCK_BASE_LOGGER_NAME):
+ caplog.set_level(logging.INFO, logger=_MOCK_BASE_LOGGER_NAME)
+ caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME)
+
+ assert not _helpers.is_logging_enabled(base_logger)
+ assert _helpers.is_logging_enabled(logger)
+
+
+def test_is_logging_enabled_with_base_logger_debug_child_logger_info(
+ caplog, logger, base_logger
+):
+ with mock.patch("google.auth._helpers._BASE_LOGGER_NAME", _MOCK_BASE_LOGGER_NAME):
+ caplog.set_level(logging.DEBUG, logger=_MOCK_BASE_LOGGER_NAME)
+ caplog.set_level(logging.INFO, logger=_MOCK_CHILD_LOGGER_NAME)
+
+ assert _helpers.is_logging_enabled(base_logger)
+ assert not _helpers.is_logging_enabled(logger)
+
+
+def test_request_log_debug_enabled(logger, caplog, base_logger):
+ caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME)
+ _helpers.request_log(
+ logger,
+ "GET",
+ "http://example.com",
+ b'{"key": "value"}',
+ {"Authorization": "Bearer token"},
+ )
+ assert len(caplog.records) == 1
+ record = caplog.records[0]
+ assert record.message == "Making request..."
+ assert record.httpRequest == {
+ "method": "GET",
+ "url": "http://example.com",
+ "body": {"key": "value"},
+ "headers": {"Authorization": "Bearer token"},
+ }
+
+
+def test_request_log_plain_text_debug_enabled(logger, caplog, base_logger):
+ caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME)
+ _helpers.request_log(
+ logger,
+ "GET",
+ "http://example.com",
+ b"This is plain text.",
+ {"Authorization": "Bearer token", "Content-Type": "text/plain"},
+ )
+ assert len(caplog.records) == 1
+ record = caplog.records[0]
+ assert record.message == "Making request..."
+ assert record.httpRequest == {
+ "method": "GET",
+ "url": "http://example.com",
+ "body": "<class 'str'>",
+ "headers": {"Authorization": "Bearer token", "Content-Type": "text/plain"},
+ }
+
+
+def test_request_log_debug_disabled(logger, caplog, base_logger):
+ caplog.set_level(logging.INFO, logger=_MOCK_CHILD_LOGGER_NAME)
+ _helpers.request_log(
+ logger,
+ "POST",
+ "https://api.example.com",
+ "data",
+ {"Content-Type": "application/json"},
+ )
+ assert "Making request: POST https://api.example.com" not in caplog.text
+
+
+def test_response_log_debug_enabled(logger, caplog, base_logger):
+ caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME)
+ _helpers.response_log(logger, {"payload": None})
+ assert len(caplog.records) == 1
+ record = caplog.records[0]
+ assert record.message == "Response received..."
+ assert record.httpResponse == "<class 'NoneType'>"
+
+
+def test_response_log_debug_disabled(logger, caplog):
+ caplog.set_level(logging.INFO, logger=_MOCK_CHILD_LOGGER_NAME)
+ _helpers.response_log(logger, "another_response")
+ assert "Response received..." not in caplog.text
+
+
+def test_response_log_base_logger_configured(logger, caplog, base_logger):
+ caplog.set_level(logging.DEBUG, logger=_MOCK_BASE_LOGGER_NAME)
+ _helpers.response_log(logger, "another_response")
+ assert "Response received..." in caplog.text
+
+
+def test_response_log_debug_enabled_response_list(logger, caplog, base_logger):
+ # NOTE: test the response log when response.json() returns a list as per
+ # https://requests.readthedocs.io/en/latest/api/#requests.Response.json.
+ class MockResponse:
+ def json(self):
+ return ["item1", "item2", "item3"]
+
+ response = MockResponse()
+ caplog.set_level(logging.DEBUG, logger=_MOCK_CHILD_LOGGER_NAME)
+ _helpers.response_log(logger, response)
+ assert len(caplog.records) == 1
+ record = caplog.records[0]
+ assert record.message == "Response received..."
+ assert record.httpResponse == ["<class 'str'>", "<class 'str'>", "<class 'str'>"]
+
+
+def test_parse_request_body_bytes_valid():
+ body = b"key1=value1&key2=value2"
+ expected = {"key1": "value1", "key2": "value2"}
+ assert (
+ _helpers._parse_request_body(
+ body, content_type="application/x-www-form-urlencoded"
+ )
+ == expected
+ )
+
+
+def test_parse_request_body_bytes_empty():
+ body = b""
+ assert _helpers._parse_request_body(body) == ""
+
+
+def test_parse_request_body_bytes_invalid_encoding():
+ body = b"\xff\xfe\xfd" # Invalid UTF-8 sequence
+ assert _helpers._parse_request_body(body) is None
+
+
+def test_parse_request_body_bytes_malformed_query():
+ body = b"key1=value1&key2=value2" # missing equals
+ expected = {"key1": "value1", "key2": "value2"}
+ assert (
+ _helpers._parse_request_body(
+ body, content_type="application/x-www-form-urlencoded"
+ )
+ == expected
+ )
+
+
+def test_parse_request_body_none():
+ assert _helpers._parse_request_body(None) is None
+
+
+def test_parse_request_body_bytes_no_content_type():
+ body = b'{"key": "value"}'
+ expected = {"key": "value"}
+ assert _helpers._parse_request_body(body) == expected
+
+
+def test_parse_request_body_bytes_content_type_json():
+ body = b'{"key": "value"}'
+ expected = {"key": "value"}
+ assert (
+ _helpers._parse_request_body(body, content_type="application/json") == expected
+ )
+
+
+def test_parse_request_body_content_type_urlencoded():
+ body = b"key=value"
+ expected = {"key": "value"}
+ assert (
+ _helpers._parse_request_body(
+ body, content_type="application/x-www-form-urlencoded"
+ )
+ == expected
+ )
+
+
+def test_parse_request_body_bytes_content_type_text():
+ body = b"This is plain text."
+ expected = "This is plain text."
+ assert _helpers._parse_request_body(body, content_type="text/plain") == expected
+
+
+def test_parse_request_body_content_type_invalid():
+ body = b'{"key": "value"}'
+ assert _helpers._parse_request_body(body, content_type="invalid") is None
+
+
+def test_parse_request_body_other_type():
+ assert _helpers._parse_request_body(123) is None
+ assert _helpers._parse_request_body("string") is None
+
+
+def test_parse_response_json_valid():
+ class MockResponse:
+ def json(self):
+ return {"data": "test"}
+
+ response = MockResponse()
+ expected = {"data": "test"}
+ assert _helpers._parse_response(response) == expected
+
+
+def test_parse_response_json_invalid():
+ class MockResponse:
+ def json(self):
+ raise json.JSONDecodeError("msg", "doc", 0)
+
+ response = MockResponse()
+ assert _helpers._parse_response(response) is None
+
+
+def test_parse_response_no_json_method():
+ response = "plain text"
+ assert _helpers._parse_response(response) is None
+
+
+def test_parse_response_none():
+ assert _helpers._parse_response(None) is None
diff --git a/contrib/python/google-auth/py3/ya.make b/contrib/python/google-auth/py3/ya.make
index eddecb079e5..7ceec35306e 100644
--- a/contrib/python/google-auth/py3/ya.make
+++ b/contrib/python/google-auth/py3/ya.make
@@ -2,7 +2,7 @@
PY3_LIBRARY()
-VERSION(2.39.0)
+VERSION(2.40.0)
LICENSE(Apache-2.0)
@@ -39,6 +39,7 @@ PY_SRCS(
google/auth/_refresh_worker.py
google/auth/_service_account_info.py
google/auth/aio/__init__.py
+ google/auth/aio/_helpers.py
google/auth/aio/credentials.py
google/auth/aio/transport/__init__.py
google/auth/aio/transport/aiohttp.py