diff options
author | Nejc Habjan <nejc.habjan@siemens.com> | 2022-05-02 00:22:20 +0200 |
---|---|---|
committer | Nejc Habjan <nejc.habjan@siemens.com> | 2022-07-29 23:38:46 +0200 |
commit | 08519b78e827278dc5d66b5d5a66eaeea0a500d5 (patch) | |
tree | e7380c1272bf49dac337a77f7b3143a59e398a2e | |
parent | 17414f787a70a0d916193ac71bccce0297c4e4e8 (diff) | |
download | gitlab-feat/logging-mask-tokens.tar.gz |
feat(client): mask tokens by default when loggingfeat/logging-mask-tokens
-rw-r--r-- | docs/api-usage.rst | 21 | ||||
-rw-r--r-- | gitlab/client.py | 31 | ||||
-rw-r--r-- | gitlab/utils.py | 26 |
3 files changed, 73 insertions, 5 deletions
diff --git a/docs/api-usage.rst b/docs/api-usage.rst index 434de38..3c561b1 100644 --- a/docs/api-usage.rst +++ b/docs/api-usage.rst @@ -384,6 +384,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 diff --git a/gitlab/client.py b/gitlab/client.py index 97ca636..9f99c4f 100644 --- a/gitlab/client.py +++ b/gitlab/client.py @@ -524,18 +524,39 @@ class Gitlab: 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(), diff --git a/gitlab/utils.py b/gitlab/utils.py index f3d97f7..83642c0 100644 --- a/gitlab/utils.py +++ b/gitlab/utils.py @@ -15,6 +15,7 @@ # You should have received a copy of the GNU Lesser General Public License # along with this program. If not, see <http://www.gnu.org/licenses/>. +import logging import pathlib import traceback import urllib.parse @@ -31,6 +32,31 @@ class _StdoutStream: print(chunk) +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: str = "%", # should be Literal, but needs extensions for 3.7 + validate: bool = True, + masked: Optional[str] = None, + ) -> None: + super().__init__(fmt, datefmt, style, validate) # type: ignore[arg-type] + 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, |