Ce serveur Gitlab sera éteint le 30 juin 2020, pensez à migrer vos projets vers les serveurs gitlab-research.centralesupelec.fr et gitlab-student.centralesupelec.fr !

models.py 52.8 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.
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.
"""

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
from django.db.models.signals import post_save, post_delete, m2m_changed
56
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
from django.db import transaction
from django.utils import timezone
63 64
from django.contrib.auth.models import (
    AbstractBaseUser,
65
    BaseUserManager,
66 67
    PermissionsMixin,
    Group
68
)
chirac's avatar
chirac committed
69
from django.core.validators import RegexValidator
70

71 72
from reversion import revisions as reversion

73 74 75
import ldapdb.models
import ldapdb.models.fields

chirac's avatar
chirac committed
76
from re2o.settings import LDAP, GID_RANGES, UID_RANGES
77
from re2o.login import hashNT
78
from re2o.field_permissions import FieldPermissionModelMixin
79
from re2o.mixins import AclMixin, RevMixin
lhark's avatar
lhark committed
80

chibrac's avatar
chibrac committed
81
from cotisations.models import Cotisation, Facture, Paiement, Vente
chirac's avatar
chirac committed
82 83 84
from machines.models import Domain, Interface, Machine, regen
from preferences.models import GeneralOption, AssoOption, OptionalUser
from preferences.models import OptionalMachine, MailMessageOption
85

chirac's avatar
chirac committed
86

chirac's avatar
chirac committed
87
# Utilitaires généraux
chirac's avatar
chirac committed
88

89 90

def linux_user_check(login):
chirac's avatar
chirac committed
91
    """ Validation du pseudo pour respecter les contraintes unix"""
92
    UNIX_LOGIN_PATTERN = re.compile("^[a-zA-Z0-9-]*[$]?$")
93 94 95 96
    return UNIX_LOGIN_PATTERN.match(login)


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

chirac's avatar
chirac committed
105

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

chirac's avatar
chirac committed
119

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

chirac's avatar
chirac committed
130

131
class UserManager(BaseUserManager):
chirac's avatar
chirac committed
132 133 134 135 136 137 138 139 140
    """User manager basique de django"""
    def _create_user(
            self,
            pseudo,
            surname,
            email,
            password=None,
            su=False
    ):
141 142 143 144
        if not pseudo:
            raise ValueError('Users must have an username')

        if not linux_user_check(pseudo):
145
            raise ValueError('Username shall only contain [a-z0-9-]')
146

147
        user = Adherent(
148 149
            pseudo=pseudo,
            surname=surname,
150
            name=surname,
151 152 153 154 155
            email=self.normalize_email(email),
        )

        user.set_password(password)
        if su:
156
            user.is_superuser = True
157
        user.save(using=self._db)
158 159
        return user

160
    def create_user(self, pseudo, surname, email, password=None):
161 162 163 164
        """
        Creates and saves a User with the given pseudo, name, surname, email,
        and password.
        """
165
        return self._create_user(pseudo, surname, email, password, False)
166

167
    def create_superuser(self, pseudo, surname, email, password):
168 169 170 171
        """
        Creates and saves a superuser with the given pseudo, name, surname,
        email, and password.
        """
172
        return self._create_user(pseudo, surname, email, password, True)
173

174 175 176

class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
           PermissionsMixin, AclMixin):
chirac's avatar
chirac committed
177 178 179
    """ 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
180
    PRETTY_NAME = "Utilisateurs (clubs et adhérents)"
lhark's avatar
lhark committed
181
    STATE_ACTIVE = 0
chirac's avatar
chirac committed
182 183
    STATE_DISABLED = 1
    STATE_ARCHIVE = 2
lhark's avatar
lhark committed
184
    STATES = (
chirac's avatar
chirac committed
185 186 187 188
        (0, 'STATE_ACTIVE'),
        (1, 'STATE_DISABLED'),
        (2, 'STATE_ARCHIVE'),
    )
lhark's avatar
lhark committed
189 190

    surname = models.CharField(max_length=255)
chirac's avatar
chirac committed
191 192 193 194 195 196
    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
197
    email = models.EmailField()
chirac's avatar
chirac committed
198 199 200 201 202 203 204 205 206 207 208 209 210 211 212 213 214
    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
215
    pwd_ntlm = models.CharField(max_length=255)
216
    state = models.IntegerField(choices=STATES, default=STATE_ACTIVE)
217
    registered = models.DateTimeField(auto_now_add=True)
218
    telephone = models.CharField(max_length=15, blank=True, null=True)
219 220 221 222
    uid_number = models.PositiveIntegerField(
        default=get_fresh_user_uid,
        unique=True
    )
223 224 225 226 227
    rezo_rez_uid = models.PositiveIntegerField(
        unique=True,
        blank=True,
        null=True
    )
lhark's avatar
lhark committed
228

229
    USERNAME_FIELD = 'pseudo'
230
    REQUIRED_FIELDS = ['surname', 'email']
231 232 233

    objects = UserManager()

234 235
    class Meta:
        permissions = (
236 237
            ("change_user_password",
             "Peut changer le mot de passe d'un user"),
238 239 240
            ("change_user_state", "Peut éditer l'etat d'un user"),
            ("change_user_force", "Peut forcer un déménagement"),
            ("change_user_shell", "Peut éditer le shell d'un user"),
241 242 243 244 245 246 247
            ("change_user_groups",
             "Peut éditer les groupes d'un user ! Permission critique"),
            ("change_all_users",
             "Peut éditer tous les users, y compris ceux dotés de droits. "
             "Superdroit"),
            ("view_user",
             "Peut voir un objet user quelquonque"),
248 249
        )

250 251 252 253 254 255 256 257
    @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 ''

258 259 260 261 262 263 264 265 266 267
    @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")

268 269 270 271
    @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
272
            return "Adherent"
273 274 275 276 277 278 279
        elif hasattr(self, 'club'):
            return "Club"
        else:
            raise NotImplementedError("Type inconnu")

    @cached_property
    def is_class_club(self):
280 281
        """ Returns True if the object is a Club (subclassing User) """
        # TODO : change to isinstance (cleaner)
282 283 284 285
        return hasattr(self, 'club')

    @cached_property
    def is_class_adherent(self):
286 287
        """ Returns True if the object is a Adherent (subclassing User) """
        # TODO : change to isinstance (cleaner)
288 289
        return hasattr(self, 'adherent')

290 291
    @property
    def is_active(self):
chirac's avatar
chirac committed
292
        """ Renvoie si l'user est à l'état actif"""
293 294 295 296
        return self.state == self.STATE_ACTIVE

    @property
    def is_staff(self):
chirac's avatar
chirac committed
297
        """ Fonction de base django, renvoie si l'user est admin"""
298 299 300 301
        return self.is_admin

    @property
    def is_admin(self):
chirac's avatar
chirac committed
302
        """ Renvoie si l'user est admin"""
303
        admin, _ = Group.objects.get_or_create(name="admin")
304
        return self.is_superuser or admin in self.groups.all()
305 306

    def get_full_name(self):
chirac's avatar
chirac committed
307
        """ Renvoie le nom complet de l'user formaté nom/prénom"""
308 309 310 311 312
        name = self.name
        if name:
            return '%s %s' % (name, self.surname)
        else:
            return self.surname
313 314

    def get_short_name(self):
chirac's avatar
chirac committed
315
        """ Renvoie seulement le nom"""
316
        return self.surname
317

318 319 320 321 322 323
    @property
    def get_shell(self):
        """ A utiliser de préférence, prend le shell par défaut
        si il n'est pas défini"""
        return self.shell or OptionalUser.get_cached_value('shell_default')

324
    def end_adhesion(self):
chirac's avatar
chirac committed
325 326
        """ Renvoie la date de fin d'adhésion d'un user. Examine les objets
        cotisation"""
chirac's avatar
chirac committed
327 328 329 330 331 332
        date_max = Cotisation.objects.filter(
            vente__in=Vente.objects.filter(
                facture__in=Facture.objects.filter(
                    user=self
                ).exclude(valid=False)
            )
333 334 335 336 337 338 339 340 341 342 343 344 345 346 347 348
        ).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
349
        ).aggregate(models.Max('date_end'))['date_end__max']
350 351 352
        return date_max

    def is_adherent(self):
chirac's avatar
chirac committed
353 354
        """ Renvoie True si l'user est adhérent : si
        self.end_adhesion()>now"""
355
        end = self.end_adhesion()
356 357
        if not end:
            return False
358
        elif end < timezone.now():
359 360 361 362
            return False
        else:
            return True

363 364 365 366 367 368
    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
369
        elif end < timezone.now():
370 371 372 373
            return False
        else:
            return self.is_adherent()

374 375
    def end_ban(self):
        """ Renvoie la date de fin de ban d'un user, False sinon """
chirac's avatar
chirac committed
376 377 378
        date_max = Ban.objects.filter(
            user=self
        ).aggregate(models.Max('date_end'))['date_end__max']
379 380 381
        return date_max

    def end_whitelist(self):
382
        """ Renvoie la date de fin de whitelist d'un user, False sinon """
chirac's avatar
chirac committed
383 384 385
        date_max = Whitelist.objects.filter(
            user=self
        ).aggregate(models.Max('date_end'))['date_end__max']
386 387 388 389
        return date_max

    def is_ban(self):
        """ Renvoie si un user est banni ou non """
390
        end = self.end_ban()
391 392
        if not end:
            return False
393
        elif end < timezone.now():
394 395 396 397 398 399
            return False
        else:
            return True

    def is_whitelisted(self):
        """ Renvoie si un user est whitelisté ou non """
400
        end = self.end_whitelist()
401 402
        if not end:
            return False
403
        elif end < timezone.now():
404 405 406 407 408 409
            return False
        else:
            return True

    def has_access(self):
        """ Renvoie si un utilisateur a accès à internet """
410 411 412
        return (self.state == User.STATE_ACTIVE and
                not self.is_ban() and
                (self.is_connected() or self.is_whitelisted()))
413

414 415
    def end_access(self):
        """ Renvoie la date de fin normale d'accès (adhésion ou whiteliste)"""
416
        if not self.end_connexion():
417
            if not self.end_whitelist():
418 419
                return None
            else:
420
                return self.end_whitelist()
421
        else:
422
            if not self.end_whitelist():
423
                return self.end_connexion()
chirac's avatar
chirac committed
424
            else:
425
                return max(self.end_connexion(), self.end_whitelist())
426

chibrac's avatar
chibrac committed
427 428
    @cached_property
    def solde(self):
429
        """ Renvoie le solde d'un user.
chirac's avatar
chirac committed
430
        Somme les crédits de solde et retire les débit payés par solde"""
431
        solde_objects = Paiement.objects.filter(is_balance=True)
432 433 434 435 436 437 438 439 440 441 442 443 444 445 446 447 448 449 450 451 452 453
        somme_debit = Vente.objects.filter(
            facture__in=Facture.objects.filter(
                user=self,
                paiement__in=solde_objects,
                valid=True
            )
        ).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, valid=True),
            name="solde"
        ).aggregate(
            total=models.Sum(
                models.F('prix')*models.F('number'),
                output_field=models.FloatField()
            )
        )['total'] or 0
        return somme_credit - somme_debit
chibrac's avatar
chibrac committed
454

chirac's avatar
chirac committed
455
    def user_interfaces(self, active=True):
chirac's avatar
chirac committed
456 457 458 459 460
        """ 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')
461

462 463 464 465 466 467 468 469 470 471 472
    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
473
        """ Désassigne les ipv4 aux machines de l'user"""
474 475 476 477 478 479 480 481
        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
482
        """ Filling the user; no more active"""
483 484 485
        self.unassign_ips()

    def unarchive(self):
chirac's avatar
chirac committed
486
        """Unfilling the user"""
487
        self.assign_ips()
Gabriel Detraz's avatar
Gabriel Detraz committed
488 489 490 491 492 493 494

    def state_sync(self):
        """Archive, or unarchive, if the user was not active/or archived before"""
        if self.__original_state != self.STATE_ACTIVE and self.state == self.STATE_ACTIVE:
            self.unarchive()
        elif self.__original_state != self.STATE_ARCHIVE and self.state == self.STATE_ARCHIVE:
            self.archive()
495

496 497
    def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True,
                  group_refresh=False):
chirac's avatar
chirac committed
498 499 500 501 502 503
        """ 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
504
        mac_refresh : synchronise les machines de l'user
505
        group_refresh : synchronise les group de l'user
506
        Si l'instance n'existe pas, on crée le ldapuser correspondant"""
chirac's avatar
chirac committed
507
        self.refresh_from_db()
508
        try:
root's avatar
root committed
509
            user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
510
        except LdapUser.DoesNotExist:
root's avatar
root committed
511
            user_ldap = LdapUser(uidNumber=self.uid_number)
512 513 514
            base = True
            access_refresh = True
            mac_refresh = True
515
        if base:
chirac's avatar
chirac committed
516
            user_ldap.name = self.pseudo
517
            user_ldap.sn = self.pseudo
518
            user_ldap.dialupAccess = str(self.has_access())
519 520
            user_ldap.home_directory = '/home/' + self.pseudo
            user_ldap.mail = self.email
chirac's avatar
chirac committed
521 522
            user_ldap.given_name = self.surname.lower() + '_'\
                + self.name.lower()[:3]
523
            user_ldap.gid = LDAP['user_gid']
524
            user_ldap.user_password = self.password[:6] + self.password[7:]
Gabriel Detraz's avatar
Gabriel Detraz committed
525
            user_ldap.sambat_nt_password = self.pwd_ntlm.upper()
526 527
            if self.get_shell:
                user_ldap.login_shell = str(self.get_shell)
chibrac's avatar
chibrac committed
528 529 530 531
            if self.state == self.STATE_DISABLED:
                user_ldap.shadowexpire = str(0)
            else:
                user_ldap.shadowexpire = None
532
        if access_refresh:
533
            user_ldap.dialupAccess = str(self.has_access())
534
        if mac_refresh:
535 536 537
            user_ldap.macs = [str(mac) for mac in Interface.objects.filter(
                machine__user=self
            ).values_list('mac_address', flat=True).distinct()]
538
        if group_refresh:
539 540 541 542
            # Need to refresh all groups because we don't know which groups
            # were updated during edition of groups and the user may no longer
            # be part of the updated group (case of group removal)
            for group in Group.objects.all():
543 544
                if hasattr(group, 'listright'):
                    group.listright.ldap_sync()
545 546 547
        user_ldap.save()

    def ldap_del(self):
chirac's avatar
chirac committed
548
        """ Supprime la version ldap de l'user"""
549 550 551 552 553 554
        try:
            user_ldap = LdapUser.objects.get(name=self.pseudo)
            user_ldap.delete()
        except LdapUser.DoesNotExist:
            pass

555 556
    def notif_inscription(self):
        """ Prend en argument un objet user, envoie un mail de bienvenue """
chirac's avatar
chirac committed
557 558 559 560
        template = loader.get_template('users/email_welcome')
        mailmessageoptions, _created = MailMessageOption\
            .objects.get_or_create()
        context = Context({
561
            'nom': self.get_full_name(),
562 563
            'asso_name': AssoOption.get_cached_value('name'),
            'asso_email': AssoOption.get_cached_value('contact'),
chirac's avatar
chirac committed
564 565 566
            'welcome_mail_fr': mailmessageoptions.welcome_mail_fr,
            'welcome_mail_en': mailmessageoptions.welcome_mail_en,
            'pseudo': self.pseudo,
567
        })
568 569 570 571 572 573 574 575 576
        send_mail(
            'Bienvenue au %(name)s / Welcome to %(name)s' % {
                'name': AssoOption.get_cached_value('name')
                },
            '',
            GeneralOption.get_cached_value('email_from'),
            [self.email],
            html_message=template.render(context)
        )
577 578 579
        return

    def reset_passwd_mail(self, request):
chirac's avatar
chirac committed
580 581
        """ Prend en argument un request, envoie un mail de
        réinitialisation de mot de pass """
582 583 584 585
        req = Request()
        req.type = Request.PASSWD
        req.user = self
        req.save()
chirac's avatar
chirac committed
586 587
        template = loader.get_template('users/email_passwd_request')
        context = {
588
            'name': req.user.get_full_name(),
589 590
            'asso': AssoOption.get_cached_value('name'),
            'asso_mail': AssoOption.get_cached_value('contact'),
591
            'site_name': GeneralOption.get_cached_value('site_name'),
592
            'url': request.build_absolute_uri(
593 594 595 596 597 598
                reverse('users:process', kwargs={'token': req.token})
            ),
            'expire_in': str(
                GeneralOption.get_cached_value('req_expire_hrs')
            ) + ' heures',
        }
599
        send_mail(
600 601
            'Changement de mot de passe du %(name)s / Password renewal for '
            '%(name)s' % {'name': AssoOption.get_cached_value('name')},
602 603 604 605 606
            template.render(context),
            GeneralOption.get_cached_value('email_from'),
            [req.user.email],
            fail_silently=False
        )
607 608
        return

609
    def autoregister_machine(self, mac_address, nas_type):
chirac's avatar
chirac committed
610 611
        """ Fonction appellée par freeradius. Enregistre la mac pour
        une machine inconnue sur le compte de l'user"""
chirac's avatar
chirac committed
612
        all_interfaces = self.user_interfaces(active=False)
613
        if all_interfaces.count() > OptionalMachine.get_cached_value(
614 615
                'max_lambdauser_interfaces'
            ):
616
            return False, "Maximum de machines enregistrees atteinte"
617
        if not nas_type:
chirac's avatar
chirac committed
618 619
            return False, "Re2o ne sait pas à quel machinetype affecter cette\
            machine"
620
        machine_type_cible = nas_type.machine_type
621 622 623 624 625
        try:
            machine_parent = Machine()
            machine_parent.user = self
            interface_cible = Interface()
            interface_cible.mac_address = mac_address
626
            interface_cible.type = machine_type_cible
627 628 629
            interface_cible.clean()
            machine_parent.clean()
            domain = Domain()
630
            domain.name = self.get_next_domain_name()
631 632
            domain.interface_parent = interface_cible
            domain.clean()
633 634 635 636 637 638
            machine_parent.save()
            interface_cible.machine = machine_parent
            interface_cible.save()
            domain.interface_parent = interface_cible
            domain.clean()
            domain.save()
639
            self.notif_auto_newmachine(interface_cible)
chirac's avatar
chirac committed
640 641
        except Exception as error:
            return False, error
642 643
        return True, "Ok"

644 645 646 647 648 649
    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')
        context = Context({
            'nom': self.get_full_name(),
650
            'mac_address': interface.mac_address,
651
            'asso_name': AssoOption.get_cached_value('name'),
652
            'interface_name': interface.domain,
653
            'asso_email': AssoOption.get_cached_value('contact'),
654 655 656 657 658
            'pseudo': self.pseudo,
        })
        send_mail(
            "Ajout automatique d'une machine / New machine autoregistered",
            '',
659
            GeneralOption.get_cached_value('email_from'),
660 661 662 663 664
            [self.email],
            html_message=template.render(context)
        )
        return

665
    def set_password(self, password):
chirac's avatar
chirac committed
666
        """ A utiliser de préférence, set le password en hash courrant et
chirac's avatar
chirac committed
667
        dans la version ntlm"""
668
        super().set_password(password)
669 670 671
        self.pwd_ntlm = hashNT(password)
        return

672 673 674
    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
675 676 677

        Recherche un nom disponible, pour une machine. Doit-être
        unique, concatène le nom, le pseudo et le numero de machine
678 679 680
        """

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

chirac's avatar
chirac committed
684 685 686
        def composed_pseudo(name):
            """Renvoie le resultat de simplepseudo et rajoute le nom"""
            return simple_pseudo() + str(name)
687 688

        num = 0
chirac's avatar
chirac committed
689
        while Domain.objects.filter(name=composed_pseudo(num)):
690 691 692
            num += 1
        return composed_pseudo(num)

693 694
    def can_edit(self, user_request, *_args, **_kwargs):
        """Check if a user can edit a user object.
695 696 697 698

        :param self: The user which is to be edited.
        :param user_request: The user who requests to edit self.
        :return: a message and a boolean which is True if self is a club and
699 700
            user_request one of its member, or if user_request is self, or if
            user_request has the 'cableur' right.
701
        """
702
        if self.is_class_club and user_request.is_class_adherent:
703 704 705
            if (self == user_request or
                    user_request.has_perm('users.change_user') or
                    user_request.adherent in self.club.administrators.all()):
706 707 708
                return True, None
            else:
                return False, u"Vous n'avez pas le droit d'éditer ce club"
709
        else:
710 711 712 713 714 715
            if self == user_request:
                return True, None
            elif user_request.has_perm('users.change_all_users'):
                return True, None
            elif user_request.has_perm('users.change_user'):
                if self.groups.filter(listright__critical=True):
716 717
                    return False, (u"Utilisateurs avec droits critiques, ne "
                                   "peut etre édité")
718
                elif self == AssoOption.get_cached_value('utilisateur_asso'):
719 720
                    return False, (u"Impossible d'éditer l'utilisateur asso "
                                   "sans droit change_all_users")
721 722 723
                else:
                    return True, None
            elif user_request.has_perm('users.change_all_users'):
724 725
                return True, None
            else:
726 727
                return False, (u"Vous ne pouvez éditer un autre utilisateur "
                               "que vous même")
728

729 730 731 732 733 734 735 736 737
    def can_change_password(self, user_request, *_args, **_kwargs):
        """Check if a user can change a user's password

        :param self: The user which is to be edited
        :param user_request: The user who request to edit self
        :returns: a message and a boolean which is True if self is a club
            and user_request one of it's admins, or if user_request is self,
            or if user_request has the right to change other's password
        """
738
        if self.is_class_club and user_request.is_class_adherent:
739 740 741
            if (self == user_request or
                    user_request.has_perm('users.change_user_password') or
                    user_request.adherent in self.club.administrators.all()):
742 743 744 745
                return True, None
            else:
                return False, u"Vous n'avez pas le droit d'éditer ce club"
        else:
746 747 748 749
            if (self == user_request or
                    user_request.has_perm('users.change_user_groups')):
                # Peut éditer les groupes d'un user,
                # c'est un privilège élevé, True
750
                return True, None
751 752
            elif (user_request.has_perm('users.change_user') and
                  not self.groups.all()):
753 754
                return True, None
            else:
755 756
                return False, (u"Vous ne pouvez éditer un autre utilisateur "
                               "que vous même")
757

758 759 760 761
    def check_selfpasswd(self, user_request, *_args, **_kwargs):
        """ Returns (True, None) if user_request is self, else returns
        (False, None)
        """
762 763
        return user_request == self, None

764
    @staticmethod
765 766 767 768 769 770 771
    def can_change_state(user_request, *_args, **_kwargs):
        """ Check if a user can change a state

        :param user_request: The user who request
        :returns: a message and a boolean which is True if the user has
        the right to change a state
        """
772 773 774 775
        return (
            user_request.has_perm('users.change_user_state'),
            "Droit requis pour changer l'état"
        )
776

777
    @staticmethod
778 779 780 781 782 783 784
    def can_change_shell(user_request, *_args, **_kwargs):
        """ Check if a user can change a shell

        :param user_request: The user who request
        :returns: a message and a boolean which is True if the user has
        the right to change a shell
        """
785 786 787 788
        return (
            user_request.has_perm('users.change_user_shell'),
            "Droit requis pour changer le shell"
        )
789

790
    @staticmethod
791 792 793 794 795 796 797
    def can_change_force(user_request, *_args, **_kwargs):
        """ Check if a user can change a force

        :param user_request: The user who request
        :returns: a message and a boolean which is True if the user has
        the right to change a force
        """
798 799 800 801
        return (
            user_request.has_perm('users.change_user_force'),
            "Droit requis pour forcer le déménagement"
        )
802 803

    @staticmethod
804 805 806 807 808 809 810
    def can_change_groups(user_request, *_args, **_kwargs):
        """ Check if a user can change a group

        :param user_request: The user who request
        :returns: a message and a boolean which is True if the user has
        the right to change a group
        """
811 812 813 814
        return (
            user_request.has_perm('users.change_user_groups'),
            "Droit requis pour éditer les groupes de l'user"
        )
815

Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
816 817 818 819 820 821 822 823 824 825 826 827
    @staticmethod
    def can_change_is_superuser(user_request, *_args, **_kwargs):
        """ Check if an user can change a is_superuser flag

        :param user_request: The user who request
        :returns: a message and a boolean which is True if permission is granted.
        """
        return (
            user_request.is_superuser,
            "Droit superuser requis pour éditer le flag superuser"
        )

828
    def can_view(self, user_request, *_args, **_kwargs):
829 830 831 832 833
        """Check if an user can view an user object.

        :param self: The targeted user.
        :param user_request: The user who ask for viewing the target.
        :return: A boolean telling if the acces is granted and an explanation
834
            text
835
        """
836
        if self.is_class_club and user_request.is_class_adherent:
837 838 839 840
            if (self == user_request or
                    user_request.has_perm('users.view_user') or
                    user_request.adherent in self.club.administrators.all() or
                    user_request.adherent in self.club.members.all()):
841 842 843
                return True, None
            else:
                return False, u"Vous n'avez pas le droit de voir ce club"
844
        else:
845 846
            if (self == user_request or
                    user_request.has_perm('users.view_user')):
847 848
                return True, None
            else:
849 850
                return False, (u"Vous ne pouvez voir un autre utilisateur "
                               "que vous même")
851

852 853
    @staticmethod
    def can_view_all(user_request, *_args, **_kwargs):
854 855 856
        """Check if an user can access to the list of every user objects

        :param user_request: The user who wants to view the list.
857 858
        :return: True if the user can view the list and an explanation
            message.
859
        """
860 861 862 863
        return (
            user_request.has_perm('users.view_user'),
            u"Vous n'avez pas accès à la liste des utilisateurs."
        )
864

865
    def can_delete(self, user_request, *_args, **_kwargs):
866 867 868 869
        """Check if an user can delete an user object.

        :param self: The user who is to be deleted.
        :param user_request: The user who requests deletion.
870 871
        :return: True if user_request has the right 'bureau', and a
            message.
872
        """
873 874 875 876
        return (
            user_request.has_perm('users.delete_user'),
            u"Vous ne pouvez pas supprimer cet utilisateur."
        )
877

878 879 880
    def __init__(self, *args, **kwargs):
        super(User, self).__init__(*args, **kwargs)
        self.field_permissions = {
881 882 883
            'shell': self.can_change_shell,
            'force': self.can_change_force,
            'selfpasswd': self.check_selfpasswd,
884
        }
Gabriel Detraz's avatar
Gabriel Detraz committed
885
        self.__original_state = self.state
886

887
    def __str__(self):
888
        return self.pseudo
lhark's avatar
lhark committed
889

chirac's avatar
chirac committed
890

891
class Adherent(User):
892 893
    """ A class representing a member (it's a user with special
    informations) """
Gabriel Detraz's avatar
Gabriel Detraz committed
894
    PRETTY_NAME = "Adhérents"
895
    name = models.CharField(max_length=255)
896 897 898 899 900 901
    room = models.OneToOneField(
        'topologie.Room',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
902

903 904
    @classmethod
    def get_instance(cls, adherentid, *_args, **_kwargs):
905
        """Try to find an instance of `Adherent` with the given id.
906

907 908 909
        :param adherentid: The id of the adherent we are looking for.
        :return: An adherent.
        """
910
        return cls.objects.get(pk=adherentid)
911

912 913
    @staticmethod
    def can_create(user_request, *_args, **_kwargs):
914 915 916 917
        """Check if an user can create an user object.

        :param user_request: The user who wants to create a user object.
        :return: a message and a boolean which is True if the user can create
918
            a user or if the `options.all_can_create` is set.
919
        """
920 921
        if (not user_request.is_authenticated and
                not OptionalUser.get_cached_value('self_adhesion')):
922 923
            return False, None
        else:
924 925
            if (OptionalUser.get_cached_value('all_can_create_adherent') or
                    OptionalUser.get_cached_value('self_adhesion')):
926 927
                return True, None
            else:
928 929 930 931
                return (
                    user_request.has_perm('users.add_user'),
                    u"Vous n'avez pas le droit de créer un utilisateur"
                )
932

933

934
class Club(User):
935 936
    """ A class representing a club (it is considered as a user
    with special informations) """
Gabriel Detraz's avatar
Gabriel Detraz committed
937
    PRETTY_NAME = "Clubs"
938 939 940 941 942 943
    room = models.ForeignKey(
        'topologie.Room',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
944 945 946 947 948 949 950 951 952 953
    administrators = models.ManyToManyField(
        blank=True,
        to='users.Adherent',
        related_name='club_administrator'
    )
    members = models.ManyToManyField(
        blank=True,
        to='users.Adherent',
        related_name='club_members'
    )
954
    mailing = models.BooleanField(
955
        default=False
956
    )
957

958 959
    @staticmethod
    def can_create(user_request, *_args, **_kwargs):
960 961 962 963
        """Check if an user can create an user object.

        :param user_request: The user who wants to create a user object.
        :return: a message and a boolean which is True if the user can create
964
            an user or if the `options.all_can_create` is set.
965 966 967 968 969 970 971
        """
        if not user_request.is_authenticated:
            return False, None
        else:
            if OptionalUser.get_cached_value('all_can_create_club'):
                return True, None
            else:
972 973 974 975
                return (
                    user_request.has_perm('users.add_user'),
                    u"Vous n'avez pas le droit de créer un club"
                )
976

977 978
    @staticmethod
    def can_view_all(user_request, *_args, **_kwargs):
979 980 981
        """Check if an user can access to the list of every user objects

        :param user_request: The user who wants to view the list.
982 983
        :return: True if the user can view the list and an explanation
            message.
984
        """
985
        if user_request.has_perm('users.view_user'):
986
            return True, None
987 988 989 990
        if (hasattr(user_request, 'is_class_adherent') and
                user_request.is_class_adherent):
            if (user_request.adherent.club_administrator.all() or
                    user_request.adherent.club_members.all()):
991 992 993
                return True, None
        return False, u"Vous n'avez pas accès à la liste des utilisateurs."

994 995
    @classmethod
    def get_instance(cls, clubid, *_args, **_kwargs):
996
        """Try to find an instance of `Club` with the given id.
997

998 999 1000
        :param clubid: The id of the adherent we are looking for.
        :return: A club.
        """
1001
        return cls.objects.get(pk=clubid)
1002

1003

1004 1005
@receiver(post_save, sender=Adherent)
@receiver(post_save, sender=Club)
1006
@receiver(post_save, sender=User)
1007
def user_post_save(**kwargs):
chirac's avatar
chirac committed
1008 1009
    """ Synchronisation post_save : envoie le mail de bienvenue si creation
    Synchronise le ldap"""
1010
    is_created = kwargs['created']
1011
    user = kwargs['instance']
1012 1013
    if is_created:
        user.notif_inscription()
Gabriel Detraz's avatar
Gabriel Detraz committed
1014
    user.state_sync()
1015 1016 1017 1018 1019 1020
    user.ldap_sync(
        base=True,
        access_refresh=True,
        mac_refresh=False,
        group_refresh=True
    )
1021
    regen('mailing')
1022

chirac's avatar
chirac committed
1023

1024 1025 1026 1027 1028 1029 1030 1031 1032 1033
@receiver(m2m_changed, sender=User.groups.through)
def user_group_relation_changed(**kwargs):
    action = kwargs['action']
    if action in ('post_add', 'post_remove', 'post_clear'):
        user = kwargs['instance']
        user.ldap_sync(base=False,
                       access_refresh=False,
                       mac_refresh=False,
                       group_refresh=True)

1034 1035
@receiver(post_delete, sender=Adherent)
@receiver(post_delete, sender=Club)
1036
@receiver(post_delete, sender=User)
1037
def user_post_delete(**kwargs):
chirac's avatar
chirac committed
1038
    """Post delete d'un user, on supprime son instance ldap"""
1039
    user = kwargs['instance']
1040
    user.ldap_del()
1041
    regen('mailing')
1042

1043

1044
class ServiceUser(RevMixin, AclMixin, AbstractBaseUser):
chirac's avatar
chirac committed
1045
    """ Classe des users daemons, règle leurs accès au ldap"""
1046 1047
    readonly = 'readonly'
    ACCESS = (
chirac's avatar
chirac committed
1048 1049 1050 1051
        ('auth', 'auth'),
        ('readonly', 'readonly'),
        ('usermgmt', 'usermgmt'),
    )
1052

1053
    PRETTY_NAME = "Utilisateurs de service"
chirac's avatar
chirac committed
1054

chirac's avatar
chirac committed
1055 1056 1057 1058 1059 1060 1061 1062 1063 1064 1065 1066 1067 1068 1069 1070
    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
1071 1072 1073 1074

    USERNAME_FIELD = 'pseudo'
    objects = UserManager()

1075 1076 1077 1078 1079
    class Meta:
        permissions = (
            ("view_serviceuser", "Peut voir un objet serviceuser"),
        )

1080 1081 1082 1083 1084 1085 1086 1087
    def get_full_name(self):
        """ Renvoie le nom complet du serviceUser formaté nom/prénom"""
        return "ServiceUser <{name}>".format(name=self.pseudo)

    def get_short_name(self):
        """ Renvoie seulement le nom"""
        return self.pseudo

chirac's avatar
chirac committed
1088
    def ldap_sync(self):
chirac's avatar
chirac committed
1089
        """ Synchronisation du ServiceUser dans sa version ldap"""
chirac's avatar
chirac committed
1090 1091 1092 1093
        try:
            user_ldap = LdapServiceUser.objects.get(name=self.pseudo)
        except LdapServiceUser.DoesNotExist:
            user_ldap = LdapServiceUser(name=self.pseudo)
1094
        user_ldap.user_password = self.password[:6] + self.password[7:]
chirac's avatar
chirac committed
1095
        user_ldap.save()
1096
        self.serviceuser_group_sync()
chirac's avatar
chirac committed
1097 1098

    def ldap_del(self):
chirac's avatar
chirac committed
1099
        """Suppression de l'instance ldap d'un service user"""
chirac's avatar
chirac committed
1100 1101 1102 1103 1104
        try:
            user_ldap = LdapServiceUser.objects.get(name=self.pseudo)
            user_ldap.delete()
        except LdapUser.DoesNotExist:
            pass
1105 1106 1107
        self.serviceuser_group_sync()

    def serviceuser_group_sync(self):
chirac's avatar
chirac committed
1108
        """Synchronise le groupe et les droits de groupe dans le ldap"""
1109 1110 1111 1112
        try:
            group = LdapServiceUserGroup.objects.get(name=self.access_group)
        except:
            group = LdapServiceUserGroup(name=self.access_group)
chirac's avatar
chirac committed
1113 1114 1115 1116
        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))
1117
        group.save()
chirac's avatar
chirac committed
1118

1119 1120
    def __str__(self):
        return self.pseudo
chirac's avatar
chirac committed
1121

1122

chirac's avatar
chirac committed
1123
@receiver(post_save, sender=ServiceUser)
1124
def service_user_post_save(**kwargs):
chirac's avatar
chirac committed
1125
    """ Synchronise un service user ldap après modification django"""
chirac's avatar
chirac committed
1126
    service_user = kwargs['instance']
1127
    service_user.ldap_sync()
chirac's avatar
chirac committed
1128

chirac's avatar
chirac committed
1129

chirac's avatar
chirac committed
1130
@receiver(post_delete, sender=ServiceUser)
1131
def service_user_post_delete(**kwargs):
chirac's avatar
chirac committed
1132
    """ Supprime un service user ldap après suppression django"""
chirac's avatar
chirac committed
1133
    service_user = kwargs['instance']
1134
    service_user.ldap_del()
chirac's avatar
chirac committed
1135

chirac's avatar
chirac committed
1136

1137
class School(RevMixin, AclMixin, models.Model):
chirac's avatar
chirac committed
1138
    """ Etablissement d'enseignement"""
Gabriel Detraz's avatar
Gabriel Detraz committed
1139
    PRETTY_NAME = "Établissements enregistrés"
1140

lhark's avatar
lhark committed
1141 1142
    name = models.CharField(max_length=255)

1143 1144 1145 1146 1147
    class Meta:
        permissions = (
            ("view_school", "Peut voir un objet school"),
        )

1148 1149 1150
    def __str__(self):
        return self.name

1151

1152
class ListRight(RevMixin, AclMixin, Group):
chirac's avatar
chirac committed
1153 1154
    """ Ensemble des droits existants. Chaque droit crée un groupe
    ldap synchronisé, avec gid.
chirac's avatar
chirac committed
1155
    Permet de gérer facilement les accès serveurs et autres
chirac's avatar
chirac committed
1156 1157
    La clef de recherche est le gid, pour cette raison là
    il n'est plus modifiable après creation"""
1158 1159
    PRETTY_NAME = "Liste des droits existants"

1160
    unix_name = models.CharField(
chirac's avatar
chirac committed
1161 1162 1163 1164
        max_length=255,
        unique=True,
        validators=[RegexValidator(
            '^[a-z]+$',
1165 1166
            message=("Les groupes unix ne peuvent contenir que des lettres "
                     "minuscules")
chirac's avatar
chirac committed
1167 1168
        )]
    )
1169
    gid = models.PositiveIntegerField(unique=True, null=True)
1170
    critical = models.BooleanField(default=False)
chirac's avatar
chirac committed
1171 1172 1173 1174 1175
    details = models.CharField(
        help_text="Description",
        max_length=255,
        blank=True
    )
1176

1177 1178 1179 1180 1181
    class Meta:
        permissions = (
            ("view_listright", "Peut voir un objet Group/ListRight"),
        )

1182
    def __str__(self):
1183
        return self.name
1184

1185
    def ldap_sync(self):
chirac's avatar
chirac committed
1186
        """Sychronise les groups ldap avec le model listright coté django"""
1187 1188 1189 1190
        try:
            group_ldap = LdapUserGroup.objects.get(gid=self.gid)
        except LdapUserGroup.DoesNotExist:
            group_ldap = LdapUserGroup(gid=self.gid)
1191
        group_ldap.name = self.unix_name
1192 1193
        group_ldap.members = [user.pseudo for user
                              in self.user_set.all()]
1194 1195 1196
        group_ldap.save()

    def ldap_del(self):
chirac's avatar
chirac committed
1197
        """Supprime un groupe ldap"""
1198 1199 1200 1201 1202 1203
        try:
            group_ldap = LdapUserGroup.objects.get(gid=self.gid)
            group_ldap.delete()
        except LdapUserGroup.DoesNotExist:
            pass

chirac's avatar
chirac committed
1204

1205
@receiver(post_save, sender=ListRight)
1206
def listright_post_save(**kwargs):
chirac's avatar
chirac committed
1207
    """ Synchronise le droit ldap quand il est modifié"""
1208
    right = kwargs['instance']
1209
    right.ldap_sync()
1210

chirac's avatar
chirac committed
1211

1212
@receiver(post_delete, sender=ListRight)
1213
def listright_post_delete(**kwargs):
chirac's avatar
chirac committed
1214
    """Suppression d'un groupe ldap après suppression coté django"""
1215
    right = kwargs['instance']
1216
    right.ldap_del()
1217

chirac's avatar
chirac committed
1218

1219
class ListShell(RevMixin, AclMixin, models.Model):
chirac's avatar
chirac committed
1220 1221
    """Un shell possible. Pas de check si ce shell existe, les
    admin sont des grands"""
1222 1223
    PRETTY_NAME = "Liste des shells disponibles"

1224 1225
    shell = models.CharField(max_length=255, unique=True)

1226 1227 1228 1229 1230
    class Meta:
        permissions = (
            ("view_listshell", "Peut voir un objet shell quelqu'il soit"),
        )

Krokmou's avatar
Krokmou committed
1231 1232 1233 1234
    def get_pretty_name(self):
        """Return the canonical name of the shell"""
        return self.shell.split("/")[-1]

1235 1236
    def __str__(self):
        return self.shell
1237

chirac's avatar
chirac committed
1238

1239
class Ban(RevMixin, AclMixin, models.Model):
chirac's avatar
chirac committed
1240 1241
    """ Bannissement. Actuellement a un effet tout ou rien.
    Gagnerait à être granulaire"""
1242 1243
    PRETTY_NAME = "Liste des bannissements"

1244 1245 1246 1247
    STATE_HARD = 0
    STATE_SOFT = 1
    STATE_BRIDAGE = 2
    STATES = (