Skip to content

Commit c2b0b92

Browse files
authored
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
1 parent 98bc124 commit c2b0b92

File tree

2 files changed

+92
-19
lines changed

2 files changed

+92
-19
lines changed

google/auth/_helpers.py

Lines changed: 29 additions & 13 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@
2222
import json
2323
import logging
2424
import sys
25-
from typing import Any, Mapping, Optional
25+
from typing import Any, Dict, Mapping, Optional, Union
2626
import urllib
2727

2828
from google.auth import exceptions
@@ -298,7 +298,7 @@ def is_python_3():
298298
return sys.version_info > (3, 0)
299299

300300

301-
def hash_sensitive_info(data: dict) -> dict:
301+
def _hash_sensitive_info(data: Union[dict, list]) -> Union[dict, list, str]:
302302
"""
303303
Hashes sensitive information within a dictionary.
304304
@@ -307,14 +307,30 @@ def hash_sensitive_info(data: dict) -> dict:
307307
308308
Returns:
309309
A new dictionary with sensitive values replaced by their SHA512 hashes.
310+
If the input is a list, returns a list with each element recursively processed.
311+
If the input is neither a dict nor a list, returns the type of the input as a string.
312+
310313
"""
311-
hashed_data = {}
312-
for key, value in data.items():
313-
if key in _SENSITIVE_FIELDS:
314-
hashed_data[key] = _hash_value(value, key)
315-
else:
316-
hashed_data[key] = value
317-
return hashed_data
314+
if isinstance(data, dict):
315+
hashed_data : Dict[Any, Union[Optional[str], dict, list]] = {}
316+
for key, value in data.items():
317+
if key in _SENSITIVE_FIELDS and not isinstance(value, (dict, list)):
318+
hashed_data[key] = _hash_value(value, key)
319+
elif isinstance(value, (dict, list)):
320+
hashed_data[key] = _hash_sensitive_info(value)
321+
else:
322+
hashed_data[key] = value
323+
return hashed_data
324+
elif isinstance(data, list):
325+
hashed_list = []
326+
for val in data:
327+
hashed_list.append(_hash_sensitive_info(val))
328+
return hashed_list
329+
else:
330+
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1701):
331+
# Investigate and hash sensitive info before logging when the data type is
332+
# not a dict or a list.
333+
return str(type(data))
318334

319335

320336
def _hash_value(value, field_name: str) -> Optional[str]:
@@ -358,19 +374,19 @@ def request_log(
358374
body: The request body (can be None).
359375
headers: The request headers (can be None).
360376
"""
361-
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1681): Hash sensitive information.
362377
if is_logging_enabled(logger):
363378
content_type = (
364379
headers["Content-Type"] if headers and "Content-Type" in headers else ""
365380
)
366381
json_body = _parse_request_body(body, content_type=content_type)
382+
logged_body = _hash_sensitive_info(json_body)
367383
logger.debug(
368384
"Making request...",
369385
extra={
370386
"httpRequest": {
371387
"method": method,
372388
"url": url,
373-
"body": json_body,
389+
"body": logged_body,
374390
"headers": headers,
375391
}
376392
},
@@ -446,7 +462,7 @@ def response_log(logger: logging.Logger, response: Any) -> None:
446462
logger: The logging.Logger instance to use.
447463
response: The HTTP response object to log.
448464
"""
449-
# TODO(https://github.com/googleapis/google-auth-library-python/issues/1681): Hash sensitive information.
450465
if is_logging_enabled(logger):
451466
json_response = _parse_response(response)
452-
logger.debug("Response received...", extra={"httpResponse": json_response})
467+
logged_response = _hash_sensitive_info(json_response)
468+
logger.debug("Response received...", extra={"httpResponse": logged_response})

tests/test__helpers.py

Lines changed: 63 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -212,7 +212,7 @@ def test_hash_sensitive_info_basic():
212212
"scope": "https://www.googleapis.com/auth/test-api",
213213
"token_type": "Bearer",
214214
}
215-
hashed_data = _helpers.hash_sensitive_info(test_data)
215+
hashed_data = _helpers._hash_sensitive_info(test_data)
216216
assert hashed_data["expires_in"] == 3599
217217
assert hashed_data["scope"] == "https://www.googleapis.com/auth/test-api"
218218
assert hashed_data["access_token"].startswith("hashed_access_token-")
@@ -226,7 +226,7 @@ def test_hash_sensitive_info_multiple_sensitive():
226226
"expires_in": 3599,
227227
"token_type": "Bearer",
228228
}
229-
hashed_data = _helpers.hash_sensitive_info(test_data)
229+
hashed_data = _helpers._hash_sensitive_info(test_data)
230230
assert hashed_data["expires_in"] == 3599
231231
assert hashed_data["token_type"] == "Bearer"
232232
assert hashed_data["access_token"].startswith("hashed_access_token-")
@@ -235,21 +235,57 @@ def test_hash_sensitive_info_multiple_sensitive():
235235

236236
def test_hash_sensitive_info_none_value():
237237
test_data = {"username": "user3", "secret": None, "normal_data": "abc"}
238-
hashed_data = _helpers.hash_sensitive_info(test_data)
238+
hashed_data = _helpers._hash_sensitive_info(test_data)
239239
assert hashed_data["secret"] is None
240240
assert hashed_data["normal_data"] == "abc"
241241

242242

243243
def test_hash_sensitive_info_non_string_value():
244244
test_data = {"username": "user4", "access_token": 12345, "normal_data": "def"}
245-
hashed_data = _helpers.hash_sensitive_info(test_data)
245+
hashed_data = _helpers._hash_sensitive_info(test_data)
246246
assert hashed_data["access_token"].startswith("hashed_access_token-")
247247
assert hashed_data["normal_data"] == "def"
248248

249249

250+
def test_hash_sensitive_info_list_value():
251+
test_data = [
252+
{"name": "Alice", "access_token": "12345"},
253+
{"name": "Bob", "client_id": "1141"},
254+
]
255+
hashed_data = _helpers._hash_sensitive_info(test_data)
256+
assert hashed_data[0]["access_token"].startswith("hashed_access_token-")
257+
assert hashed_data[1]["client_id"].startswith("hashed_client_id-")
258+
259+
260+
def test_hash_sensitive_info_nested_list_value():
261+
test_data = [{"names": ["Alice", "Bob"], "tokens": [{"access_token": "1234"}]}]
262+
hashed_data = _helpers._hash_sensitive_info(test_data)
263+
assert hashed_data[0]["tokens"][0]["access_token"].startswith(
264+
"hashed_access_token-"
265+
)
266+
267+
268+
def test_hash_sensitive_info_int_value():
269+
test_data = 123
270+
hashed_data = _helpers._hash_sensitive_info(test_data)
271+
assert hashed_data == "<class 'int'>"
272+
273+
274+
def test_hash_sensitive_info_bool_value():
275+
test_data = True
276+
hashed_data = _helpers._hash_sensitive_info(test_data)
277+
assert hashed_data == "<class 'bool'>"
278+
279+
280+
def test_hash_sensitive_info_byte_value():
281+
test_data = b"1243"
282+
hashed_data = _helpers._hash_sensitive_info(test_data)
283+
assert hashed_data == "<class 'bytes'>"
284+
285+
250286
def test_hash_sensitive_info_empty_dict():
251287
test_data = {}
252-
hashed_data = _helpers.hash_sensitive_info(test_data)
288+
hashed_data = _helpers._hash_sensitive_info(test_data)
253289
assert hashed_data == {}
254290

255291

@@ -321,6 +357,27 @@ def test_request_log_debug_enabled(logger, caplog):
321357
}
322358

323359

360+
def test_request_log_plain_text_debug_enabled(logger, caplog):
361+
logger.setLevel(logging.DEBUG)
362+
with mock.patch("google.auth._helpers.CLIENT_LOGGING_SUPPORTED", True):
363+
_helpers.request_log(
364+
logger,
365+
"GET",
366+
"http://example.com",
367+
b"This is plain text.",
368+
{"Authorization": "Bearer token", "Content-Type": "text/plain"},
369+
)
370+
assert len(caplog.records) == 1
371+
record = caplog.records[0]
372+
assert record.message == "Making request..."
373+
assert record.httpRequest == {
374+
"method": "GET",
375+
"url": "http://example.com",
376+
"body": "<class 'str'>",
377+
"headers": {"Authorization": "Bearer token", "Content-Type": "text/plain"},
378+
}
379+
380+
324381
def test_request_log_debug_disabled(logger, caplog):
325382
logger.setLevel(logging.INFO)
326383
with mock.patch("google.auth._helpers.CLIENT_LOGGING_SUPPORTED", True):
@@ -365,7 +422,7 @@ def json(self):
365422
assert len(caplog.records) == 1
366423
record = caplog.records[0]
367424
assert record.message == "Response received..."
368-
assert record.httpResponse == ["item1", "item2", "item3"]
425+
assert record.httpResponse == ["<class 'str'>", "<class 'str'>", "<class 'str'>"]
369426

370427

371428
def test_parse_request_body_bytes_valid():

0 commit comments

Comments
 (0)