Skip to content

Commit 2d1d69f

Browse files
committed
Redact potentially sensitive fields when logging
1 parent 113193a commit 2d1d69f

File tree

2 files changed

+56
-2
lines changed

2 files changed

+56
-2
lines changed

pylitterbot/session.py

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -10,7 +10,7 @@
1010
from aiohttp import ClientSession
1111

1212
from .exceptions import InvalidCommandException
13-
from .utils import decode, utcnow
13+
from .utils import decode, redact, utcnow
1414

1515
T = TypeVar("T", bound="Session")
1616

@@ -101,7 +101,7 @@ async def request(
101101

102102
resp.raise_for_status()
103103
data = await resp.json()
104-
_LOGGER.debug("Received %s response: %s", resp.status, data)
104+
_LOGGER.debug("Received %s response: %s", resp.status, redact(data))
105105
return data # type: ignore
106106

107107
async def __aenter__(self: T) -> T:

pylitterbot/utils.py

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -5,13 +5,32 @@
55
import logging
66
import re
77
from base64 import b64decode, b64encode
8+
from collections.abc import Mapping
89
from datetime import datetime, time, timezone
10+
from typing import TypeVar, cast, overload
911
from urllib.parse import urljoin as _urljoin
1012
from warnings import warn
1113

1214
_LOGGER = logging.getLogger(__name__)
15+
_T = TypeVar("_T")
1316

1417
ENCODING = "utf-8"
18+
REDACTED = "**REDACTED**"
19+
REDACT_FIELDS = [
20+
"token",
21+
"idToken",
22+
"refreshToken",
23+
"userId",
24+
"userEmail",
25+
"sessionId",
26+
"oneSignalPlayerId",
27+
"deviceId",
28+
"id",
29+
"litterRobotId",
30+
"unitId",
31+
"litterRobotSerial",
32+
"serial",
33+
]
1534

1635

1736
def decode(value: str) -> str:
@@ -79,3 +98,38 @@ def send_deprecation_warning(
7998
message = f"{old_name} has been deprecated{'' if new_name is None else f' in favor of {new_name}'} and will be removed in a future release"
8099
warn(message, DeprecationWarning, stacklevel=2)
81100
_LOGGER.warning(message)
101+
102+
103+
@overload
104+
def redact(data: Mapping) -> dict: # type: ignore[misc]
105+
...
106+
107+
108+
@overload
109+
def redact(data: _T) -> _T:
110+
...
111+
112+
113+
def redact(data: _T) -> _T:
114+
"""Redact sensitive data in a dict."""
115+
if not isinstance(data, (Mapping, list)):
116+
return data
117+
118+
if isinstance(data, list):
119+
return cast(_T, [redact(val) for val in data])
120+
121+
redacted = {**data}
122+
123+
for key, value in redacted.items():
124+
if value is None:
125+
continue
126+
if isinstance(value, str) and not value:
127+
continue
128+
if key in REDACT_FIELDS:
129+
redacted[key] = REDACTED
130+
elif isinstance(value, Mapping):
131+
redacted[key] = redact(value)
132+
elif isinstance(value, list):
133+
redacted[key] = [redact(item) for item in value]
134+
135+
return cast(_T, redacted)

0 commit comments

Comments
 (0)