]> Frank Brehm's Git Trees - pixelpark/pp-admin-tools.git/commitdiff
Cleaning up password hashing methods, starting underlaying password change
authorFrank Brehm <frank.brehm@pixelpark.com>
Fri, 23 Sep 2022 14:15:28 +0000 (16:15 +0200)
committerFrank Brehm <frank.brehm@pixelpark.com>
Fri, 23 Sep 2022 14:15:28 +0000 (16:15 +0200)
lib/pp_admintools/app/set_ldap_password.py

index 7320f00347dd3a95dd3b215d77a5d4f9971e15cc..4dea773b826e662f447b7a676f458456e2e0d91c 100644 (file)
@@ -20,7 +20,7 @@ import passlib.apps
 
 # Own modules
 # from fb_tools.common import to_bool, is_sequence, pp
-from fb_tools.common import is_sequence
+from fb_tools.common import is_sequence, pp
 
 from ..xlate import XLATOR
 
@@ -28,7 +28,7 @@ from .ldap import LdapAppError
 from .ldap import BaseLdapApplication
 from .ldap import PasswordFileOptionAction
 
-__version__ = '0.4.1'
+__version__ = '0.5.1'
 LOG = logging.getLogger(__name__)
 
 _ = XLATOR.gettext
@@ -55,17 +55,14 @@ class SetLdapPasswordApplication(BaseLdapApplication):
 
     possible_schemes = (
         'ldap_des_crypt',
-        'ldap_bcrypt',
         'ldap_md5',
         'ldap_md5_crypt',
         'ldap_salted_md5',
         'ldap_sha1',
         'ldap_sha1_crypt',
         'ldap_salted_sha1',
-        'ldap_pbkdf2_sha1',
         'ldap_sha256_crypt',
         'ldap_salted_sha256',
-        'ldap_pbkdf2_sha256',
         'ldap_sha512_crypt',
         'ldap_salted_sha512',
         'ldap_pbkdf2_sha512',
@@ -76,24 +73,40 @@ class SetLdapPasswordApplication(BaseLdapApplication):
 
     schema_ids = {
         'ldap_des_crypt': 'CRYPT',
-        'ldap_bcrypt': 'BCRYPT',
         'ldap_md5': 'MD5',
-        'ldap_md5_crypt': 'MD5-CRYPT',
+        'ldap_md5_crypt': 'CRYPT-MD5',
         'ldap_salted_md5': 'SMD5',
         'ldap_sha1': 'SHA',
         'ldap_sha1_crypt': 'SHA-CRYPT',
         'ldap_salted_sha1': 'SSHA',
-        'ldap_pbkdf2_sha1': 'PBKDF2-SHA',
-        'ldap_sha256_crypt': 'SHA256-CRYPT',
+        'ldap_sha256_crypt': 'CRYPT-SHA256',
         'ldap_salted_sha256': 'SSHA256',
-        'ldap_pbkdf2_sha256': 'PBKDF2-SHA256',
-        'ldap_sha512_crypt': 'SHA512-CRYPT',
+        'ldap_sha512_crypt': 'CRYPT-SHA512',
         'ldap_salted_sha512': 'SSHA512',
-        'ldap_pbkdf2_sha512': 'PBKDF2-SHA512',
+        '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'),
     }
 
     passlib_context = None
     default_schema = 'ldap_salted_sha256'
+    default_schema_id = 'SSHA256'
+    default_pbkdf2_rounds = 30000
 
     # -------------------------------------------------------------------------
     @classmethod
@@ -106,7 +119,8 @@ class SetLdapPasswordApplication(BaseLdapApplication):
             if schema in all_handlers:
                 cls.available_schemes.append(schema)
 
-        cls.passlib_context = passlib.context.CryptContext(schemes=cls.available_schemes)
+        cls.passlib_context = passlib.context.CryptContext(
+            schemes=cls.available_schemes, ldap_pbkdf2_sha512__rounds=cls.default_pbkdf2_rounds)
         cls.passlib_context.update(default=cls.default_schema)
 
     # -------------------------------------------------------------------------
@@ -120,12 +134,14 @@ class SetLdapPasswordApplication(BaseLdapApplication):
 
         self.current_password = None
         self.need_current_password = False
+        self.current_password_hash = None
         self.do_user_bind = False
         self.ask_for_password = False
         self.new_password = None
         self.user_uid = None
         self.user_dn = None
-        self.schema = None
+        self.schema = self.default_schema
+        self.schema_id = self.default_schema_id
 
         my_appname = self.get_generic_appname(appname)
 
@@ -196,6 +212,24 @@ class SetLdapPasswordApplication(BaseLdapApplication):
                 "asked for it.").format(_("PASSWORD")),
         )
 
+        schema_list = []
+        def_schema = ''
+        for method in self.available_schemes:
+            schema_id = self.schema_ids[method]
+            schema_list.append(schema_id)
+            if method == self.default_schema:
+                def_schema = schema_id
+        schema_list.append('list')
+
+        app_group.add_argument(
+            '-S', '--schema', metavar=_("SCHEMA"), dest="schema", choices=schema_list,
+            help=_(
+                "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)
+        )
+
         user_help = _(
             "The user, which password in the given LDAP instance should be changed. "
             "It may be given by its Uid (the alphanumeric POSIX name), its mail address "
@@ -225,6 +259,25 @@ class SetLdapPasswordApplication(BaseLdapApplication):
 
         super(SetLdapPasswordApplication, self).post_init()
 
+        if self.verbose > 5:
+            msg = "Given args:\n" + pp(self.args.__dict__)
+            LOG.debug(msg)
+
+        given_schema = getattr(self.args, 'schema', None)
+        if given_schema:
+            if given_schema == 'list':
+                self._show_hashing_schemes()
+                self.exit(0)
+                return
+            for method in self.available_schemes:
+                schema_id = self.schema_ids[method]
+                LOG.debug("Testing for {m!r} ({s}) ...".format(m=method, s=schema_id))
+                if schema_id == given_schema:
+                    self.passlib_context.update(default=method)
+                    self.schema = method
+                    self.schema_id = schema_id
+                    break
+
         given_user = getattr(self.args, 'user', None)
         if given_user:
             self.user_uid = given_user
@@ -260,6 +313,36 @@ class SetLdapPasswordApplication(BaseLdapApplication):
         if not ldap.is_admin or ldap.readonly:
             self.do_user_bind = True
 
+    # -------------------------------------------------------------------------
+    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 pre_run(self):
 
@@ -274,7 +357,7 @@ class SetLdapPasswordApplication(BaseLdapApplication):
         msg = _("Using LDAP instance {inst!r} - {url}.").format(inst=inst, url=connect_info.url)
         LOG.info(msg)
 
-        self.user_dn = self.search_user_dn()
+        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) + ' '
@@ -291,6 +374,9 @@ 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()
+
     # -------------------------------------------------------------------------
     def test_user_bind(self):
 
@@ -332,6 +418,35 @@ class SetLdapPasswordApplication(BaseLdapApplication):
                         connect_info.url))
             del ldap_server
 
+    # -------------------------------------------------------------------------
+    def get_current_password_hash(self):
+
+        inst = self.ldap_instances[0]
+        connect_info = self.cfg.ldap_connection[inst]
+
+        if self.verbose > 1:
+            msg = _(
+                "Trying to get current password hash of user {!r} ...").format(self.user_dn)
+            LOG.debug(msg)
+
+        attributes = ['carLicense', 'ppFirstPassword', 'userPassword']
+
+        entry = self.get_entry(self.user_dn, inst, attributes=attributes)
+
+        if not entry:
+            msg = _("User with DN {dn!r} not found in {uri}.").format(
+                dn=self.user_dn, uri=connect_info.url)
+            LOG.error(msg)
+            self.exit(6)
+            return None
+
+        cur_pwd_hash = None
+        attribs = self.normalized_attributes(entry)
+        if attribs['userPassword']:
+            cur_pwd_hash = attribs['userPassword'][0]
+
+        self.current_password_hash = cur_pwd_hash
+
     # -------------------------------------------------------------------------
     def search_user_dn(self):
         """Searching the LDAP DN of the user, whos password should be changed."""
@@ -369,6 +484,48 @@ class SetLdapPasswordApplication(BaseLdapApplication):
         LOG.info(_("Changing the password of user {dn!r} in LDAP instance {inst}.").format(
             dn=self.user_dn, inst=connect_info.url))
 
+    # -------------------------------------------------------------------------
+    def do_set_password(self):
+
+        print()
+        msg = _("Setting password of {dn!r} with hashing schema {schema!r}.").format(
+            dn=self.user_dn, schema=self.schema_id)
+        msg = _("Setting password of '{dn}' with hashing schema '{schema}' ...").format(
+            dn=self.colored(self.user_dn, 'CYAN'), schema=self.colored(self.schema_id, 'CYAN'))
+        print(msg)
+
+        if self.current_password_hash:
+            msg = _("Current password hash: '{}'.").format(
+                self.colored(self.current_password_hash, 'CYAN'))
+        else:
+            msg = _("The user '{}' has currently no password.").format(
+                self.colored(self.user_dn, 'CYAN'))
+        print(msg)
+
+        LOG.debug(_("Used schema: {!r}.").format(self.schema))
+        hashed_passwd = self.passlib_context.hash(self.new_password, self.schema)
+        msg = _("New password hash: '{}'.").format(self.colored(hashed_passwd, 'CYAN'))
+        print(msg)
+
+        print()
+        msg = _("Apply new password? [{yes}/{no}]?").format(
+            yes=self.colored(_('yes'), 'RED'), no=self.colored(_('No'), 'GREEN')) + ' '
+        do_set_passwd = False
+        if self.yes:
+            do_set_passwd = True
+        else:
+            do_set_passwd = self.ask_for_yes_or_no(msg, default_on_empty=False)
+            print()
+
+        if not do_set_passwd:
+            msg = _("Do not setting password for {!r}.").format(self.user_dn)
+            LOG.info(msg)
+            self.exit(0)
+            return
+
+        msg = _("Setting password ...")
+        LOG.info(msg)
+
 
 # =============================================================================
 if __name__ == "__main__":