From 5b235b50ed377f0562082d5cb718265a22996daf Mon Sep 17 00:00:00 2001 From: Ian Fisher Date: Mon, 29 May 2023 20:45:30 -0400 Subject: [PATCH] Add check for callable settings --- axes/checks.py | 49 ++++++++++++++++++++++++++++++++++++++++++++ tests/test_checks.py | 19 +++++++++++++++++ 2 files changed, 68 insertions(+) diff --git a/axes/checks.py b/axes/checks.py index 65fb79f..8b6b0f4 100644 --- a/axes/checks.py +++ b/axes/checks.py @@ -21,6 +21,7 @@ class Messages: ) BACKEND_INVALID = "You do not have 'axes.backends.AxesStandaloneBackend' or a subclass in your settings.AUTHENTICATION_BACKENDS." SETTING_DEPRECATED = "You have a deprecated setting {deprecated_setting} configured in your project settings" + CALLABLE_INVALID = "{callable_setting} is not a valid callable." class Hints: @@ -28,6 +29,7 @@ class Hints: MIDDLEWARE_INVALID = None BACKEND_INVALID = "AxesModelBackend was renamed to AxesStandaloneBackend in django-axes version 5.0." SETTING_DEPRECATED = None + CALLABLE_INVALID = None class Codes: @@ -35,6 +37,7 @@ class Codes: MIDDLEWARE_INVALID = "axes.W002" BACKEND_INVALID = "axes.W003" SETTING_DEPRECATED = "axes.W004" + CALLABLE_INVALID = "axes.W005" @register(Tags.security, Tags.caches, Tags.compatibility) @@ -153,3 +156,49 @@ def axes_deprecation_check(app_configs, **kwargs): # pylint: disable=unused-arg pass return warnings + + +@register +def axes_conf_check(app_configs, **kwargs): # pylint: disable=unused-argument + warnings = [] + + callable_settings = [ + "AXES_CLIENT_IP_CALLABLE", + "AXES_CLIENT_STR_CALLABLE", + "AXES_LOCKOUT_CALLABLE", + "AXES_USERNAME_CALLABLE", + "AXES_WHITELIST_CALLABLE", + "AXES_COOLOFF_TIME", + "AXES_LOCKOUT_PARAMETERS", + ] + + for callable_setting in callable_settings: + value = getattr(settings, callable_setting) + if not is_valid_callable(value): + warnings.append( + Warning( + msg=Messages.CALLABLE_INVALID.format( + callable_setting=callable_setting + ), + hint=Hints.CALLABLE_INVALID, + id=Codes.CALLABLE_INVALID, + ) + ) + + return warnings + + +def is_valid_callable(value) -> bool: + if value is None: + return True + + if callable(value): + return True + + if isinstance(value, str): + try: + import_string(value) + except ImportError: + return False + + return True diff --git a/tests/test_checks.py b/tests/test_checks.py index 1ccad7c..b9ed5f6 100644 --- a/tests/test_checks.py +++ b/tests/test_checks.py @@ -110,3 +110,22 @@ class DeprecatedSettingsTestCase(AxesTestCase): def test_deprecated_success_access_log_flag(self): warnings = run_checks() self.assertEqual(warnings, [self.disable_success_access_log_warning]) + + +class ConfCheckTestCase(AxesTestCase): + @override_settings(AXES_USERNAME_CALLABLE="module.not_defined") + def test_invalid_import_path(self): + warnings = run_checks() + warning = Warning( + msg=Messages.CALLABLE_INVALID.format( + callable_setting="AXES_USERNAME_CALLABLE" + ), + hint=Hints.CALLABLE_INVALID, + id=Codes.CALLABLE_INVALID, + ) + self.assertEqual(warnings, [warning]) + + @override_settings(AXES_COOLOFF_TIME=lambda: 1) + def test_valid_callable(self): + warnings = run_checks() + self.assertEqual(warnings, [])