#!/usr/bin/env python # -*- coding: utf-8 -*- # """ Définition des classes permettant d'instancier les objets LDAP. """ # # Copyright (C) 2010-2013 Cr@ns # Authors: Antoine Durand-Gasselin # Nicolas Dandrimont # Olivier Iffrig # Valentin Samir # Daniel Stan # Vincent Le Gallic # Pierre-Elliott Bécue # # Redistribution and use in source and binary forms, with or without # modification, are permitted provided that the following conditions are met: # * Redistributions of source code must retain the above copyright # notice, this list of conditions and the following disclaimer. # * Redistributions in binary form must reproduce the above copyright # notice, this list of conditions and the following disclaimer in the # documentation and/or other materials provided with the distribution. # * Neither the name of the Cr@ns nor the names of its contributors may # be used to endorse or promote products derived from this software # without specific prior written permission. # # THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS # "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT # LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR # A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, # EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, # PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR # PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF # LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING # NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE OF THIS # SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. ## import de la lib standard import os import sys import re import datetime import time import ldap import random import traceback from ldap.modlist import addModlist, modifyModlist ## import locaux import lc_ldap import crans_utils import attributs import ldap_locks import variables import printing ## import de /usr/scripts/ if not "/usr/scripts/" in sys.path: sys.path.append('/usr/scripts/') import gestion.config as config import gestion.config.impression import cranslib.deprecated #: Champs à ignorer dans l'historique HIST_IGNORE_FIELDS = ["modifiersName", "entryCSN", "modifyTimestamp", "historique"] def new_cransldapobject(conn, dn, mode='ro', uldif=None, lockId=None): """Crée un objet :py:class:`CransLdapObject` en utilisant la classe correspondant à l'``objectClass`` du ``ldif`` --pour usage interne à la librairie uniquement !""" classe = None if dn == variables.base_dn: classe = AssociationCrans elif dn == variables.invite_dn: classe = BaseInvites elif uldif: classe = ObjectFactory.get(uldif['objectClass'][0]) else: res = conn.search_s(dn, 0) if not res: raise ValueError ('objet inexistant: %s' % dn) _, attrs = res[0] classe = ObjectFactory.get(attrs['objectClass'][0]) try: _clas = classe(conn, dn, mode, uldif, lockId=lockId) except Exception as e: print "dn=%s,uldif=%r" % (dn, uldif) raise return _clas class CransLdapObject(object): """Classe de base des objets :py:class:`CransLdap`. Cette classe ne devrait pas être utilisée directement.""" """ Qui peut faire quoi ? """ __slots__ = ("in_context", "conn", "lockId", "attrs", "_modifs", "dn", "parent_dn", "mode") can_be_by = { variables.created: [attributs.nounou], variables.modified: [attributs.nounou], variables.deleted: [attributs.nounou], } attribs = [] ldap_name = "CransLdapObject" def update_attribs(self): """ Fonction permettant de mettre à jours attribs, dès que les valeurs des attributs ont été instancié """ pass def exists(self): """Renvois True si l'objet existe dans la base de donnée, False sinon""" try: if self.conn.search(dn=self.dn, scope=0): return True else: return False except ldap.NO_SUCH_OBJECT: return False def rights(self): """ Retourne les droits courant de l'utilisateur sur l'objet. Ces droits sont égaux aux droits de l'utilisateur plus : * soit si le dn de l'utilisateur est égal au dn de l'objet * parent si le dn de l'utilisateur est préfixe du dn de l'objet la méthode est en anglais pour ne pas interférer avec les attributs droits et jinja2 où les méthodes de l'objet et ses attributs sont appelé de la même manière """ return self.conn.droits + self.conn._check_parent(self.dn) + self.conn._check_self(self.dn) + self.conn._check_respo(self) def __init__(self, conn, dn, mode='ro', uldif=None, lockId=None): ''' Créée une instance d'un objet Crans (machine, adhérent, etc...) à ce ``dn``, si ``uldif`` est précisé, n'effectue pas de recherche dans la base ldap. ''' if not isinstance(conn, lc_ldap.lc_ldap): raise TypeError("conn doit être une instance de lc_ldap") self.in_context = False self.conn = conn if lockId: self.lockId = lockId else: self.lockId = '%s_%s' % (os.getpid(), id(self)) self.attrs = attributs.AttrsDict(conn, Parent=self) # Contient un dico ldif qui doit représenter ce qui # est dans la base. On attrify paresseusement au moment où on utilise un attribut self._modifs = {} # C'est là qu'on met les modifications self.dn = dn self.parent_dn = dn.split(',', 1)[1] if ',' in dn else '' orig = {} if uldif: self.attrs = attributs.AttrsDict(self.conn, uldif, Parent=self) self._modifs = attributs.AttrsDict(self.conn, uldif, Parent=self) else: res = self.conn.search_s(dn, 0) if not res: raise ValueError ('objet inexistant: %s' % dn) self.dn, ldif = res[0] # L'objet sortant de la base ldap, on ne fait pas de vérifications sur # l'état des données. uldif = lc_ldap.ldif_to_uldif(ldif) self.attrs = attributs.AttrsDict(self.conn, uldif, Parent=self) self._modifs = attributs.AttrsDict(self.conn, uldif, Parent=self) if dn == variables.base_dn: mode = 'ro' self.mode = mode if self.mode in ['w', 'rw']: if not self.may_be(variables.modified): # On ne peut pas modifier l'objet, on droppe silencieusement ici, et # on fera un raise quand la tentative de modif aura effectivement lieu. self.mode = 'ro' self.update_attribs() if self.mode in ['w', 'rw']: # Vérification que `λv. str(Attr(v))` est bien une projection # C'est-à-dire que si on str(Attr(str(Attr(v)))) on retombe sur str(Attr(v)) oldif = lc_ldap.ldif_to_uldif(self.attrs.to_ldif()) nldif = lc_ldap.ldif_to_uldif(attributs.AttrsDict(self.conn, lc_ldap.ldif_to_uldif(self.attrs.to_ldif()), Parent=self).to_ldif()) for attr, vals in oldif.items(): if nldif[attr] != vals: for v in nldif[attr]: if v in vals: vals.remove(v) nvals = [nldif[attr][vals.index(v)] for v in vals ] raise EnvironmentError("λv. str(Attr(v)) n'est peut-être pas une projection (ie non idempotente):", attr, nvals, vals) def _id(self): """Retourne l'id de l'objet courant""" if isinstance(self, adherent): return self['aid'] elif isinstance(self, machine): return self['mid'] elif isinstance(self, club): return self['cid'] elif isinstance(self, facture): return self['fid'] else: return [self.dn] def _out_of_context(self, *args, **kwargs): raise EnvironmentError("Hors du context, impossible de faire des écritures") def __exit__(self, exc_type, exc_value, traceback): # Sortie du context manager self.in_context = False # On rend les écriture impossible #self.save = self._out_of_context #self.create = self._out_of_context #self.delete = self._out_of_context # On retombe en read only self.mode = 'ro' # On purge les lock de l'objet self.conn.lockholder.purge(self.lockId) def __enter__(self): # On est dans un context, normalement, c'est locksafe self.in_context = True return self def __ne__(self, obj): return not self == obj def __eq__(self, obj): if isinstance(obj, self.__class__): if self.mode in ['w', 'rw']: return self.dn == obj.dn and self._modifs == obj._modifs and self.attrs == obj.attrs else: return self.dn == obj.dn and self.attrs == obj.attrs elif (isinstance(obj, str) or isinstance(obj, unicode)) and '=' in obj: attr, val = obj.split('=', 1) return attr in self.attrs.keys() and val in self[attr] else: return False def __hash__(self): if self.mode in ['w', 'rw']: raise TypeError("Mutable structure are not hashable, please use mode = 'ro' to do so") def c_mul(a, b): return eval(hex((long(a) * b) & 0xFFFFFFFFL)[:-1]) value = 0x345678 l=0 keys = self.keys() keys.sort() for key in keys: l+=len(self.attrs[key]) for item in self.attrs[key]: value = c_mul(1000003, value) ^ hash(item) value = value ^ l if value == -1: value = -2 return value def __iter__(self): if self.mode in [ 'w', 'rw' ]: return self._modifs.__iter__() else: return self.attrs.__iter__() def keys(self): if self.mode in [ 'w', 'rw' ]: return self._modifs.keys() else: return self.attrs.keys() def values(self): if self.mode in [ 'w', 'rw' ]: return self._modifs.values() else: return self.attrs.values() def items(self): if self.mode in [ 'w', 'rw' ]: return self._modifs.items() else: return self.attrs.items() def display(self, historique=5, blacklist=5): print printing.sprint(self, historique=historique, blacklist=blacklist) def history_add(self, login, chain): """Ajoute une ligne à l'historique de l'objet. ###ATTENTION : C'est un kludge pour pouvoir continuer à faire "comme avant", ### mais on devrait tout recoder pour utiliser l'historique LDAP""" assert isinstance(login, unicode) assert isinstance(chain, unicode) new_line = u"%s, %s : %s" % (time.strftime(attributs.historique.FORMAT), login, chain) self["historique"] = self.get("historique", []) + [new_line] def history_gen(self, attr=None, login=None): "Génère une ligne d'historique pour l'arribut attr ou une ligne par attributs pour l'objet courant" if attr is None: for attr in self.keys(): self.history_gen(attr) def partial_name(name, max_len=14, start=7, end=7): if len(name) > max_len: return "%s…%s" % (name[:start], name[-end:]) else: return name if login is None: login = self.conn.current_login if isinstance(attr, str) or isinstance(attr, unicode): attr = attributs.AttributeFactory.get(attr) elif isinstance(attr, Attr): attr = type(attr) elif issubclass(attr, Attr): pass else: raise ValueError("%r ne correspont pas a un attribut" % attr) if not attr.historique: return if attr.historique not in ["full", "partial", "info"]: raise ValueError("Format d'historique %s inconnu" % attr.historique) old_values = self.attrs.get(attr.ldap_name, []) new_values = self._modifs.get(attr.ldap_name, []) if old_values == new_values: return comm = None if attr.singlevalue: # modification if old_values and new_values: if attr.historique == "full": comm = u"%s (%s -> %s)" % (attr.ldap_name, old_values[0], new_values[0]) elif attr.historique == "partial": old = partial_name(str(old_values[0])) new = partial_name(str(new_values[0])) comm = u"%s (%s -> %s)" % (attr.ldap_name, old, new) elif attr.historique == "info": comm = u"%s" % attr.ldap_name # création elif not old_values and new_values: if attr.historique == "info": comm = u"+%s" % attr.ldap_name elif attr.historique in ["full", "partial"]: new = str(new_values[0]) if attr.historique == "partial": new = partial_name(new) comm = u"%s+%s" % (attr.ldap_name, new) # suppréssion elif not new_values and old_values: if attr.historique == "info": comm = u"-%s" % attr.ldap_name elif attr.historique in ["full", "partial"]: old = str(old_values[0]) if attr.historique == "partial": old = partial_name(old) comm = u"%s-%s" % (attr.ldap_name, old) else: added = [] deleted = [] if attr.historique == "partial": append = lambda x: partial_name(str(x)) else: append = lambda x: str(x) for a in new_values: if not a in old_values: added.append(append(a)) for a in old_values: if not a in new_values: deleted.append(append(a)) if attr.historique == "info": comm = u"%s%s%s" % ('+' if added else "", '-' if deleted else "", attr.ldap_name) elif attr.historique in ["full", "partial"]: comm = u"%s%s%s%s%s" % (attr.ldap_name, '+' if added else "", '+'.join(added), '-' if deleted else "", '-'.join(deleted)) if comm: new_line = u"%s, %s : %s" % (time.strftime(attributs.historique.FORMAT), login, comm) if not new_line in self["historique"]: self["historique"].append(new_line) else: raise ValueError("impossible de générer l'historique pour %s %s %s" % (attr, old_values, new_values)) def _check_optionnal(self, comment): """Vérifie que les attributs qui ne sont pas optionnels sont effectivement peuplés.""" objet = self.ldap_name for attribut in self.attribs: if not attribut.optional: nom_attr = attribut.ldap_name if len(self._modifs.get(nom_attr, [])) <= 0: raise attributs.OptionalError("L'objet %s que vous %s doit posséder au moins un attribut %s" % (objet, comment, nom_attr)) def _post_creation(self): """Fonction qui effectue quelques tâches lorsque la création est faite""" pass def _post_deletion(self): """Fonction qui effectue quelques tâches lorsque la création est faite""" pass def check_changes(self): """ Vérifie la consistence d'un objet """ pass def validate_changes(self): """ Après vérification, harmonise l'objet """ pass def create(self, login=None): """Crée l'objet dans la base ldap, cette méthode vise à faire en sorte que l'objet se crée lui-même, si celui qui essaye de le modifier a les droits de le faire.""" if not self.in_context: # forcer l'utilisation d'un context manager permet d'être certain que les locks seront libéré quoi qu'il arrive cranslib.deprecated.usage("Les nouveaux objets ne devrait être initialisés qu'avec des contexts managers (with func() as variable)", level=2) try: if login is None: login = self.conn.current_login self._check_optionnal(comment="créez") try: if self.conn.search(dn=self.dn): raise ValueError ('objet existant: %s' % self.dn) except ldap.NO_SUCH_OBJECT: pass binary = set() for attr in self.keys(): for attribut in self[attr]: attribut.check_uniqueness([]) if self[attr] and self[attr][0].binary: binary.add(attr) self.history_add(login, u"Inscription") ldif = self._modifs.to_ldif() for attr in binary: ldif['%s;binary' % attr]=ldif[attr] del(ldif[attr]) # Création de la requête LDAP modlist = addModlist(ldif) # Requête LDAP de création de l'objet try: self.conn.lockholder.check(self.lockId, delai=10) self.conn.add_s(self.dn, modlist) except Exception: print traceback.format_exc() return finally: # On nettoie les locks self.conn.lockholder.purge(self.lockId) # Services à relancer services.services_to_restart(self.conn, {}, self._modifs, created_object=[self]) self._post_creation() # Vérifications après insertion. self.check_modifs() def bury(self, comm, login): """Sauvegarde l'objet dans un fichier dans le cimetière.""" self.history_add(login, u"destruction (%s)" % comm) self.save() # On produit un ldif ldif = u"dn: %s\n" % self.dn for key in self.keys(): for value in self[key]: ldif += u"%s: %s\n" % (key, value) file = "%s %s" % (datetime.datetime.now(), self.dn) try: os.mkdir('%s/%s' % (lc_ldap.cimetiere.cimetiere_root, self['objectClass'][0])) except OSError: pass f = open('%s/%s/%s' % (lc_ldap.cimetiere.cimetiere_root, self['objectClass'][0], file.replace(' ', '_')), 'w') f.write(ldif.encode("UTF-8")) f.close() def delete(self, comm="", login=None): """Supprime l'objet de la base LDAP. Appelle :py:meth:`CransLdapObject.bury`.""" if login is None: login = self.conn.current_login if self.mode not in ['w', 'rw']: raise EnvironmentError("Objet en lecture seule, réessayer en lecture/écriture") if not self.may_be(variables.deleted): raise EnvironmentError("Vous n'avez pas le droit de supprimer %s." % self.dn) if not self.in_context: # forcer l'utilisation d'un context manager permet d'être certain que les locks seront libéré quoi qu'il arrive cranslib.deprecated.usage("La suppression d'un objet ne devrait être faite qu'avec des contexts managers (with func() as variable)", level=2) self.conn.lockholder.check(self.lockId, delai=10) self.bury(comm, login) self.conn.delete_s(self.dn) self.conn.lockholder.purge(self.lockId) self._post_deletion() services.services_to_restart(self.conn, self.attrs, {}, deleted_object=[self]) def save(self): """Sauvegarde dans la base les modifications apportées à l'objet. Interne: Vérifie que ``self._modifs`` contient des valeurs correctes et enregistre les modifications.""" if self.mode not in ['w', 'rw']: raise EnvironmentError("Objet en lecture seule, réessayer en lecture/écriture") if not self.in_context: # forcer l'utilisation d'un context manager permet d'être certain que les locks seront libéré quoi qu'il arrive cranslib.deprecated.usage("Les écritures sur un objet ne devrait être faite qu'avec des contexts managers (with func() as variable)", level=2) self._check_optionnal(comment="modifiez") # On récupère la liste des modifications modlist = self.get_modlist() try: self.conn.lockholder.check(self.lockId, delai=10) self.conn.modify_s(self.dn, modlist) self.conn.lockholder.purge(self.lockId) except Exception as error: self.cancel() raise EnvironmentError("Impossible de modifier l'objet: %r" % error) # On programme le redémarrage des services services.services_to_restart(self.conn, self.attrs, self._modifs) # Vérification des modifications. self.check_modifs() def cancel(self): """ Annule les changements en attente """ old_uldif = lc_ldap.ldif_to_uldif(self.conn.search_s(self.dn, ldap.SCOPE_BASE)[0][1]) self._modifs = attributs.AttrsDict(self.conn, old_uldif, Parent=self) # On nettoie les locks self.conn.lockholder.purge(self.lockId) def check_modifs(self): """ Fonction qui vérifie que les modifications se sont bien passées. """ # Vérification des modifications old_uldif = lc_ldap.ldif_to_uldif(self.conn.search_s(self.dn, ldap.SCOPE_BASE)[0][1]) self.attrs = attributs.AttrsDict(self.conn, old_uldif, Parent=self) differences = [] # On fait les différences entre les deux dicos for attr in set(self.attrs.keys()).union(set(self._modifs.keys())): exp_vals = set([str(i) for i in self.attrs.get(attr, [])]) new_vals = set([str(i) for i in self._modifs.get(attr, [])]) if exp_vals != new_vals: differences.append({"missing": exp_vals - new_vals, "having": new_vals - exp_vals}) print differences[-1] if differences: raise EnvironmentError("Les modifications apportées à l'objet %s n'ont pas été correctement sauvegardées\n%s" % (self.dn, differences)) def may_be(self, what, liste=None): """Teste si liste peut faire ce qui est dans what, pour what élément de {create, delete, modify}. On passe une liste de droits plutôt que l'objet car il faut ajouter les droits soi et parent. Retourne un booléen """ if liste is None: liste = self.rights() if set(liste).intersection(self.can_be_by[what]) != set([]): return True else: return False def get_modlist(self): """Renvoie un dictionnaire des modifications apportées à l'objet""" # unicode -> utf-8 binary = set() for attr in self.keys(): if self[attr] and self[attr][0].binary: binary.add(attr) ldif = self._modifs.to_ldif() orig_ldif = self.attrs.to_ldif() for attr in binary: ldif['%s;binary' % attr]=ldif[attr] orig_ldif['%s;binary' % attr]=orig_ldif.get(attr, []) del(ldif[attr]) try: del(orig_ldif[attr]) except KeyError: pass return modifyModlist(orig_ldif, ldif) def get(self, attr, default): """Renvoie l'attribut demandé ou default si introuvable""" try: return self.__getitem__(attr, default) except KeyError: return default def __getitem__(self, attr, default=None): if self._modifs.has_key(attr) and self.mode in [ 'w', 'rw' ]: return attributs.AttrsList(self, attr, [ v for v in self._modifs[attr] ]) elif self.attrs.has_key(attr): return attributs.AttrsList(self, attr, [ v for v in self.attrs[attr] ]) elif self.has_key(attr): return attributs.AttrsList(self, attr, []) if default is None else default else: raise KeyError(attr) def has_key(self, attr): """Est-ce que notre objet a l'attribut en question ?""" return attr in [attrib.__name__ for attrib in self.attribs] def _check_setitem(self, attr, values): """ Vérifie des contraintes non liées à LDAP lors d'un __setitem__, lève une exception si elles ne sont pas vérifiées """ pass def replace_id(self, attr, realm=None): ids = ["aid", "mid", "fid", "cid", "xid", "rid"] if attr not in ids or not attr in self.keys(): raise ValueError("On ne peut remplacer que les id suivant : %s" % ", ".join(attr for attr in ids if attr in self.keys())) id = self.conn._find_id(attr, realm, lockId=self.lockId) # Pas de lock sur l'unicité car _find_id vient d'en placer un self.__setitem__(attr, unicode(id), no_uniq_lock=True) def __setitem__(self, attr, values, no_uniq_lock=False, no_concurrent_lock=False): """Permet d'affecter des valeurs à l'objet comme s'il était un dictionnaire.""" # Quand on est pas en mode d'écriture, ça plante. if self.mode not in ['w', 'rw']: raise ValueError("Objet en lecture seule") if not self.has_key(attr): raise ValueError("L'objet que vous modifiez n'a pas d'attribut %s" % (attr)) # Les valeurs sont nécessairement stockées en liste if not isinstance(values, list): values = [values] # On génére une liste des attributs, le dictionnaire ldif # sert à permettre les vérifications de cardinalité # (on peut pas utiliser self._modifs, car il ne faut # faire le changement que si on peut) attrs_before_verif = [attributs.attrify(val, attr, self.conn, Parent=self) for val in values] # Methode de vérification diverse ayant pour but d'être surcharger # par les classes enfants. Ainsi, elle ne touche pas à __setitem__ # qui est assez sensible. self._check_setitem(attr, attrs_before_verif) old_attrs = self[attr] # On groupe les attributs précédents, et les nouveaux mixed_attrs = attrs_before_verif + old_attrs # Si c'est vide, on fait pas de vérifs, on avait une liste # vide avant, puis on en a une nouvelle après. if mixed_attrs: # Tests de droits. if not mixed_attrs[0].is_modifiable(self.conn.droits + self.conn._check_parent(self.dn) + self.conn._check_self(self.dn) + self.conn._check_respo(self)): raise EnvironmentError("Vous ne pouvez pas modifier l'attribut %r de l'objet %r." % (attr, self)) # On ajoute des locks sur les nouvelles valeurs locked = [] try: for attribut in attrs_before_verif: # Vérification que (attr, value) est localement unique if attrs_before_verif.count(attribut) > 1: raise ValueError("%s en double\n(%s)" % (attribut.legend if attribut.legend else attr, attribut)) # On lock les nouvelles valeurs globalement unique if not no_uniq_lock and attribut.unique and not attribut in self._modifs.get(attr, []) and not attribut in attribut.unique_exclue: if not self.in_context: cranslib.deprecated.usage("Des locks ne devrait être ajoutés que dans un context manager", level=2) self.conn.lockholder.addlock(attr, str(attribut), self.lockId) locked.append((attr, str(attribut), self.lockId)) # si attr doit être globalement unique, vérification de l'unicité globale # Dans ce cas, on ne tiens pas compte de old_attrs cas ils # vont être effacé si le setitem réussi attribut.check_uniqueness(old_attrs) # On lock si l'attribut ne supporte pas les modifications concurrente (comme pour le solde) si : # * on effectue réellement un modification sur l'attribut # * on a pas déjà effectuer un modification qui nous a déjà fait acquérir le lock if not no_concurrent_lock and not attributs.AttributeFactory.get(attr).concurrent and self._modifs.get(attr, []) == self.attrs.get(attr, []) and attrs_before_verif != self.attrs.get(attr, []): if not self.in_context: cranslib.deprecated.usage("Des locks ne devrait être ajoutés que dans un context manager", level=2) self.conn.lockholder.addlock("dn", "%s_%s" % (self.dn.replace('=', '-').replace(',','_'), attr), self.lockId) locked.append(("dn", "%s_%s" % (self.dn.replace('=', '-').replace(',','_'), attr), self.lockId)) try: # une fois le lock acquit, on vérifie que l'attribut n'a pas été édité entre temps if self.conn.search(dn=self.dn, scope=0)[0].get(attr, []) != self.attrs.get(attr, []): raise ldap_locks.LockError("L'attribut %s a été modifié dans la base ldap avant l'acquisition du lock" % attr) # L'objet n'existe pas dans la base ldap (resurection par exemple), donc pas de problème except ldap.NO_SUCH_OBJECT: pass except (ldap_locks.LockError, ValueError, attributs.UniquenessError): # Si on ne parvient pas à prendre le lock pour l'une des valeurs # on libère les locks pris jusque là et on propage l'erreur # les anciens locks et self._modifs reste bien inchangés for (a, b, c) in locked: self.conn.lockholder.removelock(a, b, c) raise # On retire les locks des attributs que l'on ne va plus utiliser for attribut in self._modifs.get(attr, []): if attribut.unique and not attribut in attrs_before_verif and not attribut in attribut.unique_exclue: self.conn.lockholder.removelock(attr, str(attribut), self.lockId) # Si on remet la valeur antérieure au lock, on le libère if not attributs.AttributeFactory.get(attr).concurrent and self._modifs.get(attr, []) != self.attrs.get(attr, []) and attrs_before_verif == self.attrs.get(attr, []): self.conn.lockholder.removelock("dn", "%s_%s" % (self.dn.replace('=', '-').replace(',','_'), attr), self.lockId) # On met à jour self._modifs avec les nouvelles valeurs self._modifs[attr] = attrs_before_verif def search_historique(self, ign_fields=HIST_IGNORE_FIELDS): u"""Récupère l'historique l'argument optionnel ign_fields contient la liste des champs à ignorer, HIST_IGNORE_FIELDS par défaut Renvoie une liste de lignes de texte.""" res = self.conn.search_s(variables.log_dn, ldap.SCOPE_SUBTREE, 'reqDN=%s' % self.dn) res.sort(key=(lambda a: a[1]['reqEnd'][0])) out = [] for cn, attrs in res: date = crans_utils.format_ldap_time(attrs['reqEnd'][0]) author = attrs['reqAuthzID'][0] if author == "cn=admin,dc=crans,dc=org": author = u"respbats" else: author = author.split(",", 1)[0] res = self.conn.search(author, scope=ldap.SCOPE_ONELEVEL) if res != []: author = res[0].compte() if attrs['reqType'][0] == variables.deleted: out.append(u"%s : [%s] Suppression" % (date, author)) elif attrs['reqType'][0] == variables.modified: fields = {} for mod in attrs['reqMod']: mod = mod.decode('utf-8') field, change = mod.split(':', 1) if field not in ign_fields: if field in fields: fields[field].append(change) else: fields[field] = [change] mod_list = [] for field in fields: mods = fields[field] mod_list.append(u"%s %s" %(field, ", ".join(mods))) if mod_list != []: out.append(u"%s : [%s] %s" % (date, author, u" ; ".join(mod_list))) return out # On utilise carte_ok et paiement_ok dans blacklist_actif, qui sont définis dans les # objets enfants. On définit des méthodes vides ici pour la cohérence. def carte_ok(self): pass def paiement_ok(self, no_bl=False): pass def blacklist_actif(self, excepts=[]): """Renvoie la liste des blacklistes actives sur l'entité Améliorations possibles: - Vérifier les blacklistes des machines pour les adhérents ? """ blacklist_liste=[] # blacklistes virtuelle si on est un adhérent pour carte étudiant et chambre invalides if isinstance(self, adherent): if self['chbre'][0] == '????': bl = attributs.blacklist(u'%s$%s$%s$%s' % ('-', '-', 'chambre_invalide', ''), {}, self.conn) blacklist_liste.append(bl) if isinstance(self, proprio): if not self.paiement_ok(no_bl=True): bl = attributs.blacklist(u'%s$%s$%s$%s' % ('-', '-', 'paiement', ''), {}, self.conn) blacklist_liste.append(bl) blacklist_liste.extend(bl for bl in self.get("blacklist", []) if bl.is_actif()) if excepts: return [ b for b in blacklist_liste if b['type'] not in excepts ] else: return blacklist_liste def blacklist(self, sanction, commentaire, debut="now", fin = '-'): """ Blacklistage de la ou de toutes la machines du propriétaire * debut et fin sont le nombre de secondes depuis epoch * pour un début ou fin immédiate mettre now * pour une fin indéterminée mettre '-' Les données sont stockées dans la base sous la forme : debut$fin$sanction$commentaire """ if debut == 'now': debut = int(time.time()) if fin == 'now': fin = int(time.time()) bl = attributs.blacklist(u'%s$%s$%s$%s' % (debut, fin, sanction, commentaire), {}, self.conn) self._modifs.setdefault('blacklist', []).append(bl) class ObjectFactory(object): """Utilisée pour enregistrer toutes les classes servant à instancier un objet LDAP. Elle sert à les récupérer à partir de leur nom LDAP. Cette classe n'est jamais instanciée. """ __slots__ = () _classes = {} @classmethod def register(cls, name, classe): """Enregistre l'association ``name`` -> ``classe``""" cls._classes[name] = classe @classmethod def get(cls, name): """Retourne la classe qui a ``name`` pour ``ldap_name``. Pas de fallback, on ne veut pas instancier des objets de manière hasardeuse. """ return cls._classes.get(name) def crans_object(classe): """Pour décorer les classes permettant d'instancier des attributs LDAP, afin de les enregistrer dans :py:class:`ObjectFactory`. """ ObjectFactory.register(classe.ldap_name, classe) return classe @crans_object class InetOrgPerson(CransLdapObject): __slots__ = () ldap_name = "inetOrgPerson" def __repr__(self): return str(self.__class__.__name__) + " : cn=" + str(self['cn'][0]) pass class proprio(CransLdapObject): u""" Un propriétaire de machine (adhérent, club…) """ __slots__ = ("_machines", "_factures", "full") can_be_by = { variables.created: [attributs.nounou, attributs.bureau, attributs.cableur], variables.modified: [attributs.nounou, attributs.bureau, attributs.soi, attributs.cableur], variables.deleted: [attributs.nounou, attributs.bureau,], } crans_account_attribs = [attributs.uid, attributs.canonicalAlias, attributs.solde, attributs.contourneGreylist, attributs.derniereConnexion, attributs.homepageAlias, attributs.loginShell, attributs.gecos, attributs.uidNumber, attributs.homeDirectory, attributs.gidNumber, attributs.userPassword, attributs.mailAlias, attributs.cn, attributs.rewriteMailHeaders, attributs.mailExt, attributs.compteWiki, attributs.droits, attributs.shadowExpire] default_attribs = [attributs.nom, attributs.chbre, attributs.paiement, attributs.info, attributs.blacklist, attributs.controle, attributs.historique, attributs.debutAdhesion, attributs.finAdhesion, attributs.debutConnexion, attributs.finConnexion] @property def attribs(self): if u'cransAccount' in self['objectClass']: return self.default_attribs + self.crans_account_attribs else: return self.default_attribs def clubs(self): """Renvoie la liste des clubs dont l'adhérent est responsable (surchargée dans les objets adherent)""" return [] def imprimeur_clubs(self): """Renvoie la liste des clubs dont l'adherent est imprimeur (surchargée dans les objets adherent)""" return [] def __repr__(self): return str(self.__class__.__name__) + " : nom=" + str(self['nom'][0]) def __init__(self, *args, **kwargs): super(proprio, self).__init__(*args, **kwargs) self._machines = None self._factures = None def delete_compte(self, mail): # Je pense qu'en pratique cette vérification ne sert à rien puisqu'on se fera jetter à la tentative de modification # de userPassword, mail, homeDirectory, canonicalAlias, etc… if not self.may_be(variables.deleted): raise EnvironmentError("Vous n'avez pas le droit de supprimer %s et donc vous ne pouvez supprimer son compte." % self.dn) if not u'cransAccount' in self['objectClass']: raise EnvironmentError("L'adhérent n'a pas de compte crans") else: self['userPassword'] = [] self['mail'] = mail self['homeDirectory'] = [] self['canonicalAlias'] = [] self['cn'] = [] self['loginShell'] = [] self['uidNumber'] = [] self['gidNumber'] = [] self['gecos'] = [] self['shadowExpire']=[] self['derniereConnexion']=[] self['mailExt']=[] self['uid' ]=[] self._modifs['objectClass'] = [u'adherent'] self.full = False def compte(self, login = None, uidNumber=0, hash_pass = '', shell=config.login_shell): u"""Renvoie le nom du compte crans. S'il n'existe pas, et que login est précisé, le crée.""" if u'posixAccount' in self['objectClass']: return self['uid'][0] elif login: fn = crans_utils.strip_accents(unicode(self['prenom'][0]).capitalize()) ln = crans_utils.strip_accents(unicode(self['nom'][0]).capitalize()) login = crans_utils.strip_spaces(crans_utils.strip_accents(login), by=u'-').lower() if not re.match('^[a-z][-a-z]{1,15}$', login): raise ValueError("Le login a entre 2 et 16 lettres, il peut contenir (pas au début) des - ") if crans_utils.mailexist(login): raise ValueError("Login existant ou correspondant à un alias mail.") home = u'/home/' + login if os.path.exists(home): raise ValueError('Création du compte impossible : home existant') if os.path.exists("/var/mail/" + login): raise ValueError('Création du compte impossible : /var/mail/%s existant' % str(login)) self._modifs['objectClass'] = [u'adherent', u'cransAccount', u'posixAccount', u'shadowAccount'] self['uid' ] = [login] self['homeDirectory'] = [home] self['mail'] = [login + u"@crans.org"] calias = crans_utils.strip_spaces(fn) + u'.' + crans_utils.strip_spaces(ln) + '@crans.org' if crans_utils.mailexist(calias): calias = login self['canonicalAlias'] = [calias] self['cn'] = [ fn + u' ' + ln ] self['loginShell'] = [unicode(shell)] self['userPassword'] = [unicode(hash_pass)] self["solde"] = 0.0 if uidNumber: if self.conn.search(u'(uidNumber=%s)' % uidNumber): raise ValueError(u'uidNumber pris') else: pool_uid = range(1001, 9999) random.shuffle(pool_uid) while len(pool_uid) > 0: uidNumber = pool_uid.pop() # On choisit un uid if not self.conn.search(u'(uidNumber=%s)' % uidNumber): break if not len(pool_uid): raise ValueError("Plus d'uid disponibles !") self['uidNumber'] = [unicode(uidNumber)] self['gidNumber'] = [unicode(config.gid)] self['gecos'] = [unicode(self._modifs['cn'][0]) + u',,,'] else: raise EnvironmentError("L'adhérent n'a pas de compte crans") def may_be(self, what, liste=None): """Teste si liste peut faire ce qui est dans what, pour what élément de {create, delete, modify}. On passe une liste de droits plutôt que l'objet car il faut ajouter les droits soi et parent. Retourne un booléen """ if liste is None: liste = self.rights() # On ne peut supprimer un objet que si on a au moins autant de droit que lui if what == variables.deleted: modifiables = set() for i in liste: if i in attributs.DROITS_SUPERVISEUR: modifiables = modifiables.union(attributs.DROITS_SUPERVISEUR[i]) modifiables = list(modifiables) for droit in self.get('droits', []): if droit not in modifiables: return False # Notez qu'en pratique, ça ne sert à rien puisque can_be_by[variables.deleted] # ne contient que nounou et bureau. Ça va juste empêcher le bureau de supprimer # des nounous return super(proprio, self).may_be(what, liste) def access_ok(self): u"""Renvoie si le propriétaire a payé et donné sa carte pour l'année en cours""" return self.paiement_ok() def fin_adhesion(self): """Retourne la date de fin d'adhésion""" return max([float(facture.get('finAdhesion', [crans_utils.from_generalized_time_format(attributs.finAdhesion.default)])[0]) for facture in self.factures(refresh=True, mode="ro") if facture.get('controle', [''])[0] != u"FALSE" and facture.get('recuPaiement', [''])[0] != ''] + [0.0]) def fin_connexion(self): """Retourne la date de fin de connexion""" return max([float(facture.get('finConnexion', [crans_utils.from_generalized_time_format(attributs.finConnexion.default)])[0]) for facture in self.factures(refresh=True, mode="ro") if facture.get('controle', [''])[0] != u"FALSE" and facture.get('recuPaiement', [''])[0] != ''] + [0.0]) def paiement_ok(self, no_bl=False): u""" Renvoie si le propriétaire a payé pour l'année en cours, en prenant en compte les périodes de transition et les blacklistes. ``no_bl`` ne devrait être utilisé que par la fonction blacklist_actif lors de la construction des blacklistes virtuelles """ if self.dn == variables.base_dn: return True if not no_bl: for bl in self.blacklist_actif(): if bl['type'] == 'paiement': return False if isinstance(self, adherent): fin_paiement = min(self.fin_adhesion(), self.fin_connexion()) else: fin_paiement = self.fin_adhesion() paiement = time.time() < fin_paiement or (config.periode_transitoire and config.debut_periode_transitoire <= fin_paiement <= config.fin_periode_transitoire) return paiement def carte_ok(self): """Dummy""" return True def carte_controle(self): """Dummy""" return True # TODO: gérer cela en modifiant un attribut ? def solde(self, diff, comment=u"", login=None): """Modifie le solde du proprio. diff peut être négatif ou positif.""" if login is None: login = self.conn.current_login assert isinstance(diff, int) or isinstance(diff, float) assert isinstance(comment, unicode) solde = float((self["solde"] or [0.])[0]) new_solde = solde + diff # On vérifie qu'on ne dépasse par le découvert autorisé if new_solde < config.impression.decouvert: raise ValueError(u"Solde minimal atteint, opération non effectuée.") transaction = u"credit" if diff >=0 else u"debit" new_solde = u"%.2f" % new_solde self.history_add(login, u"%s %.2f Euros [%s]" % (transaction, abs(diff), comment)) self["solde"] = new_solde def update_solde(self, *args, **kw): cranslib.deprecated.usage("Utilisez la méthode solde !", level=2) return self.solde(*args, **kw) def machines(self, mode=None, refresh=False): """Renvoie la liste des machines""" if self._machines is None or refresh: try: self._machines = self.conn.search(u'mid=*', dn = self.dn, scope = 1, mode=self.mode if mode is None else mode) for m in self._machines: m._proprio = self except ldap.NO_SUCH_OBJECT: self._machines = [] return self._machines def factures(self, refresh=False, mode=None): """Renvoie la liste des factures""" if mode is None: mode = self.mode if self._factures: if self._factures[0].mode != mode: refresh = True if self._factures is None or refresh: try: self._factures = self.conn.search(u'fid=*', dn = self.dn, scope = 1, mode=mode) for m in self._factures: m._proprio = self # Si on manipule un objet pas encore enregistré dans la la bdd except ldap.NO_SUCH_OBJECT: self._factures = [] return self._factures def delete(self, comm="", login=None): """Supprimme l'objet de la base LDAP. En supprimant ses enfants d'abord.""" if login is None: login = self.conn.current_login if self.mode not in ['w', 'rw']: raise EnvironmentError("Objet en lecture seule, réessayer en lecture/écriture") if not self.may_be(variables.deleted): raise EnvironmentError("Vous n'avez pas le droit de supprimer %s." % self.dn) for machine in self.machines(): machine.delete(comm, login) for facture in self.factures(): with facture: facture.delete(comm, login) super(proprio, self).delete(comm, login) def get_mail(self): """Renvoie un mail de contact valide, or None""" # On récupère les mails parmi les diverses possibilités. mails = ( self.get('canonicalAlias', []) or self.get('mail', []) or self.get('mailExt', []) or self.get('uid', []) ) # On vérifie si l'adhérent est en mail invalide mail_invalide = any([ b['type'] == 'mail_invalide' and b['fin'] == '-' for b in self.get('blacklist', []) ]) # Si pas de mail ou mail invalide if not mails or mail_invalide: # Pour les clubs, on essaye le proprio for respo in self.get('responsable', []): mail = respo.get_mail() if mail: return mail return None # On récupère le premier de la liste. mail = mails[0].value # Login ? @crans.org if '@' not in mail: mail += '@crans.org' return mail class machine(CransLdapObject): u""" Une machine """ __slots__ = ("_proprio", "_certificats") can_be_by = { variables.created: [attributs.nounou, attributs.bureau, attributs.cableur, attributs.parent, attributs.respo], variables.modified: [attributs.nounou, attributs.bureau, attributs.cableur, attributs.parent, attributs.respo], variables.deleted: [attributs.nounou, attributs.bureau, attributs.cableur, attributs.parent, attributs.respo], } attribs = [attributs.mid, attributs.macAddress, attributs.host, attributs.rid, attributs.info, attributs.blacklist, attributs.hostAlias, attributs.exempt, attributs.portTCPout, attributs.portTCPin, attributs.portUDPout, attributs.portUDPin, attributs.sshFingerprint, attributs.ipHostNumber, attributs.ip6HostNumber, attributs.historique, attributs.dnsIpv6, attributs.machineAlias] def __repr__(self): return str(self.__class__.__name__) + " : host=" + str(self['host'][0]) def __init__(self, *args, **kwargs): super(machine, self).__init__(*args, **kwargs) self._proprio = None self._certificats = None def _check_setitem(self, attr, values): """ Vérifie des contraintes non liées à LDAP lors d'un __setitem__, lève une exception si elles ne sont pas vérifiées. Ici on vérifie qu'on ne retire pas un host à une machine tant qu'un de ses certificats l'utilise. """ if attr in ['host', 'hostAlias']: deleted = [ value for value in self[attr] if value not in values ] for domain in deleted: for certificat in self.certificats(): if domain in certificat['hostCert']: raise EnvironmentError("Vous devez d'abord supprimer ou éditer les certificats utilisant le nom de domaine %s avant de le retirer de la machine" % domain) def proprio(self, mode=None, refresh=False): u"""Renvoie le propriétaire de la machine""" if not hasattr(self, '_proprio') or not self._proprio or refresh: self._proprio = new_cransldapobject(self.conn, self.parent_dn, self.mode if mode is None else mode) return self._proprio def certificats(self, refresh=False): """Renvoie la liste des certificats de la machine""" if refresh or self._certificats is None: try: self._certificats = self.conn.search(u'xid=*', dn = self.dn, scope = 1, mode=self.mode) for m in self._certificats: m._machine = self except ldap.NO_SUCH_OBJECT: self._certificats = [] return self._certificats def blacklist_actif(self, excepts=[]): u"""Renvoie la liste des blacklistes actives sur la machine et le proprio""" black=self.proprio().blacklist_actif(excepts) black.extend(super(machine, self).blacklist_actif(excepts)) return black def _post_creation(self): """Fonction qui effectue quelques tâches lorsque la création est faite""" if self._proprio is not None: if self._proprio._machines is not None: self._proprio._machines.append(self) def _post_deletion(self): """Fonction qui effectue quelques tâches lorsque l'on veut effacer""" if self._proprio is not None: if self._proprio._machines is not None: # On duplique la liste pour ne pas la modifier en place # C'est très important, car des scripts pourraient se servir # de l'ancienne (itérer dessus par exemple) self._proprio._machines = list(self._proprio._machines) self._proprio._machines.remove(self) def check_changes(self): """Certaines propriétés sont liées les unes aux autres de façon non- intrinsèque à LDAP, parce qu'on veut qu'il en soit ainsi. Cette fonction crée un dictionnaire des choses qui devraient être modifiées, sbm, et le renvoie. Par exemple, si le rid devrait passer de 42 à 1337, alors la clef 'rid' de sbm contiendra le tuple (42, 1337).""" old = {} new = {} sbm = {'rid' : (), 'ipHostNumber' : (), 'ip6HostNumber' : ()} default = {'rid': -1, 'ipHostNumber': u'', 'macAddress': u''} for i in ['rid', 'ipHostNumber', 'macAddress']: # On stocke dans old et new l'ancienne et la nouvelle # valeur de chaque attribut concerné. try: old[i] = self.attrs[i][0].value except: old[i] = default[i] try: new[i] = self._modifs[i][0].value except: new[i] = default[i] # Si le rid est changé, on met à jour ip4 et 6, en tenant compte # des éventuels changements de mac. if old['rid'] != new['rid']: nip4 = unicode(crans_utils.ip4_of_rid(new['rid'])) oip4 = unicode(new['ipHostNumber']) if oip4 != nip4: sbm['ipHostNumber'] = (oip4, nip4) nip6 = unicode(crans_utils.ip6_of_mac(new['macAddress'], new['rid'])) try: oip6 = unicode(self._modifs['ip6HostNumber'][0]) except: oip6 = u"" if oip6 != nip6: sbm['ip6HostNumber'] = (oip6, nip6) # Les ipHostNumber sont des objets netaddr, on les cast en unicode # si l'ip4 a changé, il suffit de changer le rid, en effet, l'ip6 peut # subsister malgré tout. On retourne alors -1 pour le rid, et on fera # le changement adapté ensuite. elif unicode(old['ipHostNumber']) != unicode(new['ipHostNumber']): nrid = crans_utils.rid_of_ip4(new['ipHostNumber']) orid = new['rid'] if nrid != orid: sbm['rid'] = (orid, nrid) # Les macAddress sont déjà des unicodes. # On change l'ip6 elif old['macAddress'] != new['macAddress']: nip6 = unicode(crans_utils.ip6_of_mac(new['macAddress'], new['rid'])) try: oip6 = unicode(self._modifs['ip6HostNumber'][0]) except: oip6 = u"" if oip6 != nip6: sbm['ip6HostNumber'] = (oip6, nip6) return sbm def validate_changes(self): sbm = self.check_changes() if sbm['rid']: # Si le rid est à -1, on agit en conséquence si on a une ipv6. # Je me demande simplement pourquoi je l'ai pas fait au dessus, dans # check_changes. if sbm['rid'][1] == -1: try: ip6 = unicode(self._modifs['ip6HostNumber'][0]) except: ip6 = u"" if ip6 != u"": realm = crans_utils.find_rid_plage(sbm['rid'][0])[0] if 'v6' not in realm: realm = realm + "-v6" self['rid'] = [unicode(self.conn._find_id('rid', realm))] self['ip6HostNumber'] = [unicode(crans_utils.ip6_of_mac(self['macAddress'][0].value, self['rid'][0].value))] else: self['ipHostNumber'] = [] self['ip6HostNumber'] = [] else: if unicode(self['ipHostNumber'][0]) != unicode(crans_utils.ip4_of_rid(sbm['rid'][1])): raise ValueError("L'ipv4 et le rid ne concordent pas !") self['ip6HostNumber'] = [unicode(crans_utils.ip6_of_mac(self['macAddress'][0].value, self['rid'][0].value))] if sbm['ipHostNumber']: if sbm['ipHostNumber'][1] == u"": ip4 = [] else: ip4 = sbm['ipHostNumber'][1] self['ipHostNumber'] = ip4 if sbm['ip6HostNumber']: if sbm['ip6HostNumber'][1] == u"": ip6 = [] else: ip6 = sbm['ip6HostNumber'][1] self['ip6HostNumber'] = ip6 def delete(self, comm="", login=None): """Supprimme l'objet de la base LDAP. En supprimant ses enfants d'abord.""" if login is None: login = self.conn.current_login if self.mode not in ['w', 'rw']: raise EnvironmentError("Objet en lecture seule, réessayer en lecture/écriture") if not self.may_be(variables.deleted): raise EnvironmentError("Vous n'avez pas le droit de supprimer %s." % self.dn) for certificat in self.certificats(): certificat.delete(comm, login) super(machine, self).delete(comm, login) class AssociationCrans(proprio): """ Association crans (propriétaire particulier).""" __slots__ = () def save(self): pass def ressuscite(self, comm, login): pass def delete(self, comm, login): pass def __repr__(self): return str(self.__class__.__name__) + " : Le Crans" class BaseInvites(proprio): u"""Un artefact de la base ldap""" __slots__ = () def __repr__(self): return str(self.__class__.__name__) def delete(self, comm, login): raise EnvironmentError("Les pauvres invites") @crans_object class adherent(proprio): u"""Adhérent crans.""" __slots__ = ("_clubs", "_imprimeur_clubs") @property def attribs(self): return super(adherent, self).attribs + [attributs.aid, attributs.prenom, attributs.tel, attributs.mail, attributs.mailInvalide, attributs.charteMA, attributs.derniereConnexion, attributs.gpgFingerprint, attributs.carteEtudiant, attributs.etudes, attributs.postalAddress, attributs.gpgMail, ] ldap_name = "adherent" def __repr__(self): return "Adhérent : " + str(self['prenom'][0]) + " " + str(self['nom'][0]) def __init__(self, *args, **kwargs): super(adherent, self).__init__(*args, **kwargs) self.full = False self._clubs = None self._imprimeur_clubs = None def clubs(self): """Renvoie la liste des clubs dont l'adherent est responsable""" if self._clubs is None: self._clubs = self.conn.search(u'responsable=%s' % self['aid'][0], scope = 1, mode=self.mode) return self._clubs def imprimeur_clubs(self): """Renvoie la liste des clubs dont l'adherent est imprimeur""" if self._imprimeur_clubs is None: self._imprimeur_clubs = self.conn.search(u'imprimeurClub=%s' % self['aid'][0], scope = 1, mode=self.mode) return self._imprimeur_clubs def delete(self, comm="", login=None): clubs = self.conn.search(u"(|(responsable=%s)(imprimeurClub=%s))" % (self['aid'][0], self['aid'][0])) if clubs: raise EnvironmentError("L'adhérent est responsable ou imprimeur pour les clubs %s, suppression impossible" % ", ".join(str(c["cid"][0]) for c in clubs)) super(adherent, self).delete(comm, login) @crans_object class club(proprio): u"""Club crans""" __slots__ = () can_be_by = { variables.created: [attributs.nounou, attributs.bureau, attributs.cableur], variables.modified: [attributs.nounou, attributs.bureau, attributs.respo, attributs.cableur, attributs.soi], variables.deleted: [attributs.nounou, attributs.bureau], } ldap_name = "club" @property def attribs(self): return super(club, self).attribs + [attributs.cid, attributs.responsable, attributs.imprimeurClub] def __init__(self, *args, **kwargs): super(club, self).__init__(*args, **kwargs) def __repr__(self): return "Club : " + str(self['nom'][0]) @crans_object class machineFixe(machine): u"""Machine fixe""" __slots__ = () ldap_name = "machineFixe" class machineMulticast(machine): u"""Machine pour inféré à partir des announces sap""" __slots__ = () ldap_name = None def save(self): pass def delete(self): pass def create(self): pass def ressuscite(self, comm, login): pass def proprio(self, mode=None): return None def certificats(self): return [] @crans_object class machineWifi(machine): u"""Machine wifi""" __slots__ = () attribs = machine.attribs + [attributs.ipsec] ldap_name = "machineWifi" # À passer là où il faut # def set_ipv4(self, login=None): # u"""Définie une ipv4 à la machine si elle n'est possède pas déjà une.""" # if login is None: # login = self.conn.current_login # if not 'ipHostNumber' in self.attrs.keys() or not self['ipHostNumber']: # rid = self['rid']=[ unicode(self.conn._find_id('rid', range(config.rid['wifi'][0], config.rid['wifi'][1]+1))) ] # ip = self['ipHostNumber'] = [ unicode(crans_utils.ip4_of_rid(int(rid[0]))) ] # self.history_add(login, u"rid") # self.history_add(login, u"ipHostNumber (N/A -> %s)" % ip[0]) # self.save() # from gen_confs.dhcpd_new import dydhcp # dhcp=dydhcp() # dhcp.add_host(str(self['ipHostNumber'][0]), str(self['macAddress'][0]), str(self['host'][0])) @crans_object class machineCrans(machine): __slots__ = () can_be_by = { variables.created: [attributs.nounou], variables.modified: [attributs.nounou], variables.deleted: [attributs.nounou], } attribs = machine.attribs + [attributs.prise, attributs.nombrePrises] ldap_name = "machineCrans" @crans_object class borneWifi(machine): __slots__ = () can_be_by = { variables.created: [attributs.nounou], variables.modified: [attributs.nounou], variables.deleted: [attributs.nounou], } attribs = machine.attribs + [attributs.canal, attributs.puissance, attributs.hotspot, attributs.prise, attributs.positionBorne, attributs.nvram] ldap_name = "borneWifi" @crans_object class switchCrans(machine): __slots__ = () can_be_by = { variables.created: [attributs.nounou], variables.modified: [attributs.nounou], variables.deleted: [attributs.nounou], } attribs = machine.attribs + [attributs.nombrePrises] ldap_name = "switchCrans" @crans_object class facture(CransLdapObject): __slots__ = ("_proprio", "_recuPaiement") can_be_by = { variables.created: [attributs.nounou, attributs.bureau, attributs.cableur], variables.modified: [attributs.nounou, attributs.bureau, attributs.cableur], variables.deleted: [attributs.nounou, attributs.bureau, attributs.cableur], } attribs = [attributs.fid, attributs.modePaiement, attributs.recuPaiement, attributs.historique, attributs.article, attributs.info, attributs.debutAdhesion, attributs.finAdhesion, attributs.debutConnexion, attributs.finConnexion, attributs.controle ] ldap_name = "facture" def __repr__(self): return str(self.__class__.__name__) + " : fid=" + str(self['fid'][0]) def __init__(self, *args, **kwargs): self._proprio = None super(facture, self).__init__(*args, **kwargs) self._recuPaiement = True if self['recuPaiement'] else False def __setitem__(self, attr, value): if self._recuPaiement and attr in ['article', 'modePaiement', 'recuPaiement']: raise EnvironmentError("Paiement déjà effectué pour cette facture, impossible de modifier son contenu") return super(facture, self).__setitem__(attr, value) def total(self): total=0 for article in self["article"]: total+=int(article['nombre'])*float(article['pu']) return total def crediter(self): """ Crédite les articles à son propriétaire """ def credite_arts(proprio, cancel=False): sign = -1 if cancel else +1 proprio_save = False # on crédite les articles for art in self['article']: # solde impression (on débite d'abord si jamais quelqu'un s'amuse à recharger son solde avec son solde) if self['modePaiement'][0] == 'solde': proprio.solde(sign * (0.0 - self.total()), u"Facture n°%s" % self['fid'][0]) proprio_save = True if art["code"] == "SOLDE": proprio.solde(sign * (int(art['nombre'])*float(art["pu"])), u"Facture n°%s : %s" % (self['fid'][0], art['designation'])) proprio_save = True return proprio_save if not self._recuPaiement: with self.proprio() as proprio: proprio_save = credite_arts(proprio) # On vient de créditer, le paiement a été reçu self['recuPaiement']=unicode(time.strftime("%Y-%m-%d %H:%M:%S")) self._recuPaiement = True # Il faudrait faire quelquechose pour que si l'enregistrement suivant de la facture crash, # on défait ce qu'on fait sur le proprio plus proprement if proprio_save: proprio.save() # On force l'enregistrement de la facture après avoir crédité try: if self.exists(): self.save() else: self.create() except: if proprio_save: credite_arts(proprio, cancel=True) proprio.save() raise def proprio(self, refresh=False): u"""Renvoie le propriétaire de la facture""" if refresh or not self._proprio: self._proprio = new_cransldapobject(self.conn, self.parent_dn, self.mode) return self._proprio @crans_object class baseCert(CransLdapObject): __slots__ = ("_machine",) can_be_by = { variables.created: [attributs.nounou, attributs.bureau, attributs.parent], variables.modified: [attributs.nounou, attributs.bureau, attributs.parent], variables.deleted: [attributs.nounou, attributs.bureau, attributs.parent], } default_attribs = [ attributs.xid, attributs.certificat, attributs.hostCert, attributs.historique, attributs.info, attributs.csr ] tlsa_attribs = [ attributs.certificatUsage, attributs.selector, attributs.matchingType, attributs.portTCPin, attributs.portUDPin] x509_attribs = [ attributs.issuerCN, attributs.start, attributs.end, attributs.crlUrl, attributs.revocked, attributs.serialNumber ] private_attribs = [ attributs.privatekey, attributs.encrypted ] @property def attribs(self): attribs = list(self.default_attribs) if "TLSACert" in self['objectClass']: attribs.extend(self.tlsa_attribs) if 'x509Cert' in self['objectClass']: attribs.extend(self.x509_attribs) if "privateKey" in self['objectClass']: attribs.extend(self.private_attribs) return attribs ldap_name = "baseCert" protected_issuer = [u'CAcert Class 3 Root', u'CA Cert Signing Authority'] def __init__(self, *args, **kwargs): self._machine = None super(baseCert, self).__init__(*args, **kwargs) def __repr__(self): if self['info']: return "Certificat : %s (xid=%s)" % (self['info'][0], str(self['xid'][0])) else: return "Certificat : xid=" + str(self['xid'][0]) def _check_setitem(self, attr, values): """ Vérifie des contraintes non liées à LDAP lors d'un __setitem__, lève une exception si elles ne sont pas vérifiées. Ici on vérifie d'on ne retire pas un host du certificat s'il est réèlement présent dans les données du certificat. """ if attr in ['hostCert']: deleted = [ value for value in self[attr] if value not in values ] for domain in deleted: if domain in [self['certificat'][0]['subject']['CN']] + self['certificat'][0]['extensions'].get('subjectAltName',[]): raise EnvironmentError("Vous ne pouvez pas retirer le domaine %s alors qu'il est déclaré dans le certificat" % domain) def private(self, privatekey, encrypted): if not self.mode in ['w', 'rw']: return if u"privateKey" in self['objectClass']: return self._modifs['objectClass'].append(u"privateKey") #self.attribs.extend(self.private_attribs) self['encrypted']=encrypted self['privatekey']=privatekey def tlsa(self, certificatUsage, matchingType): if not self.mode in ['w', 'rw']: return if u"TLSACert" in self['objectClass']: return self._modifs['objectClass'].append(u"TLSACert") #self.attribs.extend(self.tlsa_attribs) self['certificatUsage']=certificatUsage self['matchingType']=matchingType self['selector']=0 def x509(self, issuerCN, start, end, serialNumber, crlUrl=None): if not self.mode in ['w', 'rw']: return if u"x509Cert" in self['objectClass']: return self._modifs['objectClass'].append(u"x509Cert") #self.attribs.extend(self.x509_attribs) self['issuerCN'] = issuerCN self['start'] = start self['end'] = end self['serialNumber'] = serialNumber if crlUrl: self['crlUrl'] = crlUrl def machine(self, refresh=False): u"""Renvoie la machine du certificat""" if refresh or not self._machine: self._machine = new_cransldapobject(self.conn, self.parent_dn, self.mode) return self._machine def delete(self, comm="", login=None): """Supprimme l'objet de la base LDAP.""" if u"x509Cert" in self['objectClass']: # Si le certificat est encore valide et qu'il n'a pas été révoqué if self['end'] > int(time.time()) and (not self['revocked'] or not self['revocked'][0]): for issuer in self['issuerCN']: if issuer in self.protected_issuer: raise EnvironmentError("Vous n'avez pas le droit de supprimer %s tant que le certificat n'aura pas expiré ou été marqué comme révoqué" % self.dn) super(baseCert, self).delete(comm, login) @crans_object class service(CransLdapObject): __slots__ = () ldap_name = "service" import services