models.py 41.3 KB
Newer Older
1
# -*- mode: python; coding: utf-8 -*-
chirac's avatar
chirac committed
2
3
4
# Re2o est un logiciel d'administration développé initiallement au rezometz.
# Il  se veut agnostique au réseau considéré, de manière à être installable
# en quelques clics.
lhark's avatar
lhark committed
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
#
# Copyright © 2017  Gabriel Détraz
# Copyright © 2017  Goulven Kermarec
# Copyright © 2017  Augustin Lemesle
#
# This program is free software; you can redistribute it and/or modify
# it under the terms of the GNU General Public License as published by
# the Free Software Foundation; either version 2 of the License, or
# (at your option) any later version.
#
# This program is distributed in the hope that it will be useful,
# but WITHOUT ANY WARRANTY; without even the implied warranty of
# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
# GNU General Public License for more details.
#
# You should have received a copy of the GNU General Public License along
# with this program; if not, write to the Free Software Foundation, Inc.,
# 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
chirac's avatar
chirac committed
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
"""
Models de l'application users.

On défini ici des models django classiques:
- users, qui hérite de l'abstract base user de django. Permet de définit
un utilisateur du site (login, passwd, chambre, adresse, etc)
- les whiteslist
- les bannissements
- les établissements d'enseignement (school)
- les droits (right et listright)
- les utilisateurs de service (pour connexion automatique)

On défini aussi des models qui héritent de django-ldapdb :
- ldapuser
- ldapgroup
- ldapserviceuser

Ces utilisateurs ldap sont synchronisés à partir des objets
models sql classiques. Seuls certains champs essentiels sont
dupliqués.
"""

lhark's avatar
lhark committed
45

46
47
from __future__ import unicode_literals

chirac's avatar
chirac committed
48
49
50
51
import re
import uuid
import datetime

lhark's avatar
lhark committed
52
from django.db import models
53
from django.db.models import Q
54
from django import forms
55
56
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
57
from django.utils.functional import cached_property
chirac's avatar
chirac committed
58
from django.template import Context, loader
59
60
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
chirac's avatar
chirac committed
61
62
63
64
from django.db import transaction
from django.utils import timezone
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.core.validators import RegexValidator
65

66
67
from reversion import revisions as reversion

68
69
70
import ldapdb.models
import ldapdb.models.fields

chirac's avatar
chirac committed
71
from re2o.settings import RIGHTS_LINK, LDAP, GID_RANGES, UID_RANGES
72
from re2o.login import hashNT
lhark's avatar
lhark committed
73

chibrac's avatar
chibrac committed
74
from cotisations.models import Cotisation, Facture, Paiement, Vente
chirac's avatar
chirac committed
75
76
77
from machines.models import Domain, Interface, Machine, regen
from preferences.models import GeneralOption, AssoOption, OptionalUser
from preferences.models import OptionalMachine, MailMessageOption
78

chirac's avatar
chirac committed
79
DT_NOW = timezone.now()
80

chirac's avatar
chirac committed
81

chirac's avatar
chirac committed
82
# Utilitaires généraux
chirac's avatar
chirac committed
83

84
85

def linux_user_check(login):
chirac's avatar
chirac committed
86
    """ Validation du pseudo pour respecter les contraintes unix"""
87
    UNIX_LOGIN_PATTERN = re.compile("^[a-zA-Z0-9-]*[$]?$")
88
89
90
91
    return UNIX_LOGIN_PATTERN.match(login)


def linux_user_validator(login):
chirac's avatar
chirac committed
92
    """ Retourne une erreur de validation si le login ne respecte
chirac's avatar
chirac committed
93
    pas les contraintes unix (maj, min, chiffres ou tiret)"""
94
    if not linux_user_check(login):
chirac's avatar
chirac committed
95
        raise forms.ValidationError(
chirac's avatar
chirac committed
96
97
            ", ce pseudo ('%(label)s') contient des carractères interdits",
            params={'label': login},
chirac's avatar
chirac committed
98
99
        )

chirac's avatar
chirac committed
100

Gabriel Detraz's avatar
Gabriel Detraz committed
101
def get_fresh_user_uid():
chirac's avatar
chirac committed
102
    """ Renvoie le plus petit uid non pris. Fonction très paresseuse """
chirac's avatar
chirac committed
103
104
105
106
    uids = list(range(
        int(min(UID_RANGES['users'])),
        int(max(UID_RANGES['users']))
    ))
107
    try:
108
        used_uids = list(User.objects.values_list('uid_number', flat=True))
109
110
    except:
        used_uids = []
chirac's avatar
chirac committed
111
    free_uids = [id for id in uids if id not in used_uids]
Gabriel Detraz's avatar
Gabriel Detraz committed
112
113
    return min(free_uids)

chirac's avatar
chirac committed
114

Gabriel Detraz's avatar
Gabriel Detraz committed
115
def get_fresh_gid():
chirac's avatar
chirac committed
116
    """ Renvoie le plus petit gid libre  """
chirac's avatar
chirac committed
117
118
119
120
    gids = list(range(
        int(min(GID_RANGES['posix'])),
        int(max(GID_RANGES['posix']))
    ))
121
    used_gids = list(ListRight.objects.values_list('gid', flat=True))
chirac's avatar
chirac committed
122
    free_gids = [id for id in gids if id not in used_gids]
Gabriel Detraz's avatar
Gabriel Detraz committed
123
    return min(free_gids)
124

chirac's avatar
chirac committed
125

126
def get_admin_right():
chirac's avatar
chirac committed
127
    """ Renvoie l'instance droit admin. La crée si elle n'existe pas
chirac's avatar
chirac committed
128
    Lui attribue un gid libre"""
129
130
131
132
    try:
        admin_right = ListRight.objects.get(listright="admin")
    except ListRight.DoesNotExist:
        admin_right = ListRight(listright="admin")
Gabriel Detraz's avatar
Gabriel Detraz committed
133
        admin_right.gid = get_fresh_gid()
134
135
136
        admin_right.save()
    return admin_right

137

138
class UserManager(BaseUserManager):
chirac's avatar
chirac committed
139
140
141
142
143
144
145
146
147
    """User manager basique de django"""
    def _create_user(
            self,
            pseudo,
            surname,
            email,
            password=None,
            su=False
    ):
148
149
150
151
        if not pseudo:
            raise ValueError('Users must have an username')

        if not linux_user_check(pseudo):
152
            raise ValueError('Username shall only contain [a-z0-9-]')
153
154
155
156
157
158
159
160
161
162
163
164
165

        user = self.model(
            pseudo=pseudo,
            surname=surname,
            email=self.normalize_email(email),
        )

        user.set_password(password)
        user.save(using=self._db)
        if su:
            user.make_admin()
        return user

166
    def create_user(self, pseudo, surname, email, password=None):
167
168
169
170
        """
        Creates and saves a User with the given pseudo, name, surname, email,
        and password.
        """
171
        return self._create_user(pseudo, surname, email, password, False)
172

173
    def create_superuser(self, pseudo, surname, email, password):
174
175
176
177
        """
        Creates and saves a superuser with the given pseudo, name, surname,
        email, and password.
        """
178
        return self._create_user(pseudo, surname, email, password, True)
179
180
181


class User(AbstractBaseUser):
chirac's avatar
chirac committed
182
183
184
    """ Definition de l'utilisateur de base.
    Champs principaux : name, surnname, pseudo, email, room, password
    Herite du django BaseUser et du système d'auth django"""
Gabriel Detraz's avatar
Gabriel Detraz committed
185
    PRETTY_NAME = "Utilisateurs (clubs et adhérents)"
lhark's avatar
lhark committed
186
    STATE_ACTIVE = 0
chirac's avatar
chirac committed
187
188
    STATE_DISABLED = 1
    STATE_ARCHIVE = 2
lhark's avatar
lhark committed
189
    STATES = (
chirac's avatar
chirac committed
190
191
192
193
        (0, 'STATE_ACTIVE'),
        (1, 'STATE_DISABLED'),
        (2, 'STATE_ARCHIVE'),
    )
lhark's avatar
lhark committed
194

chirac's avatar
chirac committed
195
    def auto_uid():
chirac's avatar
chirac committed
196
        """Renvoie un uid libre"""
Gabriel Detraz's avatar
Gabriel Detraz committed
197
        return get_fresh_user_uid()
chirac's avatar
chirac committed
198

lhark's avatar
lhark committed
199
    surname = models.CharField(max_length=255)
chirac's avatar
chirac committed
200
201
202
203
204
205
    pseudo = models.CharField(
        max_length=32,
        unique=True,
        help_text="Doit contenir uniquement des lettres, chiffres, ou tirets",
        validators=[linux_user_validator]
    )
lhark's avatar
lhark committed
206
    email = models.EmailField()
chirac's avatar
chirac committed
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
    school = models.ForeignKey(
        'School',
        on_delete=models.PROTECT,
        null=True,
        blank=True
    )
    shell = models.ForeignKey(
        'ListShell',
        on_delete=models.PROTECT,
        null=True,
        blank=True
    )
    comment = models.CharField(
        help_text="Commentaire, promo",
        max_length=255,
        blank=True
    )
lhark's avatar
lhark committed
224
    pwd_ntlm = models.CharField(max_length=255)
225
    state = models.IntegerField(choices=STATES, default=STATE_ACTIVE)
226
    registered = models.DateTimeField(auto_now_add=True)
Gabriel Detraz's avatar
Gabriel Detraz committed
227
    telephone = models.CharField(max_length=15, blank=True, null=True)
228
229
    uid_number = models.PositiveIntegerField(default=auto_uid, unique=True)
    rezo_rez_uid = models.PositiveIntegerField(unique=True, blank=True, null=True)
lhark's avatar
lhark committed
230

231
    USERNAME_FIELD = 'pseudo'
232
    REQUIRED_FIELDS = ['surname', 'email']
233
234
235

    objects = UserManager()

236
237
238
239
240
241
242
243
    @cached_property
    def name(self):
        """Si il s'agit d'un adhérent, on renvoie le prénom"""
        if self.is_class_adherent:
            return self.adherent.name
        else:
            return ''

244
245
246
247
248
249
250
251
252
253
    @cached_property
    def room(self):
        """Alias vers room """
        if self.is_class_adherent:
            return self.adherent.room
        elif self.is_class_club:
            return self.club.room
        else:
            raise NotImplementedError("Type inconnu")

254
255
256
257
    @cached_property
    def class_name(self):
        """Renvoie si il s'agit d'un adhérent ou d'un club"""
        if hasattr(self, 'adherent'):
Gabriel Detraz's avatar
Gabriel Detraz committed
258
            return "Adherent"
259
260
261
262
263
264
265
266
267
268
269
270
271
        elif hasattr(self, 'club'):
            return "Club"
        else:
            raise NotImplementedError("Type inconnu")

    @cached_property
    def is_class_club(self):
        return hasattr(self, 'club')

    @cached_property
    def is_class_adherent(self):
        return hasattr(self, 'adherent')

272
273
    @property
    def is_active(self):
chirac's avatar
chirac committed
274
        """ Renvoie si l'user est à l'état actif"""
275
276
277
278
        return self.state == self.STATE_ACTIVE

    @property
    def is_staff(self):
chirac's avatar
chirac committed
279
        """ Fonction de base django, renvoie si l'user est admin"""
280
281
282
283
        return self.is_admin

    @property
    def is_admin(self):
chirac's avatar
chirac committed
284
        """ Renvoie si l'user est admin"""
285
286
287
288
289
290
291
292
        try:
            Right.objects.get(user=self, right__listright='admin')
        except Right.DoesNotExist:
            return False
        return True

    @is_admin.setter
    def is_admin(self, value):
chirac's avatar
chirac committed
293
294
        """ Change la valeur de admin à true ou false suivant la valeur de
        value"""
295
296
297
298
299
300
        if value and not self.is_admin:
            self.make_admin()
        elif not value and self.is_admin:
            self.un_admin()

    def get_full_name(self):
chirac's avatar
chirac committed
301
        """ Renvoie le nom complet de l'user formaté nom/prénom"""
302
303
304
305
306
        name = self.name
        if name:
            return '%s %s' % (name, self.surname)
        else:
            return self.surname
307
308

    def get_short_name(self):
chirac's avatar
chirac committed
309
        """ Renvoie seulement le nom"""
310
        return self.surname
311

312
    def has_perms(self, perms, obj=None):
chirac's avatar
chirac committed
313
314
315
        """ Renvoie true si l'user dispose de la permission.
        Prend en argument une liste de permissions.
        TODO : Arranger cette fonction"""
316
        for perm in perms:
317
318
319
320
321
            if perm in RIGHTS_LINK:
                query = Q()
                for right in RIGHTS_LINK[perm]:
                    query = query | Q(right__listright=right)
                if Right.objects.filter(Q(user=self) & query):
chirac's avatar
chirac committed
322
                    return True
323
324
325
326
            try:
                Right.objects.get(user=self, right__listright=perm)
            except Right.DoesNotExist:
                return False
327
328
        return True

329
    def has_perm(self, perm, obj=None):
chirac's avatar
chirac committed
330
        """Ne sert à rien"""
331
332
        return True

333
    def has_right(self, right):
chirac's avatar
chirac committed
334
335
        """ Renvoie si un user a un right donné. Crée le right si il n'existe
        pas"""
336
337
338
339
340
        try:
            list_right = ListRight.objects.get(listright=right)
        except:
            list_right = ListRight(listright=right, gid=get_fresh_gid())
            list_right.save()
chirac's avatar
chirac committed
341
342
343
        return Right.objects.filter(user=self).filter(
            right=list_right
        ).exists()
344
345
346

    @cached_property
    def is_bureau(self):
chirac's avatar
chirac committed
347
        """ True si user a les droits bureau """
348
        return self.has_right('bureau')
349
350
351

    @cached_property
    def is_bofh(self):
chirac's avatar
chirac committed
352
        """ True si l'user a les droits bofh"""
353
        return self.has_right('bofh')
354
355
356

    @cached_property
    def is_cableur(self):
chirac's avatar
chirac committed
357
        """ True si l'user a les droits cableur
chirac's avatar
chirac committed
358
        (également true si bureau, infra  ou bofh)"""
chirac's avatar
chirac committed
359
360
        return self.has_right('cableur') or self.has_right('bureau') or\
            self.has_right('infra') or self.has_right('bofh')
361
362
363

    @cached_property
    def is_trez(self):
chirac's avatar
chirac committed
364
        """ Renvoie true si droits trésorier pour l'user"""
365
        return self.has_right('tresorier')
366
367
368

    @cached_property
    def is_infra(self):
chirac's avatar
chirac committed
369
        """ True si a les droits infra"""
370
        return self.has_right('infra')
371

372
    def end_adhesion(self):
chirac's avatar
chirac committed
373
374
        """ Renvoie la date de fin d'adhésion d'un user. Examine les objets
        cotisation"""
chirac's avatar
chirac committed
375
376
377
378
379
380
        date_max = Cotisation.objects.filter(
            vente__in=Vente.objects.filter(
                facture__in=Facture.objects.filter(
                    user=self
                ).exclude(valid=False)
            )
Gabriel Detraz's avatar
Gabriel Detraz committed
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
        ).filter(
            Q(type_cotisation='All') | Q(type_cotisation='Adhesion')
        ).aggregate(models.Max('date_end'))['date_end__max']
        return date_max

    def end_connexion(self):
        """ Renvoie la date de fin de connexion d'un user. Examine les objets
        cotisation"""
        date_max = Cotisation.objects.filter(
            vente__in=Vente.objects.filter(
                facture__in=Facture.objects.filter(
                    user=self
                ).exclude(valid=False)
            )
        ).filter(
            Q(type_cotisation='All') | Q(type_cotisation='Connexion')
chirac's avatar
chirac committed
397
        ).aggregate(models.Max('date_end'))['date_end__max']
398
399
400
        return date_max

    def is_adherent(self):
chirac's avatar
chirac committed
401
402
        """ Renvoie True si l'user est adhérent : si
        self.end_adhesion()>now"""
Gabriel Detraz's avatar
Gabriel Detraz committed
403
        end = self.end_adhesion()
404
405
        if not end:
            return False
chirac's avatar
chirac committed
406
        elif end < DT_NOW:
407
408
409
410
            return False
        else:
            return True

Gabriel Detraz's avatar
Gabriel Detraz committed
411
412
413
414
415
416
417
418
419
420
421
    def is_connected(self):
        """ Renvoie True si l'user est adhérent : si
        self.end_adhesion()>now et end_connexion>now"""
        end = self.end_connexion()
        if not end:
            return False
        elif end < DT_NOW:
            return False
        else:
            return self.is_adherent()

422
    @cached_property
423
424
    def end_ban(self):
        """ Renvoie la date de fin de ban d'un user, False sinon """
chirac's avatar
chirac committed
425
426
427
        date_max = Ban.objects.filter(
            user=self
        ).aggregate(models.Max('date_end'))['date_end__max']
428
429
        return date_max

430
    @cached_property
431
    def end_whitelist(self):
432
        """ Renvoie la date de fin de whitelist d'un user, False sinon """
chirac's avatar
chirac committed
433
434
435
        date_max = Whitelist.objects.filter(
            user=self
        ).aggregate(models.Max('date_end'))['date_end__max']
436
437
        return date_max

438
    @cached_property
439
440
    def is_ban(self):
        """ Renvoie si un user est banni ou non """
441
        end = self.end_ban
442
443
        if not end:
            return False
chirac's avatar
chirac committed
444
        elif end < DT_NOW:
445
446
447
448
            return False
        else:
            return True

449
    @cached_property
450
451
    def is_whitelisted(self):
        """ Renvoie si un user est whitelisté ou non """
452
        end = self.end_whitelist
453
454
        if not end:
            return False
chirac's avatar
chirac committed
455
        elif end < DT_NOW:
456
457
458
459
460
461
            return False
        else:
            return True

    def has_access(self):
        """ Renvoie si un utilisateur a accès à internet """
chirac's avatar
chirac committed
462
        return self.state == User.STATE_ACTIVE\
Gabriel Detraz's avatar
Gabriel Detraz committed
463
            and not self.is_ban and (self.is_connected() or self.is_whitelisted)
464

465
466
    def end_access(self):
        """ Renvoie la date de fin normale d'accès (adhésion ou whiteliste)"""
Gabriel Detraz's avatar
Gabriel Detraz committed
467
        if not self.end_connexion():
468
            if not self.end_whitelist:
469
470
                return None
            else:
471
                return self.end_whitelist
472
        else:
473
            if not self.end_whitelist:
Gabriel Detraz's avatar
Gabriel Detraz committed
474
                return self.end_connexion()
chirac's avatar
chirac committed
475
            else:
Gabriel Detraz's avatar
Gabriel Detraz committed
476
                return max(self.end_connexion(), self.end_whitelist)
477

chibrac's avatar
chibrac committed
478
479
    @cached_property
    def solde(self):
chirac's avatar
chirac committed
480
481
        """ Renvoie le solde d'un user. Vérifie que l'option solde est
        activé, retourne 0 sinon.
chirac's avatar
chirac committed
482
        Somme les crédits de solde et retire les débit payés par solde"""
chirac's avatar
chirac committed
483
        options, _created = OptionalUser.objects.get_or_create()
chibrac's avatar
chibrac committed
484
485
        user_solde = options.user_solde
        if user_solde:
chirac's avatar
chirac committed
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
            solde_object, _created = Paiement.objects.get_or_create(
                moyen='Solde'
            )
            somme_debit = Vente.objects.filter(
                facture__in=Facture.objects.filter(
                    user=self,
                    paiement=solde_object
                )
            ).aggregate(
                total=models.Sum(
                    models.F('prix')*models.F('number'),
                    output_field=models.FloatField()
                )
            )['total'] or 0
            somme_credit = Vente.objects.filter(
                facture__in=Facture.objects.filter(user=self),
                name="solde"
            ).aggregate(
                total=models.Sum(
                    models.F('prix')*models.F('number'),
                    output_field=models.FloatField()
                )
            )['total'] or 0
chibrac's avatar
chibrac committed
509
510
511
512
            return somme_credit - somme_debit
        else:
            return 0

chirac's avatar
chirac committed
513
    def user_interfaces(self, active=True):
chirac's avatar
chirac committed
514
515
516
517
518
        """ Renvoie toutes les interfaces dont les machines appartiennent à
        self. Par defaut ne prend que les interfaces actives"""
        return Interface.objects.filter(
            machine__in=Machine.objects.filter(user=self, active=active)
        ).select_related('domain__extension')
519

520
521
522
523
524
525
526
527
528
529
530
    def assign_ips(self):
        """ Assign une ipv4 aux machines d'un user """
        interfaces = self.user_interfaces()
        for interface in interfaces:
            if not interface.ipv4:
                with transaction.atomic(), reversion.create_revision():
                    interface.assign_ipv4()
                    reversion.set_comment("Assignation ipv4")
                    interface.save()

    def unassign_ips(self):
chirac's avatar
chirac committed
531
        """ Désassigne les ipv4 aux machines de l'user"""
532
533
534
535
536
537
538
539
        interfaces = self.user_interfaces()
        for interface in interfaces:
            with transaction.atomic(), reversion.create_revision():
                interface.unassign_ipv4()
                reversion.set_comment("Désassignation ipv4")
                interface.save()

    def archive(self):
chirac's avatar
chirac committed
540
541
        """ Archive l'user : appelle unassign_ips() puis passe state à
        ARCHIVE"""
542
        self.unassign_ips()
chirac's avatar
chirac committed
543
        self.state = User.STATE_ARCHIVE
544
545

    def unarchive(self):
chirac's avatar
chirac committed
546
547
        """ Désarchive l'user : réassigne ses ip et le passe en state
        ACTIVE"""
548
549
550
        self.assign_ips()
        self.state = User.STATE_ACTIVE

551
    def has_module_perms(self, app_label):
chirac's avatar
chirac committed
552
        """True, a toutes les permissions de module"""
553
554
555
556
557
558
559
560
        return True

    def make_admin(self):
        """ Make User admin """
        user_admin_right = Right(user=self, right=get_admin_right())
        user_admin_right.save()

    def un_admin(self):
chirac's avatar
chirac committed
561
        """Supprime les droits admin d'un user"""
562
        try:
chirac's avatar
chirac committed
563
            user_right = Right.objects.get(user=self, right=get_admin_right())
564
565
566
567
        except Right.DoesNotExist:
            return
        user_right.delete()

568
    def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True):
chirac's avatar
chirac committed
569
570
571
572
573
574
        """ Synchronisation du ldap. Synchronise dans le ldap les attributs de
        self
        Options : base : synchronise tous les attributs de base - nom, prenom,
        mail, password, shell, home
        access_refresh : synchronise le dialup_access notant si l'user a accès
        aux services
575
576
        mac_refresh : synchronise les machines de l'user
        Si l'instance n'existe pas, on crée le ldapuser correspondant"""
chirac's avatar
chirac committed
577
        self.refresh_from_db()
578
        try:
root's avatar
root committed
579
            user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
580
        except LdapUser.DoesNotExist:
root's avatar
root committed
581
            user_ldap = LdapUser(uidNumber=self.uid_number)
582
583
584
            base = True
            access_refresh = True
            mac_refresh = True
585
        if base:
chirac's avatar
chirac committed
586
            user_ldap.name = self.pseudo
587
            user_ldap.sn = self.pseudo
Gabriel Detraz's avatar
Gabriel Detraz committed
588
            user_ldap.dialupAccess = str(self.has_access())
589
590
            user_ldap.home_directory = '/home/' + self.pseudo
            user_ldap.mail = self.email
chirac's avatar
chirac committed
591
592
            user_ldap.given_name = self.surname.lower() + '_'\
                + self.name.lower()[:3]
593
            user_ldap.gid = LDAP['user_gid']
594
            user_ldap.user_password = self.password[:6] + self.password[7:]
Gabriel Detraz's avatar
Gabriel Detraz committed
595
            user_ldap.sambat_nt_password = self.pwd_ntlm.upper()
chirac's avatar
chirac committed
596
            if self.shell:
chibrac's avatar
chibrac committed
597
598
599
600
601
                user_ldap.login_shell = self.shell.shell
            if self.state == self.STATE_DISABLED:
                user_ldap.shadowexpire = str(0)
            else:
                user_ldap.shadowexpire = None
602
        if access_refresh:
Gabriel Detraz's avatar
Gabriel Detraz committed
603
            user_ldap.dialupAccess = str(self.has_access())
604
        if mac_refresh:
605
606
607
            user_ldap.macs = [str(mac) for mac in Interface.objects.filter(
                machine__user=self
            ).values_list('mac_address', flat=True).distinct()]
608
609
610
        user_ldap.save()

    def ldap_del(self):
chirac's avatar
chirac committed
611
        """ Supprime la version ldap de l'user"""
612
613
614
615
616
617
        try:
            user_ldap = LdapUser.objects.get(name=self.pseudo)
            user_ldap.delete()
        except LdapUser.DoesNotExist:
            pass

618
619
    def notif_inscription(self):
        """ Prend en argument un objet user, envoie un mail de bienvenue """
chirac's avatar
chirac committed
620
621
622
623
624
625
        template = loader.get_template('users/email_welcome')
        assooptions, _created = AssoOption.objects.get_or_create()
        mailmessageoptions, _created = MailMessageOption\
            .objects.get_or_create()
        general_options, _created = GeneralOption.objects.get_or_create()
        context = Context({
626
            'nom': self.get_full_name(),
627
628
            'asso_name': assooptions.name,
            'asso_email': assooptions.contact,
chirac's avatar
chirac committed
629
630
631
            'welcome_mail_fr': mailmessageoptions.welcome_mail_fr,
            'welcome_mail_en': mailmessageoptions.welcome_mail_en,
            'pseudo': self.pseudo,
632
        })
chirac's avatar
chirac committed
633
634
635
636
637
638
639
640
641
        send_mail(
            'Bienvenue au %(name)s / Welcome to %(name)s' % {
                'name': assooptions.name
                },
            '',
            general_options.email_from,
            [self.email],
            html_message=template.render(context)
        )
642
643
644
        return

    def reset_passwd_mail(self, request):
chirac's avatar
chirac committed
645
646
        """ Prend en argument un request, envoie un mail de
        réinitialisation de mot de pass """
647
648
649
650
        req = Request()
        req.type = Request.PASSWD
        req.user = self
        req.save()
chirac's avatar
chirac committed
651
652
653
654
        template = loader.get_template('users/email_passwd_request')
        options, _created = AssoOption.objects.get_or_create()
        general_options, _created = GeneralOption.objects.get_or_create()
        context = {
655
            'name': req.user.get_full_name(),
656
657
658
659
            'asso': options.name,
            'asso_mail': options.contact,
            'site_name': general_options.site_name,
            'url': request.build_absolute_uri(
chirac's avatar
chirac committed
660
                reverse('users:process', kwargs={'token': req.token})),
661
662
            'expire_in': str(general_options.req_expire_hrs) + ' heures',
            }
chirac's avatar
chirac committed
663
664
665
666
667
668
669
670
        send_mail(
            'Changement de mot de passe du %(name)s / Password\
            renewal for %(name)s' % {'name': options.name},
            template.render(context),
            general_options.email_from,
            [req.user.email],
            fail_silently=False
        )
671
672
        return

673
    def autoregister_machine(self, mac_address, nas_type):
chirac's avatar
chirac committed
674
675
        """ Fonction appellée par freeradius. Enregistre la mac pour
        une machine inconnue sur le compte de l'user"""
chirac's avatar
chirac committed
676
        all_interfaces = self.user_interfaces(active=False)
chirac's avatar
chirac committed
677
        options, _created = OptionalMachine.objects.get_or_create()
chirac's avatar
chirac committed
678
        if all_interfaces.count() > options.max_lambdauser_interfaces:
679
            return False, "Maximum de machines enregistrees atteinte"
680
        if not nas_type:
chirac's avatar
chirac committed
681
682
            return False, "Re2o ne sait pas à quel machinetype affecter cette\
            machine"
683
        machine_type_cible = nas_type.machine_type
684
685
686
687
688
        try:
            machine_parent = Machine()
            machine_parent.user = self
            interface_cible = Interface()
            interface_cible.mac_address = mac_address
689
            interface_cible.type = machine_type_cible
690
691
692
            interface_cible.clean()
            machine_parent.clean()
            domain = Domain()
693
            domain.name = self.get_next_domain_name()
694
695
            domain.interface_parent = interface_cible
            domain.clean()
696
697
698
699
700
701
            machine_parent.save()
            interface_cible.machine = machine_parent
            interface_cible.save()
            domain.interface_parent = interface_cible
            domain.clean()
            domain.save()
702
            self.notif_auto_newmachine(interface_cible)
chirac's avatar
chirac committed
703
704
        except Exception as error:
            return False, error
705
706
        return True, "Ok"

707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
724
725
726
727
728
729
    def notif_auto_newmachine(self, interface):
        """Notification mail lorsque une machine est automatiquement
        ajoutée par le radius"""
        template = loader.get_template('users/email_auto_newmachine')
        assooptions, _created = AssoOption.objects.get_or_create()
        general_options, _created = GeneralOption.objects.get_or_create()
        context = Context({
            'nom': self.get_full_name(),
            'mac_address' : interface.mac_address,
            'asso_name': assooptions.name,
            'interface_name' : interface.domain,
            'asso_email': assooptions.contact,
            'pseudo': self.pseudo,
        })
        send_mail(
            "Ajout automatique d'une machine / New machine autoregistered",
            '',
            general_options.email_from,
            [self.email],
            html_message=template.render(context)
        )
        return

730
    def set_user_password(self, password):
chirac's avatar
chirac committed
731
        """ A utiliser de préférence, set le password en hash courrant et
chirac's avatar
chirac committed
732
        dans la version ntlm"""
733
734
735
736
        self.set_password(password)
        self.pwd_ntlm = hashNT(password)
        return

737
738
739
    def get_next_domain_name(self):
        """Look for an available name for a new interface for
        this user by trying "pseudo0", "pseudo1", "pseudo2", ...
chirac's avatar
chirac committed
740
741
742

        Recherche un nom disponible, pour une machine. Doit-être
        unique, concatène le nom, le pseudo et le numero de machine
743
744
745
        """

        def simple_pseudo():
chirac's avatar
chirac committed
746
            """Renvoie le pseudo sans underscore (compat dns)"""
747
748
            return self.pseudo.replace('_', '-').lower()

chirac's avatar
chirac committed
749
750
751
        def composed_pseudo(name):
            """Renvoie le resultat de simplepseudo et rajoute le nom"""
            return simple_pseudo() + str(name)
752
753

        num = 0
chirac's avatar
chirac committed
754
        while Domain.objects.filter(name=composed_pseudo(num)):
755
756
757
            num += 1
        return composed_pseudo(num)

758
    def __str__(self):
759
        return self.pseudo
lhark's avatar
lhark committed
760

chirac's avatar
chirac committed
761

762
class Adherent(User):
Gabriel Detraz's avatar
Gabriel Detraz committed
763
    PRETTY_NAME = "Adhérents"
764
    name = models.CharField(max_length=255)
765
766
767
768
769
770
    room = models.OneToOneField(
        'topologie.Room',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
771
772
773
774
    pass


class Club(User):
Gabriel Detraz's avatar
Gabriel Detraz committed
775
    PRETTY_NAME = "Clubs"
776
777
778
779
780
781
    room = models.ForeignKey(
        'topologie.Room',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
782
783
784
    pass


785
786
@receiver(post_save, sender=Adherent)
@receiver(post_save, sender=Club)
787
@receiver(post_save, sender=User)
788
def user_post_save(sender, **kwargs):
chirac's avatar
chirac committed
789
790
    """ Synchronisation post_save : envoie le mail de bienvenue si creation
    Synchronise le ldap"""
791
    is_created = kwargs['created']
792
    user = kwargs['instance']
793
794
    if is_created:
        user.notif_inscription()
795
    user.ldap_sync(base=True, access_refresh=True, mac_refresh=False)
796
    regen('mailing')
797

chirac's avatar
chirac committed
798

799
800
@receiver(post_delete, sender=Adherent)
@receiver(post_delete, sender=Club)
801
@receiver(post_delete, sender=User)
802
def user_post_delete(sender, **kwargs):
chirac's avatar
chirac committed
803
    """Post delete d'un user, on supprime son instance ldap"""
804
    user = kwargs['instance']
805
    user.ldap_del()
806
    regen('mailing')
807

chirac's avatar
chirac committed
808

chirac's avatar
chirac committed
809
class ServiceUser(AbstractBaseUser):
chirac's avatar
chirac committed
810
    """ Classe des users daemons, règle leurs accès au ldap"""
811
812
    readonly = 'readonly'
    ACCESS = (
chirac's avatar
chirac committed
813
814
815
816
        ('auth', 'auth'),
        ('readonly', 'readonly'),
        ('usermgmt', 'usermgmt'),
    )
817

818
    PRETTY_NAME = "Utilisateurs de service"
chirac's avatar
chirac committed
819

chirac's avatar
chirac committed
820
821
822
823
824
825
826
827
828
829
830
831
832
833
834
835
    pseudo = models.CharField(
        max_length=32,
        unique=True,
        help_text="Doit contenir uniquement des lettres, chiffres, ou tirets",
        validators=[linux_user_validator]
    )
    access_group = models.CharField(
        choices=ACCESS,
        default=readonly,
        max_length=32
    )
    comment = models.CharField(
        help_text="Commentaire",
        max_length=255,
        blank=True
    )
chirac's avatar
chirac committed
836
837
838
839
840

    USERNAME_FIELD = 'pseudo'
    objects = UserManager()

    def ldap_sync(self):
chirac's avatar
chirac committed
841
        """ Synchronisation du ServiceUser dans sa version ldap"""
chirac's avatar
chirac committed
842
843
844
845
        try:
            user_ldap = LdapServiceUser.objects.get(name=self.pseudo)
        except LdapServiceUser.DoesNotExist:
            user_ldap = LdapServiceUser(name=self.pseudo)
846
        user_ldap.user_password = self.password[:6] + self.password[7:]
chirac's avatar
chirac committed
847
        user_ldap.save()
848
        self.serviceuser_group_sync()
chirac's avatar
chirac committed
849
850

    def ldap_del(self):
chirac's avatar
chirac committed
851
        """Suppression de l'instance ldap d'un service user"""
chirac's avatar
chirac committed
852
853
854
855
856
        try:
            user_ldap = LdapServiceUser.objects.get(name=self.pseudo)
            user_ldap.delete()
        except LdapUser.DoesNotExist:
            pass
857
858
859
        self.serviceuser_group_sync()

    def serviceuser_group_sync(self):
chirac's avatar
chirac committed
860
        """Synchronise le groupe et les droits de groupe dans le ldap"""
861
862
863
864
        try:
            group = LdapServiceUserGroup.objects.get(name=self.access_group)
        except:
            group = LdapServiceUserGroup(name=self.access_group)
chirac's avatar
chirac committed
865
866
867
868
        group.members = list(LdapServiceUser.objects.filter(
            name__in=[user.pseudo for user in ServiceUser.objects.filter(
                access_group=self.access_group
            )]).values_list('dn', flat=True))
869
        group.save()
chirac's avatar
chirac committed
870
871
872
873

    def __str__(self):
        return self.pseudo

chirac's avatar
chirac committed
874

chirac's avatar
chirac committed
875
876
@receiver(post_save, sender=ServiceUser)
def service_user_post_save(sender, **kwargs):
chirac's avatar
chirac committed
877
    """ Synchronise un service user ldap après modification django"""
chirac's avatar
chirac committed
878
    service_user = kwargs['instance']
879
    service_user.ldap_sync()
chirac's avatar
chirac committed
880

chirac's avatar
chirac committed
881

chirac's avatar
chirac committed
882
883
@receiver(post_delete, sender=ServiceUser)
def service_user_post_delete(sender, **kwargs):
chirac's avatar
chirac committed
884
    """ Supprime un service user ldap après suppression django"""
chirac's avatar
chirac committed
885
    service_user = kwargs['instance']
886
    service_user.ldap_del()
chirac's avatar
chirac committed
887

chirac's avatar
chirac committed
888

889
class Right(models.Model):
chirac's avatar
chirac committed
890
891
    """ Couple droit/user. Peut-être aurait-on mieux fait ici d'utiliser un
    manytomany
chirac's avatar
chirac committed
892
    Ceci dit le résultat aurait été le même avec une table intermediaire"""
893
894
    PRETTY_NAME = "Droits affectés à des users"

895
    user = models.ForeignKey('User', on_delete=models.PROTECT)
896
    right = models.ForeignKey('ListRight', on_delete=models.PROTECT)
897

898
899
900
    class Meta:
        unique_together = ("user", "right")

901
    def __str__(self):
902
        return str(self.user)
903

chirac's avatar
chirac committed
904

905
906
@receiver(post_save, sender=Right)
def right_post_save(sender, **kwargs):
chirac's avatar
chirac committed
907
    """ Synchronise les users ldap groups avec les groupes de droits"""
908
    right = kwargs['instance'].right
909
    right.ldap_sync()
910

chirac's avatar
chirac committed
911

912
913
@receiver(post_delete, sender=Right)
def right_post_delete(sender, **kwargs):
chirac's avatar
chirac committed
914
    """ Supprime l'user du groupe"""
915
    right = kwargs['instance'].right
916
    right.ldap_sync()
917

chirac's avatar
chirac committed
918

lhark's avatar
lhark committed
919
class School(models.Model):
chirac's avatar
chirac committed
920
    """ Etablissement d'enseignement"""
921
922
    PRETTY_NAME = "Etablissements enregistrés"

lhark's avatar
lhark committed
923
924
    name = models.CharField(max_length=255)

925
926
927
    def __str__(self):
        return self.name

928

929
class ListRight(models.Model):
chirac's avatar
chirac committed
930
931
    """ Ensemble des droits existants. Chaque droit crée un groupe
    ldap synchronisé, avec gid.
chirac's avatar
chirac committed
932
    Permet de gérer facilement les accès serveurs et autres
chirac's avatar
chirac committed
933
934
    La clef de recherche est le gid, pour cette raison là
    il n'est plus modifiable après creation"""
935
936
    PRETTY_NAME = "Liste des droits existants"

chirac's avatar
chirac committed
937
938
939
940
941
942
943
944
945
    listright = models.CharField(
        max_length=255,
        unique=True,
        validators=[RegexValidator(
            '^[a-z]+$',
            message="Les groupes unix ne peuvent contenir\
            que des lettres minuscules"
        )]
    )
946
    gid = models.PositiveIntegerField(unique=True, null=True)
chirac's avatar
chirac committed
947
948
949
950
951
    details = models.CharField(
        help_text="Description",
        max_length=255,
        blank=True
    )
952
953
954
955

    def __str__(self):
        return self.listright

956
    def ldap_sync(self):
chirac's avatar
chirac committed
957
        """Sychronise les groups ldap avec le model listright coté django"""
958
959
960
961
962
        try:
            group_ldap = LdapUserGroup.objects.get(gid=self.gid)
        except LdapUserGroup.DoesNotExist:
            group_ldap = LdapUserGroup(gid=self.gid)
        group_ldap.name = self.listright
chirac's avatar
chirac committed
963
964
        group_ldap.members = [right.user.pseudo for right
                              in Right.objects.filter(right=self)]
965
966
967
        group_ldap.save()

    def ldap_del(self):
chirac's avatar
chirac committed
968
        """Supprime un groupe ldap"""
969
970
971
972
973
974
        try:
            group_ldap = LdapUserGroup.objects.get(gid=self.gid)
            group_ldap.delete()
        except LdapUserGroup.DoesNotExist:
            pass

chirac's avatar
chirac committed
975

976
977
@receiver(post_save, sender=ListRight)
def listright_post_save(sender, **kwargs):
chirac's avatar
chirac committed
978
    """ Synchronise le droit ldap quand il est modifié"""
979
    right = kwargs['instance']
980
    right.ldap_sync()
981

chirac's avatar
chirac committed
982

983
984
@receiver(post_delete, sender=ListRight)
def listright_post_delete(sender, **kwargs):
chirac's avatar
chirac committed
985
    """Suppression d'un groupe ldap après suppression coté django"""
986
    right = kwargs['instance']
987
    right.ldap_del()
988

chirac's avatar
chirac committed
989

990
class ListShell(models.Model):
chirac's avatar
chirac committed
991
992
    """Un shell possible. Pas de check si ce shell existe, les
    admin sont des grands"""
993
994
    PRETTY_NAME = "Liste des shells disponibles"

995
996
997
998
    shell = models.CharField(max_length=255, unique=True)

    def __str__(self):
        return self.shell
999

chirac's avatar
chirac committed
1000

chirac's avatar
chirac committed
1001
class Ban(models.Model):
chirac's avatar
chirac committed
1002
1003
    """ Bannissement. Actuellement a un effet tout ou rien.
    Gagnerait à être granulaire"""
1004
1005
    PRETTY_NAME = "Liste des bannissements"

Gabriel Detraz's avatar
Gabriel Detraz committed
1006
1007
1008
1009
    STATE_HARD = 0
    STATE_SOFT = 1
    STATE_BRIDAGE = 2
    STATES = (
chirac's avatar
chirac committed
1010
1011
1012
1013
        (0, 'HARD (aucun accès)'),
        (1, 'SOFT (accès local seulement)'),
        (2, 'BRIDAGE (bridage du débit)'),
    )
Gabriel Detraz's avatar
Gabriel Detraz committed
1014

chirac's avatar
chirac committed
1015
1016
    user = models.ForeignKey('User', on_delete=models.PROTECT)
    raison = models.CharField(max_length=255)
1017
    date_start = models.DateTimeField(auto_now_add=True)