From: Frank Brehm Date: Thu, 10 Nov 2022 17:33:32 +0000 (+0100) Subject: Filling module pp_admintools.handler.ldap_password with methods for class LdapPasswor... X-Git-Tag: 0.7.2^2^2~15 X-Git-Url: https://git.uhu-banane.org/?a=commitdiff_plain;h=33e2ac62fcfa74b8c690033761be2bab7bb033de;p=pixelpark%2Fpp-admin-tools.git Filling module pp_admintools.handler.ldap_password with methods for class LdapPasswordHandler --- diff --git a/lib/pp_admintools/handler/ldap_password.py b/lib/pp_admintools/handler/ldap_password.py index 5591c53..dc3115d 100644 --- a/lib/pp_admintools/handler/ldap_password.py +++ b/lib/pp_admintools/handler/ldap_password.py @@ -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