#!/bin/bash /usr/scripts/python.sh # -*- coding: utf-8 -*- u""" Interface utilisateur du système de gestion des machines et adhérents du crans Copyright (C) Valentin Samir Licence : GPLv3 """ ### sur le dialog (1.1) de zamok, il manque les fonctionnalité suivante présente dans la 1.2 ### --default-button pour choisir le bouton sélectionner par defaut ### --not-tags pour masquer les tags quand il ne servent à rien pour l'utilisateur (mais sont utilisé comme index par le programme) import os import sys import ssl import time import copy import ldap import signal import inspect import tempfile import collections sys.path.append('/usr/scripts/') from pythondialog import Dialog from pythondialog import DialogError, DialogTerminatedBySignal from OpenSSL import crypto, SSL from gestion.cert_utils import createCertRequest from gestion.affich_tools import get_screen_size, coul from gestion.chgpass import check_password import gestion.config as config import gestion.config.factures import lc_ldap.shortcuts import lc_ldap.objets as objets import lc_ldap.attributs as attributs import lc_ldap.printing as printing import lc_ldap.crans_utils as lc_utils from lc_ldap.attributs import UniquenessError import gestion.secrets_new as secrets debugf=None debug_enable = False def mydebug(txt): """Petit fonction pour écrire des messages de débug dans /tmp/gest_crans_lc.log""" global debugf, debug_enable if debug_enable: if debugf is None: debugf = open('/tmp/gest_crans_lc.log', 'w') if isinstance(txt, list): for v in txt: mydebug(' ' + str(v)) else: debugf.write(str(txt)+'\n') debugf.flush() os.fsync(debugf) # Implémentation "à la main" de la tail récursion en python # voir http://kylem.net/programming/tailcall.html # je trouve ça assez sioux # En plus, ça nous permet de gérer plus facilement le # code en CPS class TailCaller(object) : """ Classe permetant, en décorant des fonctions avec, d'avoir de la tail récursion faite "à la main" en python (voir http://kylem.net/programming/tailcall.html) """ other_callers = {} def __init__(self, f) : self.f = f self.func_name = f.func_name TailCaller.other_callers[id(self)]=f.func_name def __del__(self): del(TailCaller.other_callers[id(self)]) def __call__(self, *args, **kwargs) : mydebug("***%s calling" % self.func_name) stacklvl = len(inspect.stack()) try: ret = self.f(*args, **kwargs) except Continue as c: ret = c.tailCall while isinstance(ret, TailCall): # Si la continuation a été créer par un TailCaller précédent, on propage # Cela permet d'assurer qu'un TailCaller ne s'occupe que des TailCall # Crée après lui. if stacklvl>=ret.stacklvl: mydebug("***%s terminate" % self.func_name) raise Continue(ret) mydebug("***%s doing %s" % (self.func_name, ret)) try: ret = ret.handle() except Continue as c: ret = c.tailCall mydebug("***%s returning %s" % (self.func_name, ret)) return ret def tailcaller(f): """ Décorateur retardant la décoration par TailCaller d'une fonction À utiliser sur les fonctions faisant de la tail récursion et devant retourner une valeur. On l'utilise de toute façon sur la fonction d'entrée dans l'application Une fonction de devrait pas instancier de TailCall sans être décoré par ce décorteur de façon générale """ f.tailCaller = True return f class Continue(Exception): """Exception pour envoyer des TailCall en les raisant""" def __init__(self, tailCall): self.tailCall = tailCall class TailCall(object) : """ Appel tail récursif à une fonction et ses argument On peut voir un TailCall comme le fait de retarder l'appel à une fonction (avec ses listes d'arguements) à un moment futur. La fonction sera appelée dans les cas suivant : * Le TailCall est retourné par une fonction qui est un TailCaller * L'exception Continue(TailCall(...)) est levée et traverse une fonction qui est un TailCaller """ def __init__(self, call, *args, **kwargs) : self.stacklvl = len(inspect.stack()) if isinstance(call, TailCall): call.kwargs.update(**kwargs) kwargs = call.kwargs args = call.args + args call = call.call self.call = call self.args = args self.kwargs = kwargs def __str__(self): return "TailCall<%s(%s%s%s)>" % ( self.call.func_name, ', '.join(repr(a) for a in self.args), ', ' if self.args and self.kwargs else '', ', '.join("%s=%s" % (repr(k),repr(v)) for (k,v) in self.kwargs.items()) ) def copy(self): ''' Renvois une copie de l'objet courant attention les elements et args ou kwargs sont juste linké ça n'est pas gennant dans la mesure où ils ne sont normalement pas éditer mais remplacé par d'autres éléments ''' result = TailCall(self.call, *list(self.args), **dict(self.kwargs)) result.stacklvl = self.stacklvl return result def __call__(self, *args, **kwargs): self.kwargs.update(kwargs) self.args = self.args + args return self def handle(self) : """ Exécute la fonction call sur sa liste d'argument. on déréférence les TailCaller le plus possible pour réduire la taille de la stack """ caller = None call = self.call while isinstance(call, TailCaller) : caller = call call = self.call.f return call(*self.args, **self.kwargs) def unicode_of_Error(x): """Formatte des exception""" return u"\n".join(unicode(i, 'utf-8') if type(i) == str else repr(i) for i in x.args) def handle_exit_code(d, code): """Gère les codes de retour dialog du menu principal""" if code in (d.DIALOG_CANCEL, d.DIALOG_ESC): if code == d.DIALOG_CANCEL: #msg = "Vous avez choisi Annuler dans la dernière fenêtre de dialogue.\n\n" \ # "Voulez vous quitter le programme ?" os.system('clear') sys.exit(0) else: msg = "Vous avez appuyer sur ESC ou CTRL+C dans la dernière fenêtre de dialogue.\n\n" \ "Voulez vous quitter le programme ?" if d.yesno(msg, width=60) == d.DIALOG_OK: os.system('clear') sys.exit(0) return False else: return True # code est d.DIALOG_OK def raiseKeyboardInterrupt(x, y): """fonction utilisée pour réactiver les Ctrl-C""" raise KeyboardInterrupt() class GestCrans(object): """Interface de gestion des machines et des adhérents du crans, version lc_ldap""" def __getattribute__(self, attr): ret = super(GestCrans, self).__getattribute__(attr) # Petit hack pour que ça soit les methodes de l'objet instancié qui soient # décorée par TailCaller et pas les méthodes statiques de la classe GestCrans if getattr(ret, 'tailCaller', False) and not isinstance(ret, TailCaller): ret = TailCaller(ret) setattr(self, attr, ret) return ret def __init__(self): signal.signal(signal.SIGINT, signal.SIG_IGN) # On initialise le moteur de rendu en spécifiant qu'on va faire du dialog printing.template(dialog=True) self.check_ldap() # On met un timeout à 10min d'innactivité sur dialog self.timeout = 600 self.error_to_raise = (Continue, DialogError, ldap.SERVER_DOWN) def check_ldap(self): """Se connecte à la base ldap et vérifie les droits de l'utilisateur courant""" self.check_ldap_last = time.time() # S'il y a --test dans les argument, on utilise la base de test if '--test' in sys.argv[1:]: self.conn = lc_ldap.shortcuts.lc_ldap_test() else: # On ouvre une connexion lc_ldap self.conn = lc_ldap.shortcuts.lc_ldap_admin() # On vérifie que l'utilisateur système existe dans ldap (pour la gestion des droits) luser=self.conn.search(u'(&(uid=%s)(objectClass=cransAccount))' % self.conn.current_login) if not luser: sys.stderr.write("L'utilisateur %s n'existe pas dans la base de donnée" % self.conn.current_login) sys.exit(1) self.conn.droits = [str(d) for d in luser[0]['droits']] self.conn.dn = luser[0].dn # Si un nom d'utilisateur est donné sur la ligne de commande # et qu'on a les droits nounou, on l'utilise if sys.argv[1:] and attributs.nounou in self.conn.droits: for u in sys.argv[1:]: luser=self.conn.search(u'(&(uid=%s)(objectClass=cransAccount))' % u) if luser: self.conn.current_login = u self.conn.droits = [str(d) for d in luser[0]['droits']] self.conn.dn = luser[0].dn break a = attributs allowed_right = [a.cableur, a.tresorier, a.bureau, a.nounou, a.imprimeur] for droit in allowed_right: if droit in self.conn.droits: break else: sys.stderr.write( u"%s ne possède aucun des droits :\n * %s\nnécessaire à utiliser ce programme\n" % ( self.conn.current_login, '\n * '.join(allowed_right) ) ) sys.exit(1) _dialog = None @property def dialog(self): """ Renvois l'objet dialog. De plus renouvelle régulièrement la connexion à la base ldap """ # Tous les self.timeout, on refraichie la connexion ldap if time.time() - self.check_ldap_last > self.timeout: self.check_ldap_last = time.time() if self._dialog is None: self._dialog = Dialog() self.dialog_last_access = time.time() return self._dialog def nyan(self, cont, *args, **kwargs): """ Nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan nyan """ (lines, cols) = get_screen_size() print "\033[48;5;17m" print " "*(lines * cols) cols = int(min(cols/2, 65)) lines = int(lines) -1 cmd = "/usr/bin/nyancat -W %s -H %s" % (cols, lines) os.system(cmd) raise Continue(cont) @tailcaller def handle_dialog(self, cancel_cont, box, *args): """ Gère les fonctions appelant une fênetre dialog : gestion de l'appuie sur Ctrl-C et rattrapage des exception / segfault de dialog. cancel_cont représente l'endroit où retourner si Ctrl-C """ ctrlC=False ret=None try: signal.signal(signal.SIGINT, raiseKeyboardInterrupt) # Ctrl-C ret = box(*args) signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C except KeyboardInterrupt: signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C raise Continue(cancel_cont) except DialogTerminatedBySignal as e: signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C if e[1] == 11: self.dialog.msgbox( "La fenêtre dialog à été fermée par une erreur de segmentation", timeout=self.timeout, title="Erreur rencontrée", width=73, height=10 ) raise Continue(cancel_cont) else: raise finally: if ret: return ret else: EnvironmentError("Pas de ret ?!? c'est pas possible ") @tailcaller def handle_dialog_result(self, code, output, cancel_cont, error_cont, codes_todo=[]): """ Gère les fonctions traitant les résultat d'appels à dialog. s'occupe de gérer les exceptions, Ctrl-C, propagation de certaine exceptions, l'appuis sur annuler. Le code à exécuté lui ai passé via la liste codes_todo, qui doit contenir une liste de triple : (code de retour dialog, fonction à exécuter, liste des arguements de la fonction) la fonction est appelée sur ses arguements si le code retourné par dialog correspond. codes_todo = [(code, todo, todo_args)] """ # Si on a appuyé sur annulé ou ESC, on s'en va via la continuation donnée en argument if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC): raise Continue(cancel_cont) # Sinon, c'est OK else: for (codes, todo, todo_args) in codes_todo: if code in codes: try: signal.signal(signal.SIGINT, raiseKeyboardInterrupt) # Ctrl-C # On effectue ce qu'il y a a faire dans todo ret = todo(*todo_args) signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C return ret # On propage les Continue except self.error_to_raise: signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C raise # En cas d'une autre erreur, on l'affiche et on retourne au menu d'édition except (Exception, ldap.OBJECT_CLASS_VIOLATION) as e: signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C self.dialog.msgbox("%s" % unicode_of_Error(e), timeout=self.timeout, title="Erreur rencontrée", width=73, height=10) raise Continue(error_cont) except KeyboardInterrupt: signal.signal(signal.SIGINT, signal.SIG_IGN) # Pas de Ctrl-C raise Continue(cancel_cont) # En cas de code de retour dialog non attendu, on prévient et on retourne au menu d'édition self.dialog.msgbox("Le code de retour dialog est %s, c'est étrange" % code, timeout=self.timeout, title="Erreur rencontrée", width=73, height=10) raise Continue(error_cont) @tailcaller def edit_blacklist_select(self, obj, title, cont): """ Permet de choisir une blackliste parmis celle de obj oubien une nouvelle blacklist. Retourne (index, bl) où bl est un dictionnaire représentant la blackliste et index l'index de la blackliste dans obj['blacklist'] ou new pour une nouvelle blacklist """ def box(): choices = [('new', 'Ajouter une nouvelle blackliste')] index = 0 for bl in obj['blacklist']: choices.append( (str(index), coul("%s [%s]" % (bl['type'], bl['comm']), 'rouge' if bl['actif'] else None, dialog=True) ) ) index+=1 return self.dialog.menu( "Éditer une blacklist ou en ajouter une nouvelle ?\n(les blacklistes actives apparaissent en rouge)", width=0, timeout=self.timeout, height=0, menu_height=0, item_help=0, title=title, scrollbar=True, colors=True, cancel_label="Retour", backtitle=u"Vous êtes connecté en tant que %s" % self.conn.current_login, choices=choices) def todo(tag): if tag == 'new': return tag, {'debut':0, 'fin':0, 'type':'', 'comm':''} else: bl = {} bl.update(obj['blacklist'][int(tag)].value) return tag, bl (code, tag) = self.handle_dialog(cont, box) retry_cont = TailCall(self.edit_blacklist_select, obj=obj, title=title, cont=cont) return self.handle_dialog_result( code=code, output=tag, cancel_cont=cont, error_cont=retry_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [tag])] ) @tailcaller def edit_blacklist_type(self, title, cont): """Permet de choisir un type de blackliste pour les nouvelles blacklistes""" retry_cont = TailCall(self.edit_blacklist_type, title=title, cont=cont) def box(): return self.dialog.menu( "Choisissez un type de blacklist", width=0, height=0, menu_height=0, item_help=0, timeout=self.timeout, title=title, scrollbar=True, colors=True, cancel_label="Retour", backtitle=u"Vous êtes connecté en tant que %s" % self.conn.current_login, choices=[(k,v) for (k,v) in config.blacklist_items.items()]) def todo(tag, retry_cont): if tag in config.blacklist_items: return tag else: self.dialog.msgbox("%s n'est pas une blacklist" % tag, timeout=self.timeout, title="Erreur rencontrée", width=73, height=10) raise Continue(retry_cont) (code, tag) = self.handle_dialog(cont, box) return self.handle_dialog_result( code=code, output=tag, cancel_cont=cont(bl=None), error_cont=retry_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [tag, retry_cont])] ) @tailcaller def get_timestamp(self, title, text, cont, hour=-1, minute=-1, second=-1, day=0, month=0, year=0): """Fait choisir une date et une heure et retourne le tuple (year, month, day, hour, minute, second)""" retry_cont = TailCall(self.get_timestamp, title=title, text=text, cont=cont, hour=hour, minute=minute, second=second, day=day, month=month, year=year) def get_date(day, month, year): (code, output) = self.dialog.calendar(text, day=day, month=month, year=year, timeout=self.timeout, title=title) if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC): raise Continue(cont) elif output: (day, month, year) = output return (year, month, day) else: raise EnvironmentError("Pourquoi je n'ai pas de date ?") def get_time(hour, minute, second, day, month, year): (code, output) = self.dialog.timebox(text, timeout=self.timeout, hour=hour, minute=minute, second=second) if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC): raise Continue(retry_cont(day=day, month=month, year=year)) elif output: (hour, minute, second) = output return (hour, minute, second) else: raise EnvironmentError("Pourquoi je n'ai pas d'horaire ?") (year, month, day) = get_date(day, month, year) (hour, minute, second) = get_time(hour, minute, second, day, month, year) return (year, month, day) + (hour, minute, second) def edit_blacklist(self, obj, title, update_obj, cont, bl=None, tag=None, bl_type=None, debut=None, fin=None, comm=None): """Pour éditer les blacklistes d'un objet lc_ldap""" self_cont = TailCall(self.edit_blacklist, obj=obj, title=title, update_obj=update_obj, cont=cont, bl=bl, tag=tag, bl_type=bl_type, debut=debut, fin=fin, comm=comm) # Si bl ou tag ne sont pas définit on les demande if bl is None or tag is None: bl_type = None debut = None fin = None comm = None tag, bl = self.edit_blacklist_select(obj, title, cont(**{update_obj:obj})) if bl_type is None and tag == 'new': bl['type'] = self.edit_blacklist_type(title, self_cont(obj=obj)) elif tag == 'new': bl['type'] = bl_type # Cas de l'ajout d'un blacklist if tag == 'new': # Si debut n'est pas encore spécifié, on le demande if debut is None: debut_tuple = self.get_timestamp(title=title, text="Choisir le début de la blacklist", cont=self_cont(bl=bl, tag=tag, bl_type=None, debut=None, fin=None, comm=None)) debut = int(time.mktime(time.struct_time(debut_tuple + (0, 0, -1)))) # Idem pour fin if fin is None: if self.dialog.yesno("Mettre une date de fin ?", title=title, timeout=self.timeout) == self.dialog.DIALOG_OK: fin_tuple = self.get_timestamp(title=title, text="Choisir la date de fin :", cont=self_cont(bl=bl, tag=tag, bl_type=bl_type, debut=None, fin=None, comm=None)) fin = int(time.mktime(time.struct_time(debut_tuple + (0, 0, -1)))) else: fin = '-' bl['debut']=debut bl['fin']=fin bl['comm']=self.get_comment(title=title, text="Commentaire ?", cont=self_cont(bl=bl, tag=tag, bl_type=bl['type'], debut=debut, fin=None, comm=None)) if self.confirm_item(item=attributs.attrify(bl, 'blacklist', self.conn), title="Ajouter la blacklist ?"): try: with self.conn.search(dn=obj.dn, scope=0, mode='rw')[0] as obj: obj['blacklist'].append(bl) obj.save() # On s'en va en mettant à jour dans la continuation la valeur de obj raise Continue(self_cont(bl=None, obj=obj)) # On propage les Continue except self.error_to_raise: raise # En cas d'une autre erreur, on l'affiche et on retourne except (Exception, ldap.OBJECT_CLASS_VIOLATION) as e: self.dialog.msgbox("%s" % unicode_of_Error(e), timeout=self.timeout, title="Erreur rencontrée", width=73) raise Continue(self_cont(obj=obj)) else: raise Continue(self_cont(bl=None, obj=obj)) # Cas de l'édition d'une blacklist else: if debut is None: # Mettre un warning pour éditer (seulement quand debut vaut None pour ne pas le répéter à chaque fois que l'on revient en arrière par la suite if not self.confirm_item(item=attributs.attrify(bl, 'blacklist', self.conn), title="Éditer la blackliste ?"): raise Continue(self_cont(bl=None, obj=obj)) debut = time.localtime(bl['debut']) debut_tuple = self.get_timestamp(title=title, text="Choisir le début de la blacklist", cont=self_cont(bl=bl, tag=tag, debut=None, fin=None, comm=None), day=debut.tm_mday, month=debut.tm_mon, year=debut.tm_year, hour=debut.tm_hour, minute=debut.tm_min, second=debut.tm_sec ) debut = int(time.mktime(time.struct_time(debut_tuple + (0, 0, -1)))) bl['debut'] = debut if fin is None: if self.dialog.yesno("Mettre une date de fin ?", timeout=self.timeout, title=title) == self.dialog.DIALOG_OK: if bl['fin'] == '-': fin = time.localtime() else: fin = time.localtime(bl['fin']) fin_tuple = self.get_timestamp(title=title, text="Choisir la date de fin :", cont=self_cont(bl=bl, tag=tag, debut=debut, fin=None, comm=None), day=fin.tm_mday, month=fin.tm_mon, year=fin.tm_year, hour=fin.tm_hour, minute=fin.tm_min, second=fin.tm_sec ) fin = int(time.mktime(time.struct_time(debut_tuple + (0, 0, -1)))) else: fin = '-' bl['fin'] = fin bl['comm']=self.get_comment(title=title, text="Commentaire ?", init=bl['comm'], cont=self_cont(bl=bl, tag=tag, bl_type=bl['type'], debut=debut, fin=None, comm=None)) if self.confirm_item(item=attributs.attrify(bl, 'blacklist', self.conn), title="Modifier la blacklist ?"): try: with self.conn.search(dn=obj.dn, scope=0, mode='rw')[0] as obj: obj['blacklist'][int(tag)]=bl obj.save() # On s'en va en mettant à jour dans la continuation la valeur de obj raise Continue(self_cont(bl=None, obj=obj)) # On propage les Continue except self.error_to_raise: raise # En cas d'une autre erreur, on l'affiche et on retourne au menu d'édition except (Exception, ldap.OBJECT_CLASS_VIOLATION) as e: self.dialog.msgbox("%s" % unicode_of_Error(e), timeout=self.timeout, title="Erreur rencontrée", width=73) raise Continue(self_cont) else: raise Continue(self_cont(bl=None, obj=obj)) def edit_boolean_attributs(self, obj, attribs, title, update_obj, cont, values={}): """ Permet d'éditer des attributs booléen de l'objet obj listé dans attribs. update_obj est le nom du paramètre à mettre à jour dans cont pour passer l'objet modifié """ # Dictionnaire décrivant quelle est la valeur booléenne à donner à l'absence de l'attribut missing = { 'default' : False, # par défaut, on dit que c'est False attributs.dnsIpv6 : True # pour dnsIpv6, c'est True } choices = [(a.ldap_name, a.legend, 1 if values.get(a.ldap_name, obj[a.ldap_name][0] if obj[a.ldap_name] else missing.get(a, missing['default'])) else 0) for a in attribs] (code, output) = self.dialog.checklist("Activier ou désactiver les propriétés suivantes", height=0, width=0, timeout=self.timeout, list_height=7, choices=choices, title=title) def todo(values, obj, attribs, cont): # On met à jour chaque attribut si sa valeur à changé with self.conn.search(dn=obj.dn, scope=0, mode='rw')[0] as obj: for a in attribs: if obj[a.ldap_name] and obj[a.ldap_name][0] != values[a.ldap_name]: obj[a.ldap_name]=values[a.ldap_name] elif not obj[a.ldap_name] and missing.get(a, missing['default']) != values[a.ldap_name]: obj[a.ldap_name]=values[a.ldap_name] obj.save() # On s'en va en mettant à jour dans la continuation la valeur de obj raise Continue(cont(**{update_obj:obj})) # On transforme la liste des cases dialog cochée en dictionnnaire values = dict((a.ldap_name, a.ldap_name in output) for a in attribs) # Une continuation que l'on suivra si quelque chose se passe mal retry_cont = TailCall(self.edit_boolean_attributs, obj=obj, update_obj=update_obj, attribs=attribs, title=title, cont=cont, values=values) return self.handle_dialog_result( code=code, output=output, cancel_cont=cont, error_cont=retry_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [values, obj, attribs, cont])] ) def edit_attributs(self, obj, attr, title, update_obj, cont, tag=None, values=None): """ Permet d'éditer la liste d'attribut attr de l'objet obj. update_obj est le nom du paramètre à mettre à jour dans cont pour passer l'objet modifié """ # Il n'y a pas inputmenu dans la lib dialog, du coup, on traite les arguments à la main. # Ça reste relativement acceptable comme on utilise la fonction de la lib pour appeler dialog def box(values, default_tag): cmd = ['--inputmenu', "Édition de l'attribut %s :" % attr, "0", "0", "20"] index=0 for value in values: cmd.extend([str(index), str(value)]) index+=1 cmd.extend(['new', '']) (code, output) = self.dialog._perform(*(cmd,), timeout=self.timeout, title=title, default_item=str(default_tag)) if code == self.dialog.DIALOG_EXTRA: output = output.split(' ', 2)[1:] else: output = '' return (code, output) def todo_extra(output, values, retry_cont): tag, value = output if tag == 'new': if value: values.append(value) elif value == '': values.pop(int(tag)) else: values[int(tag)] = value raise Continue(retry_cont(values=values, tag=tag)) def todo(obj, values, cont): with self.conn.search(dn=obj.dn, scope=0, mode='rw')[0] as obj: obj[attr] = [unicode(value, 'utf-8') for value in values] obj.save() raise Continue(cont(**{update_obj:obj})) if values is None: values = [str(a) for a in obj[attr]] retry_cont = TailCall(self.edit_attributs, obj=obj, attr=attr, title=title, update_obj=update_obj, cont=cont, values=values) (code, output) = self.handle_dialog(cont, box, values, tag) return self.handle_dialog_result( code=code, output=output, cancel_cont=cont, error_cont=retry_cont, codes_todo=[ ([self.dialog.DIALOG_OK], todo, [obj, values, cont]), ([self.dialog.DIALOG_EXTRA], todo_extra, [output, values, retry_cont]), ] ) def search(self, objectClassS, title, values={}, cont=None, disable_field=[]): """ Rechercher des adhérents ou des machines dans la base ldap retourne le tuple (code de retour dialog, valeurs entrée par l'utilisateur, liste d'objets trouvés) La fonction est découpé en trois partie : * affichage dialog et récupération du résultat * construction de filtres de recherche ldap et recherches ldap * filtre sur les résultats des recherches ldap """ select_dict = { #label attribut ldap search for substring param dialog: line col valeur input-line icol len max-chars 'Nom' : {'ldap':'nom', 'sub':True, 'params' : [ 1, 1, values.get('nom', ""), 1, 13, 20, 20]}, 'Prénom' : {'ldap':'prenom', 'sub':True, 'params' : [ 2, 1, values.get('prenom', ""), 2, 13, 20, 20]}, 'Téléphone' : {'ldap':'tel', 'sub':True, 'params' : [ 3, 1, values.get('tel', ""), 3, 13, 10, 00]}, 'Chambre' : {'ldap':'chbre','sub':True, 'params' : [ 4, 1, values.get('chbre',""), 4, 13, 05, 00]}, 'aid' : {'ldap' : 'aid', 'sub':False, 'params' : [ 5, 1, values.get('aid',""), 5, 13, 05, 05]}, 'mail' : {'ldap' : 'mail', 'sub':True, 'params' : [ 6, 1, values.get('mail',""), 6, 13, 40, 00]}, # seconde colone 'Machine' : {'ldap' : '*', 'sub':True, 'params' : [1, 35, "", 1, 43, 0, 0]}, 'Host' : {'ldap' : 'host', 'sub':True, 'params' : [2, 37, values.get('host',""), 2, 43, 17, 17]}, 'Mac' : {'ldap' : 'macAddress', 'sub':False, 'params' : [3, 37, values.get('macAddress',""), 3, 43, 17, 17]}, 'IP' : {'ldap' : 'ipHostNumber', 'sub':False,'params' : [4, 37, values.get('ipHostNumber',""), 4, 43, 15, 15]}, 'mid' : {'ldap' : 'mid', 'sub':False, 'params' : [5, 37, values.get('mid',""), 5, 43, 5, 5]}, } # On a besoin de l'ordre pour récupérer les valeurs ensuite select_adherent = ['Nom', 'Prénom', 'Téléphone', 'Chambre', 'aid', 'mail'] select_machine = ['Host', 'Mac', 'IP', 'mid'] if 'club' in objectClassS and not 'adherent' in objectClassS: select_dict['cid']=select_dict['aid'] select_dict['cid']['ldap']='cid' select_dict['cid']['params'][2]=values.get('cid', "") select_adherent[select_adherent.index('aid')]='cid' def box(): # On met les argument à dialog à la main ici, sinon, c'est difficile de choisir comment mettre une seconde colone cmd = ["--mixedform", "Entrez vos paramètres de recherche", '0', '0', '0'] for key in select_adherent: cmd.extend(['%s :' % key] + [str(e) for e in select_dict[key]['params']] + ['2' if key in disable_field else '0']) cmd.extend(['Machine :'] + [str(e) for e in select_dict['Machine']['params']] + ['2']) for key in select_machine: cmd.extend(['%s :' % key] + [str(e) for e in select_dict[key]['params']] + ['2' if key in disable_field else '0']) cmd.extend(["Les champs vides sont ignorés.", '7', '1', "", '0', '0', '0', '0', '2' ]) # On utilise quand même la fonction de la bibliothèques pour passer les arguments (code, output) = self.dialog._perform(*(cmd,), timeout=self.timeout, title=title, backtitle="Entrez vos paramètres de recherche") if output: return (code, output.split('\n')[:-1]) else: # empty selection return (code, []) (code, dialog_values) = self.handle_dialog(cont, box) # Si il a appuyé sur annuler ou sur escape, on saute sur la continuation if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC): raise Continue(cont) else: # Transformation de la liste des valeures entrée en dictionnnaire dialog_values = dict(zip(select_adherent + select_machine, dialog_values)) ldap_values = dict([(select_dict[key]['ldap'], value) for key, value in dialog_values.items()]) # Construction des filtres ldap pour les adhérents et les machines filter_adherent = [] filter_machine = [] for (key, value) in dialog_values.items(): if value: if key in select_adherent: filter_adherent.append((u"(%s=*%s*)" if select_dict[key]['sub'] and not '*' in value else u"(%s=%s)") % (select_dict[key]['ldap'], unicode(value, 'utf-8'))) elif key in select_machine: filter_machine.append((u"(%s=*%s*)" if select_dict[key]['sub'] and not '*' in value else u"(%s=%s)") % (select_dict[key]['ldap'], unicode(value, 'utf-8'))) if filter_adherent: filter_adherent=u"(&%s)" % "".join(filter_adherent) if filter_machine: filter_machine=u"(&%s)" % "".join(filter_machine) # Récupération des adhérents et des machines adherents=self.conn.search(filter_adherent) if filter_adherent else [] machines=self.conn.search(filter_machine) if filter_machine else [] # Filtrage des machines en fonction des adhérents if filter_adherent: if filter_machine: # Si on filtre sur des adhérent et des machines, on calcule l'intersection adherents_dn = set([a.dn for a in adherents]) machines_f = [m for m in machines if m.parent_dn in adherents_dn] else: # Sinon on filtre seulement sur les adhérents, récupère les machines des adhérents trouvés machines_f = [m for a in adherents for m in a.machines()] else: # Sinon si on filtre seulement sur des machines machines_f = machines # Filtrage des adhérents en fonction des machines if filter_machine: if filter_adherent: # Si on filtre sur des adhérents et des machines, on calcule l'intersection machines_dn = set([m.parent_dn for m in machines]) adherents_f = [a for a in adherents if a.dn in machines_dn] else: # Sinon on récupères les proprios des machines trouvées adherents_f = [m.proprio() for m in machines] else: # Sinon si on filtre seulement sur des adhérents adherents_f = adherents # On filtre sur les objectClassS return ldap_values, [ o for objectClass in objectClassS for o in machines_f+adherents_f if objectClass in o['objectClass'] ] @tailcaller def select_one(self, items, title, text="Que souhaitez vous faire ?", default_item=None, cont=None): """Fait selectionner un item parmis une liste d'items à l'utisateur""" def box(items, default_item): choices=[] olist={} count = 0 # On sépare les item d'items en fonction de leur type for o in items: olist[o.__class__] = olist.get(o.__class__, []) + [o] classes = olist.keys() classes.sort() default_tag = items.index(default_item) if default_item in items else default_item # On se débrouille pour faire corresponde l'ordre d'affichache des objets # et leur ordre dans la liste items. On donne la largeur de l'affichage à la main # pour prendre en compte la largeur du widget dialog items=[] items_id = {} (line, col) = get_screen_size() for c in classes: items.extend(olist[c]) items_s = printing.sprint_list(olist[c], col-20).encode('utf-8').split('\n') choices.append(("", str(items_s[0]))) next=1 if items_s[next:]: choices.append(("", str(items_s[next]))) next+=1 for i in items_s[next:]: choices.append((str(count), str(i))) count+=1 # On laisse une ligne vide pour séparer les listes d'objets de type différent choices.append(("", "")) # On supprime la dernière ligne qui est vide del(choices[-1]) return self.dialog.menu( text, width=0, height=0, menu_height=0, timeout=self.timeout, item_help=0, default_item=str(default_tag), title=title, scrollbar=True, choices=choices, colors=True) def todo(tag, items, title, cont, retry_cont): # Si l'utilisateur n'a pas choisis une ligne correspondant à quelque chose if not tag: self.dialog.msgbox("Merci de choisir l'un des item de la liste ou d'annuler", timeout=self.timeout, title="Sélection", width=0, height=0) raise Continue(retry_cont) # Sinon on retourne l'item choisis elif self.confirm_item(items[int(tag)], title): return items[int(tag)] else: raise Continue(cont) (code, tag) = self.handle_dialog(cont, box, items, default_item) retry_cont = TailCall(self.select_one, items=items, title=title, default_item=tag, cont=cont) return self.handle_dialog_result( code=code, output=tag, cancel_cont=cont, error_cont=retry_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [tag, items, title, cont, retry_cont])] ) @tailcaller def get_comment(self, title, text, cont, init='', force=False): """ Fait entrer à l'utilisateur un commentaire et le retourne. Si force est à True, on oblige le commentaire à être non vide """ (code, output) = self.dialog.inputbox(text=text, title=title, timeout=self.timeout, init=init) retry_cont = TailCall(self.get_comment, title=title, text=text, cont=cont, force=force) def todo(output, force, title, retry_cont): if force and not output: self.dialog.msgbox("Entrée vide, merci d'indiquer quelque chose", timeout=self.timeout, title=title) raise Continue(retry_cont) else: return unicode(output, 'utf-8') return self.handle_dialog_result( code=code, output=output, cancel_cont=cont, error_cont=retry_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [output, force, title, retry_cont])] ) def confirm_item(self, item, title, defaultno=False, text='', text_bottom="", **params): """Affiche un item et demande si c'est bien celui là que l'on veux (supprimer, éditer, créer,...)""" return self.dialog.yesno( text + printing.sprint(item, **params) + text_bottom, no_collapse=True, colors=True, no_mouse=True, timeout=self.timeout, title=title, defaultno=defaultno, width=0, height=0, backtitle="Appuyez sur MAJ pour selectionner du texte" ) == self.dialog.DIALOG_OK def display_item(self, item, title, **params): """Affiche un item""" return self.dialog.msgbox( printing.sprint(item, **params), no_collapse=True, colors=True, timeout=self.timeout, title=title, width=0, height=0, backtitle="Appuyez sur MAJ pour selectionner du texte" ) # On a besoin du décorateur ici car select va retourner un item après avoir # possblement traiter plusieurs tailcall @tailcaller def select(self, objectClassS, title, values={}, cont=None, disable_field=[]): """Permet de choisir un objet adhérent ou machine dans la base ldap""" try: # On fait effectuer une recherche à l'utilisateur values, items = self.search(objectClassS, title, values, cont=cont, disable_field=disable_field) # S'il n'y a pas de résultas, on recommence if not items: self.dialog.msgbox("Aucun Résultat", timeout=self.timeout, title="Recherche", width=0, height=0) raise Continue(TailCall(self.select, objectClassS=objectClassS, title=title, values=values, disable_field=disable_field, cont=cont)) # S'il y a plusieurs résultats elif len(items)>1: # On en fait choisir un, si c'est une continuation qui est renvoyé, elle est gérée par select return self.select_one(items, title, cont=TailCall(self.select, objectClassS=objectClassS, title=title, values=values, disable_field=disable_field, cont=cont)) # S'il y a exactement 1 résultat à la recherche, on fait confirmer son choix à l'utilisateur elif len(items) == 1: item=items[0] # On fait confirmer son choix à l'utilisateur if self.confirm_item(item, title): return item else: raise Continue(TailCall(self.select, objectClassS=objectClassS, title=title, values=values, disable_field=disable_field, cont=cont)) except self.error_to_raise: raise except Exception as e: self.dialog.msgbox("%r" % e, timeout=self.timeout, title="Erreur rencontrée", width=0, height=0) raise Continue(TailCall(self.select, objectClassS=objectClassS, title=title, values=values, disable_field=disable_field, cont=cont)) def machine_information(self, cont, machine=None, objectClass=None, proprio=None, realm=None, fields_values=None): """ Permet de modifier une machine si elle est fournit par le paramètre machine sinon, crée une machine à partir de proprio, objectClass et realm. Si on ne fait qu'éditer une machine, proprio, objectClass et realm sont ignoré D'une machinère générale, il faudrait mettre ici tous les attributs single value et les multivalué que l'on peut simplement représenter de façon textuelle avec un séparateur. Pour le moment il y a : * host * macAddress * ipHostNumber * port(TCP|UDP)(in|out) """ a = attributs # Quel sont les attributs ldap dont on veut afficher et la taille du champs d'édition correspondant to_display = [(a.host, 30), (a.macAddress, 17), (a.ipHostNumber, 15), (a.portTCPout, 50), (a.portTCPin, 50), (a.portUDPout, 50), (a.portUDPin, 50) ] # Quel séparateur on utilise pour les champs multivalué separateur = ' ' def box(): if machine: attrs = dict((k,[str(a) for a in at]) for k,at in machine.items()) else: attrs = {} fields = [("%s :" % a.legend, separateur.join(attrs.get(a.ldap_name, [a.default] if a.default else [])), l+1, l) for a,l in to_display] return self.dialog.form( text="", timeout=self.timeout, height=0, width=0, form_height=0, fields=fields_values if fields_values else fields, title="Paramètres machine", backtitle="Gestion des machines du Crans") def check_host(host, objectClass): # Si c'est une machine wifi, host doit finir par wifi.crans.org if "machineWifi" == objectClass or 'borneWifi' == objectClass: hostend = ".wifi.crans.org" # Si c'est une machine wifi, host doit finir par crans.org elif "machineFixe" == objectClass: hostend = ".crans.org" # Si l'object class est machineCrans, pas de vérification elif "machineCrans" == objectClass: return host # Sinon, libre à chachun d'ajouter d'autres objectClass ou de filtrer # plus finement fonction des droits de self.conn.droits else: raise ValueError("La machine n'est ni une machine fixe, ni une machine wifi mais %s ?!?" % objectClass) if not host.endswith(hostend) and not '.' in host: host = "%s.wifi.crans.org" % host elif host.endswith(hostend) and '.' in host[:-len(hostend)]: raise ValueError("Nom d'hôte invalide, devrait finir par %s et être sans point dans la première partie" % hostend) elif not host.endswith(hostend) and '.' in host: raise ValueError("Nom d'hôte invalide, devrait finir par %s et être sans point dans la première partie" % hostend) return host def modif_machine(machine, attrs): with self.conn.search(dn=machine.dn, scope=0, mode='rw')[0] as machine: for (key, values) in attrs.items(): machine[key]=values machine.validate_changes() machine.save() return machine def create_machine(proprio, realm, attrs): # Dans ce cas, on a besoin d'un proprio et d'un realm pour déterminer le rid if proprio is None or realm is None: raise EnvironmentError("On essaye de créer une machine mais proprio ou realm vaut None") ldif = { 'macAddress': ['%s' % attrs['macAddress']], 'host': ['%s' % attrs['host']] } with self.conn.newMachine(proprio.dn, realm, ldif) as machine: for (key, values) in attrs.items(): machine[key]=values if attributs.ipsec in machine.attribs: machine[attributs.ipsec.ldap_name]=attributs.ipsec.default machine.validate_changes() if self.confirm_item(machine, "Voulez vous vraiement créer cette machine ?"): machine.create() self.display_item(machine, "La machine à bien été créée", ipsec=True) return machine else: raise Continue(cont) def todo(to_display, tags, objectClass, machine, proprio, realm, separateur, cont): attrs = {} # On traite les valeurs reçues for ((a,l),values) in zip(to_display, tags): values = unicode(values, 'utf-8') # Si le champs n'est pas single value, on utilise separateur pour découper # et on ne garde que les valeurs non vides if not a.singlevalue: values = [v for v in values.split(separateur) if v] # Pour host, on fait quelques vérification de syntaxe if a.ldap_name == 'host': attrs[a.ldap_name]=check_host(values, objectClass) else: attrs[a.ldap_name]=values # Soit on édite une machine existante if machine: machine = modif_machine(machine, attrs) # Soit on crée une nouvelle machine else: machine = create_machine(proprio, realm, attrs) raise Continue(cont(machine=machine)) if machine: objectClass = machine["objectClass"][0] (code, tags) = self.handle_dialog(cont, box) # On prépare les fiels à afficher à l'utilisateur si une erreure à lieu # pendant le traitement des donnée (on n'éfface pas ce qui a déjà été entré # c'est au cableur de corriger ou d'annuler fields_values = [("%s :" % a.legend, values, l) for ((a,l),values) in zip(to_display, tags)] retry_cont = TailCall(self.machine_information, machine=machine, cont=cont, objectClass=objectClass, proprio=proprio, realm=realm, fields_values=fields_values) return self.handle_dialog_result( code=code, output=tags, cancel_cont=cont, error_cont=retry_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [to_display, tags, objectClass, machine, proprio, realm, separateur, cont])] ) def modif_machine_blacklist(self, machine, cont): """Raccourci vers edit_blacklist spécifique aux machines""" return self.edit_blacklist(obj=machine, title="Éditions des blacklist de la machine %s" % machine['host'][0], update_obj='machine', cont=cont) def certificat_tlsa(self, certificat, cont, values=None): """Menu d'éditions des paramètres TLSA d'un certificat""" separateur = ' ' form = { 'Type de certificat' : {'ldap_name' : 'certificatUsage', 'text':"".join(str(s) for s in certificat.get('certificatUsage', [])), 'len':1}, 'Type de correspondance' : {'ldap_name' : 'matchingType', 'text':"".join(str(s) for s in certificat.get('matchingType', [])), 'len':1}, 'Ports TCP' : {'ldap_name' : 'portTCPin', 'text':separateur.join(str(p) for p in certificat.get('portTCPin', [])), 'len':30}, 'Ports UDP' : {'ldap_name' : 'portUDPin', 'text':separateur.join(str(p) for p in certificat.get('portUDPin', [])), 'len':30}, } form_order = ['Type de certificat', 'Type de correspondance', 'Ports TCP', 'Ports UDP'] def box(fields_values=None): fields = [("%s : " % k, form[k]['text'], form[k]['len'] + 1, form[k]['len']) for k in form_order] return self.dialog.form( text="""Type de certificat : Type de correspondance : * 0 - CA pinning * 0 - certificat entier * 1 - Cert pinning * 1 - sha256 * 2 - CA auto signé * 2 - sha512 * 3 - Cert autosigné""", no_collapse=True, height=0, width=0, form_height=0, timeout=self.timeout, fields=fields_values if fields_values else fields, title="Paramètres TLS d'un certificat de la machine %s" % certificat.machine()['host'][0], backtitle="Gestion des certificats des machines du Crans") def todo(form, values, certificat, cont): if not values['certificatUsage'] in ['0', '1', '2', '3']: raise ValueError("""Type de certificat invalide : les valeurs valident sont : * 0 pour du CA pinning (le certificat doit être une autorité de certification valide) * 1 pour du certificat pinning (le certificat doit déjà être validé par les navigateur) * 2 pour ajouter un CA (pour les autorité de certification autosigné) * 3 pour les certificats autosigné""" ) if not values['matchingType'] in ['0', '1', '2']: raise ValueError("""Type de correspondance invalide : les valeurs valident sont : * 0 le certificat sera mis entièrement dans le dns * 1 le sha256 du certificat sera mis dans le dns * 2 le sha512 du certificat sera mis dans le dns""" ) with self.conn.search(dn=certificat.dn, scope=0, mode='rw')[0] as certificat: if "TLSACert" in certificat['objectClass']: certificat['certificatUsage'] = unicode(values['certificatUsage']) certificat['matchingType'] = unicode(values['matchingType']) else: certificat.tlsa(values['certificatUsage'], values['matchingType']) certificat['portTCPin'] = [unicode(s, 'utf-8') for s in values['portTCPin'].split(separateur) if s] certificat['portUDPin'] = [unicode(s, 'utf-8') for s in values['portUDPin'].split(separateur) if s] certificat.save() raise Continue(cont(certificat=certificat)) (code, output) = self.handle_dialog(cont, box, values) values = dict(zip([form[k]['ldap_name'] for k in form_order], output)) fields_values = [("%s : " % k, values.get(form[k]['ldap_name'], ""), form[k]['len'] + 1, form[k]['len']) for k in form_order] self_cont=TailCall(self.certificat_tlsa, certificat=certificat, cont=cont, values=fields_values) return self.handle_dialog_result( code=code, output=output, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [form, values, certificat, cont])] ) def create_certificat(self, cont, machine=None, certificat=None): """Permet d'ajouter un certificat à une machine à partir du PEM du certificat""" if machine is None and certificat is None: raise EnvironmentError("Il faut fournir au moins une machine ou un certificat") # input multiline en utilisant un editbox def box(): fp, path = tempfile.mkstemp() os.close(fp) cmd = ['--editbox', path, "0", "0"] (code, output) = self.dialog._perform(*(cmd,), no_mouse=True, # On désactive la sourie sinon dialog segfault si on clic backtitle="Appuyez sur CTRL+MAJ+V pour coller", timeout=self.timeout, title="Création d'un certificat, entrez le PEM du certificat") os.remove(path) if code == self.dialog.DIALOG_OK: return code, output else: return code, None def todo(machine, certificat, cont): if certificat: with self.conn.search(dn=certificat.dn, scope=0, mode='rw')[0] as certificat: certificat['certificat'] = unicode(pem.strip(), 'utf-8') certificat.save() else: with self.conn.newCertificat(machine.dn, {}) as certificat: certificat['certificat'] = unicode(pem.strip(), 'utf-8') certificat.create() raise Continue(cont(certificat=certificat, machine=certificat.machine())) (code, pem) = self.handle_dialog(cont, box) self_cont = TailCall(self.create_certificat, machine=machine, certificat=certificat, cont=cont) return self.handle_dialog_result( code=code, output=pem, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [machine, certificat, cont])] ) @tailcaller def get_password(self, cont, confirm=True, title="Choix d'un mot de passe", **kwargs): """ Affiche une série d'inpuxbox pour faire entrer un mot de passe puis le retourne, si confirm=True, il y a une confirmation du mot de passe de demandée """ def todo(self_cont, cont): (code, pass1) = self.dialog.passwordbox("Entrez un mot de passe", title=title, timeout=self.timeout, **kwargs) if code != self.dialog.DIALOG_OK: raise Continue(cont) elif not pass1: raise ValueError("Mot de pass vide !") if confirm: (code, pass2) = self.dialog.passwordbox("Comfirmer le mot de passe", timeout=self.timeout, title=title, **kwargs) if code != self.dialog.DIALOG_OK: raise Continue(self_cont) if pass1 != pass2: raise ValueError("Les deux mots de passe ne concordent pas") return pass1 self_cont = TailCall(self.get_password, cont=cont) return self.handle_dialog_result( code=self.dialog.DIALOG_OK, output="", cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [self_cont, cont])] ) def create_privatekey(self, cont, machine=None, certificat=None, imp=False, size=4096): """Permet de générer ou importer une clef privée à une machine""" if machine is None and certificat is None: raise EnvironmentError("Il faut fournir au moins une machine ou un certificat") # input multiline en utilisant un editbox def box(): fp, path = tempfile.mkstemp() os.close(fp) cmd = ['--editbox', path, "0", "0"] (code, output) = self.dialog._perform(*(cmd,), no_mouse=True, # On désactive la sourie sinon dialog segfault si on clic backtitle="Appuyez sur CTRL+MAJ+V pour coller", timeout=self.timeout, title="Création d'un certificat, entrez le PEM du certificat") os.remove(path) if code == self.dialog.DIALOG_OK: return code, output else: return code, None def todo(machine, certificat, pem, imp, size, cont): if not imp: if not machine: machine=certificat.machine() if "machineCrans" in machine['objectClass']: passphrase = secrets.get('privatekey_passphrase') else: self.dialog.msgbox("Vous aller être inviter à entrez un mot de passe. Ce mot de passe est utilisé pour chiffrer la clef privée qui va être générée dans la base de donnée du crans.\n\nCe mot de passe n'est pas conservé, sous quelque forme que se soit par le crans.\nAussi, en cas de perte, la clef privée deviendrait inutilisable.\n Pensez à le sauvegarder quelque part", title="Génération d'une clée privée", width=70, timeout=self.timeout, height=12) passphrase = self.get_password(cont) self.dialog.infobox("Génération d'une clef privée RSA de taille %s en cours.\nMerci de patienter" % size) pem = crypto.PKey() pem.generate_key(crypto.TYPE_RSA, size) pem = crypto.dump_privatekey(crypto.FILETYPE_PEM, pem, "des3", passphrase) elif not pem.startswith("-----BEGIN ENCRYPTED PRIVATE KEY-----"): raise ValueError("On n'accepte que des clef chiffrée PKCS#8 en PEM. Donc la clef doit commencer par -----BEGIN ENCRYPTED PRIVATE KEY-----") if certificat: if "privatekey" in certificat: raise ValueError("Il y a déjà une clef privée, merci d'annuler") with self.conn.search(dn=certificat.dn, scope=0, mode='rw')[0] as certificat: certificat.private(pem, encrypted=True) certificat.save() self.dialog.msgbox("Clef privée bien ajouté", timeout=self.timeout, title="Ajout d'une clef privée") raise Continue(cont(certificat=certificat, machine=certificat.machine())) else: with self.conn.newCertificat(machine.dn, {}) as certificat: certificat['hostCert']=unicode(machine['host'][0]) certificat.private(pem, encrypted=True) certificat.create() self.dialog.msgbox("Clef privée créée avec succès", timeout=self.timeout, title="Création d'une clef privée") raise Continue(cont(certificat=certificat, machine=certificat.machine())) if imp: (code, pem) = self.handle_dialog(cont, box) else: (code, pem) = (self.dialog.DIALOG_OK, "") self_cont = TailCall(self.create_privatekey, machine=machine, certificat=certificat, cont=cont, imp=imp, size=size) return self.handle_dialog_result( code=code, output=pem, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [machine, certificat, pem, imp, size, cont])] ) def delete_certificat(self, certificat, cont): """Supprime un certificat""" def todo(certificat, cont): if self.confirm_item(item=certificat, title="Voulez vous vraiement supprimer le certificat ?"): with self.conn.search(dn=certificat.dn, scope=0, mode='rw')[0] as certificat: certificat.delete() self.dialog.msgbox("Le certificat a bien été supprimé", timeout=self.timeout, title="Suppression d'un certificat") raise Continue(cont(certificat=None, machine=certificat.machine(refresh=True))) else: raise Continue(cont(certificat=certificat)) return self.handle_dialog_result( code=self.dialog.DIALOG_OK, output="", cancel_cont=cont, error_cont=TailCall(self.delete_certificat, certificat=certificat, cont=cont), codes_todo=[([self.dialog.DIALOG_OK], todo, [certificat, cont])] ) def gen_csr(self, certificat, cont): """Permet de générer un csr à partir de la clef privée du certificat""" def todo(certificat, self_cont, cont): if certificat['encrypted']: if "machineCrans" in certificat.machine()["objectClass"]: passphrase = secrets.get('privatekey_passphrase') else: self.dialog.msgbox("Mercie de fournir le mot de passe chiffrant la clef privée.\nIl a été choisis lors de la création de la clef.", title="Génération d'un CSR", width=70, height=10, timeout=self.timeout) passphrase = self.get_password(cont, confirm=False) else: passphrase = None try: if passphrase: pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, str(certificat['privatekey'][0]), passphrase) else: pkey = crypto.load_privatekey(crypto.FILETYPE_PEM, str(certificat['privatekey'][0])) except crypto.Error as e: if len(e.message) > 2 and len(e.message[2]) > 2 and e.message[2][2] == 'bad password read': self.dialog.msgbox("Mauvais mot de passe", timeout=self.timeout) raise Continue(self_cont) else: raise req = createCertRequest(pkey, digest="sha1", subjectAltName=[str(host) for host in certificat['hostCert'][1:]], C=u"FR", ST=u"Ile de France", L=u"Cachan", O=u"Association Cachan Réseaux A Normal SUP (C.R.A.N.S)", OU=u"Crans", CN=unicode(certificat['hostCert'][0]), ) csr = crypto.dump_certificate_request(crypto.FILETYPE_PEM, req) with self.conn.search(dn=certificat.dn, scope=0, mode='rw')[0] as certificat: certificat['csr']=unicode(csr) certificat.save() self.handle_dialog(cont, box, csr) if self.dialog.yesno("Remplacer le certificat actuel ?", timeout=self.timeout) == self.dialog.DIALOG_OK: return self.create_certificat(certificat=certificat, cont=cont(certificat=certificat)) else: raise Continue(cont(certificat=certificat)) self_cont = TailCall(self.gen_csr, certificat=certificat, cont=cont) return self.handle_dialog_result( code=self.dialog.DIALOG_OK, output="", cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [certificat, self_cont, cont])] ) def get_certificat(self, certificat, cont, privatekey=False, csr=False): """Permet d'afficher le certificat courant""" def box(text): fp, path = tempfile.mkstemp() os.write(fp, text) os.close(fp) self.dialog.textbox(filename=path, height=0, width=0, backtitle="Appuyez sur CTRL+MAJ+V pour coller", title="Récupération d'un certificat", no_mouse=True, timeout=self.timeout,) os.remove(path) return if privatekey: self.handle_dialog(cont, box, unicode(certificat['privatekey'][0])) elif csr: self.handle_dialog(cont, box, unicode(certificat['csr'][0])) else: self.handle_dialog(cont, box, unicode(ssl.DER_cert_to_PEM_cert(str(certificat['certificat'][0])))) raise Continue(cont) def create_csr(self, cont, machine=None, certificat=None): """Permet d'ajouter un csr à une machine à partir du PEM du csr""" if machine is None and certificat is None: raise EnvironmentError("Il faut fournir au moins une machine ou un certificat") # input multiline en utilisant un editbox def box(): fp, path = tempfile.mkstemp() os.close(fp) cmd = ['--editbox', path, "0", "0"] (code, output) = self.dialog._perform(*(cmd,), no_mouse=True, # On désactive la sourie sinon dialog segfault si on clic backtitle="Appuyez sur CTRL+MAJ+V pour coller", timeout=self.timeout, title="Création d'un certificat, entrez le PEM du certificat") os.remove(path) if code == self.dialog.DIALOG_OK: return code, output else: return code, None def todo(machine, certificat, pem, cont): if certificat: with self.conn.search(dn=certificat.dn, scope=0, mode='rw')[0] as certificat: certificat['csr'] = unicode(pem.strip(), 'utf-8') certificat.save() else: with self.conn.newCertificat(machine.dn, {}) as certificat: certificat['hostCert']=unicode(machine['host'][0]) certificat['csr'] = unicode(pem.strip(), 'utf-8') certificat.create() raise Continue(cont(certificat=certificat, machine=certificat.machine())) (code, pem) = self.handle_dialog(cont, box) self_cont = TailCall(self.create_csr, machine=machine, certificat=certificat, cont=cont) return self.handle_dialog_result( code=code, output=pem, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [machine, certificat, pem, cont])] ) def modif_machine_certificat(self, machine, cont, tag=None, certificat=None): """Permet l'édition d'un certificat d'une machine""" self_cont = TailCall(self.modif_machine_certificat, machine=machine, cont=cont, certificat=certificat) if certificat is None: certificat_index = self.edit_certificat_select(machine=machine, title="Modification des certificats de %s" % machine['host'][0], cont=cont) if certificat_index == 'new': raise Continue(TailCall(self.create_certificat, machine=machine, cont=self_cont)) elif certificat_index == 'priv': raise Continue(TailCall(self.create_privatekey, machine=machine, cont=self_cont)) elif certificat_index == 'csr': raise Continue(TailCall(self.create_csr, machine=machine, cont=self_cont)) certificat = machine.certificats()[certificat_index] a = attributs menu_droits = { 'Hostname':[a.parent, a.nounou], 'AddPrivateKey':[a.parent, a.nounou], 'AddCertificate':[a.parent, a.nounou], 'SetCertificate':[a.parent, a.nounou], 'TLSA':[a.parent, a.nounou], 'Autre':[a.nounou], 'GetPriv':[a.parent, a.nounou], 'GetCert':[a.parent, a.nounou, a.cableur], 'GetCSR':[a.parent, a.nounou, a.cableur], 'GenCSR':[a.parent, a.nounou], 'Remarque':[a.parent, a.nounou, a.cableur], 'Supprimer':[a.parent, a.nounou], } menu = { 'Hostname' : {'text':"Noms d'hôte utilisant le certificat", "help":'Il doivent être inclus dans les host et hostAlias de la machine parente', "attribut":attributs.hostCert}, 'AddPrivateKey' : {'text': 'Ajouter la clef privée', 'help':'La clef doit être obligatoirement chiffrée avant envoi', "callback":TailCall(self.create_privatekey, imp=True)}, 'AddCertificate' : {'text': 'Ajouter un certificat X509', 'help':'', "callback":self.create_certificat}, 'SetCertificate' : {'text': 'Remplacer le certificat X509', 'help':'', "callback":self.create_certificat}, 'TLSA' : {'text':"Paramètres pour les champs dns TLSA", 'help':'Permet de configurer DANE pour le certificat X509', "callback":self.certificat_tlsa}, 'Autre': {'text' : "Modifier les attribut booléen comme revocked", 'help':'', "callback":self.modif_certificat_boolean}, 'GetPriv' : {'text' : 'Récupérer la clef privée', 'help':"Affiche la clef privée telle qu'elle est dans la base de donnée, c'est à dire chiffrée", 'callback':TailCall(self.get_certificat, privatekey=True)}, 'GetCert' : {'text' : 'Récupérer le certificat', 'help':"Affiche le certificat au format PEM", 'callback':self.get_certificat}, 'GetCSR' : {'text' : 'Récupérer la requête de signature de certificat', 'help':"Affiche le CSR au format PEM", 'callback':TailCall(self.get_certificat, csr=True)}, 'GenCSR' : {'text' : 'Générer un CSR, puis remplacer le certificat', 'help':'Généré à partir de la clef privée. Les noms (CN et subjectAltName) sont pris à partir de Hostname (attribut hostCert)', "callback":self.gen_csr}, 'Remarque' : {'text': 'Mettre des remarques', 'help':'La première apparait dans la liste des certificats', 'attribut':attributs.info}, 'Supprimer' : {'text' : "Supprimer le certificat", 'help':'', "callback":self.delete_certificat}, } if "privateKey" in certificat["objectClass"]: menu menu_order = ['Hostname'] if not "privateKey" in certificat['objectClass']: menu_order.extend(['AddPrivateKey', 'SetCertificate']) if not "x509Cert" in certificat['objectClass']: menu_order.extend([ 'AddCertificate']) if "x509Cert" in certificat['objectClass']: menu_order.extend(['TLSA', 'Autre', 'GetCert']) if certificat['csr']: menu_order.extend(['GetCSR']) if "privateKey" in certificat['objectClass']: if attributs.nounou in self.conn.droits or machine.dn.startswith(self.conn.dn): menu_order.extend(['GetPriv']) menu_order.extend(['GenCSR']) menu_order.extend(['Remarque', 'Supprimer']) def box(default_item=None): text="Certificat de %s, xid=%s :\n" % (certificat['hostCert'][0], certificat['xid'][0]) if "x509Cert" in certificat['objectClass']: text += " * Certificat N°0x%X émis par %s, valable du %s au %s\n" % ( int(str(certificat['serialNumber'][0])), certificat['issuerCN'][0], time.strftime("%d/%m/%Y", time.localtime(int(certificat['start'][0]))), time.strftime("%d/%m/%Y", time.localtime(int(certificat['end'][0]))) ) if "privateKey" in certificat['objectClass']: text += " * Clef privée\n" if certificat['csr']: text += " * Requête de signature de certificat\n" if certificat['info']: text += str(certificat['info'][0]) return self.dialog.menu( text, width=0, height=0, menu_height=0, item_help=1, timeout=self.timeout, default_item=str(default_item), title="Modification des certificats de %s" % certificat.machine()['host'][0], scrollbar=True, cancel_label="Retour", backtitle=u"Vous êtes connecté en tant que %s" % self.conn.current_login, choices=[(key, menu[key]['text'], menu[key]['help']) for key in menu_order if self.has_right(menu_droits[key], certificat)]) def todo(tag, menu, certificat, self_cont): if not tag in menu_order: raise Continue(self_cont(certificat=certificat)) else: if 'callback' in menu[tag]: raise Continue(TailCall(menu[tag]['callback'], certificat=certificat, cont=self_cont(certificat=certificat, tag=tag))) elif 'attribut' in menu[tag]: raise Continue(TailCall(self.modif_certificat_attributs, certificat=certificat, cont=self_cont(certificat=certificat, tag=tag), attr=menu[tag]['attribut'].ldap_name)) else: raise EnvironmentError("Il n'y a ni champ 'attribut' ni 'callback' pour le tag %s" % tag) cancel_cont = cont(machine=machine) if certificat is None else self_cont(machine=certificat.machine(), certificat=None, tag=tag) (code, tag) = self.handle_dialog(cancel_cont, box, tag) return self.handle_dialog_result( code=code, output=tag, cancel_cont=cancel_cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [tag, menu, certificat, self_cont])] ) @tailcaller def edit_certificat_select(self, machine, title, cont): """Permet de choisir un certificat existant ou nouveau d'une machine""" a = attributs menu_droits = { 'new':[a.parent, a.nounou], 'priv':[a.parent, a.nounou], 'csr':[a.parent, a.nounou], } menu = { 'new':'Ajouter un nouveau certificat', 'priv':'Générer une nouvelle clef privée', 'csr':'Ajouter une nouvelle requête de certificat', } menu_order = ['new', 'priv', 'csr'] menu_special = ['new', 'priv', 'csr'] def box(default_item=None): index=0 choices = [] for key in menu_order: if self.has_right(menu[key], machine): choices.append((key, menu[key])) for cert in machine.certificats(): if cert['info']: item = str(cert['info'][0]) elif "x509Cert" in cert['objectClass']: item = "Emit par %s pour %s du %s au %s" % (cert['issuerCN'][0], ', '.join(str(cn) for cn in cert['hostCert']), time.strftime("%d/%m/%Y", time.localtime(int(cert['start'][0]))), time.strftime("%d/%m/%Y", time.localtime(int(cert['end'][0])))) elif "privateKey" in cert['objectClass']: item = "Clef privée de %s, xid=%s" % (cert['hostCert'][0], cert['xid'][0]) elif cert['csr']: item = "Requête de signature de certificat pour %s, xid=%s" % (cert['hostCert'][0], cert['xid'][0]) choices.append((str(index), item)) index+=1 return self.dialog.menu( "Modifier ou ajouter un certificat ?", width=0, height=0, timeout=self.timeout, menu_height=0, item_help=0, title="Modification des certificats de %s" % machine['host'][0], scrollbar=True, default_item=str(default_item), cancel_label="Retour", backtitle=u"Vous êtes connecté en tant que %s" % self.conn.current_login, choices=choices) def todo(tag): if tag in ['new', 'priv', 'csr']: return tag else: return int(tag) (code, tag) = self.handle_dialog(cont, box) retry_cont = TailCall(self.edit_certificat_select, machine=machine, title=title, cont=cont) return self.handle_dialog_result( code=code, output=tag, cancel_cont=cont(machine=machine), error_cont=retry_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [tag])] ) def modif_machine_attributs(self, machine, attr, cont): """Juste un raccourci vers edit_attributs spécifique aux machines""" return self.edit_attributs(obj=machine, update_obj='machine', attr=attr, title="Modification de la machine %s" % machine['host'][0], cont=cont) def modif_adherent_attributs(self, adherent, attr, cont): """Juste un raccourci vers edit_attributs spécifique aux adherents""" return self.edit_attributs(obj=adherent, update_obj='adherent', attr=attr, title="Modification de %s %s" % (adherent['prenom'][0], adherent['nom'][0]), cont=cont) def modif_proprio_attributs(self, proprio, attr, cont): """Juste un raccourci vers edit_attributs spécifique aux proprios""" return self.edit_attributs(obj=proprio, update_obj='proprio', attr=attr, title="Modification de %s %s" % (proprio.get('prenom', [''])[0], proprio['nom'][0]), cont=cont) def modif_certificat_attributs(self, certificat, attr, cont): """Juste un raccourci vers edit_attributs spécifique aux certificats""" return self.edit_attributs(obj=certificat, update_obj='certificat', attr=attr, title="Modification d'un certificat de la machine %s" % certificat.machine()['host'][0], cont=cont) def modif_machine_boolean(self, machine, cont): """Juste un raccourci vers edit_boolean_attributs spécifique aux machines""" a = attributs attribs = [a.dnsIpv6] return self.edit_boolean_attributs( obj=machine, attribs=attribs, title="Édition des attributs booléen de la machine %s" % machine['host'][0], update_obj='machine', cont=cont) def modif_certificat_boolean(self, certificat, cont): """Juste un raccourci vers edit_boolean_attributs spécifique aux certificats""" a = attributs attribs = [a.revocked] return self.edit_boolean_attributs( obj=certificat, attribs=attribs, title="Édition des attributs booléen d'un certificat de la machine %s" % certificat.machine()['host'][0], update_obj='certificat', cont=cont) def modif_machine(self, cont, machine=None, tag=None): """ Permet d'éditer une machine. Si fournie en paramètre on éditer en place, sinon, on en cherche une dans la base ldap """ if machine is None: machine = self.select(["machineFixe", "machineWifi", "machineCrans", "borneWifi"], "Recherche d'une machine pour modification", cont=cont) a = attributs menu_droits = { 'Information' : [a.parent, a.cableur, a.nounou], 'Autre': [a.parent, a.cableur, a.nounou], 'Blackliste':[a.cableur, a.nounou], 'Certificat': [a.parent, a.cableur, a.nounou], 'Exemption' : [a.nounou], 'Alias' : [a.parent, a.cableur, a.nounou], 'Remarques' : [a.cableur, a.nounou], 'SshKey' : [a.parent, a.nounou], 'Supprimer' : [a.parent, a.cableur, a.nounou], } menu = { 'Information' : {'text' : "Modifier le nom de machine, l'IP, adresse MAC", "callback":self.machine_information}, 'Autre' : {'text' : "Modifier les attribut booléen comme dsnIpv6", "callback":self.modif_machine_boolean}, 'Blackliste' : {'text': 'Modifier les blacklist de la machine', 'callback':self.modif_machine_blacklist}, 'Certificat' : {'text': 'Modifier les certificats de la machine', 'callback':self.modif_machine_certificat}, 'Exemption' : {'text':"Modifier la liste d'exemption d'upload de la machine", 'attribut':attributs.exempt}, 'Alias' : {'text': 'Créer ou supprimer un alias de la machine', 'attribut':attributs.hostAlias}, 'Remarques' : {'text':'Ajouter ou supprimer une remarque de la machine', 'attribut':attributs.info}, 'SshKey' : {'text':'Ajouter ou supprimer une clef ssh pour la machine', 'attribut':attributs.sshFingerprint}, 'Supprimer' : {'text':'Supprimer la machine', 'callback':self.delete_machine}, } menu_order = ['Information', 'Blackliste', 'Certificat', 'Alias', 'Exemption', 'SshKey', 'Autre', 'Remarques', 'Supprimer'] def box(default_item=None): return self.dialog.menu( "Que souhaitez vous modifier ?", width=0, height=0, menu_height=0, timeout=self.timeout, item_help=0, default_item=str(default_item), title="Modification de %s" % machine['host'][0], scrollbar=True, cancel_label="Retour", backtitle=u"Vous êtes connecté en tant que %s" % self.conn.current_login, choices=[(key, menu[key]['text']) for key in menu_order if self.has_right(menu_droits[key], machine)]) def todo(tag, menu, machine, cont_ret): if not tag in menu_order: raise Continue(cont_ret) else: if 'callback' in menu[tag]: raise Continue(TailCall(menu[tag]['callback'], machine=machine, cont=cont_ret)) elif 'attribut' in menu[tag]: raise Continue(TailCall(self.modif_machine_attributs, machine=machine, cont=cont_ret, attr=menu[tag]['attribut'].ldap_name)) else: raise EnvironmentError("Il n'y a ni champ 'attribut' ni 'callback' pour le tag %s" % tag) (code, tag) = self.handle_dialog(cont, box, tag) cont_ret = TailCall(self.modif_machine, cont=cont, machine=machine, tag=tag) return self.handle_dialog_result( code=code, output=tag, cancel_cont=cont(machine=machine), error_cont=cont_ret, codes_todo=[([self.dialog.DIALOG_OK], todo, [tag, menu, machine, cont_ret])] ) def create_machine_proprio(self, cont, proprio, tag=None): """Permet d'ajouter une machine à un proprio (adherent, club ou AssociationCrans)""" a = attributs menu_droits = { 'Fixe' : [a.soi, a.cableur, a.nounou], 'Wifi' : [a.soi, a.cableur, a.nounou], } menu = { 'Fixe' : {'text' : "Machine filaire", 'objectClass':'machineFixe', 'realm':'adherents'}, 'Wifi' : {'text': 'Machine sans fil', 'objectClass':'machineWifi', 'realm':'wifi-adh'}, } menu_order = ['Fixe', 'Wifi'] if isinstance(proprio, objets.AssociationCrans): menu_droits.update({ 'Fixe' : [a.nounou], 'Wifi' : [a.nounou], 'Adm' : [a.nounou], }) menu.update({ 'Fixe' : {'text' : "Ajouter un serveur sur le vlan adherent", 'objectClass':'machineCrans', 'realm':'serveurs'}, 'Wifi' : {'text': 'Ajouter une borne WiFi sur le vlan wifi', 'objectClass':'borneWifi', 'realm':'bornes'}, 'Adm' : {'text' : "Ajouter un serveur sur le vlan adm", "objectClass":"machineCrans", 'realm':'adm'}, }) menu_order.append('Adm') def box(default_item=None): return self.dialog.menu( "Type de Machine ?", width=0, height=0, menu_height=0, item_help=0, timeout=self.timeout, default_item=str(default_item), title="Création de machines", scrollbar=True, cancel_label="Retour", backtitle=u"Vous êtes connecté en tant que %s" % self.conn.current_login, choices=[(key, menu[key]['text']) for key in menu_order if self.has_right(menu_droits[key], proprio)]) def todo(tag, menu, proprio, self_cont, cont): if not tag in menu_order: raise Continue(self_cont) else: return self.machine_information( cont=cont, machine=None, objectClass=menu[tag]['objectClass'], proprio=proprio, realm=menu[tag]['realm'] ) (code, tag) = self.handle_dialog(cont, box, tag) cont = cont(proprio=None) if isinstance(proprio, objets.AssociationCrans) else cont(proprio=proprio) self_cont = TailCall(self.create_machine_proprio, cont=cont, proprio=proprio, tag=tag) return self.handle_dialog_result( code=code, output=tag, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [tag, menu, proprio, self_cont, cont])] ) def create_machine_adherent(self, cont, adherent=None): """ Permet d'ajouter une machine à un adhérent. On affiche un menu pour choisir le type de machine (juste filaire et wifi pour le moment) """ if adherent is None: adherent = self.select(["adherent"], "Recherche d'un adhérent pour lui ajouter une machine", cont=cont) return self.create_machine_proprio(cont=cont, proprio=adherent) def delete_machine(self, cont, machine=None): """Permet la suppression d'une machine de la base ldap""" if machine is None: machine = self.select(["machineFixe", "machineWifi", "machineCrans", "borneWifi"], "Recherche d'une machine pour supression", cont=cont) def todo(machine): if self.confirm_item(item=machine, title="Voulez vous vraiement supprimer la machine ?", defaultno=True): with self.conn.search(dn=machine.dn, scope=0, mode='rw')[0] as machine: machine.delete() self.dialog.msgbox("La machine a bien été supprimée", timeout=self.timeout, title="Suppression d'une machine") raise Continue(cont(machine=None)) else: raise Continue(cont) return self.handle_dialog_result( code=self.dialog.DIALOG_OK, output="", cancel_cont=cont, error_cont=cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [machine])] ) def modif_adherent_blacklist(self, adherent, cont): """Raccourci vers edit_blacklist spécifique aux adherent""" return self.edit_blacklist(obj=adherent, title="Éditions des blacklist de %s %s" % (adherent['prenom'][0], adherent['nom'][0]), update_obj='adherent', cont=cont) def modif_adherent(self, cont, adherent=None, proprio=None, tag=None): """Menu d'édition d'un adhérent""" if adherent is None: adherent = self.select(["adherent"], "Recherche d'un adhérent pour modification", cont=cont) a = attributs menu_droits = { 'Administratif' : [a.cableur, a.nounou], 'Personnel':[a.cableur, a.nounou, a.soi], 'Études':[a.nounou, a.soi, a.cableur], 'Chambre':[a.cableur, a.nounou], 'Compte':[a.cableur, a.nounou], 'GPGFingerprint' : [a.nounou, a.soi], 'Remarques' : [a.cableur, a.nounou], 'Droits':[a.nounou, a.bureau], 'Blackliste':[a.cableur, a.nounou], 'Vente':[a.cableur, a.nounou], 'Supprimer':[a.nounou, a.bureau], } menu = { 'Administratif' : {'text' : "Adhésion, carte étudiant, chartes", "callback":self.adherent_administratif}, 'Personnel' : {'text' : "Nom, prénom, téléphone... (ajouter l'age ?)", 'callback':self.adherent_personnel}, 'Études' : {'text' : "Étude en cours (perso, je pense que c'est à supprimer)", "callback":self.adherent_etudes}, 'Chambre' : {'text' : 'Déménagement', "callback":self.adherent_chambre}, 'Compte' : {'text' : "Gestion du compte crans", "adherent":"proprio", "callback":self.proprio_compte, 'help':"Création/Suppression/Activation/Désactivation du compte, gestion des alias mails crans du compte"}, 'GPGFingerprint' : {'text':'Ajouter ou supprimer une empeinte GPG', 'attribut':attributs.gpgFingerprint}, 'Remarques' : {'text':'Ajouter ou supprimer une remarque de la machine', 'attribut':attributs.info}, 'Droits' : {'text':"Modifier les droits alloués à cet adhérent", "callback":self.adherent_droits}, 'Blackliste' : {'text': 'Modifier les blacklist de la machine', 'callback':self.modif_adherent_blacklist}, 'Vente' : {'text':"Chargement solde crans, vente de cable ou adaptateur ethernet ou autre", "adherent":"proprio", "callback":self.proprio_vente}, 'Supprimer' : {'text':"Supprimer l'adhérent de la base de donnée", 'callback':self.delete_adherent}, } menu_order = ['Administratif', 'Personnel', 'Études', 'Chambre', 'Compte'] menu_compte_crans = ['Droits'] medu_end = ['GPGFingerprint', 'Remarques', 'Blackliste', 'Vente', 'Supprimer'] if "cransAccount" in adherent['objectClass']: menu_order.extend(menu_compte_crans) menu_order.extend(medu_end) def box(default_item=None): choices = [] for key in menu_order: if self.has_right(menu_droits[key], adherent): choices.append((key, menu[key]['text'], menu[key].get('help', ""))) return self.dialog.menu( "Que souhaitez vous modifier ?", width=0, height=0, menu_height=0, timeout=self.timeout, item_help=1, default_item=str(default_item), title="Modification de %s %s" % (adherent['prenom'][0], adherent["nom"][0]), scrollbar=True, cancel_label="Retour", backtitle=u"Vous êtes connecté en tant que %s" % self.conn.current_login, choices=choices) def todo(tag, menu, adherent, cont_ret): if not tag in menu_order: raise Continue(cont_ret) else: if 'callback' in menu[tag]: raise Continue(TailCall(menu[tag]['callback'], cont=cont_ret, **{menu[tag].get('adherent', 'adherent'):adherent})) elif 'attribut' in menu[tag]: raise Continue(TailCall(self.modif_adherent_attributs, adherent=adherent, cont=cont_ret, attr=menu[tag]['attribut'].ldap_name)) else: raise EnvironmentError("Il n'y a ni champ 'attribut' ni 'callback' pour le tag %s" % tag) (code, tag) = self.handle_dialog(cont, box, tag) cont_ret = TailCall(self.modif_adherent, cont=cont, adherent=adherent, tag=tag) return self.handle_dialog_result( code=code, output=tag, cancel_cont=cont(proprio=adherent), error_cont=cont_ret, codes_todo=[([self.dialog.DIALOG_OK], todo, [tag, menu, adherent, cont_ret])] ) def adherent_administratif(self, cont, adherent): self.dialog.msgbox("todo", width=0, height=0) return cont def adherent_personnel(self, cont, adherent=None, fields_attrs={}, make_compte_crans=None, force_create=False): """ Permet d'éditer les nom, prénom et téléphone d'un adhérent, ou de créer un adhérent. Il faut encore trouver un moyen de récupérer des valeurs pour les attributs mail et chbre """ a = attributs # Quel sont les attributs ldap dont on veut afficher et # la taille du champs d'édition correspondant to_display = [(a.nom, 30), (a.prenom, 30), (a.tel, 30), (a.mail, 30)] non_empty = [a.nom, a.prenom, a.tel] input_type = {'default':0} # Quel séparateur on utilise pour les champs multivalué separateur = ' ' def box(make_compte_crans): if force_create and adherent is None and fields_attrs and make_compte_crans is not None: return (self.dialog.DIALOG_OK, [fields_attrs[a] for a,l in to_display], make_compte_crans) if adherent: attrs = dict((k,[str(a) for a in at]) for k,at in adherent.items()) if 'cransAccount' in adherent['objectClass']: input_type[attributs.mail] = 2 to_display.append((attributs.mailExt, 30)) else: attrs = {} if make_compte_crans is None: if self.dialog.yesno("Crééra-t-on un compte crans à l'utilisateur ?", timeout=self.timeout, title="Création d'un adhérent", width=50) == self.dialog.DIALOG_OK: input_type[attributs.mail] = 2 make_compte_crans = True to_display.append((attributs.mailExt, 30)) else: make_compte_crans = False fields = [( "%s %s:" % (a.legend, '(optionnel) ' if a.optional else ''), fields_attrs.get(a, separateur.join(attrs.get(a.ldap_name, [a.default] if a.default else []))), l+1, l, input_type.get(a, input_type['default']) ) for a,l in to_display] (code, tags) = self.dialog.form( text="", timeout=self.timeout, height=0, width=0, form_height=0, fields=fields, title="Création d'un adhérent" if adherent is None else "Édition des informations de %s %s" % (adherent['prenom'][0], adherent['nom'][0]), backtitle="Gestion des adhérents du Crans") return (code, tags, make_compte_crans) def modif_adherent(adherent, attrs): with self.conn.search(dn=adherent.dn, scope=0, mode='rw')[0] as adherent: for (key, values) in attrs.items(): adherent[key]=values adherent.validate_changes() adherent.save() return adherent def create_adherent(attrs, make_compte_crans, force_create, self_cont, cont): if not force_create: items = self.conn.search("(&(prenom=%s)(nom=%s))" % (attrs['prenom'], attrs['nom'])) if items: newadherent = self.select_one(items, title="Choisir un adhérant existant", text="Des adhérent avec les même noms et prénoms existent déjà, en utiliser un ?\n(Annuler pour continuer la création)", cont=self_cont(make_compte_crans=make_compte_crans, force_create=True)) raise Continue(cont(adherent=newadherent)) with self.conn.newAdherent({}) as adherent: delay={} for (key, values) in attrs.items(): try: adherent[key]=values # En cas d'erreur, on a peut être besoin du compte crans except ValueError: delay[key]=values print delay # on récupère la chambre adherent = self.adherent_chambre_campus(success_cont=None, cont=self_cont(make_compte_crans=make_compte_crans), adherent=adherent, create=True) # Si c'est EXT, on demande une adresse complète if 'EXT' in adherent['chbre']: adherent = self.adherent_chambre_ext(keep_machine=True, keep_compte=True, success_cont=None, cont=self_cont(make_compte_crans=make_compte_crans), adherent=adherent, create=True) # Si compte crans à créer, on le crée. # On le met en dernier pour éviter de faire entrez plusieurs fois son mdp à l'adhérent # en cas d'erreur de la part du cableur if make_compte_crans: adherent = self.proprio_compte_create(proprio=adherent, cont=self_cont(make_compte_crans=None, force_create=False, adherent=None), update_obj='adherent', return_obj=True) # On réeaffecte les attributs de tout à l'heure for (key, values) in delay.items(): adherent[key]=values # On confirme la création if self.confirm_item(adherent, title="Créer l'adhérent suivant ?"): adherent.validate_changes() adherent.create() else: adherent = None return adherent def todo(to_display, non_empty, tags, adherent, separateur, make_compte_crans, force_create, self_cont, cont): attrs = {} # On traite les valeurs reçues for ((a,l),values) in zip(to_display, tags): if not values and a in non_empty: raise ValueError("%s ne devrait pas être vide" % a.legend) values = unicode(values, 'utf-8') # Si le champs n'est pas single value, on utilise separateur pour découper # et on ne garde que les valeurs non vides if not a.singlevalue: values = [v for v in values.split(separateur) if v] attrs[a.ldap_name]=values if adherent: adherent = modif_adherent(adherent, attrs) else: adherent = create_adherent(attrs, make_compte_crans, force_create, self_cont, cont) raise Continue(cont(adherent=adherent)) (code, tags, make_compte_crans) = self.handle_dialog(cont, box, make_compte_crans) # On prépare les fiels à afficher à l'utilisateur si une erreure à lieu # pendant le traitement des donnée (on n'éfface pas ce qui a déjà été entré # c'est au cableur de corriger ou d'annuler fields_attrs = dict((a, values) for ((a,l),values) in zip(to_display, tags)) retry_cont = TailCall(self.adherent_personnel, adherent=adherent, cont=cont, fields_attrs=fields_attrs) return self.handle_dialog_result( code=code, output=tags, cancel_cont=cont, error_cont=retry_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [to_display, non_empty, tags, adherent, separateur, make_compte_crans, force_create, retry_cont, cont])] ) def adherent_etudes(self, adherent, cont): """Gestion des études de l'adhérent""" self.dialog.msgbox("todo", width=0, height=0) return cont @tailcaller def adherent_chambre_campus(self, success_cont, cont, adherent, create=False): """Permet de faire déménager d'adhérent sur le campus""" def box(): return self.dialog.inputbox( "chambre ?", title="%s de %s %s" % ("Création" if create else "Déménagement", adherent['prenom'][0], adherent["nom"][0]), cancel_label="Retour", width=50, timeout=self.timeout, ) def expulse_squatteur(adherent, chbre): with self.conn.search(u"chbre=%s" % chbre, mode='rw')[0] as squatteur: if self.confirm_item( item=squatteur, title="Chambre occupée", defaultno=True, text=u"L'adhérent ci-dessous occupé déjà la chambre %s :\n" % output, text_bottom=u"\nPasser la chambre de cet adhérent en chambre inconnue ?" ): squatteur['chbre']=u'????' squatteur.save() return True else: return False def set_chambre(adherent, chbre): try: adherent['postalAddress']=[] adherent['chbre']=unicode(output, 'utf-8') except UniquenessError: if expulse_squatteur(adherent, chbre): # La chambre est maintenant normalement libre adherent['chbre']=unicode(output, 'utf-8') else: raise Continue(self_cont) return adherent def todo(chbre, adherent, self_cont, success_cont): if not output: raise Continue(self_cont) if output == "????": raise ValueError("Chambre ???? invalide") if create: return set_chambre(adherent, chbre) else: with self.conn.search(dn=adherent.dn, scope=0, mode='rw')[0] as adherent: adherent = set_chambre(adherent, chbre) adherent.save() self.display_item(item=adherent, title="Adhérent déménagé dans la chambre %s" % output) raise Continue(success_cont(adherent=adherent)) (code, output) = self.handle_dialog(cont, box) self_cont = TailCall(self.adherent_chambre_campus, adherent=adherent, success_cont=success_cont, cont=cont) return self.handle_dialog_result( code=code, output=output, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [output, adherent, self_cont, success_cont])] ) @tailcaller def adherent_chambre_ext(self, keep_machine, keep_compte, success_cont, cont, adherent, create=False): """Permet de faire déménager l'adhérent hors campus""" if keep_machine and not keep_compte: raise EnvironmentError("On ne devrait pas supprimer un compte crans et y laisser des machines") elif keep_machine and keep_compte: def box(values={}): form = [("Adresse", 40), ("Compl. adr.", 40), ("Code postal", 7), ("Ville", 16)] fields = [("%s :" % k, values.get(k, ""), l, 50) for k,l in form] return self.dialog.form( text="", timeout=self.timeout, height=0, width=0, form_height=0, fields=fields, title="Paramètres machine", backtitle="Gestion des machines du Crans") def todo(output, adherent, success_cont, cont): if not create: if self.dialog.yesno("changer l'adresse de l'adhérent pour %s ?" % ", ".join([o for o in output if o]), title=u"Déménagement de %s %s" % (adherent['prenom'][0], adherent["nom"][0]), defaultno=True, timeout=self.timeout) == self.dialog.DIALOG_OK: with self.conn.search(dn=adherent.dn, scope=0, mode='rw')[0] as adherent: adherent['postalAddress']=[unicode(pa, 'utf-8') for pa in output] adherent['chbre']=u'EXT' adherent.save() self.display_item(item=adherent, title="Adhérent déménégé hors campus, machines conservées") raise Continue(success_cont(adherent=adherent)) else: raise Continue(cont) else: adherent['postalAddress']=[unicode(pa, 'utf-8') for pa in output] adherent['chbre']=u'EXT' return adherent elif not keep_machine and keep_compte: if create: raise EnvironmentError("On ne crée pas un adhérent en lui supprimant des machines") def box(values={}): return (self.dialog.DIALOG_OK, "") def todo(output, adherent, success_cont, cont): if self.confirm_item( item=adherent, text=u"Supprimer toutes les machines de l'adhérent ci-dessous ?\n\n", text_bottom=u"\nLa chambre de l'adhérent passera de plus en EXT.", title=u"Déménagement de %s %s" % (adherent['prenom'][0], adherent["nom"][0]), defaultno=True): with self.conn.search(dn=adherent.dn, scope=0, mode='rw')[0] as adherent: for machine in adherent.machines(): with machine: machine.delete() adherent['chbre']=u'EXT' adherent.save() self.display_item(item=adherent, title="Adhérent déménégé hors campus, machines supprimées") raise Continue(success_cont(adherent=adherent)) else: raise Continue(cont) elif not keep_machine and not keep_compte: if create: raise EnvironmentError("On ne crée pas un adhérent en lui supprimant des machines et un compte") def box(values={}): return (self.dialog.DIALOG_OK, "") def todo(output, adherent, success_cont, cont): if adherent.get('solde', [0])[0] > 0: self.dialog.msgbox("Solde de l'adhérent %s€ strictement positif, impossible de supprimer le compte\nRepasser le solde à 0€ pour supprimer le compte." % adherent.get('solde', [0])[0], title=u"Déménagement de %s %s" % (adherent['prenom'][0], adherent["nom"][0]), width=50, timeout=self.timeout) raise Continue(cont) elif self.confirm_item( item=adherent, text=u"Supprimer toutes les machines et du compte crans de l'adhérent ci-dessous ?\n\n", text_bottom=u"\nLa chambre de l'adhérent passera de plus en EXT.", title=u"Déménagement de %s %s" % (adherent['prenom'][0], adherent["nom"][0]), defaultno=True): with self.conn.search(dn=adherent.dn, scope=0, mode='rw')[0] as adherent: for machine in adherent.machines(): with machine: machine.delete() adherent['chbre']=u'EXT' adherent.save() # On supprime le compte crans (on essaye) def post_deletion(proprio, cont): if not "cransAccount" in proprio['objectClass']: self.display_item(item=adherent, title="Adhérent déménégé hors campus, machines et compte crans supprimées") else: self.display_item(item=adherent, title="Adhérent déménégé hors campus, machines supprimées et compte crans concervé") raise Continue(cont(adherent=proprio)) self.proprio_compte_delete(proprio=adherent, cont=TailCall(post_deletion, proprio=adherent, cont=success_cont(adherent=adherent)), force=True) else: raise Continue(cont) else: raise EnvironmentError("Impossible, on a fait tous les cas, python est buggué") (code, output) = self.handle_dialog(cont, box) self_cont = TailCall(self.adherent_chambre_ext, adherent=adherent, keep_machine=keep_machine, keep_compte=keep_compte, success_cont=success_cont, cont=cont) return self.handle_dialog_result( code=code, output=output, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [output, adherent, success_cont, cont])] ) def adherent_chambre(self, adherent, cont, default_item=None): """Permet de faire déménager d'adhérent""" a = attributs menu_droits = { '0' : [a.cableur, a.nounou], '1' : [a.cableur, a.nounou], '2' : [a.cableur, a.nounou], '3' : [a.cableur, a.nounou], } menu = { "0": {'text':"Déménagement sur le campus", 'callback':self.adherent_chambre_campus, 'help': "Déménagement vers une chambre sur le campus, on ne demande que le bâtiment et la chambre"}, "1": {'text':"Déménagement à l'extérieur en conservant les machines", "callback": TailCall(self.adherent_chambre_ext, keep_machine=True, keep_compte=True), "help": "Pour concerver ses machines, il faut donner un adresse postale complète"}, "2": {'text':"Départ du campus en conservant son compte", "callback":TailCall(self.adherent_chambre_ext, keep_machine=False, keep_compte=True), "help":"Supprime les machines mais concerve le compte crans, l'adresse passe en EXT sans plus d'information"}, "3": {'text':"Départ du campus en supprimant son compte", "callback":TailCall(self.adherent_chambre_ext, keep_machine=False, keep_compte=False), "help":"Supprime les machines et le compte crans, l'adhérent reste dans la base de donnée, il est possible de mettre une redirection du mail crans vers une autre adresse dans bcfg2"}, } menu_order = [str(i) for i in range(4)] def box(default_item=None): return self.dialog.menu( "Quel est le type du déménagement ?", width=0, height=0, menu_height=0, timeout=self.timeout, item_help=1, default_item=str(default_item), title="Déménagement de %s %s" % (adherent['prenom'][0], adherent["nom"][0]), scrollbar=True, cancel_label="Retour", backtitle=u"Vous êtes connecté en tant que %s" % self.conn.current_login, choices=[(k, menu[k]['text'], menu[k]['help']) for k in menu_order if self.has_right(menu_droits[k], adherent)]) def todo(tag, menu, adherent, self_cont, cont): if not tag in menu_order: raise Continue(self_cont) else: raise Continue(TailCall(menu[tag]['callback'], cont=self_cont, success_cont=cont, adherent=adherent)) (code, tag) = self.handle_dialog(cont, box, default_item) self_cont = TailCall(self.adherent_chambre, adherent=adherent, cont=cont, default_item=tag) return self.handle_dialog_result( code=code, output=tag, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [tag, menu, adherent, self_cont, cont])] ) @tailcaller def proprio_compte_create(self, proprio, cont, warning=True, guess_login=True, guess_pass=0, return_obj=False, update_obj='proprio'): """Permet de créer un compte crans à un proprio (club ou adhérent)""" def box_warning(warning, proprio, cont): # Affiche-t-on le warning sur la consutation de l'adresse crans if warning: if self.dialog.yesno( text="\Zr\Z1AVERTISSEMENT :\Zn \nL'adhérent devra impérativement consulter l'adresse mail associée\n\n\n\ZnContinuer ?", title="Création du compte de %s %s" % (proprio.get('prenom', [''])[0], proprio["nom"][0]), defaultno=True, width=70, colors=True, timeout=self.timeout) != self.dialog.DIALOG_OK: raise Continue(cont) def get_login(guess_login, guess_pass, proprio, self_cont, cont): # Essaye-t-on de deviner le login à utiliser if not guess_login: (code, login) = self.dialog.inputbox( text="Le login doit faire au maximum %s caractères\nIl ne doit pas être un pseudo ou prénom mais doit être relié au nom de famille\nSeuls les caractères alphabétiques et le trait d'union sont autorisés" % config.maxlen_login, title="Choix du login pour %s %s" % (proprio.get('prenom', [''])[0], proprio["nom"][0]), init=str(proprio['nom'][0]).lower(), width=60, height=10, timeout=self.timeout) if code != self.dialog.DIALOG_OK: raise Continue(cont) else: # Si oui, de quelle manière if guess_pass == 0: login = str(proprio['nom'][0]) elif guess_pass == 1 and proprio.get('prenom', [''])[0]: login = "%s%s" % (str(proprio['prenom'][0])[0], proprio['nom'][0]) # Si toutes les manières ont échoués, la prochaine fois, ça on n'essaye pas de deviner else: raise Continue(self_cont(warning=False, guess_login=False, guess_pass=2)) return login def create_compte(proprio, login, guess_login, self_cont, cont): try: proprio.compte(login=unicode(login, 'utf-8')) except ValueError: # Il y a eu une erreur, si on essaye de deviner, on essaye la manière suivante if guess_login: raise Continue(self_cont(warning=False, guess_login=True, guess_pass=guess_pass+1)) # Sinon on propage l'erreur pour l'afficher à l'utilisateur else: raise self.dialog.msgbox( text="Le compte ne sera créé que lors de l'enregistrement des données\n\nL'adresse mail de l'adhérent est : %s\nL'adhérent possède également l'alias :\n%s\n" % (proprio['mail'][0], proprio['canonicalAlias'][0]), title="Création du compte de %s %s" % (proprio.get('prenom', [''])[0], proprio["nom"][0]), width=75, height=12, timeout=self.timeout, ) return proprio @tailcaller def set_password(proprio, update_obj, cont): if self.dialog.yesno("Attribuer un mot de passe maintenant ?", title="Création du compte de %s %s" % (proprio.get('prenom', [''])[0], proprio["nom"][0]), timeout=self.timeout ) == self.dialog.DIALOG_OK: #return self.proprio_compte_password(proprio=proprio, return_obj=return_obj, cont=cont(**{update_obj:proprio})) proprio = self.proprio_compte_password(proprio=proprio, return_obj=True, cont=TailCall(set_password, proprio, update_obj, cont)) if return_obj: return proprio else: raise Continue(cont(**{update_obj:proprio})) elif return_obj: return proprio else: raise Continue(cont(**{update_obj:proprio})) def todo(proprio, warning, guess_login, guess_pass, return_obj, self_cont, cont): box_warning(warning, proprio, cont) login = get_login(guess_login, guess_pass, proprio, self_cont, cont) if return_obj: proprio = create_compte(proprio, login, guess_login, self_cont, cont) return set_password(proprio, update_obj, cont) else: with self.conn.search(dn=proprio.dn, scope=0, mode='rw')[0] as proprio: proprio = create_compte(proprio, login, guess_login, self_cont, cont) if not self.confirm_item(item=proprio, title="Création du compte crans pour l'adhérent ?"): raise Continue(cont) else: proprio.save() self.dialog.msgbox( text="Compte créé avec succès.", title="Création du compte de %s %s" % (proprio.get('prenom', [''])[0], proprio["nom"][0]), timeout=self.timeout ) return set_password(proprio, update_obj, cont) self_cont = TailCall(self.proprio_compte_create, proprio=proprio, cont=cont, warning=warning, guess_login=guess_login, guess_pass=guess_pass) return self.handle_dialog_result( code=self.dialog.DIALOG_OK, output="", cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [proprio, warning, guess_login, guess_pass, return_obj, self_cont, cont])] ) @tailcaller def proprio_compte_password(self, proprio, cont, return_obj=False): """Permet de changer le mot de passe d'un compte crans""" def test_password(password, self_cont): (good, msg) = check_password(password, dialog=True) if not good: self.dialog.msgbox( msg, title="Erreur dans le mot de passe de %s %s" % (proprio.get('prenom', [''])[0], proprio["nom"][0]), colors=True, width=70, timeout=self.timeout) raise Continue(self_cont) else: return True def todo(passwords, proprio, return_obj, self_cont, cont): password = self.get_password(cont=cont, title="Choix du mot de passe pour %s %s" % (proprio.get('prenom', [''])[0], proprio["nom"][0]), backtitle="Le mot de passe doit être assez difficile") if test_password(password, self_cont): if return_obj: proprio['userPassword']=unicode(lc_utils.hash_password(password)) return proprio else: with self.conn.search(dn=proprio.dn, scope=0, mode='rw')[0] as proprio: proprio['userPassword']=unicode(lc_utils.hash_password(password)) proprio.save() self.dialog.msgbox( "Mot de passe changé avec succès", title="Choix du mot de passe pour %s %s" % (proprio.get('prenom', [''])[0], proprio["nom"][0]), width=70, timeout=self.timeout ) raise Continue(cont(proprio=proprio)) #(code, passwords) = self.handle_dialog(cont, box) (code, passwords) = (self.dialog.DIALOG_OK, "") self_cont = TailCall(self.proprio_compte_password, proprio=proprio, cont=cont) return self.handle_dialog_result( code=code, output=passwords, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [passwords, proprio, return_obj, self_cont, cont])] ) @tailcaller def proprio_compte_delete(self, proprio, cont, force=False): """Permet la suppression du compte crans d'un proprio""" def todo(proprio, self_cont, cont): if force or self.confirm_item(item=proprio, title="Voulez vous vraiement supprimer le compte de %s %s ?" % (proprio.get('prenom', [''])[0], proprio["nom"][0]), defaultno=True): (code, mail) = self.dialog.inputbox( text="Il faut choisir une nouvelle adresse de contact.\n(On regarde s'il y a une adresse optionnel)", title="Choix d'une adresse de contact pour %s %s" % (proprio.get('prenom', [''])[0], proprio["nom"][0]), init=str(proprio.get("mailExt", [""])[0]), width=50, timeout=self.timeout) if not code == self.dialog.DIALOG_OK: raise Continue(cont) elif not mail: raise ValueError("Il faut entrer une adresse mail") with self.conn.search(dn=proprio.dn, scope=0, mode='rw')[0] as proprio: proprio.delete_compte(unicode(mail, 'utf-8')) proprio.save() self.dialog.msgbox("Le compte a bien été supprimée", timeout=self.timeout, title="Suppression du compte de %s %s" % (proprio.get('prenom', [''])[0], proprio["nom"][0])) raise Continue(cont(proprio=proprio)) else: raise Continue(cont) self_cont = TailCall(self.proprio_compte_delete, proprio=proprio, cont=cont, force=force) return self.handle_dialog_result( code=self.dialog.DIALOG_OK, output="", cancel_cont=cont(proprio=proprio), error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [proprio, self_cont, cont])] ) def proprio_compte_etat(self, proprio, disable, cont): """Permet de d'éastiver ou activer un compte crans avec l'attribut shadowExpire""" with self.conn.search(dn=proprio.dn, scope=0, mode='rw')[0] as proprio: if disable: proprio["shadowExpire"]=0 else: proprio["shadowExpire"]=[] proprio.save() raise Continue(cont(proprio=proprio)) def proprio_compte_shell(self, proprio, cont, choices_values=None): """Permet de modifier le shell d'un compte crans""" a = attributs # Seul les nounous peuvent changer les shells restrictifs shells_droits = { 'default' : [a.soi, a.nounou, a.cableur], 'rbash' : [a.nounou], 'rssh' : [a.nounou], 'badPassSh' : [a.nounou], 'disconnect_shell':[a.nounou], } shell = os.path.basename(str(proprio['loginShell'][0])).lower() shells = config.shells_gest_crans shells_order = config.shells_gest_crans_order def box(): # liste des shell éditables par l'utilisateur editable_sheels = [s for s in shells_order if self.has_right(shells_droits.get(s, shells_droits['default']), proprio)] # Si l'utilisateur de gest_crans peut éditer le shell courant # il peut le changer pour un autre shell qu'il peut éditer if shell in editable_sheels: choices=[(s, shells[s]['desc'], 1 if s == shell else 0) for s in editable_sheels] return self.dialog.radiolist( text="", height=0, width=0, list_height=0, choices=choices_values if choices_values else choices, title="Shell de %s %s" % (proprio.get('prenom', [""])[0], proprio['nom'][0]), timeout=self.timeout ) # Sinon, on affiche un message d'erreur et on annule else: self.dialog.msgbox("Vous ne pouvez pas changer le shell de cet utilisateur", title="Édition du shell impossible", timeout=self.timeout, width=0, height=0) return (self.dialog.DIALOG_CANCEL, None) def todo(output, shell, shells, proprio, self_cont, cont): loginShell = shells[output]['path'] if shell and shell != output: with self.conn.search(dn=proprio.dn, scope=0, mode='rw')[0] as proprio: proprio['loginShell']=unicode(loginShell) proprio.save() self.dialog.msgbox("Shell modifié avec succès.\nLa modification peut prendre une quainzaine de minute avant d'être effective.", title="Shell de %s %s" % (proprio.get('prenom', [""])[0], proprio['nom'][0]), width=50, timeout=self.timeout, ) raise Continue(cont(proprio=proprio)) (code, output) = self.handle_dialog(cont, box) self_cont = TailCall(self.proprio_compte_shell, proprio=proprio, cont=cont, choices_values=[(s, shells[s]['desc'], 1 if s == output else 0) for s in shells_order]) return self.handle_dialog_result( code=code, output=output, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [output, shell, shells, proprio, self_cont, cont])] ) def proprio_compte(self, proprio, cont, default_item=None): """Menu de gestion du compte crans d'un proprio""" has_compte = 'cransAccount' in proprio['objectClass'] disabled_compte = has_compte and 0 in proprio['shadowExpire'] a = attributs menu_droits = { "Password": [a.cableur, a.nounou], 'MailAlias': [a.cableur, a.nounou], "Activer" : [a.nounou], "Désactiver" : [a.nounou], "Créer" : [a.cableur, a.nounou], "Supprimer" : [a.cableur, a.nounou], "Shell" : [a.nounou, a.soi, a.cableur], } menu = { "Password" : {"text":"Changer le mot de passe du compte", "help":"", "callback":self.proprio_compte_password}, 'MailAlias' : {'text': 'Créer ou supprimer des alias mail', "help":"", 'attribut':attributs.mailAlias}, "Shell" : {"text" : "Changer le shell de cet utilisateur", "help":'', "callback":self.proprio_compte_shell}, "Activer" : {"text" : "Activer le compte pour la connexion mail/serveur", "help":"Permet d'autoriser les connexions smtp, imap, ssh, etc… avec le compte", "callback":TailCall(self.proprio_compte_etat, disable=False)}, "Désactiver" : {"text" : "Désactiver le compte pour la connexion mail/serveur", "help":"Permet d'interdire les connexions smtp, imap, ssh, etc… avec le compte", "callback":TailCall(self.proprio_compte_etat, disable=True)}, "Créer" : {"text": "Créer un compte", "help":'', "callback":self.proprio_compte_create}, "Supprimer" : {"text": "Supprimer le compte", "help":"Le home sera archivé dans le cimetière", "callback":self.proprio_compte_delete}, } menu_order = [] tag_translate = { "Créer":"Password", "Password":"Password", "Supprimer":"Créer", "Activer":"Désactiver", "Désactiver":"Activer", "Shell":"Shell", 'MailAlias':'MailAlias', '':'', } if has_compte: if disabled_compte: menu_order.append("Activer") else: menu_order.append("Désactiver") menu_order.extend(['MailAlias', "Shell", "Password", "Supprimer"]) else: menu_order.append("Créer") def box(default_item=None): return self.dialog.menu( "Quel action effectuer sur le compte ?", width=0, height=0, menu_height=0, timeout=self.timeout, item_help=1, default_item=str(default_item), title="Gestion du compte de %s %s" % (proprio.get('prenom', [''])[0], proprio["nom"][0]), scrollbar=True, cancel_label="Retour", backtitle=u"Vous êtes connecté en tant que %s" % self.conn.current_login, choices=[(k, menu[k]['text'], menu[k]['help']) for k in menu_order if self.has_right(menu_droits[k], proprio)]) def todo(tag, menu, proprio, self_cont): if not tag in menu_order: raise Continue(self_cont) elif 'callback' in menu[tag]: raise Continue(TailCall(menu[tag]['callback'], cont=self_cont, proprio=proprio)) elif 'attribut' in menu[tag]: raise Continue(TailCall(self.modif_proprio_attributs, proprio=proprio, cont=self_cont, attr=menu[tag]['attribut'].ldap_name)) else: raise EnvironmentError("Il n'y a ni champ 'attribut' ni 'callback' pour le tag %s" % tag) (code, tag) = self.handle_dialog(cont, box, default_item) self_cont = TailCall(self.proprio_compte, proprio=proprio, cont=cont, default_item=tag_translate.get(tag, tag)) return self.handle_dialog_result( code=code, output=tag, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [tag, menu, proprio, self_cont])] ) def adherent_droits(self, adherent, cont, choices_values=None): """Gestion des droits d'un adhérent""" def box(): return self.dialog.checklist( text="", height=0, width=0, list_height=0, choices=choices_values if choices_values else [(droit, "", 1 if droit in adherent["droits"] else 0) for droit in attributs.TOUS_DROITS], title="Droits de %s %s" % (adherent['prenom'][0], adherent['nom'][0]), timeout=self.timeout ) def todo(droits, adherent, self_cont, cont): # Les vérifications de sécurité sont faites dans lc_ldap with self.conn.search(dn=adherent.dn, scope=0, mode='rw')[0] as adherent: adherent['droits']=[unicode(d) for d in droits] adherent.save() if adherent["uid"] and adherent["uid"][0] == self.conn.current_login: self.check_ldap() raise Continue(cont(adherent=adherent)) (code, droits) = self.handle_dialog(cont, box) self_cont = TailCall(self.adherent_droits, adherent=adherent, cont=cont, choices_values=[(d, "", 1 if d in droits else 0) for d in attributs.TOUS_DROITS]) return self.handle_dialog_result( code=code, output=droits, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [droits, adherent, self_cont, cont])] ) @tailcaller def proprio_vente_set(self, article, cont): """Permet de définir la quantité de l'article à vendre""" def box(): if article['pu'] == '*': return self.dialog.inputbox(title="Montant pour %s ?" % article['designation'], text="", init=str(article.get('nombre','')), timeout=self.timeout, width=70) else: return self.dialog.inputbox(title="Nombre de %s ?" % article['designation'], text="", timeout=self.timeout, init=str(article.get('nombre','1')), width=70) def todo(article, output, cont): article['nombre']=output # Il faut entrer quelque chose if not output: raise ValueError("Merci d'entrer une valeur") # Vérification des centimes if article['pu'] == '*' and '.' in output: if len(output.split('.', 1)[1])>2: raise ValueError("Les centimes, c'est seulement deux chiffres après la virgule") typ = float if article['pu'] == '*' else int # Vérification du type de l'entré try: output=typ(output) except ValueError: raise ValueError("Merci d'entrez seulement des nombres") # On définis le nombre d'entrée. Pour pu=* il y aura du trairement à faire # avant de générer la facture : mettre pu à nombre et nombre à 1 # on le met comme ça pour pouvoir naviger aisément entre les écrans dialog article['nombre'] = output return article (code, output) = self.handle_dialog(cont, box) self_cont = TailCall(self.proprio_vente_set, article=article, cont=cont) return self.handle_dialog_result( code=code, output=output, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], todo, [article, output, cont])] ) @tailcaller def proprio_vente(self, proprio, cont, tags=[], tag_paiment=None, to_set=[], have_set=[]): """Menu de vente du crans. Permet également de recharger le solde crans""" box_paiement = { "liquide" : "Espèces", "cheque" : "Chèque", "solde" : "Solde Crans (actuel : %s€)", } def box_choose_item(tags): choices = [] for code, article in gestion.config.factures.items.items(): choices.append((code, u"%s%s" % (article['designation'], (u' (%s€)' % article['pu']) if article['pu'] != '*' else ""), 1 if code in tags else 0)) return self.dialog.checklist( text="", title="Vente de truc à %s %s" % (proprio.get("prenom", [''])[0], proprio["nom"][0]), choices=choices, timeout=self.timeout) def box_choose_paiment(tag, articles): box_paiement_order = ["liquide", "cheque"] if "cransAccount" in proprio['objectClass']: if not "SOLDE" in [art['code'] for art in articles]: box_paiement_order.append("solde") box_paiement["solde"] = box_paiement["solde"] % proprio["solde"][0] choices = [] for key in box_paiement_order: choices.append((key, box_paiement[key], 1 if key == tag else 0)) return self.dialog.radiolist( text="", title="Choix d'un mode de paiement pour %s %s" % (proprio.get("prenom", [''])[0], proprio["nom"][0]), choices=choices, timeout=self.timeout) def choose_item(proprio, tags, articles, self_cont): to_set=[] for tag in tags: articles[tag]['code']=tag to_set.append(articles[tag]) raise Continue(self_cont(to_set=to_set, have_set=[])) def number_of_items(to_set, have_set, self_cont): # Où faut-il aller si l'utilisateur appuis sur annuler if not have_set: lcont = self_cont(to_set=[]) else: lcont = self_cont(to_set=[have_set[-1]] + to_set, have_set=have_set[:-1]) art = self.proprio_vente_set(to_set[0], cont=lcont) if not to_set[1:]: total = 0 line=1 text=u"Résumé :\n" for article in have_set + [art]: if article['pu'] == '*': total += article['nombre'] text+=u" * %s pour %s€\n" % (article['designation'], article['nombre']) else: total += article['nombre']*article['pu'] text+=u" * %dx %s à %s€\n" % (article['nombre'], article['designation'], article['pu']) line+=1 text+=u"Total à payer : %s€" % total self.dialog.msgbox(text=text, title="Résumé de la facture à payer", width=70, height=5+line, timeout=self.timeout) return self_cont(to_set=to_set[1:], have_set=have_set + [art]) def choose_paiment(have_set, tag, proprio, lcont, self_cont, cont): if not tag: raise ValueError("Il faut choisir un moyen de paiement") code, comment = self.dialog.inputbox(text="Détail pour les espèce, nom de la note ou banque du chèque", title="Commentaire", width=70, timeout=self.timeout) if code != self.dialog.DIALOG_OK: raise Continue(self_cont) if not comment: raise ValueError("Commentaire nécessaire") articles = copy.deepcopy(have_set) for article in articles: if article['pu'] == '*': article['pu'] = article['nombre'] article['nombre'] = 1 with self.conn.newFacture(proprio.dn, {}) as facture: facture['modePaiement']=unicode(tag, 'utf-8') facture['article']=articles facture['info']=unicode(comment, 'utf-8') if self.confirm_item(item=facture, text=u"Le paiement de %s€ a-t-il bien été reçu (mode : %s) ?\n" % (facture.total(), tag), title=u"Validation du paiement", timeout=self.timeout): # Appeler créditer va créditer ou débiter le solde, sauver le proprio et créer la facture facture.crediter() arts = ["%s %s" % (art['nombre'], art['designation']) for art in facture['article'] if art['code'] != 'SOLDE'] if arts: self.dialog.msgbox( text=u"Vous pouvez remettre à l'adherent les articles (si se sont des articles) suivant :\n * %s" % '\n * '.join(arts), title=u"Vente terminée", width=0, height=0, timeout=self.timeout) if tag == "solde": self.dialog.msgbox(text=u"Le solde de l'adhérent à bien été débité", title="Solde débité", width=0, height=0, timeout=self.timeout) if [a for a in facture['article'] if art['code'] == 'SOLDE']: self.dialog.msgbox(text=u"Le solde de l'adhérent à bien été crédité", title="Solde crédité", width=0, height=0, timeout=self.timeout) else: self.dialog.msgbox(text=u"Le paiement n'ayant pas été reçue\nla vente est annulée", title="Annulation de la vente", width=0, height=0, timeout=self.timeout) raise Continue(cont) self_cont=TailCall(self.proprio_vente, proprio=proprio, cont=cont, tags=tags, tag_paiment=tag_paiment, to_set=to_set, have_set=have_set) # S'il y a des article dont il faut définir la quantité if to_set: return self.handle_dialog_result( code=self.dialog.DIALOG_OK, output=None, cancel_cont=None, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], number_of_items, [to_set, have_set, self_cont])] ) # Sinon, si tous les quantités de tous les articles sont définis elif have_set: lcont = self_cont.copy() lcont(to_set=[have_set[-1]] + to_set, have_set=have_set[:-1]) (code, tag) = self.handle_dialog(lcont, box_choose_paiment, tag_paiment, have_set) self_cont=self_cont(tag_paiment=tag) lcont(tag_paiment=tag) return self.handle_dialog_result( code=code, output=tag, cancel_cont=lcont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], choose_paiment, [have_set, tag, proprio, lcont, self_cont, cont])] ) # Sinon, on propose des articles à chosir else: (code, tags) = self.handle_dialog(cont, box_choose_item, tags) self_cont=self_cont(tags=tags, have_set=[], to_set=[], tag_paiment=None) return self.handle_dialog_result( code=code, output=tags, cancel_cont=cont, error_cont=self_cont, codes_todo=[([self.dialog.DIALOG_OK], choose_item, [proprio, tags, copy.deepcopy(gestion.config.factures.items), self_cont])] ) def create_adherent(self, cont): """Crée un adhérent et potentiellement son compte crans avec lui""" def mycont(adherent=None, **kwargs): if adherent: raise Continue(TailCall(self.modif_adherent, cont=cont, adherent=adherent)) else: raise Continue(cont) return self.adherent_personnel(cont=TailCall(mycont)) def delete_adherent(self, cont, adherent=None): """Permet la suppression d'un adhérent de la base ldap""" if adherent is None: adherent = self.select(["adherent"], "Recherche d'un adhérent pour supression", cont=cont) def todo(adherent): if self.confirm_item(item=adherent, title="Voulez vous vraiement supprimer l'adhérent ?", defaultno=True): with self.conn.search(dn=adherent.dn, scope=0, mode='rw')[0] as adherent: adherent.delete() self.dialog.msgbox("L'adherent a bien été supprimée", timeout=self.timeout, title="Suppression d'un adherent") raise Continue(cont(proprio=None)) else: raise Continue(cont) return self.handle_dialog_result( code=self.dialog.DIALOG_OK, output="", cancel_cont=cont(proprio=adherent), error_cont=cont(proprio=adherent), codes_todo=[([self.dialog.DIALOG_OK], todo, [adherent])] ) def create_club(self, cont): self.dialog.msgbox("todo", width=0, height=0) return cont def delete_club(self, cont): self.dialog.msgbox("todo", width=0, height=0) return cont def modif_club(self, cont, club=None): if club is None: club = self.select(["club"], "Recherche d'un club pour modification", disable_field=["Prénom", "Téléphone"], cont=cont) self.dialog.msgbox("todo", width=0, height=0) return cont(proprio=club) def create_machine_club(self, cont, club=None): """ Permet d'ajouter une machine à un club. On affiche un menu pour choisir le type de machine (juste filaire et wifi pour le moment) """ if club is None: club = self.select(["club"], "Recherche d'un club pour lui ajouter une machine", cont=cont) return self.create_machine_proprio(cont=cont, proprio=club) def create_machine_crans(self, cont): """Permet l'ajout d'une machine à l'association""" associationCrans = self.conn.search(dn="ou=data,dc=crans,dc=org", scope=0)[0] return self.create_machine_proprio(cont=cont, proprio=associationCrans) def has_right(self, list, obj=None): """Vérifie que l'un des droits de l'utilisateur courant est inclus dans list""" if obj: droits = obj.rights() else: droits = self.conn.droits for d in list: if d in droits: return True return False @tailcaller def menu_principal(self, tag=None, machine=None, proprio=None): """Menu principal de l'application affiché au lancement""" a = attributs menu_droits = { 'default' : [a.cableur, a.nounou], 'aKM' : [a.nounou], } menu = { 'aA' : {'text':"Inscrire un nouvel adhérent", 'callback': self.create_adherent,}, 'mA' : {'text':"Modifier l'inscription d'un adhérent", 'callback': self.modif_adherent, 'help':"Changer la chambre, la remarque, la section, la carte d'étudiant ou précâbler."}, 'aMA': {'text':"Ajouter une machine à un adhérent", 'callback': self.create_machine_adherent}, #'dA' : {'text':"Détruire un adhérent", 'callback': self.delete_adherent, 'help':"Suppression de l'adhérent ainsi que de ses machines"}, 'mM' : {'text':"Modifier une machine existante", 'callback': self.modif_machine, 'help':"Changer le nom ou la MAC d'une machine."}, #'dM' : {'text':"Détruire une machine", 'callback': self.delete_machine}, 'aC' : {'text':"Inscrire un nouveau club", 'callback': self.create_club}, 'mC' : {'text':"Modifier un club", 'callback': self.modif_club}, 'aMC': {'text':"Ajouter une machine à un club", 'callback': self.create_machine_club}, #'dC' : {'text':"Détruire un club", 'callback': self.delete_club}, 'aKM': {'text':"Ajouter une machine à l'association", 'callback': self.create_machine_crans}, '' : {'text':"---------------------------------------",'callback': None}, } ### Les clef qui n'existe pas sont toute renvoyé sur la clef '' #menu_order = ["aA", "mA", "aMA", "dA", "", "mM", "dM", " ", "aC", "mC", "aMC", "dC", " ", "aKM"] #menu_order = ["aA", "mA", "aMA", "", "mM", " ", "aC", "mC", "aMC", " ", "aKM"] menu_order = ["aA", "mA", "aMA", "", "mM", "", "aC", "mC", "aMC", "", "aKM"] if machine and not proprio: proprio = machine.proprio() if isinstance(proprio, objets.AssociationCrans): proprio = None if machine or proprio: menu_order = [''] + menu_order if machine: menu_machine = { 'mMc' : { 'text':"Modifier la machine %s" % machine['host'][0], 'callback': TailCall(self.modif_machine, machine=machine), 'help':"Changer le nom ou la MAC d'une machine." }, } menu_machine_order = ['mMc'] menu.update(menu_machine) menu_order = menu_machine_order + menu_order if proprio: menu_adherent = { 'mAc' : { 'text':"Modifier l'inscription de %s" % proprio.get("cn", proprio["nom"])[0], 'callback': TailCall(self.modif_adherent, adherent=proprio) }, 'aMc' : { 'text':"Ajouter une machine à %s" % proprio.get("cn", proprio["nom"])[0], 'callback': TailCall(self.create_machine_adherent, adherent=proprio) }, } menu_club = { 'mCc' : { 'text':"Modifier l'inscription de %s" % proprio.get("cn", proprio["nom"])[0], 'callback': TailCall(self.modif_club, club=proprio) }, 'aMc' : { 'text':"Ajouter une machine à %s" % proprio.get("cn", proprio["nom"])[0], 'callback': TailCall(self.create_machine_club, club=proprio) }, } if 'adherent' in proprio['objectClass']: menu_proprio = menu_adherent menu_proprio_order = ['mAc', 'aMc'] elif 'club' in proprio['objectClass']: menu_proprio = menu_club menu_proprio_order = ['mCc', 'aMc'] else: raise EnvironmentError("Je ne connais que des adherents et des club comme proprio") menu.update(menu_proprio) menu_order = menu_proprio_order + menu_order def box(default_item=None): choices = [] for key in menu_order: if self.has_right(menu_droits.get(key, menu_droits['default'])): choices.append((key, menu[key]['text'], menu[key].get('help', ""))) while choices[-1][0] == '': choices=choices[:-1] while choices[0][0] == '': choices=choices[1:] return self.dialog.menu( "Que souhaitez vous faire ?", width=0, height=0, menu_height=0, item_help=1, default_item=str(default_item), title="Menu principal", scrollbar=True, timeout=self.timeout, cancel_label="Quitter", backtitle=u"Vous êtes connecté en tant que %s" % self.conn.current_login, choices=choices) (code, tag) = self.handle_dialog(TailCall(handle_exit_code, self.dialog, self.dialog.DIALOG_ESC), box, tag) self_cont = TailCall(self.menu_principal, tag=tag, proprio=proprio, machine=machine) callback = menu.get(tag, menu[''])['callback'] if handle_exit_code(self.dialog, code) and callback: return TailCall(callback, cont=TailCall(self.menu_principal, tag=tag, machine=machine, proprio=proprio)) else: return self_cont @TailCaller def main(gc, cont=None): """ Fonction principale gérant l'appel au menu principal, le timeout des écrans dialog et les reconnexion a ldap en cas de perte de la connexion """ while True: try: # tant que le timeout est atteint on revient au menu principal gc.menu_principal() except DialogError as e: # Si l'erreur n'est pas due à un timeout, on la propage if time.time() - gc.dialog_last_access < gc.timeout: raise # Si on perd la connexion à ldap, on en ouvre une nouvelle except ldap.SERVER_DOWN: if gc.dialog.pause(title="Erreur", duration=10, height=9, width=50, text="La connection au serveur ldap à été perdue.\nTentative de reconnexion en cours…") == gc.dialog.DIALOG_OK: gc.check_ldap() else: return if __name__ == '__main__': main(GestCrans()) os.system('clear')