chgpass.py 9.48 KB
Newer Older
1
#!/bin/bash /usr/scripts/python.sh
2
# -*- coding: utf-8 -*-
bernat's avatar
bernat committed
3

4 5 6
"""
Script de changement de mots de passe LDAP

7 8
 * Change le mot de passe de l'utilisateur donné en
 argument.
9 10 11

Auteur : Pierre-Elliott Bécue <becue@crans.org>
Licence : GPLv3
12
"""
13 14
import sys
import os
15

16 17
if '/usr/scripts' not in sys.path:
    sys.path.append('/usr/scripts')
18 19 20 21 22 23 24
import gestion.config as config
import config.password
import getpass
import argparse
import gestion.affich_tools as affich_tools
import lc_ldap.shortcuts
import lc_ldap.attributs
25
import lc_ldap.objets
26
import gestion.mail as mail_module
27

28
encoding = getattr(sys.stdout, 'encoding', "UTF-8")
29 30
current_user = os.getenv("SUDO_USER") or os.getenv("USER") or os.getenv("LOGNAME") or getpass.getuser()

31
def check_password(password, no_cracklib=False, dialog=False):
32 33 34 35
    """
    Teste le mot de passe.
      * Tests custom + cracklib (sauf si no_cracklib)
    """
36 37
    problem = False
    msg = ""
38
    try:
39 40
        password.decode('ascii')
    except UnicodeDecodeError:
41 42 43 44 45
        problem = True
        if not dialog:
            affich_tools.cprint(u'Le mot de passe ne doit contenir que des caractères ascii.', "rouge")
        else:
            msg += affich_tools.coul(u'Le mot de passe ne doit contenir que des caractères ascii.\n', "rouge", dialog=dialog)
46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69

    # Nounou mode
    if no_cracklib:
        if len(password) >= config.password.root_min_len:
            return True
    else:
        upp = 0
        low = 0
        oth = 0
        cif = 0

        # Comptage des caractères
        for char in password:
            if char.isdigit():
                cif += 1
            elif char.isupper():
                upp += 1
            elif char.islower():
                low += 1
            else:
                oth += 1

        # Recherche de manque de caractères
        if cif < config.password.min_cif:
70 71 72 73
            if not dialog:
                affich_tools.cprint(u'Le mot de passe doit contenir plus de chiffres.', "rouge")
            else:
                msg += affich_tools.coul(u'Le mot de passe doit contenir plus de chiffres.\n', "rouge", dialog=dialog)
74 75
            problem = True
        if upp < config.password.min_upp:
76 77 78 79
            if not dialog:
                affich_tools.cprint(u'Le mot de passe doit contenir plus de majuscules.', "rouge")
            else:
                msg += affich_tools.coul(u'Le mot de passe doit contenir plus de majuscules.\n', "rouge", dialog=dialog)
80 81
            problem = True
        if low < config.password.min_low:
82 83 84 85
            if not dialog:
                affich_tools.cprint(u'Le mot de passe doit contenir plus de minuscules.', "rouge")
            else:
                msg += affich_tools.coul(u'Le mot de passe doit contenir plus de minuscules.\n', "rouge", dialog=dialog)
86 87
            problem = True
        if oth < config.password.min_oth:
88 89 90 91
            if not dialog:
                affich_tools.cprint(u'Le mot de passe doit contenir plus de caractères qui ne sont ni des chiffres, ni des majuscules, ni des minuscules.', "rouge")
            else:
                msg += affich_tools.coul(u'Le mot de passe doit contenir plus de caractères qui ne sont ni des chiffres, ni des majuscules, ni des minuscules.\n', "rouge", dialog=dialog)
92 93 94 95 96
            problem = True

        # Scores sur la longueur
        longueur = config.password.upp_value*upp + config.password.low_value*low + config.password.cif_value*cif + config.password.oth_value*oth
        if longueur < config.password.min_len:
97 98 99 100
            if not dialog:
                affich_tools.cprint(u'Le mot de passe devrait être plus long, ou plus difficile.', "rouge")
            else:
                msg += affich_tools.coul(u'Le mot de passe devrait être plus long, ou plus difficile.\n', "rouge", dialog=dialog)
101 102 103 104
            problem = True

        if not problem:
            try:
105 106
                import cracklib
            except ImportError:
Daniel Stan's avatar
Daniel Stan committed
107
                affich_tools.cprint("Attention : la librairie python cracklib n'est pas accessible sur ce serveur.", "rouge")
108 109 110 111 112 113 114 115 116

            if sys.modules.get('cracklib', ''):
                try:
                    # Le mot vient-il du dico (à améliorer, on voudrait pouvoir préciser
                    # la rigueur du test) ?
                    password = cracklib.VeryFascistCheck(password)
                    return True, msg
                except ValueError as e:
                    if not dialog:
117
                        affich_tools.cprint(e.message, "rouge")
118 119 120 121 122
                    else:
                        msg += affich_tools.coul(str(e).decode(), "rouge", dialog=dialog)
                    return False, msg
            else:
                return True, msg
123
        else:
124
            return False, msg
125

126
    return False, msg
127

128
@lc_ldap.shortcuts.with_ldap_conn(retries=2, delay=5, constructor=lc_ldap.shortcuts.lc_ldap_admin)
129
def change_password(ldap, login=None, verbose=False, no_cracklib=False, **args):
130 131 132
    """
    Change le mot de passe en fonction des arguments
    """
133 134 135 136 137 138 139
    if login is None:
        login = current_user
    if type(login) == str:
        login = login.decode(encoding)
    login = lc_ldap.crans_utils.escape(login)
    query = ldap.search(u"(uid=%s)" % login, mode="w")
    if not query:
140 141
        affich_tools.cprint('Utilisateur introuvable dans la base de données, modification de l\'utilisateur local.', "rouge")
        sys.exit(2)
142
    with query[0] as user:
143 144 145 146 147 148 149 150 151 152 153
        # Test pour vérifier que l'utilisateur courant peut modifier le mdp de user
        try:
            user['userPassword'] = [lc_ldap.crans_utils.hash_password("test").decode('ascii')]
            user.cancel()
        except EnvironmentError as e:
            affich_tools.cprint(str(e).decode(encoding), "rouge")

            # Génération d'un mail
            From = 'roots@crans.org'
            To = 'roots@crans.org'
            mail = """From: Root <%s>
154 155
To: %s
Subject: Tentative de changement de mot de passe !
156

157 158
Tentative de changement du mot de passe de %s par %s.
""" % (From, To , login.encode(encoding), current_user)
159 160

            # Envoi mail
161 162
            with mail_module.ServerConnection() as conn:
                conn.sendmail(From, To , mail )
163 164 165
            sys.exit(1)

        # On peut modifier le MDP
166 167 168 169
        if isinstance(user, lc_ldap.objets.club):
            prenom = "Club"
        else:
            prenom = user['prenom'][0]
170
        affich_tools.cprint("Changement du mot de passe de %s %s." %
171
                (prenom, user['nom'][0]),
172
            "vert")
173 174

        # Règles du jeu
175 176
        # (J'ai perdu)
        if verbose:
177 178 179 180
            affich_tools.cprint(u"""Règles :
Longueur standard : %s, root : %s,
Minimums : chiffres : %s, minuscules : %s, majuscules : %s, autres : %s,
Scores de longueur : chiffres : %s, minuscules : %s, majuscules : %s, autres : %s,
181 182 183 184 185 186 187 188 189 190 191 192 193 194
Cracklib : %s.""" % (
        config.password.min_len,
        config.password.root_min_len,
        config.password.min_cif,
        config.password.min_low,
        config.password.min_upp,
        config.password.min_oth,
        config.password.cif_value,
        config.password.low_value,
        config.password.upp_value,
        config.password.oth_value,
        "Oui" * (not no_cracklib) + "Non" * (no_cracklib)
    ),
    'jaune')
195 196 197
        else:
            affich_tools.cprint(u"""Le nouveau mot de passe doit comporter au minimum %s caractères.
Il ne doit pas être basé sur un mot du dictionnaire.
198 199
Il doit contenir au moins %s chiffre(s), %s minuscule(s),
%s majuscule(s) et au moins %s autre(s) caractère(s).
200 201 202 203 204 205 206 207
CTRL+D ou CTRL+C provoquent un abandon.""" %
    (
        config.password.min_len,
        config.password.min_cif,
        config.password.min_low,
        config.password.min_upp,
        config.password.min_oth
    ), 'jaune')
208 209 210 211

        try:
            while True:
                mdp = getpass.getpass("Nouveau mot de passe: ")
212
                if check_password(mdp, no_cracklib)[0]:
213 214 215 216 217 218 219 220 221 222 223 224 225 226 227 228 229 230 231 232 233
                    mdp2 = getpass.getpass("Retaper le mot de passe: ")
                    if mdp != mdp2:
                        affich_tools.cprint(u"Les deux mots de passe diffèrent.", "rouge")
                    else:
                        break

        except KeyboardInterrupt:
            affich_tools.cprint(u'\nAbandon', 'rouge')
            sys.exit(1)

        except EOFError:
            # Un Ctrl-D
            affich_tools.cprint(u'\nAbandon', 'rouge')
            sys.exit(1)

        hashedPassword = lc_ldap.crans_utils.hash_password(mdp)
        user['userPassword'] = [hashedPassword.decode('ascii')]
        user.save()
        affich_tools.cprint(u"Mot de passe de %s changé." % (user['uid'][0]), "vert")

if __name__ == "__main__":
234 235 236 237 238 239 240 241 242 243 244 245 246 247
    parser = argparse.ArgumentParser(
        description="Recherche dans la base des adhérents",
        add_help=False)
    parser.add_argument('-h', '--help',
        help="Affiche ce message et quitte.",
        action="store_true")
    parser.add_argument('-n', '--no-cracklib',
        help="Permet de contourner les règles de choix du mot de passe" +
             "(réservé aux nounous).",
             action="store_true")
    parser.add_argument('-v', '--verbose',
        help="Permet de contourner les règles de choix du mot de passe" +
             "(réservé aux nounous).",
             action="store_true")
248
    parser.add_argument('login', type=str, nargs="?",
249 250
        help="L'utilisateur dont on veut changer le mot de passe.")

251 252 253 254 255 256 257 258
    args = parser.parse_args()

    if args.help:
        parser.print_help()
        sys.exit(0)
    if args.no_cracklib:
        if not lc_ldap.attributs.nounou in ldap.droits:
            args.no_cracklib = False
259
    change_password(**vars(args))