260 lines
9.4 KiB
Python
Executable file
260 lines
9.4 KiB
Python
Executable file
#!/bin/bash /usr/scripts/python.sh
|
||
# -*- coding: utf-8 -*-
|
||
|
||
"""
|
||
Script de changement de mots de passe LDAP
|
||
|
||
* Change le mot de passe de l'utilisateur donné en
|
||
argument.
|
||
|
||
Auteur : Pierre-Elliott Bécue <becue@crans.org>
|
||
Licence : GPLv3
|
||
"""
|
||
import sys
|
||
import os
|
||
|
||
if '/usr/scripts' not in sys.path:
|
||
sys.path.append('/usr/scripts')
|
||
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
|
||
import lc_ldap.objets
|
||
import smtplib
|
||
|
||
encoding = "UTF-8"
|
||
current_user = os.getenv("SUDO_USER") or os.getenv("USER") or os.getenv("LOGNAME") or getpass.getuser()
|
||
|
||
def check_password(password, no_cracklib=False, dialog=False):
|
||
"""
|
||
Teste le mot de passe.
|
||
* Tests custom + cracklib (sauf si no_cracklib)
|
||
"""
|
||
problem = False
|
||
msg = ""
|
||
try:
|
||
password.decode('ascii')
|
||
except UnicodeDecodeError:
|
||
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)
|
||
|
||
# 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:
|
||
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)
|
||
problem = True
|
||
if upp < config.password.min_upp:
|
||
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)
|
||
problem = True
|
||
if low < config.password.min_low:
|
||
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)
|
||
problem = True
|
||
if oth < config.password.min_oth:
|
||
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)
|
||
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:
|
||
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)
|
||
problem = True
|
||
|
||
if not problem:
|
||
try:
|
||
import cracklib
|
||
except ImportError:
|
||
affich_tools.cprint("Attention : la librairie python cracklib n'est pas accessible sur ce serveur.", "rouge")
|
||
|
||
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:
|
||
affich_tools.cprint(e.message, "rouge")
|
||
else:
|
||
msg += affich_tools.coul(str(e).decode(), "rouge", dialog=dialog)
|
||
return False, msg
|
||
else:
|
||
return True, msg
|
||
else:
|
||
return False, msg
|
||
|
||
return False, msg
|
||
|
||
@lc_ldap.shortcuts.with_ldap_conn(retries=2, delay=5, constructor=lc_ldap.shortcuts.lc_ldap_admin)
|
||
def change_password(ldap, login=None, verbose=False, no_cracklib=False, **args):
|
||
"""
|
||
Change le mot de passe en fonction des arguments
|
||
"""
|
||
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:
|
||
affich_tools.cprint('Utilisateur introuvable dans la base de données, modification de l\'utilisateur local.', "rouge")
|
||
sys.exit(2)
|
||
with query[0] as user:
|
||
# 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>
|
||
To: %s
|
||
Subject: Tentative de changement de mot de passe !
|
||
|
||
Tentative de changement du mot de passe de %s par %s.
|
||
""" % (From, To , login.encode(encoding), current_user)
|
||
|
||
# Envoi mail
|
||
conn = smtplib.SMTP('localhost')
|
||
conn.sendmail(From, To , mail )
|
||
conn.quit()
|
||
sys.exit(1)
|
||
|
||
# On peut modifier le MDP
|
||
if isinstance(user, lc_ldap.objets.club):
|
||
prenom = "Club"
|
||
else:
|
||
prenom = user['prenom'][0]
|
||
affich_tools.cprint("Changement du mot de passe de %s %s." %
|
||
(prenom, user['nom'][0]),
|
||
"vert")
|
||
|
||
# Règles du jeu
|
||
# (J'ai perdu)
|
||
if verbose:
|
||
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,
|
||
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')
|
||
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.
|
||
Il doit contenir au moins %s chiffre(s), %s minuscule(s),
|
||
%s majuscule(s) et au moins %s autre(s) caractère(s).
|
||
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')
|
||
|
||
try:
|
||
while True:
|
||
mdp = getpass.getpass("Nouveau mot de passe: ")
|
||
if check_password(mdp, no_cracklib)[0]:
|
||
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__":
|
||
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")
|
||
parser.add_argument('login', type=str, nargs="?",
|
||
help="L'utilisateur dont on veut changer le mot de passe.")
|
||
|
||
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
|
||
change_password(**vars(args))
|