summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorNejc Habjan <nejc.habjan@siemens.com>2022-05-02 00:22:20 +0200
committerNejc Habjan <nejc.habjan@siemens.com>2022-07-29 23:38:46 +0200
commit08519b78e827278dc5d66b5d5a66eaeea0a500d5 (patch)
treee7380c1272bf49dac337a77f7b3143a59e398a2e
parent17414f787a70a0d916193ac71bccce0297c4e4e8 (diff)
downloadgitlab-feat/logging-mask-tokens.tar.gz
feat(client): mask tokens by default when loggingfeat/logging-mask-tokens
-rw-r--r--docs/api-usage.rst21
-rw-r--r--gitlab/client.py31
-rw-r--r--gitlab/utils.py26
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,