[chgpass] Mise en place d'un nouveau script.

* A priori plus sûr
 * Utilise lc_ldap
This commit is contained in:
Pierre-Elliott Bécue 2014-04-14 03:11:50 +02:00
parent 58edc5970a
commit 9e25812e62
3 changed files with 405 additions and 196 deletions

224
archive/gestion/chgpass.py Executable file
View file

@ -0,0 +1,224 @@
#! /usr/bin/env python
# -*- coding: utf-8 -*-
"""
Script de changement de mots de passe LDAP
Utilisation :
* cas 1 : sans arguements par un utlisateur lambda :
changement de son propre mdp
* cas 2 : avec argument par un utilisateur ayant accès
total à la base LDAP (respbats) mais PAS ROOT :
changement du mdp de l'adhérent fourni en argument,
impossibilité de modifier le mdp d'un compte privilégié
* cas 3 : lancé par root : possibilité de modifier
tous les mots de passe LDAP
Copyright (C) Frédéric Pauget
Licence : GPLv2
"""
import subprocess
import getpass, commands, os, sys, base64, syslog
from user_tests import getuser, isadm
from affich_tools import cprint, coul
import secrets_new as secrets
try:
ldap_password = secrets.get("ldap_password")
ldap_auth_dn = secrets.get("ldap_auth_dn")
except:
ldap_password = ''
ldap_auth_dn = ''
uri = 'ldap://ldap.adm.crans.org'
syslog.openlog('chgpass',syslog.LOG_PID,syslog.LOG_AUTH)
def decode64(chaine):
""" Décode une chaine de caratère utf8/64 et retourne un unicode """
try:
return base64.decodestring(chaine).decode('utf8','ignore')
except:
return chaine.decode('utf8','ignore')
def chgpass(dn, mdp = None) :
if mdp == None:
mdp = promptpass(dn)
# Changement mdp
if os.system("/usr/bin/ldappasswd -H '%s' -x -D '%s' -w '%s' '%s' -s '%s' > /dev/null" % (uri, ldap_auth_dn, ldap_password, dn, mdp) ):
cprint(u'Erreur lors du changement de mot de passe', 'rouge')
syslog.syslog("LDAP password changed for dn=%s" % dn)
else :
cprint(u'Changement effectué avec succès', u'vert')
def checkpass(mdp, dialog=False, longueur=8):
### Test du mdp
## 1 - Longueur
if len(mdp) < longueur :
return False, coul(u'Mot de passe trop court, il doit faire au moins %s caractères de long' % longueur, 'rouge', dialog=dialog)
## 2 - Empeche les mots de passe non ASCII
try:
mdp = mdp.encode('ascii')
except (UnicodeEncodeError, UnicodeDecodeError):
return False, coul(u'Les accents ou caractères bizarres ne sont pas autorisés (mais #!@*&%{}| le sont !)',
'rouge', dialog=dialog)
## 2bis - On évite une attaque de type injection de code shell
if "'" in mdp:
return False, coul(u'Les accents ou caractères bizarres ne sont pas autorisés (mais #!@*&%{}| le sont !)',
'rouge', dialog=dialog)
## 3 - assez de caractères de types différents ?
chiffres = 0
majuscules = 0
minuscules = 0
autres = 0
for c in mdp[:] :
if c.isdigit() :
# Un chiffre rapporte 1.5 point avec un maximum de 5
if chiffres < 4.5 : chiffres += 1.5
elif c.islower() :
if minuscules < 3 : minuscules += 1
elif c.isupper() :
if majuscules < 3 : majuscules += 1
else :
autres += 4
if (not majuscules and not minuscules):
return False, coul(u'Mot de passe sans majuscules et sans miniscules. Mélangez des deux ?', dialog=dialog)
if len(mdp) < 16 - minuscules - majuscules - chiffres - autres:
return False, coul(u'Mot de passe trop simple. Ajoutez des chiffres ou des caractères parmis #!@*&%{}| ou mélangez des majuscules/minuscules ?', 'rouge', dialog=dialog)
## 4 - Cracklib
p = subprocess.Popen(['/usr/sbin/cracklib-check'], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
(test, err) = p.communicate(input=mdp)
if test.split(':')[-1].lower().strip() != 'ok' :
commentaire = {
' it does not contain enough DIFFERENT characters': u'Il y a trop de caractères identiques.' ,
' it is based on a dictionary word': u'Le mot de passe est basé sur un mot du dictionnaire' ,
' it is too simplistic/systematic': u'Le mot de passe est trop simple/répétitif'
}.get(test.split(':')[-1],test.split(':')[-1])
return False, coul(commentaire.strip(), 'rouge', dialog=dialog)
return True, ""
def promptpass(dn):
cprint(u"""Le nouveau mot de passe doit comporter au minimum 6 caractères.
Il ne doit pas être basé sur un mot du dictionnaire.""", 'jaune')
print u"Il est conseillé d'utiliser une combinaison de minuscules, majuscules,\nde chiffres et d'au moins un caractère spécial."
print u"Le mot de passe tapé ne sera pas écrit à l'écran."
print u"Taper Ctrl-D pour abandonner"
try :
while 1 :
mdp = getpass.getpass('Nouveau mot de passe : ')
(good, txt) = checkpass(mdp)
if not good:
print txt
continue
### On redemande le mot de passe
mdp1 = getpass.getpass('Retaper mot de passe : ')
if mdp != mdp1 :
cprint(u'Les deux mots de passe entrés sont différents, réesayer', 'rouge')
continue
break
except KeyboardInterrupt :
cprint(u'\nAbandon', 'rouge')
sys.exit(1)
except EOFError :
# Un Ctrl-D
cprint(u'\nAbandon', 'rouge')
sys.exit(1)
return mdp
if __name__ == '__main__' :
sys.stdout.write('\r \r') # Pour esthétique lors de l'utilisation par sudo
if len(sys.argv) == 1 :
# Changement de son mot de passe
login = getuser()
self_mode = True
elif '-h' in sys.argv or '--help' in sys.argv or len(sys.argv) != 2 :
print u"%s <login>" % sys.argv[0].split('/')[-1].split('.')[0]
print u"Changement du mot de passe du compte choisi."
sys.exit(255)
else :
# Changement du mot de passe par un câbleur ou une nounou
login = sys.argv[1]
self_mode = False
for c in login[:] :
if not c.isalnum() and not c=='-' :
cprint(u'Login incorrect', 'rouge')
sys.exit(1)
if getuser() == login :
cprint(u'Utiliser passwd pour changer son propre mot de passe', 'rouge')
sys.exit(2)
if self_mode :
s = commands.getoutput('sudo -u respbats ldap_whoami')
else :
s = commands.getoutput("/usr/bin/ldapsearch -D cn=readonly,dc=crans,dc=org -y/etc/ldap/readonly -x -LLL '(&(objectClass=posixAccount)(uid=%s))' dn nom prenom droits | grep -v MultiMachines" % login).strip()
if not s :
cprint(u'Login non trouvé dans la base LDAP', 'rouge')
sys.exit(3)
# Ca a l'air bon
if s.find('\n\n') != -1 :
# Plusieurs trouvé : pas normal
cprint(u'Erreur lors de la recherche du login : plusieurs occurences !', 'rouge')
sys.exit(4)
s = s.strip().split('\n')
dn = s[0].split()[1]
try :
if len(s) == 2 :
cprint(u"Changement du mot de passe du club %s "%decode64(' '.join(s[1].split()[1:])), 'vert')
else :
cprint(u"Changement du mot de passe de %s %s " % ( s[2].split()[1], s[1].split()[1] ), 'vert')
except :
cprint(u'Erreur lors de la recherche du login', 'rouge')
sys.exit(5)
if self_mode :
# Il faut vérifier l'ancien mot de passe
ldap_auth_dn = dn
ldap_password = getpass.getpass('Mot de passe actuel : ')
s = commands.getoutput("/usr/bin/ldapwhoami -H '%s' -x -D '%s' -w '%s'" % ( uri, ldap_auth_dn, ldap_password ) ).strip()
try :
resultat = s.split('\n')[0].split(':')[1].strip()
except :
cprint(u"Erreur lors de l'authentification", 'rouge')
sys.exit(7)
if resultat != dn :
cprint({ 'Invalid credentials (49)': u'Mot de passe invalide' }.get(resultat, resultat), 'rouge')
sys.exit(8)
elif len(s) > 3 and os.getuid()!=0 and not isadm():
# Adhérent avec droits et on est pas root
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, os.getlogin() )
# Envoi mail
import smtplib
conn = smtplib.SMTP('localhost')
conn.sendmail(From, To , mail )
conn.quit()
cprint(u'Impossible de changer le mot de passe de cet adhérent : compte privilégié', 'rouge')
sys.exit(6)
# Finalement !
chgpass(dn)

View file

@ -1,224 +1,177 @@
#! /usr/bin/env python
#!/bin/bash /usr/scripts/python.sh
# -*- coding: utf-8 -*-
"""
Script de changement de mots de passe LDAP
Utilisation :
* cas 1 : sans arguements par un utlisateur lambda :
changement de son propre mdp
* cas 2 : avec argument par un utilisateur ayant accès
total à la base LDAP (respbats) mais PAS ROOT :
changement du mdp de l'adhérent fourni en argument,
impossibilité de modifier le mdp d'un compte privilégié
* cas 3 : lancé par root : possibilité de modifier
tous les mots de passe LDAP
Copyright (C) Frédéric Pauget
Licence : GPLv2
* Change le mot de passe de l'utilisateur donné en
argument.
"""
import subprocess
import getpass, commands, os, sys, base64, syslog
from user_tests import getuser, isadm
import gestion.config as config
import config.password
import getpass
import argparse
import cracklib
import gestion.affich_tools as affich_tools
import lc_ldap.shortcuts
import lc_ldap.attributs
import sys
import os
import smtplib
from affich_tools import cprint, coul
import secrets_new as secrets
encoding = "UTF-8"
ldap = lc_ldap.shortcuts.lc_ldap_admin()
current_user = os.getenv("SUDO_USER") or os.getenv("USER") or os.getenv("LOGNAME") or getpass.getuser()
try:
ldap_password = secrets.get("ldap_password")
ldap_auth_dn = secrets.get("ldap_auth_dn")
except:
ldap_password = ''
ldap_auth_dn = ''
uri = 'ldap://ldap.adm.crans.org'
syslog.openlog('chgpass',syslog.LOG_PID,syslog.LOG_AUTH)
def decode64(chaine):
""" Décode une chaine de caratère utf8/64 et retourne un unicode """
def check_password(password, no_cracklib=False):
"""
Teste le mot de passe.
* Tests custom + cracklib (sauf si no_cracklib)
"""
try:
return base64.decodestring(chaine).decode('utf8','ignore')
except:
return chaine.decode('utf8','ignore')
password.decode('ascii')
except UnicodeDecodeError:
affich_tools.cprint(u'Le mot de passe ne doit contenir que des caractères ascii.', "rouge")
return False
def chgpass(dn, mdp = None) :
if mdp == None:
mdp = promptpass(dn)
# Nounou mode
if no_cracklib:
if len(password) >= config.password.root_min_len:
return True
else:
problem = False
upp = 0
low = 0
oth = 0
cif = 0
# Changement mdp
if os.system("/usr/bin/ldappasswd -H '%s' -x -D '%s' -w '%s' '%s' -s '%s' > /dev/null" % (uri, ldap_auth_dn, ldap_password, dn, mdp) ):
cprint(u'Erreur lors du changement de mot de passe', 'rouge')
syslog.syslog("LDAP password changed for dn=%s" % dn)
else :
cprint(u'Changement effectué avec succès', u'vert')
# 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
def checkpass(mdp, dialog=False, longueur=8):
### Test du mdp
## 1 - Longueur
if len(mdp) < longueur :
return False, coul(u'Mot de passe trop court, il doit faire au moins %s caractères de long' % longueur, 'rouge', dialog=dialog)
# Recherche de manque de caractères
if cif < config.password.min_cif:
affich_tools.cprint(u'Le mot de passe doit contenir plus de chiffres.', "rouge")
problem = True
if upp < config.password.min_upp:
affich_tools.cprint(u'Le mot de passe doit contenir plus de majuscules.', "rouge")
problem = True
if low < config.password.min_low:
affich_tools.cprint(u'Le mot de passe doit contenir plus de minuscules.', "rouge")
problem = True
if oth < config.password.min_oth:
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")
problem = True
## 2 - Empeche les mots de passe non ASCII
try:
mdp = mdp.encode('ascii')
except (UnicodeEncodeError, UnicodeDecodeError):
return False, coul(u'Les accents ou caractères bizarres ne sont pas autorisés (mais #!@*&%{}| le sont !)',
'rouge', dialog=dialog)
# 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:
affich_tools.cprint(u'Le mot de passe devrait être plus long, ou plus difficile.')
problem = True
if not problem:
try:
# Le mot vient-il du dico (à améliorer, on voudrait pouvoir préciser
# la rigueur du test) ?
password = cracklib.VeryFascistCheck(password)
return True
except ValueError as e:
affich_tools.cprint(str(e).decode(), "rouge")
return False
else:
return False
## 2bis - On évite une attaque de type injection de code shell
if "'" in mdp:
return False, coul(u'Les accents ou caractères bizarres ne sont pas autorisés (mais #!@*&%{}| le sont !)',
'rouge', dialog=dialog)
return False
## 3 - assez de caractères de types différents ?
chiffres = 0
majuscules = 0
minuscules = 0
autres = 0
for c in mdp[:] :
if c.isdigit() :
# Un chiffre rapporte 1.5 point avec un maximum de 5
if chiffres < 4.5 : chiffres += 1.5
elif c.islower() :
if minuscules < 3 : minuscules += 1
elif c.isupper() :
if majuscules < 3 : majuscules += 1
else :
autres += 4
if (not majuscules and not minuscules):
return False, coul(u'Mot de passe sans majuscules et sans miniscules. Mélangez des deux ?', dialog=dialog)
if len(mdp) < 16 - minuscules - majuscules - chiffres - autres:
return False, coul(u'Mot de passe trop simple. Ajoutez des chiffres ou des caractères parmis #!@*&%{}| ou mélangez des majuscules/minuscules ?', 'rouge', dialog=dialog)
def change_password(arguments):
"""
Change le mot de passe en fonction des arguments
"""
with ldap.search(u"(uid=%s)" % (arguments.user.decode(encoding),), mode="w")[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")
## 4 - Cracklib
p = subprocess.Popen(['/usr/sbin/cracklib-check'], stdout=subprocess.PIPE, stdin=subprocess.PIPE, stderr=subprocess.PIPE)
(test, err) = p.communicate(input=mdp)
if test.split(':')[-1].lower().strip() != 'ok' :
commentaire = {
' it does not contain enough DIFFERENT characters': u'Il y a trop de caractères identiques.' ,
' it is based on a dictionary word': u'Le mot de passe est basé sur un mot du dictionnaire' ,
' it is too simplistic/systematic': u'Le mot de passe est trop simple/répétitif'
}.get(test.split(':')[-1],test.split(':')[-1])
return False, coul(commentaire.strip(), 'rouge', dialog=dialog)
# 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 !
return True, ""
Tentative de changement du mot de passe de %s par %s.
""" % (From, To , sys.argv[1], current_user)
def promptpass(dn):
cprint(u"""Le nouveau mot de passe doit comporter au minimum 6 caractères.
Il ne doit pas être basé sur un mot du dictionnaire.""", 'jaune')
print u"Il est conseillé d'utiliser une combinaison de minuscules, majuscules,\nde chiffres et d'au moins un caractère spécial."
print u"Le mot de passe tapé ne sera pas écrit à l'écran."
print u"Taper Ctrl-D pour abandonner"
# Envoi mail
conn = smtplib.SMTP('localhost')
conn.sendmail(From, To , mail )
conn.quit()
sys.exit(1)
try :
while 1 :
mdp = getpass.getpass('Nouveau mot de passe : ')
# On peut modifier le MDP
affich_tools.cprint("Changement du mot de passe de %s %s." % (user['prenom'][0], user['nom'][0]), "vert")
(good, txt) = checkpass(mdp)
if not good:
print txt
continue
# Règles du jeu
if arguments.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 arguments.no_cracklib) + "Non" * (arguments.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 %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')
### On redemande le mot de passe
mdp1 = getpass.getpass('Retaper mot de passe : ')
if mdp != mdp1 :
cprint(u'Les deux mots de passe entrés sont différents, réesayer', 'rouge')
continue
break
try:
while True:
mdp = getpass.getpass("Nouveau mot de passe: ")
if check_password(mdp, arguments.no_cracklib):
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 :
cprint(u'\nAbandon', 'rouge')
sys.exit(1)
except EOFError :
# Un Ctrl-D
cprint(u'\nAbandon', 'rouge')
sys.exit(1)
except KeyboardInterrupt:
affich_tools.cprint(u'\nAbandon', 'rouge')
sys.exit(1)
return mdp
except EOFError:
# Un Ctrl-D
affich_tools.cprint(u'\nAbandon', 'rouge')
sys.exit(1)
if __name__ == '__main__' :
sys.stdout.write('\r \r') # Pour esthétique lors de l'utilisation par sudo
if len(sys.argv) == 1 :
# Changement de son mot de passe
login = getuser()
self_mode = True
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")
elif '-h' in sys.argv or '--help' in sys.argv or len(sys.argv) != 2 :
print u"%s <login>" % sys.argv[0].split('/')[-1].split('.')[0]
print u"Changement du mot de passe du compte choisi."
sys.exit(255)
else :
# Changement du mot de passe par un câbleur ou une nounou
login = sys.argv[1]
self_mode = False
for c in login[:] :
if not c.isalnum() and not c=='-' :
cprint(u'Login incorrect', 'rouge')
sys.exit(1)
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('user', type=str, nargs="?", help="L'utilisateur dont on veut changer le mot de passe.")
args = parser.parse_args()
if getuser() == login :
cprint(u'Utiliser passwd pour changer son propre mot de passe', 'rouge')
sys.exit(2)
if self_mode :
s = commands.getoutput('sudo -u respbats ldap_whoami')
else :
s = commands.getoutput("/usr/bin/ldapsearch -D cn=readonly,dc=crans,dc=org -y/etc/ldap/readonly -x -LLL '(&(objectClass=posixAccount)(uid=%s))' dn nom prenom droits | grep -v MultiMachines" % login).strip()
if not s :
cprint(u'Login non trouvé dans la base LDAP', 'rouge')
sys.exit(3)
# Ca a l'air bon
if s.find('\n\n') != -1 :
# Plusieurs trouvé : pas normal
cprint(u'Erreur lors de la recherche du login : plusieurs occurences !', 'rouge')
sys.exit(4)
s = s.strip().split('\n')
dn = s[0].split()[1]
try :
if len(s) == 2 :
cprint(u"Changement du mot de passe du club %s "%decode64(' '.join(s[1].split()[1:])), 'vert')
else :
cprint(u"Changement du mot de passe de %s %s " % ( s[2].split()[1], s[1].split()[1] ), 'vert')
except :
cprint(u'Erreur lors de la recherche du login', 'rouge')
sys.exit(5)
if self_mode :
# Il faut vérifier l'ancien mot de passe
ldap_auth_dn = dn
ldap_password = getpass.getpass('Mot de passe actuel : ')
s = commands.getoutput("/usr/bin/ldapwhoami -H '%s' -x -D '%s' -w '%s'" % ( uri, ldap_auth_dn, ldap_password ) ).strip()
try :
resultat = s.split('\n')[0].split(':')[1].strip()
except :
cprint(u"Erreur lors de l'authentification", 'rouge')
sys.exit(7)
if resultat != dn :
cprint({ 'Invalid credentials (49)': u'Mot de passe invalide' }.get(resultat, resultat), 'rouge')
sys.exit(8)
elif len(s) > 3 and os.getuid()!=0 and not isadm():
# Adhérent avec droits et on est pas root
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, os.getlogin() )
# Envoi mail
import smtplib
conn = smtplib.SMTP('localhost')
conn.sendmail(From, To , mail )
conn.quit()
cprint(u'Impossible de changer le mot de passe de cet adhérent : compte privilégié', 'rouge')
sys.exit(6)
# Finalement !
chgpass(dn)
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(args)

View file

@ -0,0 +1,32 @@
#!/usr/bin/env python
# -*- coding: utf-8 -*-
# Longueur minimale quand on appelle le script en étant nounou avec
# le bon argument
root_min_len = 4
# Longueur minimale standard
min_len = 9
# Nombre minimal de chiffres requis
min_cif = 1
# Nombre minimal de minuscules requises
min_low = 1
# Nombre minimal de majuscules requises
min_upp = 1
# Nombre minimal d'autres caractères requis
min_oth = 1
# Valeur des majuscules
upp_value = 1
# Valeur des minuscules
low_value = 1
# Valeur des chiffres
cif_value = 1
# Valeur des autres caractères
oth_value = 2