Skip to content

Commit dacf8a3

Browse files
nejchJohnVillalovos
authored andcommitted
feat(client): mask tokens by default when logging
1 parent 17414f7 commit dacf8a3

File tree

3 files changed

+71
-5
lines changed

3 files changed

+71
-5
lines changed

docs/api-usage.rst

Lines changed: 21 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -384,6 +384,27 @@ user. For example:
384384
385385
p = gl.projects.create({'name': 'awesome_project'}, sudo='user1')
386386
387+
Logging
388+
=======
389+
390+
To enable debug logging from the underlying ``requests`` and ``http.client`` calls,
391+
you can use ``enable_debug()`` on your ``Gitlab`` instance. For example:
392+
393+
.. code-block:: python
394+
395+
import os
396+
import gitlab
397+
398+
gl = gitlab.Gitlab(private_token=os.getenv("GITLAB_TOKEN"))
399+
gl.enable_debug()
400+
401+
By default, python-gitlab will mask the token used for authentication in logging output.
402+
If you'd like to debug credentials sent to the API, you can disable masking explicitly:
403+
404+
.. code-block:: python
405+
406+
gl.enable_debug(mask_credentials=False)
407+
387408
.. _object_attributes:
388409

389410
Attributes in updated objects

gitlab/client.py

Lines changed: 26 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -524,18 +524,39 @@ def _set_auth_info(self) -> None:
524524
self.http_username, self.http_password
525525
)
526526

527-
@staticmethod
528-
def enable_debug() -> None:
527+
def enable_debug(self, mask_credentials: bool = True) -> None:
529528
import logging
530-
from http.client import HTTPConnection # noqa
529+
from http import client
531530

532-
HTTPConnection.debuglevel = 1
531+
client.HTTPConnection.debuglevel = 1
533532
logging.basicConfig()
534-
logging.getLogger().setLevel(logging.DEBUG)
533+
logger = logging.getLogger()
534+
logger.setLevel(logging.DEBUG)
535+
536+
httpclient_log = logging.getLogger("http.client")
537+
httpclient_log.propagate = True
538+
httpclient_log.setLevel(logging.DEBUG)
539+
535540
requests_log = logging.getLogger("requests.packages.urllib3")
536541
requests_log.setLevel(logging.DEBUG)
537542
requests_log.propagate = True
538543

544+
# shadow http.client prints to log()
545+
# https://stackoverflow.com/a/16337639
546+
def print_as_log(*args: Any) -> None:
547+
httpclient_log.log(logging.DEBUG, " ".join(args))
548+
549+
setattr(client, "print", print_as_log)
550+
551+
if not mask_credentials:
552+
return
553+
554+
token = self.private_token or self.oauth_token or self.job_token
555+
handler = logging.StreamHandler()
556+
handler.setFormatter(utils.MaskingFormatter(masked=token))
557+
logger.handlers.clear()
558+
logger.addHandler(handler)
559+
539560
def _get_session_opts(self) -> Dict[str, Any]:
540561
return {
541562
"headers": self.headers.copy(),

gitlab/utils.py

Lines changed: 24 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
# You should have received a copy of the GNU Lesser General Public License
1616
# along with this program. If not, see <http://www.gnu.org/licenses/>.
1717

18+
import logging
1819
import pathlib
1920
import traceback
2021
import urllib.parse
@@ -31,6 +32,29 @@ def __call__(self, chunk: Any) -> None:
3132
print(chunk)
3233

3334

35+
class MaskingFormatter(logging.Formatter):
36+
"""A logging formatter that can mask credentials"""
37+
38+
def __init__(
39+
self,
40+
*args: Any,
41+
masked: Optional[str] = None,
42+
**kwargs: Any,
43+
) -> None:
44+
super().__init__(*args, **kwargs)
45+
self.masked = masked
46+
47+
def _filter(self, entry: str) -> str:
48+
if not self.masked:
49+
return entry
50+
51+
return entry.replace(self.masked, "[MASKED]")
52+
53+
def format(self, record: logging.LogRecord) -> str:
54+
original = logging.Formatter.format(self, record)
55+
return self._filter(original)
56+
57+
3458
def response_content(
3559
response: requests.Response,
3660
streamed: bool,

0 commit comments

Comments
 (0)