]> Frank Brehm's Git Trees - pixelpark/pp-admin-tools.git/commitdiff
Hopefully finishing lib/pp_admintools/app/set_ldap_password.py
authorFrank Brehm <frank@brehm-online.com>
Fri, 30 Sep 2022 14:48:26 +0000 (16:48 +0200)
committerFrank Brehm <frank@brehm-online.com>
Fri, 30 Sep 2022 14:48:26 +0000 (16:48 +0200)
lib/pp_admintools/app/set_ldap_password.py

index 4dea773b826e662f447b7a676f458456e2e0d91c..050c0096ed99cc425487b14173736864a85fcf23 100644 (file)
@@ -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__":