From a30467ded4dac8b33d781ed8167c3fc315d7bf5a Mon Sep 17 00:00:00 2001 From: Frank Brehm Date: Fri, 30 Sep 2022 16:48:26 +0200 Subject: [PATCH] Hopefully finishing lib/pp_admintools/app/set_ldap_password.py --- lib/pp_admintools/app/set_ldap_password.py | 148 ++++++++++++++++----- 1 file changed, 116 insertions(+), 32 deletions(-) diff --git a/lib/pp_admintools/app/set_ldap_password.py b/lib/pp_admintools/app/set_ldap_password.py index 4dea773..050c009 100644 --- a/lib/pp_admintools/app/set_ldap_password.py +++ b/lib/pp_admintools/app/set_ldap_password.py @@ -14,21 +14,29 @@ import getpass # Third party modules # from ldap3 import MODIFY_REPLACE, MODIFY_ADD, MODIFY_DELETE +from ldap3 import MODIFY_REPLACE from ldap3.core.exceptions import LDAPBindError import passlib.apps +HAS_CRACKLIB = False +try: + import cracklib + HAS_CRACKLIB = True +except ImportError: + pass + # Own modules # from fb_tools.common import to_bool, is_sequence, pp from fb_tools.common import is_sequence, pp from ..xlate import XLATOR -from .ldap import LdapAppError +from .ldap import LdapAppError, FatalLDAPError from .ldap import BaseLdapApplication from .ldap import PasswordFileOptionAction -__version__ = '0.5.1' +__version__ = '0.6.1' LOG = logging.getLogger(__name__) _ = XLATOR.gettext @@ -104,8 +112,8 @@ class SetLdapPasswordApplication(BaseLdapApplication): } passlib_context = None - default_schema = 'ldap_salted_sha256' - default_schema_id = 'SSHA256' + default_schema = 'ldap_sha512_crypt' + default_schema_id = 'CRYPT-SHA512' default_pbkdf2_rounds = 30000 # ------------------------------------------------------------------------- @@ -142,6 +150,9 @@ class SetLdapPasswordApplication(BaseLdapApplication): self.user_dn = None self.schema = self.default_schema self.schema_id = self.default_schema_id + self.no_cracklib = False + + self.user_connection = None my_appname = self.get_generic_appname(appname) @@ -212,6 +223,14 @@ class SetLdapPasswordApplication(BaseLdapApplication): "asked for it.").format(_("PASSWORD")), ) + if HAS_CRACKLIB: + app_group.add_argument( + '-N', '--no-cracklib', action="store_true", dest="no_cracklib", + help=_( + "Do not check the quality of the new password with the " + "{} library.").format('cracklib'), + ) + schema_list = [] def_schema = '' for method in self.available_schemes: @@ -227,7 +246,8 @@ class SetLdapPasswordApplication(BaseLdapApplication): "The schema (hashing method) to use to hash the new password. " "It is possible to give here the value {val_list!r}, then all possible schemes " "are shown and exit. Default: {default!r}.").format( - val_list='list', default=def_schema) + val_list='list', default=def_schema) + ' ' + _("If you are not using an " + "admin account, then the password will hashed only by the default schema.") ) user_help = _( @@ -263,6 +283,8 @@ class SetLdapPasswordApplication(BaseLdapApplication): msg = "Given args:\n" + pp(self.args.__dict__) LOG.debug(msg) + self.no_cracklib = getattr(self.args, 'no_cracklib', False) + given_schema = getattr(self.args, 'schema', None) if given_schema: if given_schema == 'list': @@ -284,6 +306,8 @@ class SetLdapPasswordApplication(BaseLdapApplication): else: if self.current_user: given_user = self.current_user + if self.verbose > 1: + LOG.debug("User bind: no explicit user given.") self.do_user_bind = True self.user_uid = given_user else: @@ -300,8 +324,12 @@ class SetLdapPasswordApplication(BaseLdapApplication): self.current_password = self.args.current_pw self.do_user_bind = True elif self.args.current_pw_prompt: + if self.verbose > 1: + LOG.debug("User bind: Password input demanded.") self.do_user_bind = True elif self.args.current_pw_file: + if self.verbose > 1: + LOG.debug("User bind: Password file given.") self.current_password = self.read_password_file(self.args.current_pw_file) self.do_user_bind = True @@ -311,6 +339,8 @@ class SetLdapPasswordApplication(BaseLdapApplication): inst = self.ldap_instances[0] ldap = self.cfg.ldap_connection[inst] if not ldap.is_admin or ldap.readonly: + if self.verbose > 1: + LOG.debug("User bind: LDAP instance is readonly or not as admin.") self.do_user_bind = True # ------------------------------------------------------------------------- @@ -346,27 +376,32 @@ class SetLdapPasswordApplication(BaseLdapApplication): # ------------------------------------------------------------------------- def pre_run(self): - LOG.debug("Pre running tasks ...") - super(SetLdapPasswordApplication, self).pre_run() - - # ------------------------------------------------------------------------- - def _run(self): - inst = self.ldap_instances[0] connect_info = self.cfg.ldap_connection[inst] + + LOG.debug("Pre running tasks ...") msg = _("Using LDAP instance {inst!r} - {url}.").format(inst=inst, url=connect_info.url) LOG.info(msg) - self.search_user_dn() - if self.do_user_bind and not self.current_password: first_prompt = _("Current password of user {!r}:").format(self.user_uid) + ' ' second_prompt = _('Repeat password:') + ' ' self.current_password = self.get_password( first_prompt, second_prompt, may_empty=False, repeat=False) - if self.do_user_bind: - self.test_user_bind() + if self.do_user_bind and self.schema != self.default_schema: + for method in self.available_schemes: + schema_id = self.schema_ids[method] + if self.verbose > 2: + LOG.debug("Testing for {m!r} ({s}) ...".format(m=method, s=schema_id)) + if schema_id == self.default_schema: + self.passlib_context.update(default=method) + self.schema = method + self.schema_id = schema_id + msg = _( + "Non admin users must use the default schema {!r} for hashing " + "their password.").format(self.schema_id) + LOG.warn(msg) if not self.new_password: first_prompt = _("New password of user {!r}:").format(self.user_uid) + ' ' @@ -374,8 +409,51 @@ class SetLdapPasswordApplication(BaseLdapApplication): self.new_password = self.get_password( first_prompt, second_prompt, may_empty=False, repeat=True) - self.get_current_password_hash() - self.do_set_password() + if HAS_CRACKLIB: + if self.no_cracklib: + msg = _("Checking the quality of the new password was disabled.") + LOG.warn(msg) + else: + LOG.info(_("Testing quality of new password ...")) + try: + cracklib.VeryFascistCheck(self.new_password) + except ValueError as e: + msg = _("Quality of the new password is not sufficient:") + ' ' + str(e) + LOG.error(msg) + self.exit(1) + LOG.debug("The quality of the new password seems to be sufficient.") + else: + msg = _( + "Cannot testing the quality of the new password, because the " + "Python module {!r} is not installed.").format('cracklib') + LOG.warn(msg) + + super(SetLdapPasswordApplication, self).pre_run() + + # ------------------------------------------------------------------------- + def _run(self): + + inst = self.ldap_instances[0] + connect_info = self.cfg.ldap_connection[inst] + + self.search_user_dn() + + if self.do_user_bind: + self.test_user_bind() + else: + self.user_connection = self.ldap_connection[inst] + + try: + self.get_current_password_hash() + self.do_set_password() + finally: + if self.do_user_bind: + if self.user_connection: + if self.verbose > 1: + LOG.debug(_("Unbinding user connection from LDAP server {} ...").format( + connect_info.url)) + self.user_connection.unbind() + self.user_connection = None # ------------------------------------------------------------------------- def test_user_bind(self): @@ -398,26 +476,13 @@ class SetLdapPasswordApplication(BaseLdapApplication): msg = _("Successful connected as {dn!r} to {url}.").format( url=connect_info.url, dn=self.user_dn) LOG.debug(msg) + self.user_connection = ldap_connection except LDAPBindError as e: msg = _("Could not connect to {url} as {dn!r}: {e}").format( url=connect_info.url, dn=self.user_dn, e=e) self.exit(6, msg) - finally: - if ldap_connection: - if self.verbose > 1: - LOG.debug(_("Unbinding from LDAP server {!r} ...").format(connect_info.url)) - ldap_connection.unbind() - ldap_connection = None - del ldap_connection - - if ldap_server: - if self.verbose > 1: - LOG.debug(_("Disconnecting from LDAP server {!r} ...").format( - connect_info.url)) - del ldap_server - # ------------------------------------------------------------------------- def get_current_password_hash(self): @@ -449,7 +514,7 @@ class SetLdapPasswordApplication(BaseLdapApplication): # ------------------------------------------------------------------------- def search_user_dn(self): - """Searching the LDAP DN of the user, whos password should be changed.""" + """Searching the LDAP DN of the user, whose password should be changed.""" inst = self.ldap_instances[0] connect_info = self.cfg.ldap_connection[inst] @@ -523,9 +588,28 @@ class SetLdapPasswordApplication(BaseLdapApplication): self.exit(0) return + self.set_user_password(hashed_passwd) + + # ------------------------------------------------------------------------- + def set_user_password(self, hashed_passwd): + + changes = {} + changes['userPassword'] = [(MODIFY_REPLACE, hashed_passwd)] + + inst = self.ldap_instances[0] + connect_info = self.cfg.ldap_connection[inst] + msg = _("Setting password ...") LOG.info(msg) + try: + self.modify_entry(inst, self.user_dn, changes, ldap=self.user_connection) + except FatalLDAPError as e: + msg = _("{c} on deactivating user {dn!r}: {e}").format( + c=e.__class__.__name__, dn=self.user_dn, e=e) + msg += '\n' + _('Changes:') + '\n' + pp(changes) + LOG.error(msg) + # ============================================================================= if __name__ == "__main__": -- 2.39.5