]> Frank Brehm's Git Trees - pixelpark/pp-admin-tools.git/commitdiff
Filling module pp_admintools.handler.ldap_password with methods for class LdapPasswor...
authorFrank Brehm <frank@brehm-online.com>
Thu, 10 Nov 2022 17:33:32 +0000 (18:33 +0100)
committerFrank Brehm <frank@brehm-online.com>
Thu, 10 Nov 2022 17:33:32 +0000 (18:33 +0100)
lib/pp_admintools/handler/ldap_password.py

index 5591c53f497644e99d00d5a50f2e00a13e8ad751..dc3115d1343e85c0aab84c26fc3caa25397289e2 100644 (file)
@@ -11,6 +11,15 @@ from __future__ import absolute_import
 import logging
 
 # Third party modules
+import passlib.apps
+
+HAS_CRACKLIB = False
+try:
+    import cracklib
+    HAS_CRACKLIB = True
+except ImportError:
+    pass
+
 from fb_tools.handling_obj import HandlingObject
 from fb_tools.errors import FbHandlerError
 
@@ -22,7 +31,7 @@ LOG = logging.getLogger(__name__)
 _ = XLATOR.gettext
 ngettext = XLATOR.ngettext
 
-__version__ = '0.1.0'
+__version__ = '0.2.0'
 
 
 # =============================================================================
@@ -31,10 +40,106 @@ class LdapPwdHandlerError(FbHandlerError):
     pass
 
 
+# =============================================================================
+class WrongPwdSchemaError(FbHandlerError):
+    """Exception class for given wrong schema on update."""
+
+    # -------------------------------------------------------------------------
+    def __init__(self, schema):
+
+        self.schema = schema
+
+    # -------------------------------------------------------------------------
+    def __str__(self):
+        """Typecast into a string."""
+        return _("Encryption schema {!r} inot found.").format(self.schema)
+
+
 # =============================================================================
 class LdapPasswordHandler(HandlingObject):
     """Handler class for handling LDAP passwords."""
 
+    possible_schemes = (
+        'ldap_des_crypt',
+        'ldap_md5',
+        'ldap_md5_crypt',
+        'ldap_salted_md5',
+        'ldap_sha1',
+        'ldap_sha1_crypt',
+        'ldap_salted_sha1',
+        'ldap_sha256_crypt',
+        'ldap_salted_sha256',
+        'ldap_sha512_crypt',
+        'ldap_salted_sha512',
+        'ldap_pbkdf2_sha512',
+    )
+
+    available_schemes = []
+
+    schema_ids = {
+        'ldap_des_crypt': 'CRYPT',
+        'ldap_md5': 'MD5',
+        'ldap_md5_crypt': 'CRYPT-MD5',
+        'ldap_salted_md5': 'SMD5',
+        'ldap_sha1': 'SHA',
+        'ldap_sha1_crypt': 'SHA-CRYPT',
+        'ldap_salted_sha1': 'SSHA',
+        'ldap_sha256_crypt': 'CRYPT-SHA256',
+        'ldap_salted_sha256': 'SSHA256',
+        'ldap_sha512_crypt': 'CRYPT-SHA512',
+        'ldap_salted_sha512': 'SSHA512',
+        'ldap_pbkdf2_sha512': 'PBKDF2_SHA512',
+    }
+
+    schema_description = {
+        'ldap_des_crypt': _('The ancient and notorious 3 DES crypt method.'),
+        'ldap_md5': _('Pure {} hashing method.').format('MD5'),
+        'ldap_md5_crypt': _("A {} based hashing algorithm.").format('MD5'),
+        'ldap_salted_md5': _("Salted {} hashing method.").format('MD5'),
+        'ldap_sha1': _('Pure {} hashing method.').format('SHA-1'),
+        'ldap_sha1_crypt': _("A {} based hashing algorithm.").format('SHA-1'),
+        'ldap_salted_sha1': _("Salted {} hashing method.").format('SHA-1'),
+        'ldap_sha256_crypt': _("A {} based hashing algorithm.").format('SHA-256'),
+        'ldap_salted_sha256': _("Salted {} hashing method.").format('SHA-256'),
+        'ldap_sha512_crypt': _("A {} based hashing algorithm.").format('SHA-512'),
+        'ldap_salted_sha512': _("Salted {} hashing method.").format('SHA-512'),
+        'ldap_pbkdf2_sha512': _(
+            "A hashing method derived from {} with additional computing rounds.").format(
+            'SHA-512'),
+    }
+
+    default_rounds = {
+        'ldap_sha256_crypt': 64000,
+        'ldap_sha512_crypt': 64000,
+        'ldap_pbkdf2_sha512': 30000,
+    }
+
+    passlib_context = None
+    default_schema = 'ldap_sha512_crypt'
+    default_schema_id = 'CRYPT-SHA512'
+
+    # -------------------------------------------------------------------------
+    @classmethod
+    def init_pass_schemes(cls):
+
+        cls.available_schemes = []
+        all_handlers = passlib.registry.list_crypt_handlers()
+
+        for schema in cls.possible_schemes:
+            if schema in all_handlers:
+                cls.available_schemes.append(schema)
+
+        context_opts = {
+            'schemes': cls.available_schemes,
+            'default': cls.default_schema,
+        }
+
+        for schema in cls.default_rounds:
+            key = schema + '__rounds'
+            context_opts[key] = cls.default_rounds[key]
+
+        cls.passlib_context = passlib.context.CryptContext(**context_opts)
+
     # -------------------------------------------------------------------------
     def __init__(
         self, appname=None, verbose=0, version=__version__, base_dir=None,
@@ -47,5 +152,145 @@ class LdapPasswordHandler(HandlingObject):
             terminal_has_colors=terminal_has_colors, initialized=False,
         )
 
+    # -------------------------------------------------------------------------
+    def as_dict(self, short=True):
+        """
+        Transforms the elements of the object into a dict
+
+        @param short: don't include local properties in resulting dict.
+        @type short: bool
+
+        @return: structure as dict
+        @rtype:  dict
+        """
+
+        res = super(LdapPasswordHandler, self).as_dict(short=short)
+
+        res['available_schemes'] = self.available_schemes
+        res['default_schema'] = self.passlib_context.default_scheme()
+        res['schema_ids'] = self.schema_ids
+
+        return res
+
+    # -------------------------------------------------------------------------
+    @classmethod
+    def update_schema(
+        cls, schema, rounds=None, default_rounds=None, vary_rounds=None,
+            min_rounds=None, max_rounds=None):
+
+        if schema not in cls.available_schemes:
+            msg = _("Invalid schema {!r} given for update.").format(schema)
+            raise LdapPwdHandlerError(msg)
+
+        update_args = {}
+
+        if rounds:
+            key = "{}__rounds".format(schema)
+            update_args[key] = rounds
+
+        if default_rounds:
+            key = "{}__default_rounds".format(schema)
+            update_args[key] = default_rounds
+
+        if vary_rounds:
+            key = "{}__vary_rounds".format(schema)
+            update_args[key] = vary_rounds
+
+        if min_rounds:
+            key = "{}__min_rounds".format(schema)
+            update_args[key] = min_rounds
+
+        if max_rounds:
+            key = "{}__max_rounds".format(schema)
+            update_args[key] = max_rounds
+
+        if update_args:
+            cls.passlib_context.update(**update_args)
+
+    # -------------------------------------------------------------------------
+    def show_hashing_schemes(self):
+
+        max_len_schema = 1
+        for method in self.available_schemes:
+            schema_id = self.schema_ids[method]
+            if len(schema_id) > max_len_schema:
+                max_len_schema = len(schema_id)
+
+        title = _("Usable Hashing schemes:")
+        print(title)
+        print('-' * len(title))
+        print()
+
+        for method in self.available_schemes:
+            schema_id = self.schema_ids[method]
+            desc = self.schema_description[method]
+            if 'pbkdf2' in method:
+                desc += ' ' + _(
+                    "This schema cannot be used for authentication on a "
+                    "current freeradius server.")
+            if method == self.schema:
+                desc += ' ' + _("This is the default schema.")
+
+            line = ' * {id:<{max_len}} - '.format(id=schema_id, max_len=max_len_schema)
+            line += desc
+            print(line)
+
+        print()
+
+    # -------------------------------------------------------------------------
+    def set_schema(self, schema):
+
+        if schema not in self.available_schemes:
+            raise WrongPwdSchemaError(schema)
+
+        self.schema_id = self.schema_ids[schema]
+        self.schema = schema
+        self.passlib_context.update(default=schema)
+
+    # -------------------------------------------------------------------------
+    def set_schema_by_id(self, given_schema_id):
+
+        found = False
+
+        for schema in self.available_schemes:
+            schema_id = self.schema_ids[schema]
+            LOG.debug("Testing for {m!r} ({s}) ...".format(m=schema, s=schema_id))
+            if schema_id == given_schema_id:
+                self.passlib_context.update(default=schema)
+                self.schema = schema
+                self.schema_id = schema_id
+                found = True
+                break
+
+        if not found:
+            raise WrongPwdSchemaError(given_schema_id)
+
+    # -------------------------------------------------------------------------
+    def get_hash(self, password, schema=None):
+
+        hashed_passwd = self.passlib_context.hash(password, self.schema)
+        return hashed_passwd
+
+    # -------------------------------------------------------------------------
+    def check_password_quality(self, password):
+
+        if not HAS_CRACKLIB:
+            msg = _(
+                "Cannot testing the quality of the new password, because the "
+                "Python module {!r} is not installed.").format('cracklib')
+            LOG.warn(msg)
+            return True
+
+        LOG.info(_("Testing quality of new password ..."))
+        try:
+            cracklib.VeryFascistCheck(password)
+        except ValueError as e:
+            msg = _("Quality of the new password is not sufficient:") + ' ' + str(e)
+            LOG.error(msg)
+            return False
+
+        LOG.debug("The quality of the new password seems to be sufficient.")
+        return True
+
 
 # vim: ts=4 et list