mirror of
https://github.com/jwinterm/modoboa.git
synced 2026-10-18 05:51:26 +02:00
+27
-1
@@ -1,9 +1,18 @@
|
||||
from django.core.checks import register, Info, Warning
|
||||
from django.core.checks import register, Error, Info, Warning
|
||||
from django.conf import settings
|
||||
from django.utils.translation import gettext as _
|
||||
|
||||
from modoboa.core.utils import generate_rsa_private_key
|
||||
from modoboa.parameters.tools import get_global_parameter
|
||||
from modoboa.core.password_hashers import get_password_hasher
|
||||
|
||||
EOO1 = Error(
|
||||
_("The password scheme you use is not available anymore."),
|
||||
hint=_(
|
||||
"The password scheme you were using has been removed, please upgrade to a stronger one"
|
||||
),
|
||||
id="modoboa.EOO1",
|
||||
)
|
||||
|
||||
W001 = Warning(
|
||||
_(
|
||||
@@ -14,6 +23,11 @@ W001 = Warning(
|
||||
hint=_("Set `USE_TZ = True` in settings.py"),
|
||||
id="modoboa.W001",
|
||||
)
|
||||
W002 = Warning(
|
||||
_("The password scheme you are using is deprecated."),
|
||||
hint=_("Upgrade your password scheme to a stronger one"),
|
||||
id="modoboa.W002",
|
||||
)
|
||||
|
||||
|
||||
@register(deploy=True)
|
||||
@@ -38,3 +52,15 @@ def check_rsa_private_key_exists(app_configs, **kwargs):
|
||||
if generate_rsa_private_key(settings.BASE_DIR):
|
||||
msgs.append(Info("An RSA private key has been generated for OIDC."))
|
||||
return msgs
|
||||
|
||||
|
||||
@register()
|
||||
def check_password_hasher(app_configs, **kwargs):
|
||||
msgs = []
|
||||
scheme_in_use = get_global_parameter("password_scheme", app="core")
|
||||
hasher = get_password_hasher(scheme_in_use)
|
||||
if not hasher.available:
|
||||
msgs.append(EOO1)
|
||||
elif hasher.deprecated:
|
||||
msgs.append(W002)
|
||||
return msgs
|
||||
|
||||
+12
-3
@@ -2,6 +2,7 @@
|
||||
|
||||
import re
|
||||
from email.header import Header
|
||||
from typing import Optional
|
||||
|
||||
from django.core.serializers.json import DjangoJSONEncoder
|
||||
from django.conf import settings
|
||||
@@ -96,7 +97,7 @@ class User(AbstractUser):
|
||||
super().__init__(*args, **kwargs)
|
||||
self.parameters = param_tools.Manager("user", self._parameters)
|
||||
|
||||
def _crypt_password(self, raw_value):
|
||||
def _crypt_password(self, raw_value: str) -> str:
|
||||
"""Crypt the local password using the appropriate scheme.
|
||||
|
||||
In case we don't find the scheme (for example when the
|
||||
@@ -115,9 +116,17 @@ class User(AbstractUser):
|
||||
"password_scheme", raise_exception=False
|
||||
)
|
||||
raw_value = smart_bytes(raw_value)
|
||||
return get_password_hasher(scheme.upper())().encrypt(raw_value)
|
||||
hasher = get_password_hasher(scheme.upper())()
|
||||
if not hasher.available:
|
||||
raise InternalError(
|
||||
_(
|
||||
"The configured password scheme (%s) is not available anymore. Please update your configuration"
|
||||
)
|
||||
% (scheme)
|
||||
)
|
||||
return hasher.encrypt(raw_value)
|
||||
|
||||
def set_password(self, raw_value, curvalue=None):
|
||||
def set_password(self, raw_value: str, curvalue: Optional[str] = None) -> None:
|
||||
"""Password update.
|
||||
|
||||
Update the current mailbox's password with the given clear
|
||||
|
||||
@@ -19,7 +19,7 @@ from modoboa.core.password_hashers.base import ( # NOQA:F401
|
||||
)
|
||||
|
||||
|
||||
def get_password_hasher(scheme):
|
||||
def get_password_hasher(scheme: str) -> PasswordHasher:
|
||||
"""Retrieve the hasher corresponding to :keyword:`scheme`.
|
||||
|
||||
If no class is found, `PLAINHasher` is returned.
|
||||
|
||||
@@ -5,10 +5,15 @@ Contains weak hashers (the original ones) available with Modoboa.
|
||||
"""
|
||||
|
||||
import base64
|
||||
import crypt
|
||||
|
||||
try:
|
||||
import crypt
|
||||
except ImportError:
|
||||
crypt = None
|
||||
import hashlib
|
||||
import string
|
||||
from random import Random
|
||||
import string
|
||||
from typing import Optional
|
||||
|
||||
from django.utils.crypto import constant_time_compare
|
||||
from django.utils.encoding import force_bytes, force_str
|
||||
@@ -28,7 +33,15 @@ class MetaHasher(type):
|
||||
@property
|
||||
def label(cls):
|
||||
"""Returns the label of the hasher"""
|
||||
return cls.name if not cls._weak else "{} (weak)".format(cls.name)
|
||||
result = cls.name
|
||||
tags = []
|
||||
if cls._weak:
|
||||
tags.append("weak")
|
||||
if cls.deprecated:
|
||||
tags.append("deprecated")
|
||||
if tags:
|
||||
result += f" ({', '.join(tags)})"
|
||||
return result
|
||||
|
||||
|
||||
class PasswordHasher(metaclass=MetaHasher):
|
||||
@@ -36,12 +49,14 @@ class PasswordHasher(metaclass=MetaHasher):
|
||||
Base class of all hashers.
|
||||
"""
|
||||
|
||||
_weak = False
|
||||
_weak: bool = False
|
||||
deprecated: bool = False
|
||||
scheme: str
|
||||
|
||||
def __init__(self, target="local"):
|
||||
self._target = target
|
||||
|
||||
def _encrypt(self, clearvalue, salt=None):
|
||||
def _encrypt(self, clearvalue: str, salt: Optional[str] = None) -> str:
|
||||
raise NotImplementedError
|
||||
|
||||
def _b64encode(self, pwhash):
|
||||
@@ -54,7 +69,11 @@ class PasswordHasher(metaclass=MetaHasher):
|
||||
return base64.b64encode(pwhash)
|
||||
return pwhash
|
||||
|
||||
def encrypt(self, clearvalue):
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return True
|
||||
|
||||
def encrypt(self, clearvalue: str) -> str:
|
||||
"""Encrypt a password.
|
||||
|
||||
The format used to store passwords is the same than dovecot's one::
|
||||
@@ -94,7 +113,7 @@ class PasswordHasher(metaclass=MetaHasher):
|
||||
@classmethod
|
||||
def get_password_hashers(cls):
|
||||
"""Return all the PasswordHasher supported by Modoboa"""
|
||||
return cls.__subclasses__()
|
||||
return [hasher for hasher in cls.__subclasses__() if hasher().available]
|
||||
|
||||
|
||||
class PLAINHasher(PasswordHasher):
|
||||
@@ -120,12 +139,17 @@ class CRYPTHasher(PasswordHasher):
|
||||
"""
|
||||
|
||||
_weak = True
|
||||
deprecated = True
|
||||
|
||||
@property
|
||||
def available(self) -> bool:
|
||||
return crypt is not None
|
||||
|
||||
@property
|
||||
def scheme(self):
|
||||
return "{CRYPT}"
|
||||
|
||||
def _encrypt(self, clearvalue, salt=None):
|
||||
def _encrypt(self, clearvalue: str, salt: Optional[str] = None) -> str:
|
||||
if salt is None:
|
||||
salt = "".join(Random().sample(string.ascii_letters + string.digits, 2))
|
||||
return crypt.crypt(clearvalue, salt)
|
||||
|
||||
@@ -16,6 +16,7 @@ except ImportError:
|
||||
from modoboa.core import constants
|
||||
from modoboa.core.password_hashers import get_password_hasher
|
||||
from modoboa.core.password_hashers.utils import get_dovecot_schemes
|
||||
from modoboa.lib.exceptions import InternalError
|
||||
from modoboa.lib.tests import NO_SMTP, ModoTestCase
|
||||
from .. import factories, models
|
||||
|
||||
@@ -252,6 +253,18 @@ class AuthenticationTestCase(ModoTestCase):
|
||||
supported_schemes = get_dovecot_schemes()[0]
|
||||
self.assertEqual(supported_schemes, ["{MD5-CRYPT}", "{PLAIN}"])
|
||||
|
||||
def test_unavailable_password_scheme(self):
|
||||
self.set_global_parameter("password_scheme", "crypt")
|
||||
hasher = get_password_hasher("crypt")
|
||||
self.assertEqual(hasher.label, "crypt (weak, deprecated)")
|
||||
if hasher().available:
|
||||
return
|
||||
username = "user@test.com"
|
||||
password = "toto"
|
||||
user = models.User.objects.get(username=username)
|
||||
with self.assertRaises(InternalError):
|
||||
user.set_password(password)
|
||||
|
||||
def test_fido_auth_begin(self):
|
||||
url = reverse("core:fido_auth_begin")
|
||||
resp = self.client.post(url)
|
||||
|
||||
@@ -2,13 +2,14 @@ import os
|
||||
import shutil
|
||||
import tempfile
|
||||
|
||||
from django.test import SimpleTestCase
|
||||
from django.test.utils import override_settings
|
||||
|
||||
from modoboa.core import checks
|
||||
from modoboa.lib.tests import SimpleModoTestCase
|
||||
from modoboa.core.password_scheme.base import CRYPTHasher
|
||||
|
||||
|
||||
class CheckSessionCookieSecureTest(SimpleTestCase):
|
||||
class CheckSessionCookieSecureTest(SimpleModoTestCase):
|
||||
|
||||
def setUp(self):
|
||||
self.workdir = tempfile.mkdtemp()
|
||||
@@ -35,3 +36,11 @@ class CheckSessionCookieSecureTest(SimpleTestCase):
|
||||
os.environ["OIDC_RSA_PRIVATE_KEY"] = "KEY"
|
||||
msgs = checks.check_rsa_private_key_exists(None)
|
||||
self.assertEqual(len(msgs), 0)
|
||||
|
||||
def test_password_hasher_checks(self):
|
||||
self.set_global_parameter("password_scheme", "crypt")
|
||||
msgs = checks.check_password_hasher(None)
|
||||
if CRYPTHasher().available:
|
||||
self.assertEqual(msgs, [checks.W002])
|
||||
else:
|
||||
self.assertEqual(msgs, [checks.E001])
|
||||
|
||||
@@ -30,7 +30,7 @@ except ImportError:
|
||||
NO_LDAP = True
|
||||
|
||||
|
||||
class ParametersMixin(object):
|
||||
class ParametersMixin:
|
||||
"""Add tools to manage parameters."""
|
||||
|
||||
@classmethod
|
||||
|
||||
Reference in New Issue
Block a user