attributs.py 55.1 KB
Newer Older
1 2 3
#!/usr/bin/env python
# -*- coding: utf-8 -*-
#
4 5 6

""" Définition des classes permettant d'instancier les attributs LDAP. """

7
#
Nicolas Dandrimont's avatar
Nicolas Dandrimont committed
8 9 10 11 12
# Copyright (C) 2010-2013 Cr@ns <roots@crans.org>
# Authors: Antoine Durand-Gasselin <adg@crans.org>
#          Nicolas Dandrimont <olasd@crans.org>
#          Valentin Samir <samir@crans.org>
#          Vincent Le Gallic <legallic@crans.org>
Pierre-Elliott Bécue's avatar
Pierre-Elliott Bécue committed
13
#          Pierre-Elliott Bécue <becue@crans.org>
14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37
#
# 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.

38 39
import re
import sys
40
import ssl
41 42
import netaddr
import time
43
import base64
44
import datetime
45 46 47 48
import functools
import smtplib
import random
import string
49
from unicodedata import normalize
50
from crans_utils import format_tel, format_mac, mailexist, validate_name, ip4_of_rid, ip6_of_mac, fetch_cert_info
51
from crans_utils import to_generalized_time_format, from_generalized_time_format
52
import itertools
53

54
sys.path.append("/usr/scripts")
55

56
import cranslib.deprecated
57 58
from gestion import config
from gestion import annuaires_pg
59

60
#: Serveur SMTP
61
smtpserv = "smtp.crans.org"
62

63 64 65
### Les droits
# en cas de typo, l'appel d'une variable plante, on préfèrera donc les utiliser en lieu et place
# des chaînes de caractères
66
#: Chaine de caractère des droits nounou
67
nounou = u"Nounou"
68
#: Droit cableur
69
cableur = u"Cableur"
70
#: Droit apprenti
71
apprenti = u"Apprenti"
72
#: Droit trésorier
73
tresorier = u"Tresorier"
74
#: Droit bureau
75
bureau = u"Bureau"
76
#: Droit imprimeur
77
imprimeur = u"Imprimeur"
78
#: Droit modérateur
79
moderateur = u"Moderateur"
80
#: Droit multimachine
81
multimachines = u"Multimachines"
82
#: Droit Webmaster
83
webmaster = u"Webmaster"
84
#: Droit Webradio
85
webradio = u"Webradio"
86
#: On peut faire subir des choses à un objet si on est son parent
87
parent = u"parent"
88
#: On peut faire subir des choses à un objet si on est cet objet
89
soi = u"soi"
90
#: Le responsable d'un club peut lui faire subir des choses
91
respo = u"responsable"
92

93
#: Liste de tous les droits
94
TOUS_DROITS = [nounou, apprenti, bureau, tresorier, imprimeur, moderateur, multimachines, cableur, webmaster, webradio]
95
#: Liste des droits forts
96
DROITS_ELEVES = [nounou, bureau, tresorier]
97
#: Liste des droits intérmédiaires
98
DROITS_MOYEN = [apprenti, moderateur]
99
#: Liste des droits moins sensibles
100 101
DROITS_FAIBLES = [cableur, imprimeur, multimachines]

102
#: Qui a le droit de modifier quels droits
103 104 105 106
DROITS_SUPERVISEUR = { nounou : TOUS_DROITS,
    bureau : DROITS_FAIBLES + [bureau, tresorier],
}

107
class SingleValueError(ValueError):
108
    """Erreur levée si on essaye de multivaluer un champ monovalué."""
109 110 111
    pass

class UniquenessError(EnvironmentError):
112
    """Erreur levée si on essaye de créer un objet dont le ``dn`` existe déjà."""
113
    pass
114

115
class OptionalError(EnvironmentError):
116
    """Erreur levée si on essaye de créer un objet sans fournir un attribut obligatoire."""
117 118
    pass

119

120
def attrify(val, attr, conn, Parent=None):
121
    """Transforme un n'importe quoi en :py:class:`Attr`.
122 123 124 125

    Doit effectuer les normalisations et sanity check si un str ou un
    unicode est passé en argument.
    Devrait insulter en cas de potentiel problème d'encodage.
126
    """
127 128 129
    if isinstance(val, Attr):
        return val
    else:
130
        attr_classe = AttributeFactory.get(attr, fallback=Attr)
131
        if not isinstance(val, unicode) and not (attr_classe.python_type and isinstance(val, attr_classe.python_type)):
132
            cranslib.deprecated.usage("attrify ne devrait être appelé qu'avec des unicode (%r)" % val, level=3)
133
            val = val.decode(config.in_encoding)
134
        return attr_classe(val, conn, Parent)
135

136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151
class AttrsList(list):
    def __init__(self, Parent, attr, attr_list):
        super(AttrsList, self).__init__(attr_list)
        self._parent = Parent
        self._attr = attr

    def _start(self):
        if self._parent[self._attr] != self:
            raise ValueError("La variable que vous utilisez n'est pas à jour (modifications conccurentes ?)")

    def _commit(self):
        try:
            self._parent[self._attr] = self
        finally:
            super(AttrsList, self).__init__(self._parent[self._attr])

152 153 154 155 156
    def __delitem__(self, index):
        self._start()
        super(AttrsList, self).__delitem__(index)
        self._commit()

157 158 159 160 161
    def __setitem__(self, index, value):
        self._start()
        super(AttrsList, self).__setitem__(index, value)
        self._commit()

162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196
    def append(self, val):
        self._start()
        super(AttrsList, self).append(val)
        self._commit()

    def extend(self, vals):
        self._start()
        super(AttrsList, self).extend(vals)
        self._commit()

    def insert(self, pos, val):
        self._start()
        super(AttrsList, self).insert(pos, val)
        self._commit()

    def remove(self, val):
        self._start()
        super(AttrsList, self).remove(val)
        self._commit()

    def sort(self):
        self._start()
        super(AttrsList, self).sort()
        self._commit()

    def pop(self, index=None):
        self._start()
        ret = super(AttrsList, self).pop(index) if index else super(AttrsList, self).pop()
        self._commit()
        return ret

    def reverse(self):
        self._start()
        super(AttrsList, self).reverse()
        self._commit()
197

198
class AttrsDict(dict):
199 200
    def __init__(self, conn, uldif={}, Parent=None):
        super(AttrsDict, self).__init__(uldif)
201
        self._conn = conn
202
        self._parent = Parent
203
        self._iterator = None
204 205 206 207

    def __getitem__(self, attr):
        values = super(AttrsDict, self).__getitem__(attr)
        if not isinstance(values, list):
208
            values = [values]
209 210
        output = []
        for val in values:
211
            output.append(attrify(val, attr, self._conn, self._parent))
212 213
        self[attr] = output
        return output
214

215 216 217 218 219
    def __setitem__(self, attr, values):
        # ne devrait par arriver
        if not isinstance(values, list):
            values = [ values ]
        if self._parent.mode in ['w', 'rw']:
220 221
            if AttributeFactory.get(attr, fallback=Attr).singlevalue and len(values) > 1:
                raise SingleValueError("L'attribut %s doit être monovalué." % (attr,))
222 223 224 225
            super(AttrsDict, self).__setitem__(attr, values)
        else:
            super(AttrsDict, self).__setitem__(attr, values)

226 227 228 229 230 231
    def get(self, value, default_value):
        try:
            return self[value]
        except KeyError:
            return default_value

232 233
    def items(self):
        return [(key, self[key]) for key in self]
234

235 236 237 238 239 240 241 242 243
    def to_ldif(self):
        """
        Transforme le dico en ldif valide au sens openldap.
        Ce ldif est celui qui sera transmis à la base.
        """
        ldif = {}
        for attr, vals in self.items():
            ldif[attr] = [ str(val) for val in vals ]
        return ldif
244

245
class Attr(object):
246 247 248 249 250 251 252
    """Objet représentant un attribut.

    **Paramètres :**

    * ``val``        : valeur de l'attribut
    * ``ldif``       : objet contenant l'attribut (permet de faire les validations sur l'environnement)
    """
253
    legend = "Human-readable description of attribute"
254 255
    singlevalue = False
    optional = True
256
    conn = None
257
    unique = False
258
    concurrent = True
259
    unique_exclue = []
260 261
    #: Le nom de l'attribut dans le schéma LDAP
    ldap_name = None
262
    python_type = None
263
    binary = False
264
    default = None
265

266
    """La liste des droits qui suffisent à avoir le droit de modifier la valeur"""
267
    can_modify = [nounou]
268

269
    """Qui peut voir l'attribut. Par défaut, les Nounous et les Apprentis
270 271
       peuvent tout voir. Par transparence, et par utilité, on autorise par
       défaut l'adhérent à voir les données le concernant."""
272
    can_view = [nounou, apprenti, soi, parent, respo]
273 274

    """Catégorie de l'attribut (pour affichage futur)"""
275
    category = 'other'
276

277
    def __init__(self, val, conn, Parent):
278
        """Crée un nouvel objet représentant un attribut."""
279
        self.value = None
280
        self.conn = conn
281
        assert isinstance(val, unicode) or (self.python_type and isinstance(val, self.python_type))
282 283
        self.parent = Parent
        self.parse_value(val)
284

285
    def __hash__(self):
286 287
        if not self.parent or self.parent.mode in ['w', 'rw']:
            raise TypeError("Mutable structure are not hashable, please use mode = 'ro' to do so")
288 289 290 291 292
        if hasattr(self.value, "__hash__") and self.value.__hash__ is not None:
            return self.value.__hash__()
        else:
            return str(self).__hash__()

293
    def __getattr__(self, name):
294
        return getattr(self.value, name)
295

296 297
    def __ne__(self, obj):
        return not self == obj
298

299 300 301
    def __eq__(self, item):
        if isinstance(item, self.__class__):
            return str(self) == str(item)
302 303
        elif isinstance(item, Attr) and item.python_type == self.python_type:
            return item.value == self.value
304 305 306 307 308 309 310 311 312 313
        elif self.python_type and isinstance(item, self.python_type):
            return self.value == item
        elif isinstance(item, str):
            return str(self) == item
        elif isinstance(item, unicode):
            return unicode(self) == item
        else:
            return False

    def __nonzero__(self):
314 315 316 317
        if self.value:
            return True
        else:
            return False
318

319
    def parse_value(self, val):
320 321
        """Transforme l'attribut pour travailler avec notre validateur
        Le ldif est en dépendance car à certains endroits, il peut servir
322
        (par exemple, pour l'ipv6, ou l'ipv4…"""
323 324 325
        self.value = val

    def __str__(self):
326
        return self.__unicode__().encode(config.out_encoding)
327

328
    def __repr__(self):
329
        return str(self.__class__) + " : " + repr(self.value)
330

331
    def __unicode__(self):
332 333 334 335
        if isinstance(self.value, unicode):
            return self.value
        else:
            return unicode(self.value)
336

337
    def check_uniqueness(self, liste_exclue):
338
        """Vérifie l'unicité dans la base de la valeur (``mailAlias``, ``chbre``,
339
        etc...)"""
340
        attr = self.__class__.__name__
341
        if unicode(self) in liste_exclue + self.unique_exclue:
342
            return
343
        if self.unique:
Valentin Samir's avatar
Valentin Samir committed
344
            res = self.conn.search(u'%s=%s' % (attr, str(self)))
345
            if res:
346
                raise UniquenessError("%s déjà existant" % attr, [r.dn for r in res])
347

348 349
    def is_modifiable(self, liste_droits):
        """
Pierre-Elliott Bécue's avatar
Pierre-Elliott Bécue committed
350
        L'attribut est-il modifiable par un des droits dans liste_droits ?
351
        """
352
        return not set(liste_droits).isdisjoint(self.can_modify)
353

354 355 356 357

class AttributeFactory(object):
    """Utilisée pour enregistrer toutes les classes servant à instancier un attribut LDAP.
       Elle sert à les récupérer à partir de leur nom LDAP.
358

359
       Cette classe n'est jamais instanciée.
360

361 362 363 364 365 366 367 368 369 370 371
       """
    _classes = {}

    @classmethod
    def register(cls, name, classe):
        """Enregistre l'association ``name`` -> ``classe``"""
        cls._classes[name] = classe

    @classmethod
    def get(cls, name, fallback=Attr):
        """Retourne la classe qui a ``name`` pour ``ldap_name``.
372

373
           Si elle n'existe pas, renvoie :py:class:`Attr` (peut être override en précisant ``fallback``)
374

375 376 377 378 379 380
           """
        return cls._classes.get(name, fallback)

def crans_attribute(classe):
    """Pour décorer les classes permettant d'instancier des attributs LDAP,
       afin de les enregistrer dans :py:class:`AttributeFactory`.
381

382 383 384 385 386
       """
    AttributeFactory.register(classe.ldap_name, classe)
    return classe

@crans_attribute
387 388 389 390
class objectClass(Attr):
    singlevalue = False
    optional = False
    legend = "entité"
391
    ldap_name = "objectClass"
392

393
    """ Personne ne doit modifier de classe """
394
    can_modify = []
395 396

    """ Internal purpose (et à fin pédagogique) """
397
    can_view = [nounou, apprenti]
398

399
    def parse_value(self, val):
400
        if val not in [ 'top', 'organizationalUnit', 'inetOrgPerson', 'posixAccount', 'shadowAccount',
401
                        'proprio', 'adherent', 'club', 'machine', 'machineCrans',
402
                        'borneWifi', 'machineWifi', 'machineFixe', 'x509Cert', 'TLSACert',
403
                        'baseCert', 'cransAccount', 'service', 'facture', 'freeMid', 'privateKey' ]:
404
            raise ValueError("Pourquoi insérer un objectClass=%r ?" % val)
405
        else:
406
            self.value = unicode(val)
407 408


409
class intAttr(Attr):
410 411 412

    python_type = int

413 414 415 416 417 418 419 420 421 422 423 424
    def __add__(self, obj):
        if isinstance(obj, self.__class__):
            return self.value.__add__(obj.value)
        else:
            return self.value.__add__(obj)

    def __sub__(self, obj):
        if isinstance(obj, self.__class__):
            return self.value.__sub__(obj.value)
        else:
            return self.value.__sub__(obj)

425
    def parse_value(self, val):
426
        if self.python_type(val) < 0:
427
            raise ValueError("Valeur entière invalide : %r" % val)
428
        self.value = self.python_type(val)
429 430 431 432

    def __unicode__(self):
        return unicode(self.value)

433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454
class floatAttr(Attr):

    python_type = float

    def __add__(self, obj):
        if isinstance(obj, self.__class__):
            return self.value.__add__(obj.value)
        else:
            return self.value.__add__(obj)

    def __sub__(self, obj):
        if isinstance(obj, self.__class__):
            return self.value.__sub__(obj.value)
        else:
            return self.value.__sub__(obj)

    def parse_value(self, val):
        self.value = self.python_type(val)

    def __unicode__(self):
        return unicode(self.value)

455
class boolAttr(Attr):
456 457 458

    python_type = bool

459
    def parse_value(self, val):
460 461 462
        if isinstance(val, self.python_type):
            self.value = val
        elif val.lower() in [u'true', u'ok']:
463 464 465 466 467
            self.value = True
        elif val.lower() == u'false':
            self.value = False
        else:
            raise ValueError("%r doit être un booléen !" % val)
468 469

    def __unicode__(self):
470
        return unicode(self.value).upper()
471

472
@crans_attribute
473 474 475 476
class aid(intAttr):
    singlevalue = True
    optional = True
    legend = u"Identifiant de l'adhérent"
477
    category = 'id'
478
    unique = True
479
    ldap_name = "aid"
480 481

    """ Personne ne devrait modifier un attribut d'identification """
482
    can_modify = []
483

484
    can_view = [nounou, apprenti, cableur]
485

486 487
    def parse_value(self, aid):
        self.value = int(aid)
488

489
@crans_attribute
490
class uid(Attr):
491 492 493
    singlevalue = True
    option = False
    legend = u"L'identifiant canonique de l'adhérent"
494
    category = 'perso'
495
    unique = True
496
    can_modify = [nounou]
497
    ldap_name = "uid"
498

499 500 501 502 503 504 505 506 507
@crans_attribute
class preferedLanguage(Attr):
    singlevalue = True
    option = True
    legend = u"La langue préférée de l'adhérent"
    category = 'perso'
    unique = False
    ldap_name = "preferedLanguage"

508
@crans_attribute
509
class nom(Attr):
510 511 512
    singlevalue = True
    optional = False
    legend = "Nom"
513
    category = 'perso'
514
    can_modify = [nounou, cableur]
515
    ldap_name = "nom"
516

517 518
    def parse_value(self, nom):
        if self.parent != None:
519
            if u'club' in [o.value for o in self.parent['objectClass']]:
520 521 522
                self.value = validate_name(nom,"0123456789\[\]")
            else:
                self.value = validate_name(nom)
523
        else:
524
            self.value = validate_name(nom)
525

526
@crans_attribute
527
class prenom(Attr):
528 529 530
    singlevalue = True
    optional = False
    legend = u"Prénom"
531
    category = 'perso'
532
    can_modify = [nounou, cableur]
533
    ldap_name = "prenom"
534

535 536
    def parse_value(self, prenom):
        self.value = validate_name(prenom)
537

538
@crans_attribute
539 540
class compteWiki(Attr):
    singlevalue = False
Daniel Stan's avatar
Daniel Stan committed
541
    optional = True
542
    legend = u"Compte WiKi"
Daniel Stan's avatar
Daniel Stan committed
543 544
    category = 'perso'
    can_modify = [nounou, cableur, soi]
545
    ldap_name = "compteWiki"
546

547 548
    #def parse_value(self, compte):
        #self.value = validate_name(compte)
549
        # TODO: validate with mdp for user definition here ?
550

551
@crans_attribute
552 553 554 555
class tel(Attr):
    singlevalue = True
    optional = False
    legend = u"Téléphone"
556
    category = 'perso'
557
    can_modify = [soi, nounou, cableur]
558
    ldap_name = "tel"
559

560 561
    def parse_value(self, tel):
        self.value = format_tel(tel)
562
        if len(self.value) == 0:
563
            raise ValueError("Numéro de téléphone invalide (%r)" % tel)
564

565
class yearAttr(intAttr):
566
    singlevalue = False
567
    optional = True
568

569
    def parse_value(self, annee):
570
        annee = self.python_type(annee)
571 572 573
        if annee < 1998:
            raise ValueError("Année invalide (%r)" % annee)
        self.value = annee
574

575
@crans_attribute
576
class paiement(yearAttr):
577
    legend = u"Paiement"
578
    can_modify = [cableur, nounou, tresorier]
579
    category = 'perso'
580
    ldap_name = "paiement"
581

582
@crans_attribute
583
class carteEtudiant(Attr):
584
    legend = u"Carte d'étudiant"
585
    category = 'perso'
586
    can_modify = [cableur, nounou, tresorier]
587
    ldap_name = "carteEtudiant"
588

589 590 591
    def parse_value(self, data):
        self.value = data

592
@crans_attribute
593 594
class derniereConnexion(intAttr):
    legend = u"Dernière connexion"
595
    can_modify = [nounou, cableur, soi] # il faut bien pouvoir le modifier pour le mettre à jour
596
    ldap_name = "derniereConnexion"
597

598 599 600 601 602 603 604 605 606 607 608 609 610 611
class generalizedTimeFormat(Attr):
    """Classe définissant un ensemble de données pour manipuler
    une donnée de temps suivant la RFC 4517

    """
    default = "19700101000000Z"

    def __float__(self):
        return self._stamp

    def __int__(self):
        return int(self._stamp)

    def __eq__(self, othertime):
612
        if isinstance(othertime, generalizedTimeFormat):
613 614 615 616 617 618 619 620 621 622 623 624 625 626 627
            return self._stamp == othertime._stamp
        else:
            resource = generalizedTimeFormat(othertime, conn=None, Parent=None)
            return self._stamp == resource._stamp

    def __neq__(self, othertime):
        return not (self == othertime)

    def __lt__(self, othertime):
        if isinstance(othertime, generalizedTimeFormat):
            return self._stamp < othertime._stamp
        else:
            resource = generalizedTimeFormat(othertime, conn=None, Parent=None)
            return self._stamp < resource._stamp

628 629 630 631 632 633
    def __le__(self, othertime):
        return not (self > othertime)

    def __ge__(self, othertime):
        return not (self < othertime)

634 635 636 637 638 639 640
    def __gt__(self, othertime):
        return not (self < othertime) and not (self == othertime)

    def parse_value(self, gtf):
        if isinstance(gtf, str) or isinstance(gtf, unicode):
            if not ('Z' in gtf or '+' in gtf or '-' in gtf):
                self._stamp = gtf
641
                self.value = to_generalized_time_format(float(gtf))
642
            else:
643
                self._stamp = from_generalized_time_format(gtf)
644 645 646
                self.value = gtf
        elif isinstance(gtf, float):
            self._stamp = gtf
647
            self.value = to_generalized_time_format(gtf)
648 649 650 651 652 653 654 655 656 657 658 659 660 661 662 663 664 665 666 667 668 669 670 671 672 673 674 675 676 677

@crans_attribute
class debutAdhesion(generalizedTimeFormat):
    legend = u"Date de début de l'adhésion"
    category = 'Adh'
    can_modify = [cableur, nounou]
    ldap_name = 'debutAdhesion'


@crans_attribute
class finAdhesion(generalizedTimeFormat):
    legend = u"Date de fin de l'adhésion"
    category = 'Adh'
    can_modify = [cableur, nounou]
    ldap_name = 'finAdhesion'

@crans_attribute
class debutConnexion(generalizedTimeFormat):
    legend = u"Date de début de la connexion"
    category = 'Adh'
    can_modify = [cableur, nounou]
    ldap_name = 'debutConnexion'

@crans_attribute
class finConnexion(generalizedTimeFormat):
    legend = u"Date de fin de la connexion"
    category = 'Adh'
    can_modify = [cableur, nounou]
    ldap_name = 'finConnexion'

678
@crans_attribute
679
class mail(Attr):
680
    singlevalue = False
681 682
    optional = False
    unique = True
683
    legend = u"Adresse mail de l'adhérent"
684
    can_modify = [soi, nounou, cableur]
685
    category = 'mail'
686
    ldap_name = "mail"
687

688 689 690 691 692 693 694 695 696 697 698 699
    def is_modifiable(self, liste_droits):
        """
        Une adresse mail n'est modifiable que si on a au moins autant de droits
        que la personne à qui est l'adresse mail
        """
        modifiables = set()
        for i in liste_droits:
            if i in DROITS_SUPERVISEUR:
                modifiables = modifiables.union(DROITS_SUPERVISEUR[i])
        modifiables = list(modifiables)

        for droit in self.parent.get('droits', []):
700
            if droit not in modifiables and droit in TOUS_DROITS:
701 702 703 704
                return False
        return super(mail, self).is_modifiable(liste_droits)


705
    def check_uniqueness(self, liste_exclue):
706
        attr = self.__class__.__name__
707 708
        if str(self) in liste_exclue:
            return
709
        if attr in ["mailAlias", "canonicalAlias", 'mail']:
710
            mail, end = str(self).split('@', 1)
711 712 713 714 715 716 717
            if end.startswith('crans'):
                try:
                    smtp = smtplib.SMTP(smtpserv)
                    smtp.putcmd("vrfy", mail)
                    res = smtp.getreply()[0] in [250, 252]
                    smtp.close()
                except:
718
                    raise ValueError('Serveur de mail injoignable')
719 720 721 722

                if res:
                    raise ValueError("Le mail %s est déjà pris." % (str(self)))
            else:
Valentin Samir's avatar
Valentin Samir committed
723
                check = self.conn.search(u'mail=%s' % mail)
724 725
                if len(check) >= 1:
                    raise ValueError("Le mail %s est déjà pris." % (str(self)))
726

727
    def parse_value(self, mail):
728 729
        if not re.match(u'^[-_.0-9A-Za-z]+@([A-Za-z0-9]{1}[A-Za-z0-9-_]+[.])+[a-z]{2,6}$', mail):
            raise ValueError("%s invalide %r" % (self.legend, mail))
730
        self.value = mail
731

732

733
@crans_attribute
734
class canonicalAlias(mail):
735
    singlevalue = True
736
    optional = True
737
    unique = True
738
    legend = u"Alias mail canonique"
739
    category = 'mail'
740
    ldap_name = "canonicalAlias"
741

742 743 744 745
# à spécifier pour les nouvelles valeurs
#    def parse_value(self, mail):
#        mail = u".".join([ a.capitalize() for a in mail.split(u'.', 1) ])
#        super(canonicalAlias, self).parse_value(mail)
746

747
@crans_attribute
748 749 750 751 752 753 754
class mailAlias(mail):
    singlevalue = False
    optional = True
    unique = True
    legend = u"Alias mail"
    can_modify = [soi, cableur, nounou]
    category = 'mail'
755
    ldap_name = "mailAlias"
756

757
@crans_attribute
758 759 760 761
class mailExt(mail):
    singlevalue = False
    optional = True
    unique = True
762
    legend = u"Mail de secours"
763 764
    can_modify = [soi, cableur, nounou]
    category = 'mail'
765
    ldap_name = "mailExt"
766 767

    def parse_value(self, mail):
768 769 770 771
        # comme on utilise mailExt comme mail de secours si l'utilisateur
        # à perdu ses id crans, ça ne sert à rien de mettre ne adresse crans
        if mail.endswith("@crans.org"):
            raise ValueError("%s ne devrait pas être une adresse crans." % str(self.legend))
772
        super(mailExt, self).parse_value(mail)
773

774
@crans_attribute
775 776 777 778
class mailInvalide(boolAttr):
    optional = True
    legend = u"Mail invalide"
    can_modify = [bureau, nounou]
779
    ldap_name = "mailInvalide"
780

781
@crans_attribute
782 783 784 785 786
class contourneGreylist(boolAttr):
    optionnal = True
    legend = u"Contourner la greylist"
    category = 'mail'
    can_modify = [soi]
787
    ldap_name = "contourneGreylist"
788

789 790
    def __unicode__(self):
        return u"OK"
791

792
@crans_attribute
793 794
class etudes(Attr):
    singlevalue = False
795
    optional = True
796
    legend = u"Études"
797
    can_modify = [soi, cableur, nounou]
798
    category = 'perso'
799
    ldap_name = "etudes"
800

801
@crans_attribute
802 803 804
class chbre(Attr):
    singlevalue = True
    optional = False
805 806
    unique = True
    unique_exclue = [u'????', u'EXT']
807
    legend = u"Chambre sur le campus"
808
    can_modify = [soi, cableur, nounou]
809
    category = 'perso'
810
    ldap_name = "chbre"
811

812
    def parse_value(self, chambre):
813
        if self.parent != None and u'club' in [str(o) for o in self.parent['objectClass']]:
814 815
                if chambre in annuaires_pg.locaux_clubs():
                    self.value = chambre
816
                else:
817
                    raise ValueError("Club devrait etre en XclN, pas en %r" % chambre)
818 819 820
        elif chambre in (u"EXT", u"????"):
            self.value = chambre
        else:
821
            annuaires_pg.chbre_prises(chambre[0], chambre[1:])
822
            self.value = chambre
823

824
@crans_attribute
825 826 827 828
class droits(Attr):
    singlevalue = False
    optional = True
    legend = u"Droits sur les serveurs"
829
    can_modify = [nounou, bureau]
830
    category = 'perso'
831
    ldap_name = "droits"
832

833
    def parse_value(self, droits):
834 835
#        if val.lower() not in [i.lower() for i in TOUS_DROITS]:
#            raise ValueError("Ces droits n'existent pas (%r)" % val)
836
        self.value = droits.capitalize()
837 838 839 840

    def is_modifiable(self, liste_droits):
        """
        Le droit est-il modifiable par un des droits dans liste_droits ?
841
        L'idée étant que pour modifier un droit, il faut avoir un droit plus fort
842 843 844
        """
        modifiables = set()
        for i in liste_droits:
845 846
            if i in DROITS_SUPERVISEUR:
                modifiables = modifiables.union(DROITS_SUPERVISEUR[i])
847 848
        modifiables = list(modifiables)

849
        return self.value in modifiables and super(droits, self).is_modifiable(liste_droits)
850

851
@crans_attribute
852
class solde(floatAttr):
853
    python_type = float
854
    singlevalue = True
855
    concurrent = False
856 857
    optional = True
    legend = u"Solde d'impression"
858
    can_modify = [imprimeur, nounou, tresorier]
859
    ldap_name = "solde"
860

861
    def parse_value(self, solde):
862
        # on évite les dépassements, sauf si on nous dit de ne pas vérifier
863 864
        #if not (float(solde) >= config.impression.decouvert and float(solde) <= 1024.):
        #    raise ValueError("Solde invalide: %r" % solde)
865
        self.value = self.python_type(solde)
866

867 868
    def __unicode__(self):
        return u"%.2f" % self.value
869

870
class dnsAttr(Attr):
871
    category = 'dns'
872
    ldap_name = "dnsAttr"
873
    python_type = unicode
874

875 876
    def parse_value(self, val):
        val = val.lower()
877 878 879 880
        names = val.split('.')
        for name in names:
            if not re.match('^[a-z0-9](-*[a-z0-9]+)*$', name):
                raise ValueError("Nom d'hote invalide %r" % val)
881
        self.value = val
882

883
@crans_attribute
884
class host(dnsAttr):
885
    singlevalue = True
886
    unique = True
887 888
    optional = False
    hname = legend = u"Nom d'hôte"
889
    can_modify = [parent, nounou, cableur]
890
    category = 'base_tech'
891
    ldap_name = "host"
892

893
    def check_uniqueness(self, liste_exclue):
894
        attr = self.__class__.__name__
895 896
        if str(self) in liste_exclue:
            return
897
        if attr in ["host", "hostAlias"]:
Valentin Samir's avatar
Valentin Samir committed
898
            res = self.conn.search(u'(|(host=%s)(hostAlias=%s))' % ((str(self),)*2))
899 900 901
            if res:
                raise ValueError("Hôte déjà existant", [r.dn for r in res])

902
@crans_attribute
903 904 905 906 907
class hostAlias(host):
    singlevalue = False
    unique = True
    optional = True
    legend = u'Alias de nom de machine'
908
    can_modify = [nounou, cableur]
909
    ldap_name = "hostAlias"
910

911
@crans_attribute
912 913 914 915 916
class macAddress(Attr):
    singlevalue = True
    optional = False
    legend = u"Adresse physique de la carte réseau"
    hname = "Adresse MAC"
917
    category = 'base_tech'
918
    can_modify = [parent, nounou, cableur]
919
    ldap_name = "macAddress"
920
    default = u'<automatique>'
921

922
    def parse_value(self, mac):
923 924 925 926 927 928
        mac = mac.lower()
        if mac == macAddress.default:
            self.value = mac
        else:
            self.value = format_mac(mac)

929 930

    def __unicode__(self):
931
        return unicode(self.value).lower()
932

933
@crans_attribute
934 935
class ipHostNumber(Attr):
    singlevalue = True
936
    optional = True
937
    unique = True
938 939
    legend = u"Adresse IPv4 de la machine"
    hname = "IPv4"
940
    category = 'base_tech'
941
    can_modify = [nounou, cableur]
942
    ldap_name = "ipHostNumber"
943
    python_type = netaddr.IPAddress
944
    default = u'<automatique>'
945

946
    def parse_value(self, ip):
947
        if ip == '<automatique>':
948
            ip = ip4_of_rid(str(self.parent['rid'][0]))
949
        self.value = self.python_type(ip)
950 951 952 953

    def __unicode__(self):
        return unicode(self.value)

954
@crans_attribute
955 956 957 958 959 960 961
class ip6HostNumber(Attr):
    singlevalue = True
    optional = True
    unique = True
    legend = u"Adresse IPv6 de la machine"
    hname = "IPv6"
    category = 'base_tech'
962 963 964
    # beware: parent est nécessaire à la modification des adresses macs par
    # les adhérents lambda (see validate_changes)
    can_modify = [nounou, cableur, parent]
965
    ldap_name = "ip6HostNumber"
966
    python_type = netaddr.IPAddress
967
    default = u'<automatique>'
968

969
    def parse_value(self, ip6):
970 971
        if ip6 == '<automatique>':
            ip6 = ip6_of_mac(str(self.parent['macAddress'][0]), int(str(self.parent['rid'][0])))
972
        self.value = self.python_type(ip6)
973 974 975

    def __unicode__(self):
        return unicode(self.value)
976

977
@crans_attribute
978
class mid(intAttr):
979 980
    singlevalue = True
    optional = False
981
    unique = True
982
    legend = u"Identifiant de machine"
983
    category = 'id'
984
    ldap_name = "mid"
985

986
@crans_attribute
987
class rid(intAttr):
988
    singlevalue = True
989
    optional = True
990
    unique = True
991
    legend = u"Identifiant réseau de machine"
992
    category = 'id'
993
    can_modify = [nounou]
994
    ldap_name = "rid"
995

996
    def parse_value(self, rid):
997
        rid = self.python_type(rid)
998

999 1000
        # On veut éviter les rid qui recoupent les ipv4 finissant par
        # .0 ou .255
1001
        plages = [itertools.chain(*[xrange(plage[0], plage[1]+1) for plage in value]) for (key, value) in config.rid_primaires.iteritems() if ('v6' not in key) and ('special' not in key)]
1002

1003 1004 1005 1006
        for plage in plages:
            if rid in plage:
                if rid % 256 == 0 or rid % 256 == 255:
                    rid = -1
1007
                    break
1008 1009 1010 1011 1012
                else:
                    continue
            else:
                continue
        self.value = rid
1013 1014

    def __unicode__(self):
1015
        return unicode(self.value)
1016

1017
@crans_attribute
1018 1019 1020 1021
class ipsec(Attr):
    singlevalue = False
    optional = True
    legend = u'Clef wifi'
1022
    category = 'wifi'
1023
    ldap_name = "ipsec"
1024
    can_modify = [nounou, parent]
1025
    default = u'auto'
1026

1027
    def parse_value(self, val):
1028
        if len(val) in [10, 22]:
1029 1030
            self.value = val
        else:
1031 1032 1033
            val = u'auto'
        if val == u"auto":
            self.value = u''.join( random.choice(filter(lambda x: x != 'l' and x != 'o', string.lowercase) + filter(lambda x: x != '1' and x != '0', string.digits)) for i in range(10))
1034

1035
@crans_attribute
1036 1037 1038 1039
class puissance(Attr):
    singlevalue = True
    optional = True
    legend = u"puissance d'émission pour les bornes wifi"
1040
    category = 'wifi'
1041
    can_modify = [nounou]
1042
    ldap_name = "puissance"
1043

1044
@crans_attribute
1045
class canal(intAttr):
1046 1047 1048
    singlevalue = True
    optional = True
    legend = u'Canal d\'émission de la borne'
1049
    category = 'wifi'
1050
    can_modify = [nounou]
1051
    ldap_name = "canal"