
Cela demanderait plus d'inverstigation pour trouver pourquoi pam échoue pour les mots de passe de plus de 64 caractères
256 lines
8.8 KiB
Python
Executable file
256 lines
8.8 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 gestion.mail as mail_module
|
||
|
||
encoding = getattr(sys.stdout, '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
|
||
msg += u"Le mot de passe ne doit contenir que des caractères ascii.\n"
|
||
|
||
if len(password) >= 64:
|
||
problem = True
|
||
msg += u"Le mot de passe doit faire strictement moins de 64 caractères\n"
|
||
|
||
# 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:
|
||
msg += u'Le mot de passe doit contenir plus de chiffres.\n'
|
||
problem = True
|
||
if upp < config.password.min_upp:
|
||
msg += u'Le mot de passe doit contenir plus de majuscules.\n'
|
||
problem = True
|
||
if low < config.password.min_low:
|
||
msg += u'Le mot de passe doit contenir plus de minuscules.\n'
|
||
problem = True
|
||
if oth < config.password.min_oth:
|
||
msg += u'Le mot de passe doit contenir plus de caractères qui ne sont ni des chiffres, ni des majuscules, ni des minuscules.\n'
|
||
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:
|
||
msg += u'Le mot de passe devrait être plus long, ou plus difficile.\n'
|
||
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)
|
||
if dialog:
|
||
msg = affich_tools.coul(msg, 'rouge', dialog=dialog)
|
||
return True, msg
|
||
except ValueError as e:
|
||
msg += str(e).decode()
|
||
|
||
if dialog:
|
||
msg = affich_tools.coul(msg, 'rouge', dialog=dialog)
|
||
return False, msg
|
||
else:
|
||
if dialog:
|
||
msg = affich_tools.coul(msg, 'rouge', dialog=dialog)
|
||
return True, msg
|
||
else:
|
||
if dialog:
|
||
msg = affich_tools.coul(msg, 'rouge', dialog=dialog)
|
||
return False, msg
|
||
|
||
if dialog:
|
||
msg = affich_tools.coul(msg, 'rouge', dialog=dialog)
|
||
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
|
||
with mail_module.ServerConnection() as conn:
|
||
conn.sendmail(From, To , mail )
|
||
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: ")
|
||
(ret, msg) = check_password(mdp, no_cracklib)
|
||
if ret:
|
||
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
|
||
else:
|
||
affich_tools.cprint(msg, 'rouge')
|
||
|
||
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))
|