Skip to content

Commit

Permalink
feat(client): mask tokens by default when logging
Browse files Browse the repository at this point in the history
  • Loading branch information
nejch authored and JohnVillalovos committed Oct 13, 2023
1 parent 2a2404f commit 1611d78
Show file tree
Hide file tree
Showing 3 changed files with 74 additions and 6 deletions.
21 changes: 21 additions & 0 deletions docs/api-usage.rst
Expand Up @@ -406,6 +406,27 @@ user. For example:
p = gl.projects.create({'name': 'awesome_project'}, sudo='user1')
Logging
=======

To enable debug logging from the underlying ``requests`` and ``http.client`` calls,
you can use ``enable_debug()`` on your ``Gitlab`` instance. For example:

.. code-block:: python
import os
import gitlab
gl = gitlab.Gitlab(private_token=os.getenv("GITLAB_TOKEN"))
gl.enable_debug()
By default, python-gitlab will mask the token used for authentication in logging output.
If you'd like to debug credentials sent to the API, you can disable masking explicitly:

.. code-block:: python
gl.enable_debug(mask_credentials=False)
.. _object_attributes:

Attributes in updated objects
Expand Down
31 changes: 26 additions & 5 deletions gitlab/client.py
Expand Up @@ -517,18 +517,39 @@ def _set_auth_info(self) -> None:
self.http_username, self.http_password
)

@staticmethod
def enable_debug() -> None:
def enable_debug(self, mask_credentials: bool = True) -> None:
import logging
from http.client import HTTPConnection # noqa
from http import client

HTTPConnection.debuglevel = 1
client.HTTPConnection.debuglevel = 1
logging.basicConfig()
logging.getLogger().setLevel(logging.DEBUG)
logger = logging.getLogger()
logger.setLevel(logging.DEBUG)

httpclient_log = logging.getLogger("http.client")
httpclient_log.propagate = True
httpclient_log.setLevel(logging.DEBUG)

requests_log = logging.getLogger("requests.packages.urllib3")
requests_log.setLevel(logging.DEBUG)
requests_log.propagate = True

# shadow http.client prints to log()
# https://stackoverflow.com/a/16337639
def print_as_log(*args: Any) -> None:
httpclient_log.log(logging.DEBUG, " ".join(args))

setattr(client, "print", print_as_log)

if not mask_credentials:
return

token = self.private_token or self.oauth_token or self.job_token
handler = logging.StreamHandler()
handler.setFormatter(utils.MaskingFormatter(masked=token))
logger.handlers.clear()
logger.addHandler(handler)

def _get_session_opts(self) -> Dict[str, Any]:
return {
"headers": self.headers.copy(),
Expand Down
28 changes: 27 additions & 1 deletion gitlab/utils.py
@@ -1,9 +1,10 @@
import email.message
import logging
import pathlib
import traceback
import urllib.parse
import warnings
from typing import Any, Callable, Dict, Iterator, Optional, Tuple, Type, Union
from typing import Any, Callable, Dict, Iterator, Literal, Optional, Tuple, Type, Union

import requests

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


class MaskingFormatter(logging.Formatter):
"""A logging formatter that can mask credentials"""

def __init__(
self,
fmt: Optional[str] = logging.BASIC_FORMAT,
datefmt: Optional[str] = None,
style: Literal["%", "{", "$"] = "%",
validate: bool = True,
masked: Optional[str] = None,
) -> None:
super().__init__(fmt, datefmt, style, validate)
self.masked = masked

def _filter(self, entry: str) -> str:
if not self.masked:
return entry

return entry.replace(self.masked, "[MASKED]")

def format(self, record: logging.LogRecord) -> str:
original = logging.Formatter.format(self, record)
return self._filter(original)


def response_content(
response: requests.Response,
streamed: bool,
Expand Down

0 comments on commit 1611d78

Please sign in to comment.