
En effet, python n'aime pas que les objets multables soient utilisé dans des sets ou comme clef de dictionnaire, du coup, on va essayer de ne pas le contrarier. De toute façon, c'est logique vu que la valeur du hash change si on édite l'objet.
1054 lines
43 KiB
Python
1054 lines
43 KiB
Python
#!/usr/bin/env python
|
||
# -*- coding: utf-8 -*-
|
||
#
|
||
|
||
""" Définition des classes permettant d'instancier les objets LDAP. """
|
||
|
||
#
|
||
# Copyright (C) 2010-2013 Cr@ns <roots@crans.org>
|
||
# Authors: Antoine Durand-Gasselin <adg@crans.org>
|
||
# Nicolas Dandrimont <olasd@crans.org>
|
||
# Olivier Iffrig <iffrig@crans.org>
|
||
# Valentin Samir <samir@crans.org>
|
||
# Daniel Stan <dstan@crans.org>
|
||
# Vincent Le Gallic <legallic@crans.org>
|
||
# Pierre-Elliott Bécue <becue@crans.org>
|
||
#
|
||
# 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 <COPYRIGHT
|
||
# HOLDER> 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 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
|
||
|
||
#: Champs à ignorer dans l'historique
|
||
HIST_IGNORE_FIELDS = ["modifiersName", "entryCSN", "modifyTimestamp", "historique"]
|
||
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]
|
||
|
||
def new_cransldapobject(conn, dn, mode='ro', uldif=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])
|
||
|
||
return classe(conn, dn, mode, uldif)
|
||
|
||
class CransLdapObject(object):
|
||
"""Classe de base des objets :py:class:`CransLdap`.
|
||
Cette classe ne devrait pas être utilisée directement."""
|
||
|
||
""" Qui peut faire quoi ? """
|
||
can_be_by = { variables.created: [attributs.nounou],
|
||
variables.modified: [attributs.nounou],
|
||
variables.deleted: [attributs.nounou],
|
||
}
|
||
|
||
attribs = []
|
||
|
||
def __init__(self, conn, dn, mode='ro', uldif=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.conn = conn
|
||
|
||
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
|
||
|
||
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'
|
||
|
||
if mode in ['w', 'rw']:
|
||
if not self.may_be(variables.modified, self.conn.droits + self.conn._check_parent(dn) + self.conn._check_self(dn)):
|
||
raise EnvironmentError("Vous n'avez pas le droit de modifier cet objet.")
|
||
|
||
self.mode = mode
|
||
|
||
if 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 __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):
|
||
print printing.sprint(self)
|
||
|
||
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)
|
||
# Attention, le __setitem__ est surchargé, mais pas .append sur l'historique
|
||
self["historique"] = self.get("historique", []) + [new_line]
|
||
|
||
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."""
|
||
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
|
||
|
||
for attr in self.attrs.keys():
|
||
for attribut in self[attr]:
|
||
attribut.check_uniqueness([])
|
||
|
||
self.history_add(login, u"Inscription")
|
||
|
||
# Création de la requête LDAP
|
||
modlist = addModlist(self._modifs.to_ldif())
|
||
# Requête LDAP de création de l'objet
|
||
try:
|
||
self.conn.add_s(self.dn, modlist)
|
||
except Exception:
|
||
print traceback.format_exc()
|
||
return
|
||
finally:
|
||
# On nettoie les locks
|
||
for key, values in self._modifs.to_ldif().iteritems():
|
||
for value in values:
|
||
self.conn.lockholder.removelock(key, value)
|
||
self.conn.lockholder.purge(id(self))
|
||
|
||
# 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.attrs.keys():
|
||
for value in self.attrs[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, self.conn.droits):
|
||
raise EnvironmentError("Vous n'avez pas le droit de supprimer %s." % self.dn)
|
||
self.bury(comm, login)
|
||
self.conn.delete_s(self.dn)
|
||
self.conn.lockholder.purge(id(self))
|
||
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")
|
||
|
||
self._check_optionnal(comment="modifiez")
|
||
|
||
# On récupère la liste des modifications
|
||
modlist = self.get_modlist()
|
||
try:
|
||
self.conn.modify_s(self.dn, modlist)
|
||
self.conn.lockholder.purge(id(self))
|
||
self.conn.lockholder.purge()
|
||
except Exception as error:
|
||
# On nettoie les locks
|
||
self.conn.lockholder.purge(id(self))
|
||
self.conn.lockholder.purge()
|
||
self._modifs = self.attrs
|
||
raise EnvironmentError("Impossible de modifier l'objet, peut-être n'existe-t-il pas ? %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 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):
|
||
"""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 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
|
||
ldif = self._modifs.to_ldif()
|
||
orig_ldif = self.attrs.to_ldif()
|
||
|
||
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
|
||
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 __setitem__(self, attr, values):
|
||
"""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 ]
|
||
if attr in self.attrs.keys():
|
||
for attribut in attrs_before_verif:
|
||
attribut.check_uniqueness([unicode(content) for content in self.attrs[attr]])
|
||
|
||
# On groupe les attributs précédents, et les nouveaux
|
||
mixed_attrs = attrs_before_verif + self.attrs[attr]
|
||
else:
|
||
for attribut in attrs_before_verif:
|
||
attribut.check_uniqueness([])
|
||
mixed_attrs = attrs_before_verif
|
||
# 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)):
|
||
raise EnvironmentError("Vous ne pouvez pas toucher aux attributs de type %r." % (attr))
|
||
self._modifs[attr] = attrs_before_verif
|
||
for attribut in attrs_before_verif:
|
||
if attribut.unique:
|
||
try:
|
||
self.conn.lockholder.addlock(attr, str(attribut), id(self))
|
||
except:
|
||
self._modifs[attr] = list(self.attrs[attr])
|
||
raise
|
||
|
||
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
|
||
|
||
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 not self.carte_ok():
|
||
bl = attributs.blacklist(u'%s$%s$%s$%s' % ('-', '-', 'carte_etudiant', ''), {}, self.conn)
|
||
blacklist_liste.append(bl)
|
||
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.
|
||
|
||
"""
|
||
_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
|
||
|
||
|
||
class proprio(CransLdapObject):
|
||
u""" Un propriétaire de machine (adhérent, club…) """
|
||
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],
|
||
}
|
||
|
||
attribs = [attributs.nom, attributs.chbre, attributs.paiement, attributs.info, attributs.blacklist, attributs.controle, attributs.historique]
|
||
|
||
def __repr__(self):
|
||
return str(self.__class__) + " : " + str(self['nom'][0])
|
||
|
||
def __init__(self, conn, dn, mode='ro', ldif=None):
|
||
super(proprio, self).__init__(conn, dn, mode, ldif)
|
||
self._machines = None
|
||
self._factures = None
|
||
if u'cransAccount' in self['objectClass']:
|
||
self.attribs = self.attribs + crans_account_attribs
|
||
self.full = True
|
||
|
||
def sursis_carte(self):
|
||
for h in self['historique'][::-1]:
|
||
x = re.match("(.*),.* : .*(paiement\+%s|inscription).*" % (config.ann_scol,), h.value)
|
||
if x != None:
|
||
return ((time.time()-time.mktime(time.strptime(x.group(1),'%d/%m/%Y %H:%M'))) <= config.sursis_carte)
|
||
return False
|
||
|
||
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() and self.carte_ok()
|
||
|
||
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
|
||
return config.ann_scol in self['paiement'] or (config.periode_transitoire and (config.ann_scol - 1) in self['paiement'])
|
||
|
||
def carte_ok(self):
|
||
u"""Renvoie si le propriétaire a donné sa carte pour l'année en cours, en prenant en compte les periode transitoires et le sursis carte"""
|
||
if self.dn == variables.base_dn:
|
||
return True
|
||
elif 'club' in self["objectClass"]:
|
||
return True
|
||
elif config.periode_transitoire or not config.bl_carte_et_actif:
|
||
return True
|
||
else:
|
||
return config.ann_scol in self.get('carteEtudiant', []) or self.sursis_carte()
|
||
|
||
# XXX - To Delete
|
||
def update_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"][0].value)
|
||
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 machines(self):
|
||
"""Renvoie la liste des machines"""
|
||
if self._machines is None:
|
||
self._machines = self.conn.search(u'mid=*', dn = self.dn, scope = 1, mode=self.mode)
|
||
for m in self._machines:
|
||
m._proprio = self
|
||
return self._machines
|
||
|
||
def factures(self):
|
||
"""Renvoie la liste des factures"""
|
||
if self._factures is None:
|
||
self._factures = self.conn.search(u'fid=*', dn = self.dn, scope = 1, mode=self.mode)
|
||
for m in self._factures:
|
||
m._proprio = self
|
||
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, self.conn.droits):
|
||
raise EnvironmentError("Vous n'avez pas le droit de supprimer %s." % self.dn)
|
||
for machine in self.machines():
|
||
machine.delete(comm, login)
|
||
self.bury(comm, login)
|
||
self.conn.delete_s(self.dn)
|
||
services.services_to_restart(self.conn, self.attrs, {}, deleted_object=[self])
|
||
|
||
class machine(CransLdapObject):
|
||
u""" Une machine """
|
||
can_be_by = { variables.created: [attributs.nounou, attributs.bureau, attributs.cableur, attributs.parent],
|
||
variables.modified: [attributs.nounou, attributs.bureau, attributs.cableur, attributs.parent],
|
||
variables.deleted: [attributs.nounou, attributs.bureau, attributs.cableur, attributs.parent],
|
||
}
|
||
|
||
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__) + " : " + str(self['host'][0])
|
||
|
||
def __init__(self, conn, dn, mode='ro', ldif = None):
|
||
super(machine, self).__init__(conn, dn, mode, ldif)
|
||
self._proprio = None
|
||
|
||
def proprio(self):
|
||
u"""Renvoie le propriétaire de la machine"""
|
||
parent_dn = self.dn.split(',', 1)[1]
|
||
if not self._proprio:
|
||
self._proprio = new_cransldapobject(self.conn, parent_dn, self.mode)
|
||
return self._proprio
|
||
|
||
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:
|
||
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(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
|
||
|
||
class AssociationCrans(proprio):
|
||
""" Association crans (propriétaire particulier)."""
|
||
def save(self):
|
||
pass
|
||
|
||
def ressuscite(self, comm, login):
|
||
pass
|
||
|
||
def delete(self, comm, login):
|
||
pass
|
||
def __repr__(self):
|
||
return str(self.__class__) + " : Le Crans"
|
||
|
||
class BaseInvites(proprio):
|
||
u"""Un artefact de la base ldap"""
|
||
def delete(self, comm, login):
|
||
raise EnvironmentError("Les pauvres invites")
|
||
|
||
@crans_object
|
||
class adherent(proprio):
|
||
u"""Adhérent crans."""
|
||
attribs = proprio.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 __init__(self, conn, dn, mode='ro', ldif=None):
|
||
super(adherent, self).__init__(conn, dn, mode, ldif)
|
||
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 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 uid
|
||
est précisé, le crée."""
|
||
|
||
if u'posixAccount' in self['objectClass']:
|
||
return self.attrs['uid'][0]
|
||
|
||
elif login:
|
||
fn = crans_utils.strip_accents(unicode(self.attrs['prenom'][0]).capitalize())
|
||
ln = crans_utils.strip_accents(unicode(self.attrs['nom'][0]).capitalize())
|
||
login = crans_utils.strip_accents(login).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' % login)
|
||
|
||
if not self.full:
|
||
self.attribs = self.attribs + crans_account_attribs
|
||
self.full = True
|
||
self['homeDirectory'] = [home]
|
||
self['mail'] = [login + u"@crans.org"]
|
||
self['uid' ] = [login]
|
||
calias = crans_utils.strip_spaces(fn) + u'.' + crans_utils.strip_spaces(ln)
|
||
if crans_utils.mailexist(calias):
|
||
calias = login
|
||
self['canonicalAlias'] = [calias]
|
||
self['objectClass'] = [u'adherent', u'cransAccount', u'posixAccount', u'shadowAccount']
|
||
self['cn'] = [ fn + u' ' + ln ]
|
||
self['loginShell'] = [unicode(shell)]
|
||
self['userPassword'] = [unicode(hash_pass)]
|
||
|
||
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'] = [self._modifs['cn'][0] + u',,,']
|
||
|
||
#self.save()
|
||
else:
|
||
raise EnvironmentError("L'adhérent n'a pas de compte crans")
|
||
|
||
|
||
@crans_object
|
||
class club(proprio):
|
||
u"""Club crans"""
|
||
attribs = proprio.attribs + [attributs.cid, attributs.responsable, attributs.imprimeurClub]
|
||
ldap_name = "club"
|
||
|
||
@crans_object
|
||
class machineFixe(machine):
|
||
u"""Machine fixe"""
|
||
ldap_name = "machineFixe"
|
||
|
||
class machineMulticast(machine):
|
||
u"""Machine pour inféré à partir des announces sap"""
|
||
ldap_name = None
|
||
def save(self):
|
||
pass
|
||
def delete(self):
|
||
pass
|
||
def create(self):
|
||
pass
|
||
def ressuscite(self, comm, login):
|
||
pass
|
||
|
||
@crans_object
|
||
class machineWifi(machine):
|
||
u"""Machine wifi"""
|
||
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):
|
||
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):
|
||
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):
|
||
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):
|
||
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]
|
||
ldap_name = "facture"
|
||
|
||
_proprio = None
|
||
|
||
#### GROS HACK pour rester comptatible avec ldap_crans où l'article representant les frais n'est ajouté qu'une fois le paiement reçu
|
||
def __init__(self, conn, dn, mode='ro', ldif=None):
|
||
super(facture, self).__init__(conn, dn, mode, ldif)
|
||
self._frais = []
|
||
if not self.get('recuPaiement', []):
|
||
if str(self['modePaiement'][0]) == 'paypal':
|
||
# 25 centimes pour le paiement paypal
|
||
s = 0.25
|
||
# et on ajoute 3.5% du montant
|
||
for art in self['article']:
|
||
s += 0.035 * int(art['nombre']) * float(art['pu'])
|
||
# arrondissage-tronquage
|
||
s = float(int(s*100)/100.0)
|
||
# ajoute à la liste d'articles de frais
|
||
self._frais.append(attributs.attrify('FRAIS~~Frais de tansaction PayPal~~1~~%s' % round(s, 2), 'article', self.conn, Parent=self))
|
||
|
||
def __getitem__(self,attr, default=None):
|
||
ret = super(facture, self).__getitem__(attr, default)
|
||
if attr == 'article' and self.mode == 'ro':
|
||
return ret + self._frais
|
||
else:
|
||
return ret
|
||
#### FIN DU GROS HACK
|
||
|
||
def proprio(self):
|
||
u"""Renvoie le propriétaire de la facture"""
|
||
parent_dn = self.dn.split(',', 1)[1]
|
||
if not self._proprio:
|
||
self._proprio = new_cransldapobject(self.conn, parent_dn, self.mode)
|
||
return self._proprio
|
||
|
||
@crans_object
|
||
class service(CransLdapObject):
|
||
ldap_name = "service"
|
||
|
||
|
||
import services
|