diff --git a/django/conf/__init__.py b/django/conf/__init__.py index 3bfd8ea4a1..72a9c5f504 100644 --- a/django/conf/__init__.py +++ b/django/conf/__init__.py @@ -81,6 +81,8 @@ class LazySettings(LazyObject): # This is done here for performance reasons so the modified value is cached. if name in {'MEDIA_URL', 'STATIC_URL'} and val is not None: val = self._add_script_prefix(val) + elif name == 'SECRET_KEY' and not val: + raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.") self.__dict__[name] = val return val @@ -184,9 +186,6 @@ class Settings: setattr(self, setting, setting_value) self._explicit_settings.add(setting) - if not self.SECRET_KEY: - raise ImproperlyConfigured("The SECRET_KEY setting must not be empty.") - if self.is_overridden('PASSWORD_RESET_TIMEOUT_DAYS'): if self.is_overridden('PASSWORD_RESET_TIMEOUT'): raise ImproperlyConfigured( diff --git a/django/contrib/auth/tokens.py b/django/contrib/auth/tokens.py index 15a3b0ce88..21108ae652 100644 --- a/django/contrib/auth/tokens.py +++ b/django/contrib/auth/tokens.py @@ -12,7 +12,10 @@ class PasswordResetTokenGenerator: """ key_salt = "django.contrib.auth.tokens.PasswordResetTokenGenerator" algorithm = 'sha256' - secret = settings.SECRET_KEY + secret = None + + def __init__(self): + self.secret = self.secret or settings.SECRET_KEY def make_token(self, user): """ diff --git a/django/core/checks/security/base.py b/django/core/checks/security/base.py index b69c2a11da..38b2c786b9 100644 --- a/django/core/checks/security/base.py +++ b/django/core/checks/security/base.py @@ -1,4 +1,5 @@ from django.conf import settings +from django.core.exceptions import ImproperlyConfigured from .. import Error, Tags, Warning, register @@ -182,11 +183,15 @@ def check_ssl_redirect(app_configs, **kwargs): @register(Tags.security, deploy=True) def check_secret_key(app_configs, **kwargs): - passed_check = ( - getattr(settings, 'SECRET_KEY', None) and - len(set(settings.SECRET_KEY)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS and - len(settings.SECRET_KEY) >= SECRET_KEY_MIN_LENGTH - ) + try: + secret_key = settings.SECRET_KEY + except (ImproperlyConfigured, AttributeError): + passed_check = False + else: + passed_check = ( + len(set(secret_key)) >= SECRET_KEY_MIN_UNIQUE_CHARACTERS and + len(secret_key) >= SECRET_KEY_MIN_LENGTH + ) return [] if passed_check else [W009] diff --git a/docs/releases/3.2.txt b/docs/releases/3.2.txt index 225f3855f3..fe4b309cb5 100644 --- a/docs/releases/3.2.txt +++ b/docs/releases/3.2.txt @@ -272,7 +272,13 @@ Requests and Responses Security ~~~~~~~~ -* ... +* The :setting:`SECRET_KEY` setting is now checked for a valid value upon first + access, rather than when settings are first loaded. This enables running + management commands that do not rely on the ``SECRET_KEY`` without needing to + provide a value. As a consequence of this, calling + :func:`~django.conf.settings.configure` without providing a valid + ``SECRET_KEY``, and then going on to access ``settings.SECRET_KEY`` will now + raise an :exc:`~django.core.exceptions.ImproperlyConfigured` exception. Serialization ~~~~~~~~~~~~~ diff --git a/tests/settings_tests/tests.py b/tests/settings_tests/tests.py index 55ca0de524..1368b2ae18 100644 --- a/tests/settings_tests/tests.py +++ b/tests/settings_tests/tests.py @@ -289,15 +289,11 @@ class SettingsTests(SimpleTestCase): with self.assertRaises(AttributeError): getattr(settings, 'TEST2') + @override_settings(SECRET_KEY='') def test_no_secret_key(self): - settings_module = ModuleType('fake_settings_module') - sys.modules['fake_settings_module'] = settings_module msg = 'The SECRET_KEY setting must not be empty.' - try: - with self.assertRaisesMessage(ImproperlyConfigured, msg): - Settings('fake_settings_module') - finally: - del sys.modules['fake_settings_module'] + with self.assertRaisesMessage(ImproperlyConfigured, msg): + settings.SECRET_KEY def test_no_settings_module(self): msg = (