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 63.7 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
import re
import uuid
import datetime
51
import sys
chirac's avatar
chirac committed
52

lhark's avatar
lhark committed
53
from django.db import models
54
from django.db.models import Q
55
from django import forms
Charlie Jacomme's avatar
Charlie Jacomme committed
56
from django.forms import ValidationError
57
from django.db.models.signals import post_save, post_delete, m2m_changed
58
from django.dispatch import receiver
59
from django.utils.functional import cached_property
chirac's avatar
chirac committed
60
from django.template import Context, loader
61 62
from django.core.mail import send_mail
from django.core.urlresolvers import reverse
chirac's avatar
chirac committed
63 64
from django.db import transaction
from django.utils import timezone
65 66
from django.contrib.auth.models import (
    AbstractBaseUser,
67
    BaseUserManager,
68 69
    PermissionsMixin,
    Group
70
)
chirac's avatar
chirac committed
71
from django.core.validators import RegexValidator
72
import traceback
73 74
from django.utils.translation import ugettext_lazy as _

75 76
from reversion import revisions as reversion

77 78 79
import ldapdb.models
import ldapdb.models.fields

chirac's avatar
chirac committed
80
from re2o.settings import LDAP, GID_RANGES, UID_RANGES
81
from re2o.login import hashNT
82
from re2o.field_permissions import FieldPermissionModelMixin
83
from re2o.mixins import AclMixin, RevMixin
lhark's avatar
lhark committed
84

chibrac's avatar
chibrac committed
85
from cotisations.models import Cotisation, Facture, Paiement, Vente
86
from machines.models import Domain, Interface, Machine, regen
chirac's avatar
chirac committed
87 88
from preferences.models import GeneralOption, AssoOption, OptionalUser
from preferences.models import OptionalMachine, MailMessageOption
89

chirac's avatar
chirac committed
90

chirac's avatar
chirac committed
91
# Utilitaires généraux
chirac's avatar
chirac committed
92

93 94

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


def linux_user_validator(login):
chirac's avatar
chirac committed
101
    """ Retourne une erreur de validation si le login ne respecte
chirac's avatar
chirac committed
102
    pas les contraintes unix (maj, min, chiffres ou tiret)"""
103
    if not linux_user_check(login):
104
        raise forms.ValidationError(
105
            _("The username '%(label)s' contains forbidden characters."),
chirac's avatar
chirac committed
106
            params={'label': login},
chirac's avatar
chirac committed
107 108
        )

109

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

chirac's avatar
chirac committed
123

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

chirac's avatar
chirac committed
134

135
class UserManager(BaseUserManager):
chirac's avatar
chirac committed
136
    """User manager basique de django"""
137

chirac's avatar
chirac committed
138 139 140 141
    def _create_user(
            self,
            pseudo,
            surname,
142
            email,
chirac's avatar
chirac committed
143 144 145
            password=None,
            su=False
    ):
146
        if not pseudo:
147
            raise ValueError(_("Users must have an username."))
148 149

        if not linux_user_check(pseudo):
150
            raise ValueError(_("Username should only contain [a-z0-9-]."))
151

152
        user = Adherent(
153 154
            pseudo=pseudo,
            surname=surname,
155
            name=surname,
detraz's avatar
detraz committed
156
            email=self.normalize_email(email),
157 158 159 160
        )

        user.set_password(password)
        if su:
161
            user.is_superuser = True
162
        user.save(using=self._db)
163 164
        return user

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

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

179 180 181

class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
           PermissionsMixin, AclMixin):
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"""
185

lhark's avatar
lhark committed
186
    STATE_ACTIVE = 0
chirac's avatar
chirac committed
187 188
    STATE_DISABLED = 1
    STATE_ARCHIVE = 2
189
    STATE_NOT_YET_ACTIVE = 3
lhark's avatar
lhark committed
190
    STATES = (
chirac's avatar
chirac committed
191 192 193
        (0, 'STATE_ACTIVE'),
        (1, 'STATE_DISABLED'),
        (2, 'STATE_ARCHIVE'),
194
        (3, 'STATE_NOT_YET_ACTIVE'),
chirac's avatar
chirac committed
195
    )
lhark's avatar
lhark committed
196 197

    surname = models.CharField(max_length=255)
chirac's avatar
chirac committed
198 199 200
    pseudo = models.CharField(
        max_length=32,
        unique=True,
201
        help_text=_("Must only contain letters, numerals or dashes."),
Charlie Jacomme's avatar
Charlie Jacomme committed
202
        validators=[linux_user_validator]
chirac's avatar
chirac committed
203
    )
204 205
    email = models.EmailField(
        blank=True,
206
        null=True,
207
        help_text=_("External email address allowing us to contact you.")
208
    )
209
    local_email_redirect = models.BooleanField(
210
        default=False,
211 212
        help_text=_("Enable redirection of the local email messages to the"
                    " main email address.")
213
    )
214
    local_email_enabled = models.BooleanField(
215
        default=False,
216
        help_text=_("Enable the local email account.")
217
    )
chirac's avatar
chirac committed
218 219 220 221 222 223 224 225 226 227 228 229 230
    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(
231
        help_text=_("Comment, school year"),
chirac's avatar
chirac committed
232 233 234
        max_length=255,
        blank=True
    )
lhark's avatar
lhark committed
235
    pwd_ntlm = models.CharField(max_length=255)
236
    state = models.IntegerField(choices=STATES, default=STATE_NOT_YET_ACTIVE)
237
    registered = models.DateTimeField(auto_now_add=True)
238
    telephone = models.CharField(max_length=15, blank=True, null=True)
239 240 241 242
    uid_number = models.PositiveIntegerField(
        default=get_fresh_user_uid,
        unique=True
    )
243 244 245 246 247
    rezo_rez_uid = models.PositiveIntegerField(
        unique=True,
        blank=True,
        null=True
    )
lhark's avatar
lhark committed
248

249
    USERNAME_FIELD = 'pseudo'
250
    REQUIRED_FIELDS = ['surname', 'email']
251 252 253

    objects = UserManager()

254 255
    class Meta:
        permissions = (
256
            ("change_user_password",
257 258 259 260
             _("Can change the password of a user")),
            ("change_user_state", _("Can edit the state of a user")),
            ("change_user_force", _("Can force the move")),
            ("change_user_shell", _("Can edit the shell of a user")),
261
            ("change_user_groups",
262 263
             _("Can edit the groups of rights of a user (critical"
               " permission)")),
264
            ("change_all_users",
265
             _("Can edit all users, including those with rights.")),
266
            ("view_user",
267
             _("Can view a user object")),
268
        )
269 270
        verbose_name = _("user (member or club)")
        verbose_name_plural = _("users (members or clubs)")
271

272 273 274 275 276 277 278 279
    @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 ''

280 281 282 283 284 285 286 287
    @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:
288
            raise NotImplementedError(_("Unknown type."))
289

290 291 292 293 294 295 296 297 298 299 300 301
    @cached_property
    def get_mail_addresses(self):
        if self.local_email_enabled:
            return self.emailaddress_set.all()
        return None

    @cached_property
    def get_mail(self):
        """Return the mail address choosen by the user"""
        if not OptionalUser.get_cached_value('local_email_accounts_enabled') or not self.local_email_enabled or self.local_email_redirect:
            return str(self.email)
        else:
Charlie Jacomme's avatar
Charlie Jacomme committed
302
            return str(self.emailaddress_set.get(local_part=self.pseudo.lower()))
303

304 305 306 307
    @cached_property
    def class_name(self):
        """Renvoie si il s'agit d'un adhérent ou d'un club"""
        if hasattr(self, 'adherent'):
308
            return _("Member")
309
        elif hasattr(self, 'club'):
310
            return _("Club")
311
        else:
312
            raise NotImplementedError(_("Unknown type."))
313

314 315 316 317 318
    @cached_property
    def gid_number(self):
        """renvoie le gid par défaut des users"""
        return int(LDAP['user_gid'])

319 320
    @cached_property
    def is_class_club(self):
321 322
        """ Returns True if the object is a Club (subclassing User) """
        # TODO : change to isinstance (cleaner)
323 324 325 326
        return hasattr(self, 'club')

    @cached_property
    def is_class_adherent(self):
327 328
        """ Returns True if the object is a Adherent (subclassing User) """
        # TODO : change to isinstance (cleaner)
329 330
        return hasattr(self, 'adherent')

331 332
    @property
    def is_active(self):
chirac's avatar
chirac committed
333
        """ Renvoie si l'user est à l'état actif"""
334
        return self.state == self.STATE_ACTIVE or self.state == self.STATE_NOT_YET_ACTIVE
335

336
    def set_active(self):
337
        """Enable this user if he subscribed successfully one time before"""
338 339 340 341 342
        if self.state == self.STATE_NOT_YET_ACTIVE:
            if self.facture_set.filter(valid=True).filter(Q(vente__type_cotisation='All') | Q(vente__type_cotisation='Adhesion')).exists():
                self.state = self.STATE_ACTIVE
                self.save()

343 344
    @property
    def is_staff(self):
chirac's avatar
chirac committed
345
        """ Fonction de base django, renvoie si l'user est admin"""
346 347 348 349
        return self.is_admin

    @property
    def is_admin(self):
chirac's avatar
chirac committed
350
        """ Renvoie si l'user est admin"""
351
        admin, _ = Group.objects.get_or_create(name="admin")
352
        return self.is_superuser or admin in self.groups.all()
353 354

    def get_full_name(self):
chirac's avatar
chirac committed
355
        """ Renvoie le nom complet de l'user formaté nom/prénom"""
356 357 358 359 360
        name = self.name
        if name:
            return '%s %s' % (name, self.surname)
        else:
            return self.surname
361 362

    def get_short_name(self):
chirac's avatar
chirac committed
363
        """ Renvoie seulement le nom"""
364
        return self.surname
365

366 367 368 369 370
    @cached_property
    def gid(self):
        """return the default gid of user"""
        return LDAP['user_gid']

371 372 373 374 375 376
    @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')

377 378 379 380 381 382 383 384
    @cached_property
    def get_shadow_expire(self):
        """Return the shadow_expire value for the user"""
        if self.state == self.STATE_DISABLED:
            return str(0)
        else:
            return None

385
    def end_adhesion(self):
chirac's avatar
chirac committed
386 387
        """ Renvoie la date de fin d'adhésion d'un user. Examine les objets
        cotisation"""
chirac's avatar
chirac committed
388 389 390 391 392 393
        date_max = Cotisation.objects.filter(
            vente__in=Vente.objects.filter(
                facture__in=Facture.objects.filter(
                    user=self
                ).exclude(valid=False)
            )
394 395 396 397 398 399 400 401 402 403 404 405 406 407 408 409
        ).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
410
        ).aggregate(models.Max('date_end'))['date_end__max']
411 412 413
        return date_max

    def is_adherent(self):
chirac's avatar
chirac committed
414 415
        """ Renvoie True si l'user est adhérent : si
        self.end_adhesion()>now"""
416
        end = self.end_adhesion()
417 418
        if not end:
            return False
419
        elif end < timezone.now():
420 421 422 423
            return False
        else:
            return True

424 425 426 427 428 429
    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
430
        elif end < timezone.now():
431 432 433 434
            return False
        else:
            return self.is_adherent()

435 436
    def end_ban(self):
        """ Renvoie la date de fin de ban d'un user, False sinon """
chirac's avatar
chirac committed
437 438 439
        date_max = Ban.objects.filter(
            user=self
        ).aggregate(models.Max('date_end'))['date_end__max']
440 441 442
        return date_max

    def end_whitelist(self):
443
        """ Renvoie la date de fin de whitelist d'un user, False sinon """
chirac's avatar
chirac committed
444 445 446
        date_max = Whitelist.objects.filter(
            user=self
        ).aggregate(models.Max('date_end'))['date_end__max']
447 448 449 450
        return date_max

    def is_ban(self):
        """ Renvoie si un user est banni ou non """
451
        end = self.end_ban()
452 453
        if not end:
            return False
454
        elif end < timezone.now():
455 456 457 458 459 460
            return False
        else:
            return True

    def is_whitelisted(self):
        """ Renvoie si un user est whitelisté ou non """
461
        end = self.end_whitelist()
462 463
        if not end:
            return False
464
        elif end < timezone.now():
465 466 467 468 469 470
            return False
        else:
            return True

    def has_access(self):
        """ Renvoie si un utilisateur a accès à internet """
471 472 473
        return (self.state == User.STATE_ACTIVE and
                not self.is_ban() and
                (self.is_connected() or self.is_whitelisted()))
474

475 476
    def end_access(self):
        """ Renvoie la date de fin normale d'accès (adhésion ou whiteliste)"""
477
        if not self.end_connexion():
478
            if not self.end_whitelist():
479 480
                return None
            else:
481
                return self.end_whitelist()
482
        else:
483
            if not self.end_whitelist():
484
                return self.end_connexion()
chirac's avatar
chirac committed
485
            else:
486
                return max(self.end_connexion(), self.end_whitelist())
487

chibrac's avatar
chibrac committed
488 489
    @cached_property
    def solde(self):
490
        """ Renvoie le solde d'un user.
chirac's avatar
chirac committed
491
        Somme les crédits de solde et retire les débit payés par solde"""
492
        solde_objects = Paiement.objects.filter(is_balance=True)
493 494 495 496 497 498 499 500 501 502 503 504 505 506 507 508 509 510 511 512 513 514
        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
515

chirac's avatar
chirac committed
516
    def user_interfaces(self, active=True):
chirac's avatar
chirac committed
517 518 519 520 521
        """ 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')
522

523 524 525 526 527 528 529
    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()
530
                    reversion.set_comment(_("IPv4 assigning"))
531 532 533
                    interface.save()

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

    def archive(self):
chirac's avatar
chirac committed
543
        """ Filling the user; no more active"""
544 545 546
        self.unassign_ips()

    def unarchive(self):
chirac's avatar
chirac committed
547
        """Unfilling the user"""
548
        self.assign_ips()
Gabriel Detraz's avatar
Gabriel Detraz committed
549 550 551 552 553 554 555

    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()
556

557 558
    def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True,
                  group_refresh=False):
chirac's avatar
chirac committed
559 560 561 562 563 564
        """ 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
565
        mac_refresh : synchronise les machines de l'user
566
        group_refresh : synchronise les group de l'user
567
        Si l'instance n'existe pas, on crée le ldapuser correspondant"""
568 569 570 571 572
        if sys.version_info[0] >= 3:
            self.refresh_from_db()
            try:
                user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
            except LdapUser.DoesNotExist:
Gabriel Detraz's avatar
Gabriel Detraz committed
573 574 575 576 577 578 579
                #  Freshly created users are NOT synced in ldap base
                if self.state == self.STATE_NOT_YET_ACTIVE:
                    return
                user_ldap = LdapUser(uidNumber=self.uid_number)
                base = True
                access_refresh = True
                mac_refresh = True
580 581 582 583 584 585 586 587 588 589 590
            if base:
                user_ldap.name = self.pseudo
                user_ldap.sn = self.pseudo
                user_ldap.dialupAccess = str(self.has_access())
                user_ldap.home_directory = '/home/' + self.pseudo
                user_ldap.mail = self.get_mail
                user_ldap.given_name = self.surname.lower() + '_'\
                    + self.name.lower()[:3]
                user_ldap.gid = LDAP['user_gid']
                if '{SSHA}' in self.password or '{SMD5}' in self.password:
                    # We remove the extra $ added at import from ldap
591 592
                    user_ldap.user_password = self.password[:6] + \
                        self.password[7:]
593 594
                elif '{crypt}' in self.password:
                    # depending on the length, we need to remove or not a $
595
                    if len(self.password) == 41:
596
                        user_ldap.user_password = self.password
597
                    else:
598 599
                        user_ldap.user_password = self.password[:7] + \
                            self.password[8:]
600 601 602 603 604 605 606 607 608 609 610 611 612 613 614 615 616 617 618

                user_ldap.sambat_nt_password = self.pwd_ntlm.upper()
                if self.get_shell:
                    user_ldap.login_shell = str(self.get_shell)
                user_ldap.shadowexpire = self.get_shadow_expire
            if access_refresh:
                user_ldap.dialupAccess = str(self.has_access())
            if mac_refresh:
                user_ldap.macs = [str(mac) for mac in Interface.objects.filter(
                    machine__user=self
                ).values_list('mac_address', flat=True).distinct()]
            if group_refresh:
                # 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():
                    if hasattr(group, 'listright'):
                        group.listright.ldap_sync()
            user_ldap.save()
619 620

    def ldap_del(self):
chirac's avatar
chirac committed
621
        """ Supprime la version ldap de l'user"""
622 623 624 625 626 627
        try:
            user_ldap = LdapUser.objects.get(name=self.pseudo)
            user_ldap.delete()
        except LdapUser.DoesNotExist:
            pass

628 629
    def notif_inscription(self):
        """ Prend en argument un objet user, envoie un mail de bienvenue """
chirac's avatar
chirac committed
630 631 632 633
        template = loader.get_template('users/email_welcome')
        mailmessageoptions, _created = MailMessageOption\
            .objects.get_or_create()
        context = Context({
634
            'nom': self.get_full_name(),
635 636
            'asso_name': AssoOption.get_cached_value('name'),
            'asso_email': AssoOption.get_cached_value('contact'),
chirac's avatar
chirac committed
637 638 639
            'welcome_mail_fr': mailmessageoptions.welcome_mail_fr,
            'welcome_mail_en': mailmessageoptions.welcome_mail_en,
            'pseudo': self.pseudo,
640
        })
641 642 643
        send_mail(
            'Bienvenue au %(name)s / Welcome to %(name)s' % {
                'name': AssoOption.get_cached_value('name')
644
            },
645 646 647 648 649
            '',
            GeneralOption.get_cached_value('email_from'),
            [self.email],
            html_message=template.render(context)
        )
650 651 652
        return

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

682
    def autoregister_machine(self, mac_address, nas_type):
chirac's avatar
chirac committed
683 684
        """ Fonction appellée par freeradius. Enregistre la mac pour
        une machine inconnue sur le compte de l'user"""
chirac's avatar
chirac committed
685
        all_interfaces = self.user_interfaces(active=False)
686
        if all_interfaces.count() > OptionalMachine.get_cached_value(
687 688
            'max_lambdauser_interfaces'
        ):
689
            return False, _("Maximum number of registered machines reached.")
690
        if not nas_type:
691
            return False, _("Re2o doesn't know wich machine type to assign.")
692
        machine_type_cible = nas_type.machine_type
693 694 695 696 697
        try:
            machine_parent = Machine()
            machine_parent.user = self
            interface_cible = Interface()
            interface_cible.mac_address = mac_address
698
            interface_cible.type = machine_type_cible
699 700 701
            interface_cible.clean()
            machine_parent.clean()
            domain = Domain()
702
            domain.name = self.get_next_domain_name()
703 704
            domain.interface_parent = interface_cible
            domain.clean()
705 706 707 708 709 710
            machine_parent.save()
            interface_cible.machine = machine_parent
            interface_cible.save()
            domain.interface_parent = interface_cible
            domain.clean()
            domain.save()
711
            self.notif_auto_newmachine(interface_cible)
chirac's avatar
chirac committed
712
        except Exception as error:
713
            return False,  traceback.format_exc()
714
        return interface_cible, "Ok"
715

716 717 718 719 720 721
    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(),
722
            'mac_address': interface.mac_address,
723
            'asso_name': AssoOption.get_cached_value('name'),
724
            'interface_name': interface.domain,
725
            'asso_email': AssoOption.get_cached_value('contact'),
726 727 728 729 730
            'pseudo': self.pseudo,
        })
        send_mail(
            "Ajout automatique d'une machine / New machine autoregistered",
            '',
731
            GeneralOption.get_cached_value('email_from'),
732 733 734 735 736
            [self.email],
            html_message=template.render(context)
        )
        return

737
    def set_password(self, password):
chirac's avatar
chirac committed
738
        """ A utiliser de préférence, set le password en hash courrant et
chirac's avatar
chirac committed
739
        dans la version ntlm"""
740
        super().set_password(password)
741 742 743
        self.pwd_ntlm = hashNT(password)
        return

744
    @cached_property
745
    def email_address(self):
746 747
        if (OptionalUser.get_cached_value('local_email_accounts_enabled')
                and self.local_email_enabled):
748 749
            return self.emailaddress_set.all()
        return EMailAddress.objects.none()
750

751 752 753
    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
754 755 756

        Recherche un nom disponible, pour une machine. Doit-être
        unique, concatène le nom, le pseudo et le numero de machine
757 758 759
        """

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

chirac's avatar
chirac committed
763 764 765
        def composed_pseudo(name):
            """Renvoie le resultat de simplepseudo et rajoute le nom"""
            return simple_pseudo() + str(name)
766 767

        num = 0
chirac's avatar
chirac committed
768
        while Domain.objects.filter(name=composed_pseudo(num)):
769 770 771
            num += 1
        return composed_pseudo(num)

772 773
    def can_edit(self, user_request, *_args, **_kwargs):
        """Check if a user can edit a user object.
774 775 776 777

        :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
778 779
            user_request one of its member, or if user_request is self, or if
            user_request has the 'cableur' right.
780
        """
781
        if self.is_class_club and user_request.is_class_adherent:
782 783 784
            if (self == user_request or
                    user_request.has_perm('users.change_user') or
                    user_request.adherent in self.club.administrators.all()):
785 786
                return True, None
            else:
787
                return False, _("You don't have the right to edit this club.")
788
        else:
789 790 791 792 793 794
            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):
795 796
                    return False, (_("User with critical rights, can't be"
                                     " edited."))
797
                elif self == AssoOption.get_cached_value('utilisateur_asso'):
798 799 800
                    return False, (_("Impossible to edit the organisation's"
                                     " user without the 'change_all_users'"
                                     " right."))
801 802 803
                else:
                    return True, None
            elif user_request.has_perm('users.change_all_users'):
804 805
                return True, None
            else:
806 807
                return False, (_("You don't have the right to edit another"
                                 " user."))
808

809 810 811 812 813 814 815 816 817
    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
        """
818
        if self.is_class_club and user_request.is_class_adherent:
819 820 821
            if (self == user_request or
                    user_request.has_perm('users.change_user_password') or
                    user_request.adherent in self.club.administrators.all()):
822 823
                return True, None
            else:
824
                return False, _("You don't have the right to edit this club.")
825
        else:
826 827 828 829
            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
830
                return True, None
831 832
            elif (user_request.has_perm('users.change_user') and
                  not self.groups.all()):
833 834
                return True, None
            else:
835 836
                return False, (_("You don't have the right to edit another"
                                 " user."))
837

838 839 840 841
    def check_selfpasswd(self, user_request, *_args, **_kwargs):
        """ Returns (True, None) if user_request is self, else returns
        (False, None)
        """
842 843
        return user_request == self, None

844
    @staticmethod
845 846 847 848 849 850 851
    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
        """
852 853
        return (
            user_request.has_perm('users.change_user_state'),
854
            _("Permission required to change the state.")
855
        )
856

857
    def can_change_shell(self, user_request, *_args, **_kwargs):
858 859 860 861 862 863
        """ 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
        """
864
        if not ((self.pk == user_request.pk and OptionalUser.get_cached_value('self_change_shell'))
865
            or user_request.has_perm('users.change_user_shell')):
866
            return False, _("Permission required to change the shell.")
867 868
        else:
            return True, None
869

870
    @staticmethod
871 872
    def can_change_local_email_redirect(user_request, *_args, **_kwargs):
        """ Check if a user can change local_email_redirect.
873 874 875 876 877 878

        :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 redirection
        """
        return (
879
            OptionalUser.get_cached_value('local_email_accounts_enabled'),
880
            _("Local email accounts must be enabled.")
881 882 883
        )

    @staticmethod
884
    def can_change_local_email_enabled(user_request, *_args, **_kwargs):
885
        """ Check if a user can change internal address.
886 887 888 889 890 891

        :param user_request: The user who request
        :returns: a message and a boolean which is True if the user has
        the right to change internal address
        """
        return (
892
            OptionalUser.get_cached_value('local_email_accounts_enabled'),
893
            _("Local email accounts must be enabled.")
894
        )
895

896
    @staticmethod
897 898 899 900 901 902 903
    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
        """
904 905
        return (
            user_request.has_perm('users.change_user_force'),
906
            _("Permission required to force the move.")
907
        )
908 909

    @staticmethod
910 911 912 913 914 915 916
    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
        """
917 918
        return (
            user_request.has_perm('users.change_user_groups'),
919
            _("Permission required to edit the user's groups of rights.")
920
        )
921

Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
922 923 924 925 926 927 928 929 930
    @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,
931
            _("'superuser' right required to edit the superuser flag.")
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
932 933
        )

934
    def can_view(self, user_request, *_args, **_kwargs):
935 936 937 938 939
        """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
940
            text
941
        """
942
        if self.is_class_club and user_request.is_class_adherent:
943 944 945 946
            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()):
947 948
                return True, None
            else:
949
                return False, _("You don't have the right to view this club.")
950
        else:
951 952
            if (self == user_request or
                    user_request.has_perm('users.view_user')):
953 954
                return True, None
            else:
955 956
                return False, (_("You don't have the right to view another"
                                 " user."))
957

958 959
    @staticmethod
    def can_view_all(user_request, *_args, **_kwargs):
960 961 962
        """Check if an user can access to the list of every user objects

        :param user_request: The user who wants to view the list.
963 964
        :return: True if the user can view the list and an explanation
            message.
965
        """
966 967
        return (
            user_request.has_perm('users.view_user'),
968
            _("You don't have the right to view the list of users.")
969
        )
970

971
    def can_delete(self, user_request, *_args, **_kwargs):
972 973 974 975
        """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.
976 977
        :return: True if user_request has the right 'bureau', and a
            message.
978
        """
979 980
        return (
            user_request.has_perm('users.delete_user'),
981
            _("You don't have the right to delete this user.")
982
        )
983

984 985 986
    def __init__(self, *args, **kwargs):
        super(User, self).__init__(*args, **kwargs)
        self.field_permissions = {
987 988 989
            'shell': self.can_change_shell,
            'force': self.can_change_force,
            'selfpasswd': self.check_selfpasswd,
990
            'local_email_redirect': self.can_change_local_email_redirect,
991
            'local_email_enabled': self.can_change_local_email_enabled,
992
        }
Gabriel Detraz's avatar
Gabriel Detraz committed
993
        self.__original_state = self.state
994

995 996 997
    def clean(self, *args, **kwargs):
        """Check if this pseudo is already used by any mailalias.
        Better than raising an error in post-save and catching it"""
998
        if (EMailAddress.objects
999
            .filter(local_part=self.pseudo.lower()).exclude(user_id=self.id)
1000
            ):
1001
            raise ValidationError("This pseudo is already in use.")
1002 1003 1004
        if not self.local_email_enabled and not self.email:
            raise ValidationError(
                {'email': (
1005 1006
                    _("There is neither a local email address nor an external"
                      " email address for this user.")
1007 1008 1009 1010 1011
                ), }
            )
        if self.local_email_redirect and not self.email:
            raise ValidationError(
                {'local_email_redirect': (
1012 1013
                _("You can't redirect your local emails if no external email"
                  " address has been set.")), }
1014
            )
1015

1016
    def __str__(self):
1017
        return self.pseudo
lhark's avatar
lhark committed
1018

1019

1020
class Adherent(User):
1021 1022
    """ A class representing a member (it's a user with special
    informations) """
1023

1024
    name = models.CharField(max_length=255)
1025 1026 1027 1028 1029 1030
    room = models.OneToOneField(
        'topologie.Room',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
1031 1032 1033 1034 1035 1036
    gpg_fingerprint = models.CharField(
        max_length=40,
        blank=True,
        null=True,
        validators=[RegexValidator(
            '^[0-9A-F]{40}$',
1037 1038
            message=_("A GPG fingerprint must contain 40 hexadecimal"
                      " characters.")
1039 1040
        )]
    )
1041

1042
    class Meta(User.Meta):
1043 1044 1045
        verbose_name = _("member")
        verbose_name_plural = _("members")

1046 1047
    @classmethod
    def get_instance(cls, adherentid, *_args, **_kwargs):
1048
        """Try to find an instance of `Adherent` with the given id.
1049

1050 1051 1052
        :param adherentid: The id of the adherent we are looking for.
        :return: An adherent.
        """
1053
        return cls.objects.get(pk=adherentid)
1054

1055 1056
    @staticmethod
    def can_create(user_request, *_args, **_kwargs):
1057 1058 1059 1060
        """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
1061
            a user or if the `options.all_can_create` is set.
1062
        """
1063 1064
        if (not user_request.is_authenticated and
                not OptionalUser.get_cached_value('self_adhesion')):
1065 1066
            return False, None
        else:
1067 1068
            if (OptionalUser.get_cached_value('all_can_create_adherent') or
                    OptionalUser.get_cached_value('self_adhesion')):
1069 1070
                return True, None
            else:
1071 1072
                return (
                    user_request.has_perm('users.add_user'),
1073
                    _("You don't have the right to create a user.")
1074
                )
1075

1076

1077
class Club(User):
1078 1079
    """ A class representing a club (it is considered as a user
    with special informations) """
1080

1081 1082 1083 1084 1085 1086
    room = models.ForeignKey(
        'topologie.Room',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
1087 1088 1089 1090 1091 1092 1093 1094 1095 1096
    administrators = models.ManyToManyField(
        blank=True,
        to='users.Adherent',
        related_name='club_administrator'
    )
    members = models.ManyToManyField(
        blank=True,
        to='users.Adherent',
        related_name='club_members'
    )
1097
    mailing = models.BooleanField(
1098
        default=False
1099
    )
1100

1101
    class Meta(User.Meta):
1102 1103 1104
        verbose_name = _("club")
        verbose_name_plural = _("clubs")

1105 1106
    @staticmethod
    def can_create(user_request, *_args, **_kwargs):
1107 1108 1109 1110
        """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
1111
            an user or if the `options.all_can_create` is set.
1112 1113 1114 1115 1116 1117 1118
        """
        if not user_request.is_authenticated:
            return False, None
        else:
            if OptionalUser.get_cached_value('all_can_create_club'):
                return True, None
            else:
1119 1120
                return (
                    user_request.has_perm('users.add_user'),
1121
                    _("You don't have the right to create a club.")
1122
                )
1123

1124 1125
    @staticmethod
    def can_view_all(user_request, *_args, **_kwargs):
1126 1127 1128
        """Check if an user can access to the list of every user objects

        :param user_request: The user who wants to view the list.
1129 1130
        :return: True if the user can view the list and an explanation
            message.
1131
        """
1132
        if user_request.has_perm('users.view_user'):
1133
            return True, None
1134 1135 1136 1137
        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()):
1138
                return True, None
1139
        return False, _("You don't have the right to view the list of users.")
1140

1141 1142
    @classmethod
    def get_instance(cls, clubid, *_args, **_kwargs):
1143
        """Try to find an instance of `Club` with the given id.
1144

1145 1146 1147
        :param clubid: The id of the adherent we are looking for.
        :return: A club.
        """
1148
        return cls.objects.get(pk=clubid)
1149

1150

1151 1152
@receiver(post_save, sender=Adherent)
@receiver(post_save, sender=Club)
1153
@receiver(post_save, sender=User)
1154
def user_post_save(**kwargs):
chirac's avatar
chirac committed
1155
    """ Synchronisation post_save : envoie le mail de bienvenue si creation
1156
    Synchronise le pseudo, en créant un alias mail correspondant
chirac's avatar
chirac committed
1157
    Synchronise le ldap"""
1158
    is_created = kwargs['created']
1159
    user = kwargs['instance']
1160 1161
    EMailAddress.objects.get_or_create(
        local_part=user.pseudo.lower(), user=user)
1162 1163
    if is_created:
        user.notif_inscription()
Gabriel Detraz's avatar
Gabriel Detraz committed
1164
    user.state_sync()
1165 1166 1167 1168 1169 1170
    user.ldap_sync(
        base=True,
        access_refresh=True,
        mac_refresh=False,
        group_refresh=True
    )
1171
    regen('mailing')
1172

chirac's avatar
chirac committed
1173

1174 1175 1176 1177 1178 1179 1180 1181 1182 1183
@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)

1184

1185 1186
@receiver(post_delete, sender=Adherent)
@receiver(post_delete, sender=Club)
1187
@receiver(post_delete, sender=User)
1188
def user_post_delete(**kwargs):
chirac's avatar
chirac committed