From 0c5ef364fb13ca9d7d17100166de87732d752de8 Mon Sep 17 00:00:00 2001 From: Yujian Zhao Date: Mon, 14 Apr 2025 12:38:51 -0700 Subject: [PATCH 1/4] fix: Correct webauthn JSON parsing to be compliant with standard. (#1658) 'rpid' -> 'rpId': https://www.w3.org/TR/webauthn-3/#dom-publickeycredentialrequestoptionsjson-rpid Co-authored-by: Harkamal Jot Singh Kumar --- google/oauth2/webauthn_types.py | 2 +- tests/oauth2/test_webauthn_handler.py | 2 +- tests/oauth2/test_webauthn_types.py | 2 +- 3 files changed, 3 insertions(+), 3 deletions(-) diff --git a/google/oauth2/webauthn_types.py b/google/oauth2/webauthn_types.py index 7784e83d0..24e984f3d 100644 --- a/google/oauth2/webauthn_types.py +++ b/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/tests/oauth2/test_webauthn_handler.py b/tests/oauth2/test_webauthn_handler.py index 454e97cb6..9fba266da 100644 --- a/tests/oauth2/test_webauthn_handler.py +++ b/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/tests/oauth2/test_webauthn_types.py b/tests/oauth2/test_webauthn_types.py index 5231d2189..bafe5b050 100644 --- a/tests/oauth2/test_webauthn_types.py +++ b/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", From 9e9f813085cc9b8ece6dc5d3973ff12b285b756e Mon Sep 17 00:00:00 2001 From: sai-sunder-s <4540365+sai-sunder-s@users.noreply.github.com> Date: Mon, 28 Apr 2025 23:35:57 +0000 Subject: [PATCH 2/4] chore: update secrets (#1752) --- system_tests/secrets.tar.enc | Bin 10324 -> 10324 bytes 1 file changed, 0 insertions(+), 0 deletions(-) diff --git a/system_tests/secrets.tar.enc b/system_tests/secrets.tar.enc index c8792a28994cac62c75f6025a79c07cf3e1d9282..fe6fb3aa0cbbc05a77da905c261b30e091e46c6f 100644 GIT binary patch literal 10324 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 From 77ad53eb00c74b3badc486c8207a16dbc49f37e5 Mon Sep 17 00:00:00 2001 From: ohmayr Date: Wed, 30 Apr 2025 02:21:09 +0500 Subject: [PATCH 3/4] feat: add request response logging to auth (#1678) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit * feat: add functionality to hash data (#1677) * feat: add functionality to hash data * change sensitive fields to private * update to sha512 * update docstring * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * chore: add request-response log helpers (#1685) * chore: add request-response log helpers * fix presubmit * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: opt-in logging support for request / response (#1686) * feat: opt-in logging support for request/response * add pragma no cover * add test coverage for request/response * add code coverage * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * chore: remove logging for async requests (#1698) * chore: remove logging for async requests * change Dict to Mapping * fix mypy and lint issues * address PR feedback * link issue * feat: parse request/response for logging (#1696) * feat: parse request/response for logging * add test case for list * address PR comments * address PR feedback * fix typo * add test coverage * add code coverage * feat: hash sensitive info in logs (#1700) * feat: hash sensitive info in logs * make helper private * add code coverage * address PR feedback * fix mypy type issue * 🦉 Updates from OwlBot post-processor See https://github.com/googleapis/repo-automation-bots/blob/main/packages/owl-bot/README.md * feat: add support for async response log (#1733) * feat: add support for async response log * fix whitespace * add await * add code coverage * fix lint * fix lint issues * address PR feedback * address PR feedback * link issue * feat: add request response logs for sync api calls (#1747) * fix: remove dependency on api-core for logging (#1748) * fix: remove dep on api-core for logging * disable propagation to the root logger * update async helpers tests * fix lint issue --------- Co-authored-by: Owl Bot --- google/auth/_helpers.py | 240 ++++++++++++ google/auth/aio/_helpers.py | 57 +++ google/auth/aio/transport/aiohttp.py | 6 + google/auth/transport/_aiohttp_requests.py | 9 +- google/auth/transport/_http_client.py | 4 +- google/auth/transport/requests.py | 6 +- google/auth/transport/urllib3.py | 4 +- tests/aio/test__helpers.py | 110 ++++++ tests/test__helpers.py | 434 ++++++++++++++++++++- 9 files changed, 863 insertions(+), 7 deletions(-) create mode 100644 google/auth/aio/_helpers.py create mode 100644 tests/aio/test__helpers.py diff --git a/google/auth/_helpers.py b/google/auth/_helpers.py index a6c07f7d8..78fe22f72 100644 --- a/google/auth/_helpers.py +++ b/google/auth/_helpers.py @@ -18,15 +18,38 @@ 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/google/auth/aio/_helpers.py b/google/auth/aio/_helpers.py new file mode 100644 index 000000000..fd7d37a2f --- /dev/null +++ b/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/google/auth/aio/transport/aiohttp.py b/google/auth/aio/transport/aiohttp.py index 074d1491c..67a19f952 100644 --- a/google/auth/aio/transport/aiohttp.py +++ b/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 @@ 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 @@ async def __call__( 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 @@ async def __call__( timeout=client_timeout, **kwargs, ) + await _helpers_async.response_log_async(_LOGGER, response) return Response(response) except aiohttp.ClientError as caught_exc: diff --git a/google/auth/transport/_aiohttp_requests.py b/google/auth/transport/_aiohttp_requests.py index bc4d9dc69..36366be51 100644 --- a/google/auth/transport/_aiohttp_requests.py +++ b/google/auth/transport/_aiohttp_requests.py @@ -22,14 +22,20 @@ 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 @@ async def __call__( 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/google/auth/transport/_http_client.py b/google/auth/transport/_http_client.py index cec0ab73f..898a86519 100644 --- a/google/auth/transport/_http_client.py +++ b/google/auth/transport/_http_client.py @@ -19,6 +19,7 @@ import socket import urllib +from google.auth import _helpers from google.auth import exceptions from google.auth import transport @@ -99,10 +100,11 @@ def __call__( 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/google/auth/transport/requests.py b/google/auth/transport/requests.py index 23a69783d..0540746f8 100644 --- a/google/auth/transport/requests.py +++ b/google/auth/transport/requests.py @@ -34,6 +34,7 @@ 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 @@ def __call__( 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 @@ def request( 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 @@ def request( 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/google/auth/transport/urllib3.py b/google/auth/transport/urllib3.py index db4fa93ff..03ed75aa2 100644 --- a/google/auth/transport/urllib3.py +++ b/google/auth/transport/urllib3.py @@ -50,6 +50,7 @@ ) 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 @@ def __call__( 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/tests/aio/test__helpers.py b/tests/aio/test__helpers.py new file mode 100644 index 000000000..7642431ca --- /dev/null +++ b/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" + + +@pytest.fixture +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 + + +@pytest.fixture +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 + + +@pytest.mark.asyncio +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 == "" + + +@pytest.mark.asyncio +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 + + +@pytest.mark.asyncio +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"} + + +@pytest.mark.asyncio +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 + + +@pytest.mark.asyncio +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 + + +@pytest.mark.asyncio +async def test_parse_response_async_no_json_method(): + response = "plain text" + assert await _helpers._parse_response_async(response) is None + + +@pytest.mark.asyncio +async def test_parse_response_async_none(): + assert await _helpers._parse_response_async(None) is None diff --git a/tests/test__helpers.py b/tests/test__helpers.py index c9a3847ac..a4337c016 100644 --- a/tests/test__helpers.py +++ b/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" + + +@pytest.fixture +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 + + +@pytest.fixture +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 + + +@pytest.fixture(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 == "" + + +def test_hash_sensitive_info_bool_value(): + test_data = True + hashed_data = _helpers._hash_sensitive_info(test_data) + assert hashed_data == "" + + +def test_hash_sensitive_info_byte_value(): + test_data = b"1243" + hashed_data = _helpers._hash_sensitive_info(test_data) + assert hashed_data == "" + + +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": "", + "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 == "" + + +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 == ["", "", ""] + + +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 From 0bebddda369b27b5ec4acec2073310b0b8a35461 Mon Sep 17 00:00:00 2001 From: "release-please[bot]" <55107282+release-please[bot]@users.noreply.github.com> Date: Mon, 5 May 2025 11:15:54 -0700 Subject: [PATCH 4/4] chore(main): release 2.40.0 (#1750) Co-authored-by: release-please[bot] <55107282+release-please[bot]@users.noreply.github.com> --- CHANGELOG.md | 12 ++++++++++++ google/auth/version.py | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 01e21e7cc..eff4202b8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,18 @@ [1]: https://pypi.org/project/google-auth/#history +## [2.40.0](https://github.com/googleapis/google-auth-library-python/compare/v2.39.0...v2.40.0) (2025-04-29) + + +### Features + +* Add request response logging to auth ([#1678](https://github.com/googleapis/google-auth-library-python/issues/1678)) ([77ad53e](https://github.com/googleapis/google-auth-library-python/commit/77ad53eb00c74b3badc486c8207a16dbc49f37e5)) + + +### Bug Fixes + +* Correct webauthn JSON parsing to be compliant with standard. ([#1658](https://github.com/googleapis/google-auth-library-python/issues/1658)) ([0c5ef36](https://github.com/googleapis/google-auth-library-python/commit/0c5ef364fb13ca9d7d17100166de87732d752de8)) + ## [2.39.0](https://github.com/googleapis/google-auth-library-python/compare/v2.38.0...v2.39.0) (2025-04-14) diff --git a/google/auth/version.py b/google/auth/version.py index 393caa8ad..d1363c1ef 100644 --- a/google/auth/version.py +++ b/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"