objets.py 71.5 KB
Newer Older
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46
#!/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
47
import random
48
import traceback
49 50 51 52 53 54 55 56
from ldap.modlist import addModlist, modifyModlist

## import locaux
import lc_ldap
import crans_utils
import attributs
import ldap_locks
import variables
57
import printing
58

59 60 61 62 63
## import de /usr/scripts/
if not "/usr/scripts/" in sys.path:
    sys.path.append('/usr/scripts/')

import gestion.config as config
64
import gestion.config.impression
65
import cranslib.deprecated
66

67 68 69
#: Champs à ignorer dans l'historique
HIST_IGNORE_FIELDS = ["modifiersName", "entryCSN", "modifyTimestamp", "historique"]

70
def new_cransldapobject(conn, dn, mode='ro', uldif=None, lockId=None):
71 72 73 74 75 76 77 78 79 80
    """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
81 82
    elif uldif:
        classe = ObjectFactory.get(uldif['objectClass'][0])
83 84 85 86 87 88 89
    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])

90 91 92 93 94 95
    try:
        _clas = classe(conn, dn, mode, uldif, lockId=lockId)
    except Exception as e:
        print "dn=%s,uldif=%r" % (dn, uldif)
        raise
    return _clas
96 97 98 99 100 101

class CransLdapObject(object):
    """Classe de base des objets :py:class:`CransLdap`.
    Cette classe ne devrait pas être utilisée directement."""

    """ Qui peut faire quoi ? """
102
    __slots__ = ("in_context", "conn", "lockId", "attrs", "_modifs", "dn", "parent_dn", "mode")
103 104 105 106 107 108 109
    can_be_by = { variables.created: [attributs.nounou],
            variables.modified: [attributs.nounou],
            variables.deleted: [attributs.nounou],
    }

    attribs = []

110 111
    ldap_name = "CransLdapObject"

112 113 114 115 116 117 118
    def update_attribs(self):
        """
        Fonction permettant de mettre à jours attribs, dès que les valeurs
        des attributs ont été instancié
        """
        pass

119 120 121 122 123 124 125 126 127 128
    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

129 130 131 132 133 134 135 136 137
    def rights(self):
        """
        Retourne les droits courant de l'utilisateur sur l'objet.
        Ces droits sont égaux aux droits de l'utilisateur plus :
            * soi si le dn de l'utilisateur est égale 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
        """
138
        return self.conn.droits + self.conn._check_parent(self.dn) + self.conn._check_self(self.dn) + self.conn._check_respo(self)
139

140
    def __init__(self, conn, dn, mode='ro', uldif=None, lockId=None):
141 142
        '''
        Créée une instance d'un objet Crans (machine, adhérent,
143
        etc...) à ce ``dn``, si ``uldif`` est précisé, n'effectue pas de
144 145 146 147 148
        recherche dans la base ldap.
        '''

        if not isinstance(conn, lc_ldap.lc_ldap):
            raise TypeError("conn doit être une instance de lc_ldap")
149
        self.in_context = False
150 151
        self.conn = conn

152 153 154 155 156
        if lockId:
            self.lockId = lockId
        else:
            self.lockId = '%s_%s' % (os.getpid(), id(self))

157 158 159
        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

160
        self._modifs = {} # C'est là qu'on met les modifications
161
        self.dn = dn
162
        self.parent_dn = dn.split(',', 1)[1] if ',' in dn else ''
163 164

        orig = {}
165 166 167
        if uldif:
            self.attrs = attributs.AttrsDict(self.conn, uldif, Parent=self)
            self._modifs = attributs.AttrsDict(self.conn, uldif, Parent=self)
168

169
        else:
170 171 172
            res = self.conn.search_s(dn, 0)
            if not res:
                raise ValueError ('objet inexistant: %s' % dn)
173
            self.dn, ldif = res[0]
174

175 176
            # L'objet sortant de la base ldap, on ne fait pas de vérifications sur
            # l'état des données.
177
            uldif = lc_ldap.ldif_to_uldif(ldif)
178 179
            self.attrs = attributs.AttrsDict(self.conn, uldif, Parent=self)
            self._modifs = attributs.AttrsDict(self.conn, uldif, Parent=self)
180

181 182 183
        if dn == variables.base_dn:
            mode = 'ro'

184 185
        self.mode = mode

186 187 188 189
        if self.mode in ['w', 'rw']:
            if not self.may_be(variables.modified):
                raise EnvironmentError("Vous n'avez pas le droit de modifier cet objet. DEB(dn=%s,user=%s,rights=%s)" % (dn, self.conn.dn, self.rights()))

190 191
        self.update_attribs()

192
        if self.mode in  ['w', 'rw']:
193 194 195 196
            # 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())
197 198 199 200 201 202

            for attr, vals in oldif.items():
                if nldif[attr] != vals:
                    for v in nldif[attr]:
                        if v in vals:
                            vals.remove(v)
203
                    nvals = [nldif[attr][vals.index(v)] for v in vals ]
204 205
                    raise EnvironmentError("λv. str(Attr(v)) n'est peut-être pas une projection (ie non idempotente):", attr, nvals, vals)

206 207 208 209 210 211 212 213

    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
214 215 216
        #self.save = self._out_of_context
        #self.create = self._out_of_context
        #self.delete = self._out_of_context
217 218 219 220 221 222 223 224 225 226
        # 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

227 228
    def __ne__(self, obj):
        return not self == obj
229

230 231 232 233 234 235 236 237 238 239 240 241
    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

242 243 244 245 246 247 248 249 250 251 252 253 254 255 256 257 258 259
    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

260 261 262 263 264 265 266 267 268 269 270 271 272 273 274 275 276 277 278 279 280 281 282
    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()
283

284 285
    def display(self, historique=5, blacklist=5):
        print printing.sprint(self, historique=historique, blacklist=blacklist)
286

287 288 289 290
    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"""
291 292
        assert isinstance(login, unicode)
        assert isinstance(chain, unicode)
293

294
        new_line = u"%s, %s : %s" % (time.strftime(attributs.historique.FORMAT), login, chain)
295 296
        self["historique"] = self.get("historique", []) + [new_line]

297 298 299
    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:
300
            for attr in self.keys():
301 302 303 304 305 306 307 308 309 310 311 312 313 314 315 316 317 318 319 320
                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)
321 322
        old_values = self.attrs.get(attr.ldap_name, [])
        new_values = self._modifs.get(attr.ldap_name, [])
323 324 325 326 327 328 329 330 331 332 333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348 349 350 351 352 353 354 355 356 357 358 359 360 361 362 363 364 365 366 367 368 369 370 371 372 373 374 375 376 377 378 379
        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))

380 381 382
    def _check_optionnal(self, comment):
        """Vérifie que les attributs qui ne sont pas optionnels sont effectivement peuplés."""
        objet = self.ldap_name
383 384 385

        for attribut in self.attribs:
            if not attribut.optional:
386
                nom_attr = attribut.ldap_name
387
                if len(self._modifs.get(nom_attr, [])) <= 0:
388 389 390 391 392 393 394 395 396 397 398 399
                    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

400 401 402 403 404 405 406 407 408 409 410 411
    def check_changes(self):
        """
        Vérifie la consistence d'un objet
        """
        pass

    def validate_changes(self):
        """
        Après vérification, harmonise l'objet
        """
        pass

412
    def create(self, login=None):
413 414 415
        """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."""
416 417 418
        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)
419
        try:
420 421 422
            if login is None:
                login = self.conn.current_login
            self._check_optionnal(comment="créez")
423 424 425 426 427 428

            try:
                if self.conn.search(dn=self.dn):
                    raise ValueError ('objet existant: %s' % self.dn)
            except ldap.NO_SUCH_OBJECT:
                pass
429

Valentin Samir's avatar
Valentin Samir committed
430 431
            binary = set()
            for attr in self.keys():
432 433
                for attribut in self[attr]:
                    attribut.check_uniqueness([])
Valentin Samir's avatar
Valentin Samir committed
434 435
                if self[attr] and self[attr][0].binary:
                    binary.add(attr)
436

437 438
            self.history_add(login, u"Inscription")

Valentin Samir's avatar
Valentin Samir committed
439 440 441 442
            ldif =  self._modifs.to_ldif()
            for attr in binary:
                ldif['%s;binary' % attr]=ldif[attr]
                del(ldif[attr])
443
            # Création de la requête LDAP
Valentin Samir's avatar
Valentin Samir committed
444
            modlist = addModlist(ldif)
445 446
            # Requête LDAP de création de l'objet
            try:
447
                self.conn.lockholder.check(self.lockId, delai=10)
448 449 450 451 452 453
                self.conn.add_s(self.dn, modlist)
            except Exception:
                print traceback.format_exc()
                return
        finally:
            # On nettoie les locks
454
            self.conn.lockholder.purge(self.lockId)
455

456
        # Services à relancer
457
        services.services_to_restart(self.conn, {}, self._modifs, created_object=[self])
458
        self._post_creation()
459

460 461
        # Vérifications après insertion.
        self.check_modifs()
462 463 464 465 466

    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()
467 468
        # On produit un ldif
        ldif = u"dn: %s\n" % self.dn
469 470
        for key in self.keys():
            for value in self[key]:
471
                ldif += u"%s: %s\n" % (key, value)
472

473
        file = "%s %s" % (datetime.datetime.now(), self.dn)
474 475 476 477 478
        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')
479 480 481 482 483 484 485 486 487
        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")
488
        if not self.may_be(variables.deleted):
489
            raise EnvironmentError("Vous n'avez pas le droit de supprimer %s." % self.dn)
490 491 492
        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)
493
        self.conn.lockholder.check(self.lockId, delai=10)
494 495
        self.bury(comm, login)
        self.conn.delete_s(self.dn)
496
        self.conn.lockholder.purge(self.lockId)
497
        self._post_deletion()
498
        services.services_to_restart(self.conn, self.attrs, {}, deleted_object=[self])
499 500 501 502 503 504 505

    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")
506 507 508
        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)
509

510
        self._check_optionnal(comment="modifiez")
511 512 513 514

        # On récupère la liste des modifications
        modlist = self.get_modlist()
        try:
515
            self.conn.lockholder.check(self.lockId, delai=10)
516
            self.conn.modify_s(self.dn, modlist)
517
            self.conn.lockholder.purge(self.lockId)
518
        except Exception as error:
519
            self.cancel()
520
            raise EnvironmentError("Impossible de modifier l'objet: %r" % error)
521 522 523 524

        # On programme le redémarrage des services
        services.services_to_restart(self.conn, self.attrs, self._modifs)

525 526
        # Vérification des modifications.
        self.check_modifs()
527

528 529 530 531 532 533
    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)
534 535
        # On nettoie les locks
        self.conn.lockholder.purge(self.lockId)
536

537 538 539 540 541
    def check_modifs(self):
        """
        Fonction qui vérifie que les modifications se sont bien
        passées.
        """
542
        # Vérification des modifications
543
        old_uldif = lc_ldap.ldif_to_uldif(self.conn.search_s(self.dn, ldap.SCOPE_BASE)[0][1])
544
        self.attrs = attributs.AttrsDict(self.conn, old_uldif, Parent=self)
545 546 547
        differences = []
        # On fait les différences entre les deux dicos
        for attr in set(self.attrs.keys()).union(set(self._modifs.keys())):
548 549
            exp_vals = set([str(i) for i in self.attrs.get(attr, [])])
            new_vals = set([str(i) for i in self._modifs.get(attr, [])])
550 551
            if exp_vals != new_vals:
                differences.append({"missing": exp_vals - new_vals, "having": new_vals - exp_vals})
552
                print differences[-1]
553 554 555
        if differences:
            raise EnvironmentError("Les modifications apportées à l'objet %s n'ont pas été correctement sauvegardées\n%s" % (self.dn, differences))

556
    def may_be(self, what, liste=None):
557 558 559 560 561 562
        """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
        """
563 564
        if liste is None:
            liste = self.rights()
565 566 567 568 569 570 571 572
        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
Valentin Samir's avatar
Valentin Samir committed
573 574 575 576 577
        binary = set()
        for attr in self.keys():
            if self[attr] and self[attr][0].binary:
                binary.add(attr)

578 579
        ldif = self._modifs.to_ldif()
        orig_ldif = self.attrs.to_ldif()
Valentin Samir's avatar
Valentin Samir committed
580 581
        for attr in binary:
            ldif['%s;binary' % attr]=ldif[attr]
582
            orig_ldif['%s;binary' % attr]=orig_ldif.get(attr, [])
Valentin Samir's avatar
Valentin Samir committed
583
            del(ldif[attr])
584 585 586 587
            try:
                del(orig_ldif[attr])
            except KeyError:
                pass
588 589 590 591 592 593

        return modifyModlist(orig_ldif, ldif)

    def get(self, attr, default):
        """Renvoie l'attribut demandé ou default si introuvable"""
        try:
594
            return self.__getitem__(attr, default)
595 596 597
        except KeyError:
            return default

598
    def __getitem__(self, attr, default=None):
599
        if self._modifs.has_key(attr) and self.mode in [ 'w', 'rw' ]:
600
            return attributs.AttrsList(self, attr, [ v for v in self._modifs[attr] ])
601
        elif self.attrs.has_key(attr):
602
            return attributs.AttrsList(self, attr, [ v for v in self.attrs[attr] ])
603
        elif self.has_key(attr):
604
            return attributs.AttrsList(self, attr, []) if default is None else default
605 606
        else:
            raise KeyError(attr)
607 608 609 610 611

    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]

612 613 614 615 616 617 618
    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

619 620 621 622 623 624 625 626 627 628
    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):
629
            values = [values]
630 631 632 633 634 635

        # 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 ]
636 637 638 639

        # 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.
640
        self._check_setitem(attr, attrs_before_verif)
641

642 643 644 645 646 647 648 649 650 651 652 653 654
        # Vérification que (attr, value) est localement unique et
        # si attr doit être globalement unique, l'unicité globale
        # Dans ce cas, on ne tiens pas compte de old_attrs cas ils
        # vont être effacé si le setitem réussi
        old_attrs = self[attr]
        for attribut in attrs_before_verif:
            if attrs_before_verif.count(attribut) > 1:
                raise ValueError("%s en double\n(%s)" % (attribut.legend if attribut.legend else attr, attribut))
            attribut.check_uniqueness(old_attrs)

        # On groupe les attributs précédents, et les nouveaux
        mixed_attrs = attrs_before_verif + old_attrs

655 656 657 658 659
        # 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)):
660
                raise EnvironmentError("Vous ne pouvez pas modifier l'attribut %r de l'objet %r." % (attr, self))
661 662 663 664 665 666


        # On ajoute des locks sur les nouvelles valeurs
        locked = []
        try:
            for attribut in attrs_before_verif:
667
                if attribut.unique and not attribut in self._modifs.get(attr, []) and not attribut in attribut.unique_exclue:
668 669
                    if not self.in_context:
                        cranslib.deprecated.usage("Des locks ne devrait être ajoutés que dans un context manager", level=2)
670
                    self.conn.lockholder.addlock(attr, str(attribut), self.lockId)
671
                    locked.append((attr, str(attribut), self.lockId))
672 673 674 675 676 677 678 679
            # 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 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))
680 681 682 683 684 685 686
                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
687 688 689
        except ldap_locks.LockError:
            # 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
690
            # les anciens locks et self._modifs reste bien inchangés
691 692 693 694 695 696
            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, []):
697
            if attribut.unique and not attribut in attrs_before_verif and not attribut in attribut.unique_exclue:
698
                self.conn.lockholder.removelock(attr, str(attribut), self.lockId)
699 700 701
        # 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)
702 703 704

        # On met à jour self._modifs avec les nouvelles valeurs
        self._modifs[attr] = attrs_before_verif
705 706 707 708 709 710 711 712 713 714 715 716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735 736 737 738 739 740 741 742 743 744

    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

745 746 747 748 749 750 751 752
    # 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

753
    def blacklist_actif(self, excepts=[]):
754 755 756 757 758 759
        """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
760 761
        if isinstance(self, adherent):
            if not self.carte_ok():
762 763
                bl = attributs.blacklist(u'%s$%s$%s$%s' % ('-', '-', 'carte_etudiant', ''), {}, self.conn)
                blacklist_liste.append(bl)
764
            if self['chbre'][0] == '????':
765 766
                bl = attributs.blacklist(u'%s$%s$%s$%s' % ('-', '-', 'chambre_invalide', ''), {}, self.conn)
                blacklist_liste.append(bl)
767
        if isinstance(self, proprio):
768
            if not self.paiement_ok(no_bl=True):
769 770
                bl = attributs.blacklist(u'%s$%s$%s$%s' % ('-', '-', 'paiement', ''), {}, self.conn)
                blacklist_liste.append(bl)
771
        blacklist_liste.extend(bl for bl in self.get("blacklist", []) if bl.is_actif())
772
        if excepts:
773
            return [ b for b in blacklist_liste if b['type'] not in excepts ]
774 775
        else:
            return blacklist_liste
776 777

    def blacklist(self, sanction, commentaire, debut="now", fin = '-'):
778
        """
779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796
        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.
797

798
       Cette classe n'est jamais instanciée.
799

800
       """
801
    __slots__ = ()
802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824
    _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

825 826
@crans_object
class InetOrgPerson(CransLdapObject):
827
    __slots__ = ()
828 829 830 831
    ldap_name = "inetOrgPerson"
    def __repr__(self):
        return str(self.__class__.__name__) + " : cn=" + str(self['cn'][0])
    pass
832 833 834

class proprio(CransLdapObject):
    u""" Un propriétaire de machine (adhérent, club…) """
835
    __slots__ = ("_machines", "_factures", "full")
836 837
    can_be_by = { variables.created: [attributs.nounou, attributs.bureau, attributs.cableur],
            variables.modified: [attributs.nounou, attributs.bureau, attributs.soi, attributs.cableur],
838
            variables.deleted: [attributs.nounou, attributs.bureau,],
839 840
    }

841 842 843 844 845 846 847 848 849 850

    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,
851 852 853
            attributs.blacklist, attributs.controle, attributs.historique,
            attributs.debutAdhesion, attributs.finAdhesion, attributs.debutConnexion,
            attributs.finConnexion]
854

855 856 857 858 859 860 861
    @property
    def attribs(self):
        if u'cransAccount' in self['objectClass']:
            return self.default_attribs + self.crans_account_attribs
        else:
            return self.default_attribs

862
    def __repr__(self):
863
        return str(self.__class__.__name__) + " : nom=" + str(self['nom'][0])
864

865 866
    def __init__(self, *args, **kwargs):
        super(proprio, self).__init__(*args, **kwargs)
867
        self._machines = None
868
        self._factures = None
869

870 871 872
    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…
873
        if not self.may_be(variables.deleted):
874 875 876 877 878 879 880 881 882 883 884 885 886
            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'] = []
887 888 889
            self['shadowExpire']=[]
            self['derniereConnexion']=[]
            self['mailExt']=[]
890 891 892 893 894 895 896 897 898 899
            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']:
900
            return self['uid'][0]
901 902

        elif login:
903 904
            fn = crans_utils.strip_accents(unicode(self['prenom'][0]).capitalize())
            ln = crans_utils.strip_accents(unicode(self['nom'][0]).capitalize())
905 906 907 908 909 910 911 912 913 914 915 916 917 918 919 920 921 922 923 924 925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949
            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['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._modifs['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'] = [unicode(self._modifs['cn'][0]) + u',,,']

        else:
            raise EnvironmentError("L'adhérent n'a pas de compte crans")

950 951 952 953 954 955

    def solde(self, operation, comment):
        self['historique'].append(comment)
        raise EnvironmentError("Il faut implémenter des locks sur le solde avant d'écrire la fonction solde dans la classe proprio de lc_ldap")


956
    def may_be(self, what, liste=None):
957 958 959 960 961 962
        """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
        """
963 964
        if liste is None:
            liste = self.rights()
965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982
        # 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)


983 984
    def sursis_carte(self):
        for h in self['historique'][::-1]:
985
            x = re.match("(.*),.* : .*(paiement\+%s|inscription).*" % (config.ann_scol,), h.value)
986 987 988 989
            if x != None:
                return ((time.time()-time.mktime(time.strptime(x.group(1),'%d/%m/%Y %H:%M'))) <= config.sursis_carte)
        return False

990 991 992 993
    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()

994 995
    def fin_adhesion(self):
        """Retourne la date de fin d'adhésion"""
996
        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])
997 998 999

    def fin_connexion(self):
        """Retourne la date de fin de connexion"""
1000
        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])
1001

1002 1003 1004 1005 1006
    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
         """
1007 1008
        if self.dn == variables.base_dn:
            return True
1009 1010 1011 1012
        if not no_bl:
            for bl in self.blacklist_actif():
                if bl['type'] == 'paiement':
                    return False
1013 1014 1015 1016 1017 1018 1019
        old_style_paiement = config.ann_scol in self['paiement'] or (config.periode_transitoire and (config.ann_scol - 1) in self['paiement'])
        if isinstance(self, adherent):
            fin_paiement = min(self.fin_adhesion(), self.fin_connexion())
        else:
            fin_paiement = self.fin_adhesion()
        new_style_paiement = time.time() < fin_paiement or (config.periode_transitoire and config.debut_periode_transitoire <= fin_paiement <= config.fin_periode_transitoire)
        return (old_style_paiement or new_style_paiement)
1020

1021
    def carte_ok(self):
1022
        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"""
1023 1024 1025 1026 1027 1028 1029
        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:
1030
            return bool(self.get('carteEtudiant', [])) or self.sursis_carte()
1031

1032 1033 1034 1035 1036 1037 1038
    def carte_controle(self):
        u"""Renvoie si la carte a été controlé pour l'année en cours par le trésorier"""
        if self["controle"]:
            return "c" in str(self["controle"][0])
        else:
            return False

1039
    # XXX - To Delete
1040 1041 1042 1043 1044 1045
    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)
1046

1047 1048
        solde = float(self["solde"][0].value)
        new_solde = solde + diff
1049

1050 1051 1052
        # 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.")
1053

1054 1055 1056 1057 1058
        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

1059
    def machines(self, mode=None, refresh=False):
1060
        """Renvoie la liste des machines"""
1061
        if self._machines is None or refresh:
1062 1063 1064 1065 1066 1067
            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 = []
1068 1069
        return self._machines

1070
    def factures(self, refresh=False, mode=None):
1071
        """Renvoie la liste des factures"""
1072 1073 1074 1075 1076 1077
        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:
1078 1079 1080 1081 1082 1083 1084
            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 = []
1085 1086
        return self._factures

1087 1088 1089 1090 1091 1092
    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")
1093
        if not self.may_be(variables.deleted):
1094 1095 1096
            raise EnvironmentError("Vous n'avez pas le droit de supprimer %s." % self.dn)
        for machine in self.machines():
            machine.delete(comm, login)
1097 1098 1099
        for facture in self.factures():
            with facture:
                facture.delete(comm, login)
1100
        super(proprio, self).delete(comm, login)
1101

1102 1103 1104
    def get_mail(self):
        """Renvoie un mail de contact valide, or None"""
        mails = ( self.get('canonicalAlias', []) or \
1105 1106
                  self.get('mail', []) or \
                  self.get('uid', []))
1107 1108 1109 1110
        if not mails or \
           any(b['type'] == 'mail_invalide' and b['fin'] == '-'
              for b in self.get('blacklist', []) ):
           return None
1111 1112 1113 1114
        mail = mails[0].value
        if '@' not in mail:
            mail += '@crans.org'
        return mail
1115 1116


1117 1118
class machine(CransLdapObject):
    u""" Une machine """
1119
    __slots__ = ("_proprio", "_certificats")
1120 1121 1122
    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],
1123 1124 1125 1126 1127 1128 1129 1130 1131
    }

    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]

1132
    def __repr__(self):
1133
        return str(self.__class__.__name__) + " : host=" + str(self['host'][0])
1134

1135 1136
    def __init__(self, *args, **kwargs):
        super(machine, self).__init__(*args, **kwargs)