1289 lines
65 KiB
Python
Executable file
1289 lines
65 KiB
Python
Executable file
#!/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 time
|
|
import ldap
|
|
import signal
|
|
import inspect
|
|
import tempfile
|
|
import collections
|
|
sys.path.append('/usr/scripts/')
|
|
from pythondialog import Dialog
|
|
|
|
from gestion.affich_tools import get_screen_size, coul
|
|
import gestion.config as config
|
|
|
|
import lc_ldap.shortcuts
|
|
import lc_ldap.objets
|
|
import lc_ldap.attributs as attributs
|
|
import lc_ldap.printing as printing
|
|
|
|
debugf=None
|
|
debug_enable = False
|
|
|
|
def mydebug(txt):
|
|
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 __call__(self, *args, **kwargs):
|
|
self.kwargs.update(kwargs)
|
|
self.args = self.args + args
|
|
return self
|
|
|
|
def handle(self) :
|
|
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 l'exception de type ValueError"""
|
|
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 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
|
|
|
|
|
|
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):
|
|
if not debug_enable:
|
|
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)
|
|
|
|
# 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
|
|
|
|
self.dialog = Dialog()
|
|
|
|
@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,
|
|
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)
|
|
|
|
(code, tag) = box()
|
|
retry_cont = TailCall(self.edit_blacklist_select, obj=obj, title=title, cont=cont)
|
|
if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC):
|
|
raise Continue(cont)
|
|
elif code in [self.dialog.DIALOG_OK]:
|
|
if tag == 'new':
|
|
return tag, {'debut':0, 'fin':0, 'type':'', 'comm':''}
|
|
else:
|
|
try:
|
|
bl = {}
|
|
bl.update(obj['blacklist'][int(tag)].value)
|
|
return tag, bl
|
|
except Exception as e:
|
|
self.dialog.msgbox("%s" % unicode_of_Error(e), title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(retry_cont(values=values))
|
|
else:
|
|
self.dialog.msgbox("Le code de retour dialog est %s, c'est étrange" % code, title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(retry_cont)
|
|
|
|
@tailcaller
|
|
def edit_blacklist_type(self, title, cont):
|
|
"""Permet de choisir un type de blackliste pour les nouvelles blacklistes"""
|
|
def box():
|
|
return self.dialog.menu(
|
|
"Choisissez un type de blacklist",
|
|
width=0,
|
|
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=[(k,v) for (k,v) in config.blacklist_items.items()])
|
|
(code, tag) = box()
|
|
retry_cont = TailCall(self.edit_blacklist_type, title=title, cont=cont)
|
|
if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC):
|
|
raise Continue(cont)
|
|
elif code in [self.dialog.DIALOG_OK]:
|
|
if tag in config.blacklist_items:
|
|
return tag
|
|
else:
|
|
self.dialog.msgbox("%s n'est pas une blacklist" % tag, title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(retry_cont(values=values))
|
|
else:
|
|
self.dialog.msgbox("Le code de retour dialog est %s, c'est étrange" % code, title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(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, 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, 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)
|
|
if bl_type is None and tag == 'new':
|
|
bl['type'] = self.edit_blacklist_type(title, self_cont)
|
|
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) == 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(cont(**{update_obj:obj}))
|
|
# On propage les Continue
|
|
except Continue:
|
|
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), title="Erreur rencontrée", width=73)
|
|
raise Continue(self_cont)
|
|
else:
|
|
raise Continue(self_cont(bl=None))
|
|
|
|
# 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))
|
|
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 ?", 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(cont(**{update_obj:obj}))
|
|
# On propage les Continue
|
|
except Continue:
|
|
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), title="Erreur rencontrée", width=73)
|
|
raise Continue(self_cont)
|
|
else:
|
|
raise Continue(self_cont(bl=None))
|
|
|
|
|
|
|
|
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,
|
|
list_height=7,
|
|
choices=choices,
|
|
title=title)
|
|
|
|
# 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)
|
|
# 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(cont)
|
|
# Sinon, c'est OK
|
|
elif code in [self.dialog.DIALOG_OK]:
|
|
# 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)
|
|
try:
|
|
# 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 propage les Continue
|
|
except Continue:
|
|
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), title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(retry_cont(values=values))
|
|
else:
|
|
self.dialog.msgbox("Le code de retour dialog est %s, c'est étrange" % code, title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(retry_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,), title=title, default_item=str(default_tag))
|
|
if code == self.dialog.DIALOG_EXTRA:
|
|
output = output.split(' ', 2)[1:]
|
|
else:
|
|
output = ''
|
|
return (code, output)
|
|
|
|
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) = box(values, tag)
|
|
# Si on a fait annulé ou appuyé sur ESC
|
|
if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC):
|
|
raise Continue(cont)
|
|
# Si on a éditer ou ajouté une valeur de/dans la liste de l'attribut
|
|
elif code in [self.dialog.DIALOG_EXTRA]:
|
|
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))
|
|
# Si on valide les changement affectué, on sauvegarde les nouvelles valeurs dans l'objet
|
|
elif code in [self.dialog.DIALOG_OK]:
|
|
try:
|
|
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}))
|
|
except Continue:
|
|
raise
|
|
except (Exception, ldap.OBJECT_CLASS_VIOLATION) as e:
|
|
self.dialog.msgbox("%s" % unicode_of_Error(e), title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(retry_cont)
|
|
# Si dialog retourne un code inconnu, on le signale
|
|
else:
|
|
self.dialog.msgbox("Le code de retour dialog est %s, c'est étrange" % code, title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(retry_cont)
|
|
|
|
def search(self, objectClassS, title, values={}, cont=None):
|
|
"""
|
|
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]},
|
|
'Prenom' : {'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, 20, 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', 'Prenom', 'Téléphone', 'Chambre', 'aid', 'mail']
|
|
select_machine = ['Host', 'Mac', 'IP', 'mid']
|
|
def box():
|
|
# On met les argument à dialog à la main ici, sinon, c'est difficile de choisir comment mettre une seconde colone
|
|
cmd = ["--form", "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']])
|
|
cmd.extend(['Machine :'] + [str(e) for e in select_dict['Machine']['params']])
|
|
for key in select_machine:
|
|
cmd.extend(['%s :' % key] + [str(e) for e in select_dict[key]['params']])
|
|
cmd.extend(["Les champs vides sont ignorés.", '7', '1', "", '0', '0', '0', '0'])
|
|
# On utilise quand même la fonction de la bibliothèques pour passer les arguments
|
|
(code, output) = self.dialog._perform(*(cmd,), 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) = 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'] 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'] 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'] ]
|
|
|
|
def select_one(self, items, title, default_item=None, cont=None):
|
|
"""Fait selectionner un item parmis une liste d'items à l'utisateur"""
|
|
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]
|
|
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 olist.keys():
|
|
items.extend(olist[c])
|
|
items_s = printing.sprint_list(olist[c], col-20).encode('utf-8').split('\n')
|
|
choices.append(("", str(items_s[0])))
|
|
choices.append(("", str(items_s[1])))
|
|
for i in items_s[2:]:
|
|
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])
|
|
|
|
(code, tag) = self.dialog.menu(
|
|
"Que souhaitez vous faire ?",
|
|
width=0,
|
|
height=0,
|
|
menu_height=0,
|
|
item_help=0,
|
|
default_item=str(default_tag),
|
|
title=title,
|
|
scrollbar=True,
|
|
choices=choices,
|
|
colors=True)
|
|
|
|
# Si l'utilisateur à annulé, on coninue avec la continuation
|
|
if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC):
|
|
raise Continue(cont)
|
|
# Si l'utilisateur n'a pas choisis une ligne correspondant à quelque chose
|
|
elif not tag:
|
|
self.dialog.msgbox("Merci de choisir l'un des item de la liste ou d'annuler", title="Sélection", width=0, height=0)
|
|
raise Continue(TailCall(self.select_one, items=items, title=title, default_item=default_item, cont=cont))
|
|
# Sinon on retourne l'item choisis
|
|
elif self.confirm_item(items[int(tag)], title):
|
|
return items[int(tag)]
|
|
else:
|
|
raise Continue(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, init=init)
|
|
if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC):
|
|
raise Continue(cont)
|
|
elif force and not output:
|
|
self.dialog.msgbox("Entrée vide, merci d'indiquer quelque chose")
|
|
raise Continue(TailCall(self.get_comment, title=title, text=text, cont=cont, force=force))
|
|
else:
|
|
return unicode(output, 'utf-8')
|
|
|
|
def confirm_item(self, item, title, defaultno=False, **params):
|
|
"""Affiche un item et demande si c'est bien celui là que l'on veux (supprimer, éditer, créer,...)"""
|
|
return self.dialog.yesno(
|
|
printing.sprint(item, **params),
|
|
no_collapse=True,
|
|
colors=True,
|
|
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,
|
|
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):
|
|
"""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)
|
|
# S'il n'y a pas de résultas, on recommence
|
|
if not items:
|
|
self.dialog.msgbox("Aucun Résultat", title="Recherche", width=0, height=0)
|
|
raise Continue(TailCall(self.select, objectClassS=objectClassS, title=title, values=values, 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, 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, cont=cont))
|
|
except Continue:
|
|
raise
|
|
except Exception as e:
|
|
self.dialog.msgbox("%r" % e, title="Erreur rencontrée", width=0, height=0)
|
|
raise Continue(TailCall(self.select, objectClassS=objectClassS, title=title, values=values, cont=cont))
|
|
|
|
def machine_information(self, cont, machine=None, objectClass=None, parent=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 parent, objectClass et realm.
|
|
Si on ne fait qu'éditer une machine, parent, 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 = ' '
|
|
|
|
if machine:
|
|
objectClass = machine["objectClass"][0]
|
|
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]
|
|
|
|
(code, tags) = self.dialog.form(
|
|
text="",
|
|
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")
|
|
|
|
if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC):
|
|
raise Continue(cont)
|
|
|
|
# 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 = []
|
|
for ((a,l),values) in zip(to_display, tags):
|
|
fields_values.append(("%s :" % a.legend, values, l))
|
|
try:
|
|
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 quelqes vérification de syntaxe
|
|
if a.ldap_name == 'host':
|
|
# Si c'est une machine wifi, host doit finir par wifi.crans.org
|
|
if "machineWifi" == objectClass:
|
|
hostend = ".wifi.crans.org"
|
|
# Si c'est une machine wifi, host doit finir par crans.org
|
|
elif "machineFixe" == objectClass:
|
|
hostend = ".crans.org"
|
|
# 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 values.endswith(hostend) and not '.' in values:
|
|
values = "%s.wifi.crans.org" % values
|
|
elif values.endswith(hostend) and '.' in values[:-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 values.endswith(hostend) and '.' in values:
|
|
raise ValueError("Nom d'hôte invalide, devrait finir par %s et être sans point dans la première partie" % hostend)
|
|
|
|
attrs[a.ldap_name]=values
|
|
if machine:
|
|
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()
|
|
else:
|
|
if parent is None or realm is None:
|
|
raise EnvironmentError("On essaye de créer une machine mais parent ou realm vaut None")
|
|
ldif = {
|
|
'macAddress': ['%s' % attrs['macAddress'].encode('utf-8')],
|
|
'host': ['%s' % attrs['host'].encode('utf-8')]
|
|
}
|
|
with self.conn.newMachine(parent.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)
|
|
else:
|
|
raise Continue(cont)
|
|
return cont(machine=machine)
|
|
except (Exception, ldap.OBJECT_CLASS_VIOLATION) as e:
|
|
self.dialog.msgbox("%s" % unicode_of_Error(e), title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(TailCall(self.machine_information, machine=machine, cont=cont, objectClass=objectClass, parent=parent, realm=realm, fields_values=fields_values))
|
|
|
|
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="",
|
|
height=0, width=0, form_height=0,
|
|
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")
|
|
|
|
(code, output) = box(values)
|
|
if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC):
|
|
raise Continue(cont)
|
|
else:
|
|
values = dict(zip([form[k]['ldap_name'] for k in form_order], output))
|
|
fields_values = [("%s : " % k, values[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)
|
|
try:
|
|
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))
|
|
except Continue:
|
|
raise
|
|
except Exception as e:
|
|
self.dialog.msgbox("%s" % (unicode_of_Error(e)), title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(self_cont)
|
|
|
|
def create_certificat(self, machine, cont):
|
|
"""Permet d'ajouter un certificat à une machine à partir du PEM du 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,
|
|
backtitle="Appuyez sur CTRL+MAJ+V pour coller",
|
|
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
|
|
|
|
(code, pem) = box()
|
|
if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC):
|
|
raise Continue(cont)
|
|
else:
|
|
try:
|
|
with self.conn.newCertificat(machine.dn, {}) as certificat:
|
|
certificat['certificat'] = unicode(pem, 'utf-8')
|
|
certificat.create()
|
|
raise Continue(cont(certificat=certificat, machine=certificat.machine()))
|
|
except Continue:
|
|
raise
|
|
except Exception as e:
|
|
self.dialog.msgbox("%s" % unicode_of_Error(e), title="Erreur rencontrée", width=73, height=10)
|
|
self_cont = TailCall(self.create_certificat, machine=machine, cont=cont)
|
|
raise Continue(self_cont)
|
|
|
|
def delete_certificat(self, certificat, cont):
|
|
"""Supprime un certificat"""
|
|
if self.confirm_item(item=certificat, title="Voulez vous vraiement supprimer le certificat ?"):
|
|
try:
|
|
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é", title="Suppression d'un certificat")
|
|
raise Continue(cont(certificat=None, machine=certificat.machine(refresh=True)))
|
|
except Continue:
|
|
raise
|
|
except Exception as e:
|
|
self.dialog.msgbox("%s" % unicode_of_Error(e), title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(cont)
|
|
else:
|
|
raise Continue(cont(certificat=certificat))
|
|
|
|
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))
|
|
certificat = machine.certificats()[certificat_index]
|
|
menu = {
|
|
'Hostname' : {'text':"Noms d'hôte utilisant le certificat", "attribut":attributs.hostCert},
|
|
'TLSA' : {'text':"Paramètres pour les champs dns TLSA", "callback":self.certificat_tlsa},
|
|
'Autre': {'text' : "Modifier les attribut booléen comme revocked", "callback":self.modif_certificat_boolean},
|
|
'Supprimer' : {'text' : "Supprimer le certificat", "callback":self.delete_certificat},
|
|
}
|
|
menu_order = ['Hostname', 'TLSA', 'Autre', 'Supprimer']
|
|
def box(default_item=None):
|
|
return self.dialog.menu(
|
|
"Paramètres pour le certificat N°0x%X émis par %s, valable du %s au %s" % (
|
|
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])))
|
|
),
|
|
width=0,
|
|
height=0,
|
|
menu_height=0,
|
|
item_help=0,
|
|
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']) for key in menu_order])
|
|
(code, tag) = box(tag)
|
|
if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC):
|
|
if certificat is None:
|
|
raise Continue(cont(machine=machine))
|
|
else:
|
|
raise Continue(self_cont(certificat=None))
|
|
elif 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)
|
|
|
|
@tailcaller
|
|
def edit_certificat_select(self, machine, title, cont):
|
|
"""Permet de choisir un certificat existant ou nouveau d'une machine"""
|
|
def box(default_item=None):
|
|
index=0
|
|
choices = [('new', 'Ajouter un nouveau certificat')]
|
|
for cert in machine.certificats():
|
|
choices.append((str(index), "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]))))))
|
|
index+=1
|
|
return self.dialog.menu(
|
|
"Modifier ou ajouter un certificat ?",
|
|
width=0,
|
|
height=0,
|
|
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)
|
|
|
|
(code, tag) = box()
|
|
retry_cont = TailCall(self.edit_certificat_select, machine=machine, title=title, cont=cont)
|
|
if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC):
|
|
raise Continue(cont)
|
|
elif code in [self.dialog.DIALOG_OK]:
|
|
if tag == 'new':
|
|
return tag
|
|
else:
|
|
return int(tag)
|
|
else:
|
|
self.dialog.msgbox("Le code de retour dialog est %s, c'est étrange" % code, title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(retry_cont)
|
|
return cont
|
|
|
|
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_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 = {
|
|
'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},
|
|
}
|
|
menu_order = ['Information', 'Blackliste', 'Certificat', 'Alias', 'Exemption', 'SshKey', 'Autre', 'Remarques']
|
|
def box(default_item=None):
|
|
return self.dialog.menu(
|
|
"Que souhaitez vous modifier ?",
|
|
width=0,
|
|
height=0,
|
|
menu_height=0,
|
|
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])
|
|
(code, tag) = box(tag)
|
|
if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC):
|
|
raise Continue(cont(machine=machine))
|
|
elif not tag in menu_order:
|
|
raise Continue(TailCall(self.modif_machine, cont=cont, machine=machine, tag=tag))
|
|
else:
|
|
cont_ret = TailCall(self.modif_machine, cont=cont, machine=machine, tag=tag)
|
|
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)
|
|
|
|
def create_machine_adherent(self, cont, adherent=None, tag=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)
|
|
menu = {
|
|
'Fixe' : {'text' : "Machine filaire", 'objectClass':'machineFixe', 'realm':'adherents'},
|
|
'Wifi' : {'text': 'Machine sans fil', 'objectClass':'machineWifi', 'realm':'wifi-adh'},
|
|
}
|
|
menu_order = ['Fixe', 'Wifi']
|
|
def box(default_item=None):
|
|
return self.dialog.menu(
|
|
"Type de Machine ?",
|
|
width=0,
|
|
height=0,
|
|
menu_height=0,
|
|
item_help=0,
|
|
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])
|
|
(code, tag) = box(tag)
|
|
if code in (self.dialog.DIALOG_CANCEL, self.dialog.DIALOG_ESC):
|
|
raise Continue(cont(proprio=adherent))
|
|
elif not tag in menu_order:
|
|
raise Continue(TailCall(self.create_machine_adherent, cont=cont, adherent=adherent, tag=tag))
|
|
else:
|
|
return self.machine_information(
|
|
cont=cont(proprio=adherent),
|
|
machine=None,
|
|
objectClass=menu[tag]['objectClass'],
|
|
parent=adherent,
|
|
realm=menu[tag]['realm']
|
|
)
|
|
|
|
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)
|
|
if self.confirm_item(item=machine, title="Voulez vous vraiement supprimer la machine ?", defaultno=True):
|
|
try:
|
|
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", title="Suppression d'une machine")
|
|
raise Continue(cont(machine=None))
|
|
except Continue:
|
|
raise
|
|
except Exception as e:
|
|
self.dialog.msgbox("%s" % unicode_of_Error(e), title="Erreur rencontrée", width=73, height=10)
|
|
raise Continue(cont)
|
|
else:
|
|
raise Continue(cont)
|
|
return cont
|
|
|
|
|
|
def modif_adherent(self, cont, adherent=None):
|
|
if adherent is None:
|
|
adherent = self.select(["adherent"], "Recherche d'un adhérent pour modification", cont=cont)
|
|
self.dialog.msgbox("todo", width=0, height=0)
|
|
return cont(proprio=adherent)
|
|
|
|
def create_adherent(self, cont):
|
|
self.dialog.msgbox("todo", width=0, height=0)
|
|
return cont
|
|
|
|
def delete_adherent(self, cont):
|
|
adherent = self.select(["adherent"], "Recherche d'un adhérent pour supression", cont=cont)
|
|
self.dialog.msgbox("todo", width=0, height=0)
|
|
return cont
|
|
|
|
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):
|
|
self.dialog.msgbox("todo", width=0, height=0)
|
|
return cont
|
|
|
|
def create_machine_club(self, cont):
|
|
self.dialog.msgbox("todo", width=0, height=0)
|
|
return cont
|
|
|
|
def create_machine_crans(self, cont):
|
|
self.dialog.msgbox("todo", width=0, height=0)
|
|
return cont
|
|
|
|
def create_borne(self, cont):
|
|
self.dialog.msgbox("todo", width=0, height=0)
|
|
return cont
|
|
|
|
@tailcaller
|
|
def menu_principal(self, tag=None, machine=None, proprio=None):
|
|
"""Menu principal de l'application affiché au lancement"""
|
|
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},
|
|
'aKB': {'text':"Ajouter une borne wifi", 'callback': self.create_borne},
|
|
'' : {'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", "aKB"]
|
|
|
|
if machine and not proprio:
|
|
proprio = machine.proprio()
|
|
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_proprio = {
|
|
'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_proprio_order = ['mAc', 'aMc']
|
|
menu.update(menu_proprio)
|
|
menu_order = menu_proprio_order + menu_order
|
|
def box(default_item=None):
|
|
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,
|
|
cancel_label="Quitter",
|
|
backtitle=u"Vous êtes connecté en tant que %s" % self.conn.current_login,
|
|
choices=[(key, menu.get(key, menu[''])['text'], menu.get(key, menu['']).get('help', "")) for key in menu_order])
|
|
|
|
(code, tag) = box(tag)
|
|
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 TailCall(self.menu_principal, tag=tag, proprio=proprio, machine=machine)
|
|
|
|
if __name__ == '__main__':
|
|
GestCrans().menu_principal()
|
|
os.system('clear')
|