Skip to content

Commit 1611d78

Browse files
nejchJohnVillalovos
authored andcommitted
feat(client): mask tokens by default when logging
1 parent 2a2404f commit 1611d78

File tree

3 files changed

+74
-6
lines changed

3 files changed

+74
-6
lines changed

docs/api-usage.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -406,6 +406,27 @@ user. For example:
406406
407407
p = gl.projects.create({'name': 'awesome_project'}, sudo='user1')
408408
409+
Logging
410+
=======
411+
412+
To enable debug logging from the underlying ``requests`` and ``http.client`` calls,
413+
you can use ``enable_debug()`` on your ``Gitlab`` instance. For example:
414+
415+
.. code-block:: python
416+
417+
import os
418+
import gitlab
419+
420+
gl = gitlab.Gitlab(private_token=os.getenv("GITLAB_TOKEN"))
421+
gl.enable_debug()
422+
423+
By default, python-gitlab will mask the token used for authentication in logging output.
424+
If you'd like to debug credentials sent to the API, you can disable masking explicitly:
425+
426+
.. code-block:: python
427+
428+
gl.enable_debug(mask_credentials=False)
429+
409430
.. _object_attributes:
410431

411432
Attributes in updated objects

gitlab/client.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -517,18 +517,39 @@ def _set_auth_info(self) -> None:
517517
self.http_username, self.http_password
518518
)
519519

520-
@staticmethod
521-
def enable_debug() -> None:
520+
def enable_debug(self, mask_credentials: bool = True) -> None:
522521
import logging
523-
from http.client import HTTPConnection # noqa
522+
from http import client
524523

525-
HTTPConnection.debuglevel = 1
524+
client.HTTPConnection.debuglevel = 1
526525
logging.basicConfig()
527-
logging.getLogger().setLevel(logging.DEBUG)
526+
logger = logging.getLogger()
527+
logger.setLevel(logging.DEBUG)
528+
529+
httpclient_log = logging.getLogger("http.client")
530+
httpclient_log.propagate = True
531+
httpclient_log.setLevel(logging.DEBUG)
532+
528533
requests_log = logging.getLogger("requests.packages.urllib3")
529534
requests_log.setLevel(logging.DEBUG)
530535
requests_log.propagate = True
531536

537+
# shadow http.client prints to log()
538+
# https://stackoverflow.com/a/16337639
539+
def print_as_log(*args: Any) -> None:
540+
httpclient_log.log(logging.DEBUG, " ".join(args))
541+
542+
setattr(client, "print", print_as_log)
543+
544+
if not mask_credentials:
545+
return
546+
547+
token = self.private_token or self.oauth_token or self.job_token
548+
handler = logging.StreamHandler()
549+
handler.setFormatter(utils.MaskingFormatter(masked=token))
550+
logger.handlers.clear()
551+
logger.addHandler(handler)
552+
532553
def _get_session_opts(self) -> Dict[str, Any]:
533554
return {
534555
"headers": self.headers.copy(),

gitlab/utils.py

Lines changed: 27 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,9 +1,10 @@
11
import email.message
2+
import logging
23
import pathlib
34
import traceback
45
import urllib.parse
56
import warnings
6-
from typing import Any, Callable, Dict, Iterator, Optional, Tuple, Type, Union
7+
from typing import Any, Callable, Dict, Iterator, Literal, Optional, Tuple, Type, Union
78

89
import requests
910

@@ -22,6 +23,31 @@ def get_content_type(content_type: Optional[str]) -> str:
2223
return message.get_content_type()
2324

2425

26+
class MaskingFormatter(logging.Formatter):
27+
"""A logging formatter that can mask credentials"""
28+
29+
def __init__(
30+
self,
31+
fmt: Optional[str] = logging.BASIC_FORMAT,
32+
datefmt: Optional[str] = None,
33+
style: Literal["%", "{", "$"] = "%",
34+
validate: bool = True,
35+
masked: Optional[str] = None,
36+
) -> None:
37+
super().__init__(fmt, datefmt, style, validate)
38+
self.masked = masked
39+
40+
def _filter(self, entry: str) -> str:
41+
if not self.masked:
42+
return entry
43+
44+
return entry.replace(self.masked, "[MASKED]")
45+
46+
def format(self, record: logging.LogRecord) -> str:
47+
original = logging.Formatter.format(self, record)
48+
return self._filter(original)
49+
50+
2551
def response_content(
2652
response: requests.Response,
2753
streamed: bool,

0 commit comments

Comments
 (0)