attributs.py 58 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
    __slots__ = ("_conn", "_parent", "_iterator")
200 201
    def __init__(self, conn, uldif={}, Parent=None):
        super(AttrsDict, self).__init__(uldif)
202
        self._conn = conn
203
        self._parent = Parent
204
        self._iterator = None
205 206 207 208

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

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

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

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

236 237 238 239 240 241 242 243 244
    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
245

246
class Attr(object):
247 248 249 250 251 252 253
    """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)
    """
254
    __slots__ = ("value", "conn", "parent")
255
    legend = "Human-readable description of attribute"
256 257
    singlevalue = False
    optional = True
258
    unique = False
259
    concurrent = True
260
    historique = "full" # valeurs possibles "full", "partial", "info", None
261
    unique_exclue = []
262 263
    #: Le nom de l'attribut dans le schéma LDAP
    ldap_name = None
264
    python_type = None
265
    binary = False
266
    default = None
267

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

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

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

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

287
    def __hash__(self):
288 289
        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")
290 291 292 293 294
        if hasattr(self.value, "__hash__") and self.value.__hash__ is not None:
            return self.value.__hash__()
        else:
            return str(self).__hash__()

295
    def __getattr__(self, name):
296
        return getattr(self.value, name)
297

298 299
    def __ne__(self, obj):
        return not self == obj
300

301 302 303
    def __eq__(self, item):
        if isinstance(item, self.__class__):
            return str(self) == str(item)
304 305
        elif isinstance(item, Attr) and item.python_type == self.python_type:
            return item.value == self.value
306 307 308 309 310 311 312 313 314 315
        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):
316 317 318 319
        if self.value:
            return True
        else:
            return False
320

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

    def __str__(self):
328
        return self.__unicode__().encode(config.out_encoding)
329

330
    def __repr__(self):
331
        return str(self.__class__) + " : " + repr(self.value)
332

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

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

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

356 357 358 359

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.
360

361
       Cette classe n'est jamais instanciée.
362

363 364 365 366 367 368 369 370 371 372 373
       """
    _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``.
374

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

377 378 379 380 381 382
           """
        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`.
383

384 385 386 387 388
       """
    AttributeFactory.register(classe.ldap_name, classe)
    return classe

@crans_attribute
389
class objectClass(Attr):
390
    __slots__ = ()
391 392 393
    singlevalue = False
    optional = False
    legend = "entité"
394
    ldap_name = "objectClass"
395

396
    """ Personne ne doit modifier de classe """
397
    can_modify = []
398 399

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

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


412
class intAttr(Attr):
413
    __slots__ = ()
414 415
    python_type = int

416 417 418 419 420 421 422 423 424 425 426 427
    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)

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

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

436
class floatAttr(Attr):
437
    __slots__ = ()
438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453 454 455 456 457
    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)

458
class boolAttr(Attr):
459
    __slots__ = ()
460 461
    python_type = bool

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

    def __unicode__(self):
473
        return unicode(self.value).upper()
474

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

    """ Personne ne devrait modifier un attribut d'identification """
486
    can_modify = []
487

488
    can_view = [nounou, apprenti, cableur]
489

490 491
    def parse_value(self, aid):
        self.value = int(aid)
492

493
@crans_attribute
494
class uid(Attr):
495
    __slots__ = ()
496 497 498
    singlevalue = True
    option = False
    legend = u"L'identifiant canonique de l'adhérent"
499
    category = 'perso'
500
    unique = True
501
    can_modify = [nounou]
502
    ldap_name = "uid"
503

504 505
@crans_attribute
class preferedLanguage(Attr):
506
    __slots__ = ()
507 508 509 510 511 512 513
    singlevalue = True
    option = True
    legend = u"La langue préférée de l'adhérent"
    category = 'perso'
    unique = False
    ldap_name = "preferedLanguage"

514
@crans_attribute
515
class nom(Attr):
516
    __slots__ = ()
517 518 519
    singlevalue = True
    optional = False
    legend = "Nom"
520
    category = 'perso'
521
    can_modify = [nounou, cableur]
522
    ldap_name = "nom"
523

524 525
    def parse_value(self, nom):
        if self.parent != None:
526
            if u'club' in [o.value for o in self.parent['objectClass']]:
527 528 529
                self.value = validate_name(nom,"0123456789\[\]")
            else:
                self.value = validate_name(nom)
530
        else:
531
            self.value = validate_name(nom)
532

533
@crans_attribute
534
class prenom(Attr):
535
    __slots__ = ()
536 537 538
    singlevalue = True
    optional = False
    legend = u"Prénom"
539
    category = 'perso'
540
    can_modify = [nounou, cableur]
541
    ldap_name = "prenom"
542

543 544
    def parse_value(self, prenom):
        self.value = validate_name(prenom)
545

546
@crans_attribute
547
class compteWiki(Attr):
548
    __slots__ = ()
549
    singlevalue = False
Daniel Stan's avatar
Daniel Stan committed
550
    optional = True
551
    legend = u"Compte WiKi"
Daniel Stan's avatar
Daniel Stan committed
552 553
    category = 'perso'
    can_modify = [nounou, cableur, soi]
554
    ldap_name = "compteWiki"
555

556 557
    #def parse_value(self, compte):
        #self.value = validate_name(compte)
558
        # TODO: validate with mdp for user definition here ?
559

560
@crans_attribute
561
class tel(Attr):
562
    __slots__ = ()
563 564 565
    singlevalue = True
    optional = False
    legend = u"Téléphone"
566
    category = 'perso'
567
    can_modify = [soi, nounou, cableur]
568
    ldap_name = "tel"
569

570 571
    def parse_value(self, tel):
        self.value = format_tel(tel)
572
        if len(self.value) == 0:
573
            raise ValueError("Numéro de téléphone invalide (%r)" % tel)
574

575
class yearAttr(intAttr):
576
    __slots__ = ()
577
    singlevalue = False
578
    optional = True
579

580
    def parse_value(self, annee):
581
        annee = self.python_type(annee)
582 583 584
        if annee < 1998:
            raise ValueError("Année invalide (%r)" % annee)
        self.value = annee
585

586
@crans_attribute
587
class paiement(yearAttr):
588
    __slots__ = ()
589
    legend = u"Paiement"
590
    can_modify = [cableur, nounou, tresorier]
591
    category = 'perso'
592
    ldap_name = "paiement"
593

594
@crans_attribute
595
class carteEtudiant(Attr):
596
    __slots__ = ()
597
    legend = u"Carte d'étudiant"
598
    category = 'perso'
599
    can_modify = [cableur, nounou, tresorier]
600
    ldap_name = "carteEtudiant"
601

602 603 604
    def parse_value(self, data):
        self.value = data

605
@crans_attribute
606
class derniereConnexion(intAttr):
607
    __slots__ = ()
608
    legend = u"Dernière connexion"
609
    can_modify = [nounou, cableur, soi] # il faut bien pouvoir le modifier pour le mettre à jour
610
    ldap_name = "derniereConnexion"
611

612 613 614 615 616
class generalizedTimeFormat(Attr):
    """Classe définissant un ensemble de données pour manipuler
    une donnée de temps suivant la RFC 4517

    """
617
    __slots__ = ("_stamp",)
618 619 620 621 622 623 624 625 626
    default = "19700101000000Z"

    def __float__(self):
        return self._stamp

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

    def __eq__(self, othertime):
627
        if isinstance(othertime, generalizedTimeFormat):
628
            return self._stamp == othertime._stamp
629 630 631 632
        elif isinstance(othertime, float):
            return self._stamp == othertime
        elif isinstance(othertime, int):
            return self._stamp == othertime
633
        elif isinstance(othertime, unicode) or isinstance(othertime, str):
634 635
            resource = generalizedTimeFormat(othertime, conn=None, Parent=None)
            return self._stamp == resource._stamp
636 637
        else:
            return False
638 639 640 641 642 643 644

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

    def __lt__(self, othertime):
        if isinstance(othertime, generalizedTimeFormat):
            return self._stamp < othertime._stamp
645 646 647 648 649
        elif isinstance(othertime, float):
            return self._stamp < othertime
        elif isinstance(othertime, int):
            return self._stamp < othertime
        elif isinstance(othertime, unicode) or isinstance(othertime, str):
650 651
            resource = generalizedTimeFormat(othertime, conn=None, Parent=None)
            return self._stamp < resource._stamp
652 653
        else:
            return False
654

655 656 657 658 659 660
    def __le__(self, othertime):
        return not (self > othertime)

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

661 662 663 664 665 666 667
    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
668
                self.value = to_generalized_time_format(float(gtf))
669
            else:
670
                self._stamp = from_generalized_time_format(gtf)
671 672 673
                self.value = gtf
        elif isinstance(gtf, float):
            self._stamp = gtf
674
            self.value = to_generalized_time_format(gtf)
675 676 677

@crans_attribute
class debutAdhesion(generalizedTimeFormat):
678
    __slots__ = ()
679 680 681 682 683 684 685 686
    legend = u"Date de début de l'adhésion"
    category = 'Adh'
    can_modify = [cableur, nounou]
    ldap_name = 'debutAdhesion'


@crans_attribute
class finAdhesion(generalizedTimeFormat):
687
    __slots__ = ()
688 689 690 691 692 693 694
    legend = u"Date de fin de l'adhésion"
    category = 'Adh'
    can_modify = [cableur, nounou]
    ldap_name = 'finAdhesion'

@crans_attribute
class debutConnexion(generalizedTimeFormat):
695
    __slots__ = ()
696 697 698 699 700 701 702
    legend = u"Date de début de la connexion"
    category = 'Adh'
    can_modify = [cableur, nounou]
    ldap_name = 'debutConnexion'

@crans_attribute
class finConnexion(generalizedTimeFormat):
703
    __slots__ = ()
704 705 706 707 708
    legend = u"Date de fin de la connexion"
    category = 'Adh'
    can_modify = [cableur, nounou]
    ldap_name = 'finConnexion'

709
@crans_attribute
710
class mail(Attr):
711
    __slots__ = ()
712
    singlevalue = False
713 714
    optional = False
    unique = True
715
    legend = u"Adresse mail de l'adhérent"
716
    can_modify = [soi, nounou, cableur]
717
    category = 'mail'
718
    ldap_name = "mail"
719

720 721 722 723 724 725 726 727 728 729 730 731
    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', []):
732
            if droit not in modifiables and droit in TOUS_DROITS:
733 734 735 736
                return False
        return super(mail, self).is_modifiable(liste_droits)


737
    def check_uniqueness(self, liste_exclue):
738
        attr = self.__class__.__name__
739 740
        if str(self) in liste_exclue:
            return
741
        if attr in ["mailAlias", "canonicalAlias", 'mail']:
742
            mail, end = str(self).split('@', 1)
743 744 745 746 747 748 749
            if end.startswith('crans'):
                try:
                    smtp = smtplib.SMTP(smtpserv)
                    smtp.putcmd("vrfy", mail)
                    res = smtp.getreply()[0] in [250, 252]
                    smtp.close()
                except:
750
                    raise ValueError('Serveur de mail injoignable')
751 752 753 754

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

759
    def parse_value(self, mail):
760 761
        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))
762
        self.value = mail
763

764

765
@crans_attribute
766
class canonicalAlias(mail):
767
    __slots__ = ()
768
    singlevalue = True
769
    optional = True
770
    unique = True
771
    legend = u"Alias mail canonique"
772
    category = 'mail'
773
    ldap_name = "canonicalAlias"
774

775 776 777 778
# à 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)
779

780
@crans_attribute
781
class mailAlias(mail):
782
    __slots__ = ()
783 784 785 786 787 788
    singlevalue = False
    optional = True
    unique = True
    legend = u"Alias mail"
    can_modify = [soi, cableur, nounou]
    category = 'mail'
789
    ldap_name = "mailAlias"
790

791
@crans_attribute
792
class mailExt(mail):
793
    __slots__ = ()
794 795 796
    singlevalue = False
    optional = True
    unique = True
797
    legend = u"Mail de secours"
798 799
    can_modify = [soi, cableur, nounou]
    category = 'mail'
800
    ldap_name = "mailExt"
801 802

    def parse_value(self, mail):
803 804 805 806
        # 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))
807
        super(mailExt, self).parse_value(mail)
808

809
@crans_attribute
810
class mailInvalide(boolAttr):
811
    __slots__ = ()
812 813 814
    optional = True
    legend = u"Mail invalide"
    can_modify = [bureau, nounou]
815
    ldap_name = "mailInvalide"
816

817
@crans_attribute
818
class contourneGreylist(boolAttr):
819
    __slots__ = ()
820 821 822 823
    optionnal = True
    legend = u"Contourner la greylist"
    category = 'mail'
    can_modify = [soi]
824
    ldap_name = "contourneGreylist"
825

826 827
    def __unicode__(self):
        return u"OK"
828

829
@crans_attribute
830
class etudes(Attr):
831
    __slots__ = ()
832
    singlevalue = False
833
    optional = True
834
    legend = u"Études"
835
    can_modify = [soi, cableur, nounou]
836
    category = 'perso'
837
    ldap_name = "etudes"
838

839
@crans_attribute
840
class chbre(Attr):
841
    __slots__ = ()
842 843
    singlevalue = True
    optional = False
844 845
    unique = True
    unique_exclue = [u'????', u'EXT']
846
    legend = u"Chambre sur le campus"
847
    can_modify = [soi, cableur, nounou]
848
    category = 'perso'
849
    ldap_name = "chbre"
850

851
    def parse_value(self, chambre):
852
        if self.parent != None and u'club' in [str(o) for o in self.parent['objectClass']]:
853 854
                if chambre in annuaires_pg.locaux_clubs():
                    self.value = chambre
855
                else:
856
                    raise ValueError("Club devrait etre en XclN, pas en %r" % chambre)
857 858 859
        elif chambre in (u"EXT", u"????"):
            self.value = chambre
        else:
860
            annuaires_pg.chbre_prises(chambre[0], chambre[1:])
861
            self.value = chambre
862

863
@crans_attribute
864
class droits(Attr):
865
    __slots__ = ()
866 867 868
    singlevalue = False
    optional = True
    legend = u"Droits sur les serveurs"
869
    can_modify = [nounou, bureau]
870
    category = 'perso'
871
    ldap_name = "droits"
872

873
    def parse_value(self, droits):
874 875
#        if val.lower() not in [i.lower() for i in TOUS_DROITS]:
#            raise ValueError("Ces droits n'existent pas (%r)" % val)
876
        self.value = droits.capitalize()
877 878 879 880

    def is_modifiable(self, liste_droits):
        """
        Le droit est-il modifiable par un des droits dans liste_droits ?
881
        L'idée étant que pour modifier un droit, il faut avoir un droit plus fort
882 883 884
        """
        modifiables = set()
        for i in liste_droits:
885 886
            if i in DROITS_SUPERVISEUR:
                modifiables = modifiables.union(DROITS_SUPERVISEUR[i])
887 888
        modifiables = list(modifiables)

889
        return self.value in modifiables and super(droits, self).is_modifiable(liste_droits)
890

891
@crans_attribute
892
class solde(floatAttr):
893
    __slots__ = ()
894
    python_type = float
895
    singlevalue = True
896
    concurrent = False
897 898
    optional = True
    legend = u"Solde d'impression"
899
    can_modify = [imprimeur, nounou, tresorier]
900
    ldap_name = "solde"
901

902
    def parse_value(self, solde):
903
        # on évite les dépassements, sauf si on nous dit de ne pas vérifier
904 905
        #if not (float(solde) >= config.impression.decouvert and float(solde) <= 1024.):
        #    raise ValueError("Solde invalide: %r" % solde)
906
        self.value = self.python_type(solde)
907

908 909
    def __unicode__(self):
        return u"%.2f" % self.value
910

911
class dnsAttr(Attr):
912
    __slots__ = ()
913
    category = 'dns'
914
    ldap_name = "dnsAttr"
915
    python_type = unicode
916

917 918
    def parse_value(self, val):
        val = val.lower()
919 920 921 922
        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)
923
        self.value = val
924

925
@crans_attribute
926
class host(dnsAttr):
927
    __slots__ = ()
928
    singlevalue = True
929
    unique = True
930 931
    optional = False
    hname = legend = u"Nom d'hôte"
932
    can_modify = [parent, nounou, cableur]
933
    category = 'base_tech'
934
    ldap_name = "host"
935

936
    def check_uniqueness(self, liste_exclue):
937
        attr = self.__class__.__name__
938 939
        if str(self) in liste_exclue:
            return
940
        if attr in ["host", "hostAlias"]:
Valentin Samir's avatar
Valentin Samir committed
941
            res = self.conn.search(u'(|(host=%s)(hostAlias=%s))' % ((str(self),)*2))
942 943 944
            if res:
                raise ValueError("Hôte déjà existant", [r.dn for r in res])

945
@crans_attribute
946
class hostAlias(host):
947
    __slots__ = ()
948 949 950 951
    singlevalue = False
    unique = True
    optional = True
    legend = u'Alias de nom de machine'
952
    can_modify = [nounou, cableur]
953
    ldap_name = "hostAlias"
954

955
@crans_attribute
956
class macAddress(Attr):
957
    __slots__ = ()
958 959 960 961
    singlevalue = True
    optional = False
    legend = u"Adresse physique de la carte réseau"
    hname = "Adresse MAC"
962
    category = 'base_tech'
963
    can_modify = [parent, nounou, cableur]
964
    ldap_name = "macAddress"
965
    default = u'<automatique>'
966

967
    def parse_value(self, mac):
968 969 970 971 972 973
        mac = mac.lower()
        if mac == macAddress.default:
            self.value = mac
        else:
            self.value = format_mac(mac)

974 975

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

978
@crans_attribute
979
class ipHostNumber(Attr):
980
    __slots__ = ()
981
    singlevalue = True
982
    optional = True
983
    unique = True
984 985
    legend = u"Adresse IPv4 de la machine"
    hname = "IPv4"
986
    category = 'base_tech'
987
    can_modify = [nounou, cableur]
988
    ldap_name = "ipHostNumber"
989
    python_type = netaddr.IPAddress
990
    default = u'<automatique>'
991

992
    def parse_value(self, ip):
993
        if ip == '<automatique>':
994
            ip = ip4_of_rid(str(self.parent['rid'][0]))
995
        self.value = self.python_type(ip)
996 997 998 999

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

1000
@crans_attribute
1001
class ip6HostNumber(Attr):
1002
    __slots__ = ()
1003 1004 1005 1006 1007 1008
    singlevalue = True
    optional = True
    unique = True
    legend = u"Adresse IPv6 de la machine"
    hname = "IPv6"
    category = 'base_tech'
1009 1010 1011
    # beware: parent est nécessaire à la modification des adresses macs par
    # les adhérents lambda (see validate_changes)
    can_modify = [nounou, cableur, parent]
1012
    ldap_name = "ip6HostNumber"
1013
    python_type = netaddr.IPAddress
1014
    default = u'<automatique>'
1015