diff --git a/AUTHORS.rst b/AUTHORS.rst index 0b2fb31..2422a37 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -25,3 +25,5 @@ Maintainers & Contributors - Sylvain Marie - Craig Anderson - Hugo van Kemenade +- Jacques Troussard +- Erland Vollset diff --git a/README.rst b/README.rst index c1e99dd..901d915 100644 --- a/README.rst +++ b/README.rst @@ -56,3 +56,22 @@ To install requests and requests_oauthlib you can use pip: :alt: Documentation Status :scale: 100% :target: https://requests-oauthlib.readthedocs.io/ + +Advanced Configurations +----------------------- + +### Logger Configuration Framework + +`requests-oauthlib` now includes a flexible framework for applying custom filters and configurations to the logger, enhancing control over logging behavior and improving security. + +#### Custom Filters + +- **Debug Mode Token Filter**: To enhance security and provide more control over logging of sensitive information, requests-oauthlib introduces the Debug Mode Token Filter. This feature is controlled via the DEBUG_MODE_TOKEN_FILTER environment variable, allowing the suppression or masking of sensitive data in logs. + + ##### Configuring the Debug Mode Token Filter + + - **Environment Variable**: `DEBUG_MODE_TOKEN_FILTER` + - **Options**: + - `DEFAULT`: No alteration to logging behavior. + - `MASK`: Masks sensitive tokens in logs. + - `SUPPRESS`: Prevents logging of potentially sensitive information. (logger ignores these logs entirely) diff --git a/requests_oauthlib/__init__.py b/requests_oauthlib/__init__.py index 865d72f..302db12 100644 --- a/requests_oauthlib/__init__.py +++ b/requests_oauthlib/__init__.py @@ -6,6 +6,8 @@ from .oauth2_auth import OAuth2 from .oauth2_session import OAuth2Session, TokenUpdated +from .log_filters import DebugModeTokenFilter + __version__ = "2.0.0" import requests @@ -18,3 +20,11 @@ raise Warning(msg % requests.__version__) logging.getLogger("requests_oauthlib").addHandler(logging.NullHandler()) +logging.getLogger("requests_oauthlib").addFilter(DebugModeTokenFilter()) + +for filter_ in logging.getLogger("requests_oauthlib").filters: + if isinstance(filter_, DebugModeTokenFilter): + if filter_.mode == 'DEFAULT': + msg = "Your logger, when in DEBUG mode, will log TOKENS" + logging.warning(msg) + break diff --git a/requests_oauthlib/log_filters.py b/requests_oauthlib/log_filters.py new file mode 100644 index 0000000..9870de7 --- /dev/null +++ b/requests_oauthlib/log_filters.py @@ -0,0 +1,46 @@ +import os +import re +import logging + + +class DebugModeTokenFilter(logging.Filter): + """ + A logging filter that while in DEBUG mode can filter TOKENS dependent on configuration. + + This filter uses an environment variable to determine its mode, + which can either mask sensitive tokens in log messages, suppress logging, + or default to standard logging behavior with a warning. + + Attributes: + mode (str): The mode of operation based on the environment variable + 'DEBUG_MODE_TOKEN_FILTER'. Can be 'MASK', 'SUPPRESS', or 'DEFAULT'. + """ + + def __init__(self): + """ + Initializes the DebugModeTokenFilter with the 'DEBUG_MODE_TOKEN_FILTER' + environment variable. + """ + super().__init__() + self.mode = os.getenv( + 'REQUESTS_OAUTHLIB_DEBUG_MODE_TOKEN_FILTER', 'DEFAULT').upper() + + def filter(self, record): + """ + Filters logs of TOKENS dependent on the configured mode. + + Args: + record (logging.LogRecord): The log record to filter. + + Returns: + bool: True if the record should be logged, False otherwise. + """ + if record.levelno == logging.DEBUG: + if self.mode == "MASK": + record.msg = re.sub( + r'Bearer\s+([A-Za-z0-9\-._~+\/]+)', '[MASKED]', record.getMessage()) + elif self.mode == "SUPPRESS": + record.msg = " " + else: + return False + return True # if mode is not MASKED then DEFAULT is implied diff --git a/tests/test_log_filters.py b/tests/test_log_filters.py new file mode 100644 index 0000000..277efa3 --- /dev/null +++ b/tests/test_log_filters.py @@ -0,0 +1,30 @@ +import unittest +from unittest.mock import patch +import logging +from requests_oauthlib.log_filters import DebugModeTokenFilter + +class TestDebugModeTokenFilter(unittest.TestCase): + + def setUp(self): + self.record = logging.LogRecord(name="test", level=logging.DEBUG, pathname=None, lineno=None, msg="Bearer i-am-a-little-token-here-is-my-scope-and-here-is-my-signature", args=None, exc_info=None) + + @patch.dict('os.environ', {'DEBUG_MODE_TOKEN_FILTER': 'MASK'}) + def test_mask_mode(self): + filter = DebugModeTokenFilter() + filter.filter(self.record) + self.assertIn('[MASKED]', self.record.msg) + + @patch.dict('os.environ', {'DEBUG_MODE_TOKEN_FILTER': 'SUPPRESS'}) + def test_suppress_mode(self): + filter = DebugModeTokenFilter() + result = filter.filter(self.record) + self.assertFalse(result) # No logging + + @patch.dict('os.environ', {'DEBUG_MODE_TOKEN_FILTER': 'DEFAULT'}) + def test_default_mode_raises_warning(self): + filter = DebugModeTokenFilter() + result = filter.filter(self.record) + self.assertTrue(result) + +if __name__ == '__main__': + unittest.main()