From e0e20cdf7459cf1e3ac9a240f8cfac9dcabf127c Mon Sep 17 00:00:00 2001 From: Jacques Troussard Date: Sat, 23 Mar 2024 17:02:25 -0400 Subject: [PATCH 1/8] 532: adds a logger filter for tokens in debug mode Co-authored-by: Erlend vollset --- AUTHORS.rst | 1 + README.rst | 19 +++++++++++++++++++ requests_oauthlib/__init__.py | 3 +++ requests_oauthlib/log_filters.py | 23 +++++++++++++++++++++++ tests/test_log_filters.py | 31 +++++++++++++++++++++++++++++++ 5 files changed, 77 insertions(+) create mode 100644 requests_oauthlib/log_filters.py create mode 100644 tests/test_log_filters.py diff --git a/AUTHORS.rst b/AUTHORS.rst index 0b2fb31..58d2594 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -25,3 +25,4 @@ Maintainers & Contributors - Sylvain Marie - Craig Anderson - Hugo van Kemenade +- Jacques Troussard 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..bd04c14 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,4 @@ raise Warning(msg % requests.__version__) logging.getLogger("requests_oauthlib").addHandler(logging.NullHandler()) +logging.getLogger("requests_oauthlib").addFilter(DebugModeTokenFilter()) diff --git a/requests_oauthlib/log_filters.py b/requests_oauthlib/log_filters.py new file mode 100644 index 0000000..cca7f26 --- /dev/null +++ b/requests_oauthlib/log_filters.py @@ -0,0 +1,23 @@ +import os +import re +import logging + +class DebugModeTokenFilter(logging.Filter): # <-- inherent from the Filter class + def __init__(self): + super().__init__() + # set the behavior/configuration of the filter by the environment variable + self.mode = os.getenv('DEBUG_MODE_TOKEN_FILTER', 'DEFAULT').upper() + + def filter(self, record): + if self.mode == "MASK": + # While this doesn't directly target the headers as @erlendvollset 's post originally targets + # this wider approach of targeting the "Bearer" key word I believe provides complete coverage. + # However I would still recommend some more research to see if this regex would need to be improved + # to provide a secure/trusted solution. + record.msg = re.sub(r'Bearer (\w+)', '[MASKED]', record.getMessage()) + elif self.mode == "SUPPRESS": + return False + elif self.mode == "DEFAULT": + msg = "Your logger, when in DEBUG mode, will log TOKENS" + raise Warning(msg) + return True \ No newline at end of file diff --git a/tests/test_log_filters.py b/tests/test_log_filters.py new file mode 100644 index 0000000..11c6e79 --- /dev/null +++ b/tests/test_log_filters.py @@ -0,0 +1,31 @@ +import unittest +from unittest.mock import patch +from logging import LogRecord +from requests_oauthlib.log_filters import DebugModeTokenFilter + +class TestDebugModeTokenFilter(unittest.TestCase): + + def setUp(self): + self.record = LogRecord(name="test", level=20, pathname=None, lineno=None, msg="Bearer i-am-a-token", 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) # Check that nothing is logged + + @patch.dict('os.environ', {'DEBUG_MODE_TOKEN_FILTER': 'DEFAULT'}) + def test_default_mode(self): + filter = DebugModeTokenFilter() + with self.assertRaises(Warning) as context: + filter.filter(self.record) + self.assertTrue("Your logger, when in DEBUG mode, will log TOKENS" in str(context.exception)) + +if __name__ == '__main__': + unittest.main() From f35064230cde1aa527ad2fcf08ca3439c4cb2854 Mon Sep 17 00:00:00 2001 From: Jacques Troussard Date: Sat, 23 Mar 2024 17:22:08 -0400 Subject: [PATCH 2/8] 532: updates authorship file --- AUTHORS.rst | 1 + 1 file changed, 1 insertion(+) diff --git a/AUTHORS.rst b/AUTHORS.rst index 58d2594..2422a37 100644 --- a/AUTHORS.rst +++ b/AUTHORS.rst @@ -26,3 +26,4 @@ Maintainers & Contributors - Craig Anderson - Hugo van Kemenade - Jacques Troussard +- Erland Vollset From 103d28c69d775c2f61e6e4be81c4fd55fa4c3214 Mon Sep 17 00:00:00 2001 From: Jacques Troussard Date: Sat, 23 Mar 2024 17:47:35 -0400 Subject: [PATCH 3/8] 532: fixes logic so filter checks the logger record level, updates tests --- requests_oauthlib/log_filters.py | 48 ++++++++++++++++++++++---------- tests/test_log_filters.py | 8 +++--- 2 files changed, 38 insertions(+), 18 deletions(-) diff --git a/requests_oauthlib/log_filters.py b/requests_oauthlib/log_filters.py index cca7f26..bbdeae5 100644 --- a/requests_oauthlib/log_filters.py +++ b/requests_oauthlib/log_filters.py @@ -2,22 +2,42 @@ import re import logging -class DebugModeTokenFilter(logging.Filter): # <-- inherent from the Filter class +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__() - # set the behavior/configuration of the filter by the environment variable self.mode = os.getenv('DEBUG_MODE_TOKEN_FILTER', 'DEFAULT').upper() def filter(self, record): - if self.mode == "MASK": - # While this doesn't directly target the headers as @erlendvollset 's post originally targets - # this wider approach of targeting the "Bearer" key word I believe provides complete coverage. - # However I would still recommend some more research to see if this regex would need to be improved - # to provide a secure/trusted solution. - record.msg = re.sub(r'Bearer (\w+)', '[MASKED]', record.getMessage()) - elif self.mode == "SUPPRESS": - return False - elif self.mode == "DEFAULT": - msg = "Your logger, when in DEBUG mode, will log TOKENS" - raise Warning(msg) - return True \ No newline at end of file + """ + 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 (\w+)', '[MASKED]', record.getMessage()) + elif self.mode == "SUPPRESS": + return False + elif self.mode == "DEFAULT": + msg = "Your logger, when in DEBUG mode, will log TOKENS" + raise Warning(msg) + return True \ No newline at end of file diff --git a/tests/test_log_filters.py b/tests/test_log_filters.py index 11c6e79..133d1b7 100644 --- a/tests/test_log_filters.py +++ b/tests/test_log_filters.py @@ -1,12 +1,12 @@ import unittest from unittest.mock import patch -from logging import LogRecord +import logging from requests_oauthlib.log_filters import DebugModeTokenFilter class TestDebugModeTokenFilter(unittest.TestCase): def setUp(self): - self.record = LogRecord(name="test", level=20, pathname=None, lineno=None, msg="Bearer i-am-a-token", args=None, exc_info=None) + 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): @@ -18,10 +18,10 @@ def test_mask_mode(self): def test_suppress_mode(self): filter = DebugModeTokenFilter() result = filter.filter(self.record) - self.assertFalse(result) # Check that nothing is logged + self.assertFalse(result) # No logging @patch.dict('os.environ', {'DEBUG_MODE_TOKEN_FILTER': 'DEFAULT'}) - def test_default_mode(self): + def test_default_mode_raises_warning(self): filter = DebugModeTokenFilter() with self.assertRaises(Warning) as context: filter.filter(self.record) From a77b392c852177141efd209884cfc53842e5dc39 Mon Sep 17 00:00:00 2001 From: Jacques Troussard Date: Sat, 23 Mar 2024 18:00:07 -0400 Subject: [PATCH 4/8] 532: moves the logger warning to the requests initializer, updates tests --- requests_oauthlib/__init__.py | 7 +++++++ requests_oauthlib/log_filters.py | 7 ++----- tests/test_log_filters.py | 5 ++--- 3 files changed, 11 insertions(+), 8 deletions(-) diff --git a/requests_oauthlib/__init__.py b/requests_oauthlib/__init__.py index bd04c14..302db12 100644 --- a/requests_oauthlib/__init__.py +++ b/requests_oauthlib/__init__.py @@ -21,3 +21,10 @@ 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 index bbdeae5..d9e8ef1 100644 --- a/requests_oauthlib/log_filters.py +++ b/requests_oauthlib/log_filters.py @@ -20,7 +20,7 @@ def __init__(self): environment variable. """ super().__init__() - self.mode = os.getenv('DEBUG_MODE_TOKEN_FILTER', 'DEFAULT').upper() + self.mode = os.getenv('DEBUG_MODE_TOKEN_FILTER', 'DEFAULT').upper() def filter(self, record): """ @@ -37,7 +37,4 @@ def filter(self, record): record.msg = re.sub(r'Bearer (\w+)', '[MASKED]', record.getMessage()) elif self.mode == "SUPPRESS": return False - elif self.mode == "DEFAULT": - msg = "Your logger, when in DEBUG mode, will log TOKENS" - raise Warning(msg) - return True \ No newline at end of file + return True # if mode is not MASKED then DEFAULT is implied \ No newline at end of file diff --git a/tests/test_log_filters.py b/tests/test_log_filters.py index 133d1b7..277efa3 100644 --- a/tests/test_log_filters.py +++ b/tests/test_log_filters.py @@ -23,9 +23,8 @@ def test_suppress_mode(self): @patch.dict('os.environ', {'DEBUG_MODE_TOKEN_FILTER': 'DEFAULT'}) def test_default_mode_raises_warning(self): filter = DebugModeTokenFilter() - with self.assertRaises(Warning) as context: - filter.filter(self.record) - self.assertTrue("Your logger, when in DEBUG mode, will log TOKENS" in str(context.exception)) + result = filter.filter(self.record) + self.assertTrue(result) if __name__ == '__main__': unittest.main() From 577a085669db8ae38ed751361e75506717209cc9 Mon Sep 17 00:00:00 2001 From: Jacques Troussard Date: Tue, 26 Mar 2024 13:39:48 -0400 Subject: [PATCH 5/8] 536: updatea regex for log filter --- requests_oauthlib/log_filters.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/requests_oauthlib/log_filters.py b/requests_oauthlib/log_filters.py index d9e8ef1..6e56ee4 100644 --- a/requests_oauthlib/log_filters.py +++ b/requests_oauthlib/log_filters.py @@ -34,7 +34,7 @@ def filter(self, record): """ if record.levelno == logging.DEBUG: if self.mode == "MASK": - record.msg = re.sub(r'Bearer (\w+)', '[MASKED]', record.getMessage()) + record.msg = re.sub(r'Bearer\s+([A-Za-z0-9\-._~+\/]+)', '[MASKED]', record.getMessage()) elif self.mode == "SUPPRESS": return False return True # if mode is not MASKED then DEFAULT is implied \ No newline at end of file From 33d5bb5c0e99c41f92fdb75fede9242315ff8cbf Mon Sep 17 00:00:00 2001 From: Jacques Troussard Date: Sun, 21 Apr 2024 14:44:48 -0400 Subject: [PATCH 6/8] updates env var name, improve SUPRESSED scope implementation, black formatting --- requests_oauthlib/log_filters.py | 12 +++++++++--- 1 file changed, 9 insertions(+), 3 deletions(-) diff --git a/requests_oauthlib/log_filters.py b/requests_oauthlib/log_filters.py index 6e56ee4..9870de7 100644 --- a/requests_oauthlib/log_filters.py +++ b/requests_oauthlib/log_filters.py @@ -2,6 +2,7 @@ import re import logging + class DebugModeTokenFilter(logging.Filter): """ A logging filter that while in DEBUG mode can filter TOKENS dependent on configuration. @@ -14,13 +15,15 @@ class DebugModeTokenFilter(logging.Filter): 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('DEBUG_MODE_TOKEN_FILTER', 'DEFAULT').upper() + self.mode = os.getenv( + 'REQUESTS_OAUTHLIB_DEBUG_MODE_TOKEN_FILTER', 'DEFAULT').upper() def filter(self, record): """ @@ -34,7 +37,10 @@ def filter(self, record): """ if record.levelno == logging.DEBUG: if self.mode == "MASK": - record.msg = re.sub(r'Bearer\s+([A-Za-z0-9\-._~+\/]+)', '[MASKED]', record.getMessage()) + 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 \ No newline at end of file + return True # if mode is not MASKED then DEFAULT is implied From 6b8f87c5b7f0f11ba54e4ce29c0228c0b6c2bb2d Mon Sep 17 00:00:00 2001 From: jacques troussard Date: Thu, 18 Jul 2024 19:31:04 -0400 Subject: [PATCH 7/8] updates unit tests, pending one last test to assert warning message is printed to console when in default mode --- tests/test_log_filters.py | 25 +++++++++++++++---------- 1 file changed, 15 insertions(+), 10 deletions(-) diff --git a/tests/test_log_filters.py b/tests/test_log_filters.py index 277efa3..931ac17 100644 --- a/tests/test_log_filters.py +++ b/tests/test_log_filters.py @@ -6,25 +6,30 @@ 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) + 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-hash", args=None, exc_info=None) - @patch.dict('os.environ', {'DEBUG_MODE_TOKEN_FILTER': 'MASK'}) + @patch.dict('os.environ', {'REQUESTS_OAUTHLIB_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'}) + @patch.dict('os.environ', {'REQUESTS_OAUTHLIB_DEBUG_MODE_TOKEN_FILTER': 'SUPPRESS'}) def test_suppress_mode(self): filter = DebugModeTokenFilter() - result = filter.filter(self.record) - self.assertFalse(result) # No logging + filter.filter(self.record) + self.assertEqual(" ", self.record.msg) # No logging + + # @patch.dict('os.environ', {'REQUESTS_OAUTHLIB_DEBUG_MODE_TOKEN_FILTER': 'DEFAULT'}) + # def test_default_mode_raises_warning(self): + # with self.assertLogs('requests_oauthlib', level='WARN') as cm: + # DebugModeTokenFilter() + # logging.getLogger("requests_oauthlib").addFilter(DebugModeTokenFilter()) + # # Trigger the log event to check for the warning message + # logging.getLogger("requests_oauthlib").debug(self.record.getMessage()) + + # self.assertIn("Your logger, when in DEBUG mode, will log TOKENS", cm.output[0]) - @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() From 5a6e94a5fb45a869a0f4e4f39fba5d7b6467192c Mon Sep 17 00:00:00 2001 From: jacques troussard Date: Thu, 18 Jul 2024 19:39:04 -0400 Subject: [PATCH 8/8] updates README with correct env var and makes headers render properly --- README.rst | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/README.rst b/README.rst index 901d915..1b931ed 100644 --- a/README.rst +++ b/README.rst @@ -7,7 +7,7 @@ The OAuth 1 workflow -------------------- OAuth 1 can seem overly complicated and it sure has its quirks. Luckily, -requests_oauthlib hides most of these and let you focus at the task at hand. +requests_oauthlib hides most of these and lets you focus on the task at hand. Accessing protected resources using requests_oauthlib is as simple as: @@ -23,7 +23,7 @@ Accessing protected resources using requests_oauthlib is as simple as: Before accessing resources you will need to obtain a few credentials from your provider (e.g. Twitter) and authorization from the user for whom you wish to -retrieve resources for. You can read all about this in the full +retrieve resources. You can read all about this in the full `OAuth 1 workflow guide on RTD `_. The OAuth 2 workflow @@ -36,11 +36,11 @@ flow. Fetching a protected resource after obtaining an access token can be extremely simple. However, before accessing resources you will need to obtain a few credentials from your provider (e.g. Google) and authorization from the user -for whom you wish to retrieve resources for. You can read all about this in the +for whom you wish to retrieve resources. You can read all about this in the full `OAuth 2 workflow guide on RTD `_. Installation -------------- +------------ To install requests and requests_oauthlib you can use pip: @@ -60,17 +60,20 @@ To install requests and requests_oauthlib you can use pip: Advanced Configurations ----------------------- -### Logger Configuration Framework +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 +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 + Configuring the Debug Mode Token Filter + --------------------------------------- - - **Environment Variable**: `DEBUG_MODE_TOKEN_FILTER` + - **Environment Variable**: `REQUESTS_OAUTHLIB_DEBUG_MODE_TOKEN_FILTER` - **Options**: - `DEFAULT`: No alteration to logging behavior. - `MASK`: Masks sensitive tokens in logs.