From 2eeac7608c51451b88f7521def7d57b49d9157b5 Mon Sep 17 00:00:00 2001 From: Valentin Samir Date: Tue, 25 Mar 2014 11:55:37 +0100 Subject: [PATCH] [gest_crans_lc] Docstring et gestion des permission sur les shells MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Seul les nounou peuvent éditer les shells restrictif, les cableurs peuvent éditer les shells standard --- gestion/gest_crans_lc.py | 128 ++++++++++++++++++++++++++++++++------- 1 file changed, 106 insertions(+), 22 deletions(-) diff --git a/gestion/gest_crans_lc.py b/gestion/gest_crans_lc.py index 63728c74..01cc5dbb 100755 --- a/gestion/gest_crans_lc.py +++ b/gestion/gest_crans_lc.py @@ -47,6 +47,7 @@ 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: @@ -145,7 +146,12 @@ class TailCall(object) : 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): - ''' Patch the deepcopy dispatcher to pass modules back unchanged ''' + ''' + 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 @@ -156,6 +162,11 @@ class TailCall(object) : 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) : @@ -164,7 +175,7 @@ class TailCall(object) : return call(*self.args, **self.kwargs) def unicode_of_Error(x): - """Formatte l'exception de type ValueError""" + """Formatte des exception""" return u"\n".join(unicode(i, 'utf-8') if type(i) == str else repr(i) for i in x.args) @@ -173,8 +184,8 @@ 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 ?" + #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: @@ -188,6 +199,7 @@ def handle_exit_code(d, code): 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): @@ -212,6 +224,7 @@ class GestCrans(object): 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 @@ -251,6 +264,9 @@ class GestCrans(object): _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() @@ -260,6 +276,9 @@ class GestCrans(object): 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) @@ -272,6 +291,11 @@ class GestCrans(object): @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: @@ -296,7 +320,14 @@ class GestCrans(object): @tailcaller def handle_dialog_result(self, code, output, cancel_cont, error_cont, codes_todo=[]): - """ codes_todo = [(code, todo, todo_args)]""" + """ + 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) @@ -845,6 +876,7 @@ class GestCrans(object): text + printing.sprint(item, **params) + text_bottom, no_collapse=True, colors=True, + no_mouse=True, timeout=self.timeout, title=title, defaultno=defaultno, @@ -1143,6 +1175,10 @@ les valeurs valident sont : @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: @@ -1255,6 +1291,7 @@ les valeurs valident sont : 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"]: @@ -1267,7 +1304,7 @@ les valeurs valident sont : 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) @@ -1310,6 +1347,7 @@ les valeurs valident sont : ) 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) @@ -1369,7 +1407,7 @@ les valeurs valident sont : 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) @@ -1491,7 +1529,7 @@ les valeurs valident sont : 'csr':'Ajouter une nouvelle requête de certificat', } menu_order = ['new', 'priv', 'csr'] - menu_special = ['new', 'priv', 'csr'] + menu_special = ['new', 'priv', 'csr'] def box(default_item=None): index=0 choices = [] @@ -1646,6 +1684,7 @@ les valeurs valident sont : ) 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], @@ -1744,6 +1783,7 @@ les valeurs valident sont : 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 @@ -1836,7 +1876,7 @@ les valeurs valident sont : 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 = ' ' @@ -1954,11 +1994,13 @@ les valeurs valident sont : ) 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 ?", @@ -2019,6 +2061,7 @@ les valeurs valident sont : ) @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: @@ -2118,6 +2161,7 @@ les valeurs valident sont : ) 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], @@ -2165,6 +2209,7 @@ les valeurs valident sont : @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: @@ -2264,7 +2309,7 @@ les valeurs valident sont : @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) = checkpass(password, dialog=True) if not good: @@ -2338,6 +2383,7 @@ les valeurs valident sont : ) 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 @@ -2347,20 +2393,41 @@ les valeurs valident sont : 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(): - return self.dialog.radiolist( + # 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 [(s, shells[s]['desc'], 1 if s == shell else 0) for s in shells_order], + 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 != output: + 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() @@ -2381,6 +2448,7 @@ les valeurs valident sont : ) 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 @@ -2391,6 +2459,7 @@ les valeurs valident sont : "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}, @@ -2418,7 +2487,7 @@ les valeurs valident sont : else: menu_order.append("Désactiver") menu_order.extend(['MailAlias', "Shell", "Password", "Supprimer"]) - else: + else: menu_order.append("Créer") def box(default_item=None): return self.dialog.menu( @@ -2433,7 +2502,7 @@ les valeurs valident sont : 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[key], proprio)]) + 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: @@ -2457,6 +2526,7 @@ les valeurs valident sont : ) def adherent_droits(self, adherent, cont, choices_values=None): + """Gestion des droits d'un adhérent""" def box(): return self.dialog.checklist( text="", @@ -2484,12 +2554,10 @@ les valeurs valident sont : codes_todo=[([self.dialog.DIALOG_OK], todo, [droits, adherent, self_cont, cont])] ) - def modif_proprio_blacklist(self, proprio, cont): - self.dialog.msgbox("todo", width=0, height=0) - return 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'], @@ -2530,6 +2598,7 @@ les valeurs valident sont : @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", @@ -2630,7 +2699,7 @@ les valeurs valident sont : 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: @@ -2644,7 +2713,7 @@ les valeurs valident sont : # 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]) + 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) @@ -2668,6 +2737,7 @@ les valeurs valident sont : ) 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)) @@ -2721,10 +2791,12 @@ les valeurs valident sont : 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: @@ -2742,7 +2814,7 @@ les valeurs valident sont : 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."}, @@ -2840,7 +2912,12 @@ les valeurs valident sont : 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 @@ -2849,12 +2926,19 @@ def main(gc, cont=None): # Si l'erreur n'est pas due à un timeout, on la propage if time.time() - gc.dialog_last_access < gc.timeout: raise + else: + try: + gc.nyan(TailCall(lambda:None)) + except Continue: + pass # 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') +