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.1 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
lhark's avatar
lhark committed
189
    STATES = (
chirac's avatar
chirac committed
190 191 192 193
        (0, 'STATE_ACTIVE'),
        (1, 'STATE_DISABLED'),
        (2, 'STATE_ARCHIVE'),
    )
lhark's avatar
lhark committed
194 195

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

247
    USERNAME_FIELD = 'pseudo'
248
    REQUIRED_FIELDS = ['surname', 'email']
249 250 251

    objects = UserManager()

252 253
    class Meta:
        permissions = (
254
            ("change_user_password",
255 256 257 258
             _("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")),
259
            ("change_user_groups",
260 261
             _("Can edit the groups of rights of a user (critical"
               " permission)")),
262
            ("change_all_users",
263
             _("Can edit all users, including those with rights.")),
264
            ("view_user",
265
             _("Can view a user object")),
266
        )
267 268
        verbose_name = _("user (member or club)")
        verbose_name_plural = _("users (members or clubs)")
269

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

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

288 289 290 291 292 293 294 295 296 297 298 299
    @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
300
            return str(self.emailaddress_set.get(local_part=self.pseudo.lower()))
301

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

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

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

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

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

    @property
    def is_staff(self):
chirac's avatar
chirac committed
336
        """ Fonction de base django, renvoie si l'user est admin"""
337 338 339 340
        return self.is_admin

    @property
    def is_admin(self):
chirac's avatar
chirac committed
341
        """ Renvoie si l'user est admin"""
342
        admin, _ = Group.objects.get_or_create(name="admin")
343
        return self.is_superuser or admin in self.groups.all()
344 345

    def get_full_name(self):
chirac's avatar
chirac committed
346
        """ Renvoie le nom complet de l'user formaté nom/prénom"""
347 348 349 350 351
        name = self.name
        if name:
            return '%s %s' % (name, self.surname)
        else:
            return self.surname
352 353

    def get_short_name(self):
chirac's avatar
chirac committed
354
        """ Renvoie seulement le nom"""
355
        return self.surname
356

357 358 359 360 361
    @cached_property
    def gid(self):
        """return the default gid of user"""
        return LDAP['user_gid']

362 363 364 365 366 367
    @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')

368 369 370 371 372 373 374 375
    @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

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

    def is_adherent(self):
chirac's avatar
chirac committed
405 406
        """ Renvoie True si l'user est adhérent : si
        self.end_adhesion()>now"""
407
        end = self.end_adhesion()
408 409
        if not end:
            return False
410
        elif end < timezone.now():
411 412 413 414
            return False
        else:
            return True

415 416 417 418 419 420
    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
421
        elif end < timezone.now():
422 423 424 425
            return False
        else:
            return self.is_adherent()

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

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

    def is_ban(self):
        """ Renvoie si un user est banni ou non """
442
        end = self.end_ban()
443 444
        if not end:
            return False
445
        elif end < timezone.now():
446 447 448 449 450 451
            return False
        else:
            return True

    def is_whitelisted(self):
        """ Renvoie si un user est whitelisté ou non """
452
        end = self.end_whitelist()
453 454
        if not end:
            return False
455
        elif end < timezone.now():
456 457 458 459 460 461
            return False
        else:
            return True

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

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

chibrac's avatar
chibrac committed
479 480
    @cached_property
    def solde(self):
481
        """ Renvoie le solde d'un user.
chirac's avatar
chirac committed
482
        Somme les crédits de solde et retire les débit payés par solde"""
483
        solde_objects = Paiement.objects.filter(is_balance=True)
484 485 486 487 488 489 490 491 492 493 494 495 496 497 498 499 500 501 502 503 504 505
        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
506

chirac's avatar
chirac committed
507
    def user_interfaces(self, active=True):
chirac's avatar
chirac committed
508 509 510 511 512
        """ 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')
513

514 515 516 517 518 519 520
    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()
521
                    reversion.set_comment(_("IPv4 assigning"))
522 523 524
                    interface.save()

    def unassign_ips(self):
chirac's avatar
chirac committed
525
        """ Désassigne les ipv4 aux machines de l'user"""
526 527 528 529
        interfaces = self.user_interfaces()
        for interface in interfaces:
            with transaction.atomic(), reversion.create_revision():
                interface.unassign_ipv4()
530
                reversion.set_comment(_("IPv4 unassigning"))
531 532 533
                interface.save()

    def archive(self):
chirac's avatar
chirac committed
534
        """ Filling the user; no more active"""
535 536 537
        self.unassign_ips()

    def unarchive(self):
chirac's avatar
chirac committed
538
        """Unfilling the user"""
539
        self.assign_ips()
Gabriel Detraz's avatar
Gabriel Detraz committed
540 541 542 543 544 545 546

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

548 549
    def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True,
                  group_refresh=False):
chirac's avatar
chirac committed
550 551 552 553 554 555
        """ 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
556
        mac_refresh : synchronise les machines de l'user
557
        group_refresh : synchronise les group de l'user
558
        Si l'instance n'existe pas, on crée le ldapuser correspondant"""
559 560 561 562 563 564 565 566 567 568 569 570 571 572 573 574 575 576 577 578
        if sys.version_info[0] >= 3:
            self.refresh_from_db()
            try:
                user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
            except LdapUser.DoesNotExist:
                user_ldap = LdapUser(uidNumber=self.uid_number)
                base = True
                access_refresh = True
                mac_refresh = True
            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
579 580
                    user_ldap.user_password = self.password[:6] + \
                        self.password[7:]
581 582
                elif '{crypt}' in self.password:
                    # depending on the length, we need to remove or not a $
583
                    if len(self.password) == 41:
584
                        user_ldap.user_password = self.password
585
                    else:
586 587
                        user_ldap.user_password = self.password[:7] + \
                            self.password[8:]
588 589 590 591 592 593 594 595 596 597 598 599 600 601 602 603 604 605 606

                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()
607 608

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

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

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

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

704 705 706 707 708 709
    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(),
710
            'mac_address': interface.mac_address,
711
            'asso_name': AssoOption.get_cached_value('name'),
712
            'interface_name': interface.domain,
713
            'asso_email': AssoOption.get_cached_value('contact'),
714 715 716 717 718
            'pseudo': self.pseudo,
        })
        send_mail(
            "Ajout automatique d'une machine / New machine autoregistered",
            '',
719
            GeneralOption.get_cached_value('email_from'),
720 721 722 723 724
            [self.email],
            html_message=template.render(context)
        )
        return

725
    def set_password(self, password):
chirac's avatar
chirac committed
726
        """ A utiliser de préférence, set le password en hash courrant et
chirac's avatar
chirac committed
727
        dans la version ntlm"""
728
        super().set_password(password)
729 730 731
        self.pwd_ntlm = hashNT(password)
        return

732
    @cached_property
733
    def email_address(self):
734 735
        if (OptionalUser.get_cached_value('local_email_accounts_enabled')
                and self.local_email_enabled):
736 737
            return self.emailaddress_set.all()
        return EMailAddress.objects.none()
738

739 740 741
    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
742 743 744

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

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

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

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

760 761
    def can_edit(self, user_request, *_args, **_kwargs):
        """Check if a user can edit a user object.
762 763 764 765

        :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
766 767
            user_request one of its member, or if user_request is self, or if
            user_request has the 'cableur' right.
768
        """
769
        if self.is_class_club and user_request.is_class_adherent:
770 771 772
            if (self == user_request or
                    user_request.has_perm('users.change_user') or
                    user_request.adherent in self.club.administrators.all()):
773 774
                return True, None
            else:
775
                return False, _("You don't have the right to edit this club.")
776
        else:
777 778 779 780 781 782
            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):
783 784
                    return False, (_("User with critical rights, can't be"
                                     " edited."))
785
                elif self == AssoOption.get_cached_value('utilisateur_asso'):
786 787 788
                    return False, (_("Impossible to edit the organisation's"
                                     " user without the 'change_all_users'"
                                     " right."))
789 790 791
                else:
                    return True, None
            elif user_request.has_perm('users.change_all_users'):
792 793
                return True, None
            else:
794 795
                return False, (_("You don't have the right to edit another"
                                 " user."))
796

797 798 799 800 801 802 803 804 805
    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
        """
806
        if self.is_class_club and user_request.is_class_adherent:
807 808 809
            if (self == user_request or
                    user_request.has_perm('users.change_user_password') or
                    user_request.adherent in self.club.administrators.all()):
810 811
                return True, None
            else:
812
                return False, _("You don't have the right to edit this club.")
813
        else:
814 815 816 817
            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
818
                return True, None
819 820
            elif (user_request.has_perm('users.change_user') and
                  not self.groups.all()):
821 822
                return True, None
            else:
823 824
                return False, (_("You don't have the right to edit another"
                                 " user."))
825

826 827 828 829
    def check_selfpasswd(self, user_request, *_args, **_kwargs):
        """ Returns (True, None) if user_request is self, else returns
        (False, None)
        """
830 831
        return user_request == self, None

832
    @staticmethod
833 834 835 836 837 838 839
    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
        """
840 841
        return (
            user_request.has_perm('users.change_user_state'),
842
            _("Permission required to change the state.")
843
        )
844

845
    def can_change_shell(self, user_request, *_args, **_kwargs):
846 847 848 849 850 851
        """ 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
        """
852
        if not ((self.pk == user_request.pk and OptionalUser.get_cached_value('self_change_shell'))
853
            or user_request.has_perm('users.change_user_shell')):
854
            return False, _("Permission required to change the shell.")
855 856
        else:
            return True, None
857

858
    @staticmethod
859 860
    def can_change_local_email_redirect(user_request, *_args, **_kwargs):
        """ Check if a user can change local_email_redirect.
861 862 863 864 865 866

        :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 (
867
            OptionalUser.get_cached_value('local_email_accounts_enabled'),
868
            _("Local email accounts must be enabled.")
869 870 871
        )

    @staticmethod
872
    def can_change_local_email_enabled(user_request, *_args, **_kwargs):
873
        """ Check if a user can change internal address.
874 875 876 877 878 879

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

884
    @staticmethod
885 886 887 888 889 890 891
    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
        """
892 893
        return (
            user_request.has_perm('users.change_user_force'),
894
            _("Permission required to force the move.")
895
        )
896 897

    @staticmethod
898 899 900 901 902 903 904
    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
        """
905 906
        return (
            user_request.has_perm('users.change_user_groups'),
907
            _("Permission required to edit the user's groups of rights.")
908
        )
909

Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
910 911 912 913 914 915 916 917 918
    @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,
919
            _("'superuser' right required to edit the superuser flag.")
Levy--Falk Hugo's avatar
Levy--Falk Hugo committed
920 921
        )

922
    def can_view(self, user_request, *_args, **_kwargs):
923 924 925 926 927
        """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
928
            text
929
        """
930
        if self.is_class_club and user_request.is_class_adherent:
931 932 933 934
            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()):
935 936
                return True, None
            else:
937
                return False, _("You don't have the right to view this club.")
938
        else:
939 940
            if (self == user_request or
                    user_request.has_perm('users.view_user')):
941 942
                return True, None
            else:
943 944
                return False, (_("You don't have the right to view another"
                                 " user."))
945

946 947
    @staticmethod
    def can_view_all(user_request, *_args, **_kwargs):
948 949 950
        """Check if an user can access to the list of every user objects

        :param user_request: The user who wants to view the list.
951 952
        :return: True if the user can view the list and an explanation
            message.
953
        """
954 955
        return (
            user_request.has_perm('users.view_user'),
956
            _("You don't have the right to view the list of users.")
957
        )
958

959
    def can_delete(self, user_request, *_args, **_kwargs):
960 961 962 963
        """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.
964 965
        :return: True if user_request has the right 'bureau', and a
            message.
966
        """
967 968
        return (
            user_request.has_perm('users.delete_user'),
969
            _("You don't have the right to delete this user.")
970
        )
971

972 973 974
    def __init__(self, *args, **kwargs):
        super(User, self).__init__(*args, **kwargs)
        self.field_permissions = {
975 976 977
            'shell': self.can_change_shell,
            'force': self.can_change_force,
            'selfpasswd': self.check_selfpasswd,
978
            'local_email_redirect': self.can_change_local_email_redirect,
979
            'local_email_enabled': self.can_change_local_email_enabled,
980
        }
Gabriel Detraz's avatar
Gabriel Detraz committed
981
        self.__original_state = self.state
982

983 984 985
    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"""
986
        if (EMailAddress.objects
987
            .filter(local_part=self.pseudo.lower()).exclude(user_id=self.id)
988
            ):
989
            raise ValidationError("This pseudo is already in use.")
990 991 992
        if not self.local_email_enabled and not self.email:
            raise ValidationError(
                {'email': (
993 994
                    _("There is neither a local email address nor an external"
                      " email address for this user.")
995 996 997 998 999
                ), }
            )
        if self.local_email_redirect and not self.email:
            raise ValidationError(
                {'local_email_redirect': (
1000 1001
                _("You can't redirect your local emails if no external email"
                  " address has been set.")), }
1002
            )
1003

1004
    def __str__(self):
1005
        return self.pseudo
lhark's avatar
lhark committed
1006

1007

1008
class Adherent(User):
1009 1010
    """ A class representing a member (it's a user with special
    informations) """
1011

1012
    name = models.CharField(max_length=255)
1013 1014 1015 1016 1017 1018
    room = models.OneToOneField(
        'topologie.Room',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
1019 1020 1021 1022 1023 1024
    gpg_fingerprint = models.CharField(
        max_length=40,
        blank=True,
        null=True,
        validators=[RegexValidator(
            '^[0-9A-F]{40}$',
1025 1026
            message=_("A GPG fingerprint must contain 40 hexadecimal"
                      " characters.")
1027 1028
        )]
    )
1029

1030
    class Meta(User.Meta):
1031 1032 1033
        verbose_name = _("member")
        verbose_name_plural = _("members")

1034 1035
    @classmethod
    def get_instance(cls, adherentid, *_args, **_kwargs):
1036
        """Try to find an instance of `Adherent` with the given id.
1037

1038 1039 1040
        :param adherentid: The id of the adherent we are looking for.
        :return: An adherent.
        """
1041
        return cls.objects.get(pk=adherentid)
1042

1043 1044
    @staticmethod
    def can_create(user_request, *_args, **_kwargs):
1045 1046 1047 1048
        """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
1049
            a user or if the `options.all_can_create` is set.
1050
        """
1051 1052
        if (not user_request.is_authenticated and
                not OptionalUser.get_cached_value('self_adhesion')):
1053 1054
            return False, None
        else:
1055 1056
            if (OptionalUser.get_cached_value('all_can_create_adherent') or
                    OptionalUser.get_cached_value('self_adhesion')):
1057 1058
                return True, None
            else:
1059 1060
                return (
                    user_request.has_perm('users.add_user'),
1061
                    _("You don't have the right to create a user.")
1062
                )
1063

1064

1065
class Club(User):
1066 1067
    """ A class representing a club (it is considered as a user
    with special informations) """
1068

1069 1070 1071 1072 1073 1074
    room = models.ForeignKey(
        'topologie.Room',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
1075 1076 1077 1078 1079 1080 1081 1082 1083 1084
    administrators = models.ManyToManyField(
        blank=True,
        to='users.Adherent',
        related_name='club_administrator'
    )
    members = models.ManyToManyField(
        blank=True,
        to='users.Adherent',
        related_name='club_members'
    )
1085
    mailing = models.BooleanField(
1086
        default=False
1087
    )
1088

1089
    class Meta(User.Meta):
1090 1091 1092
        verbose_name = _("club")
        verbose_name_plural = _("clubs")

1093 1094
    @staticmethod
    def can_create(user_request, *_args, **_kwargs):
1095 1096 1097 1098
        """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
1099
            an user or if the `options.all_can_create` is set.
1100 1101 1102 1103 1104 1105 1106
        """
        if not user_request.is_authenticated:
            return False, None
        else:
            if OptionalUser.get_cached_value('all_can_create_club'):
                return True, None
            else:
1107 1108
                return (
                    user_request.has_perm('users.add_user'),
1109
                    _("You don't have the right to create a club.")
1110
                )
1111

1112 1113
    @staticmethod
    def can_view_all(user_request, *_args, **_kwargs):
1114 1115 1116
        """Check if an user can access to the list of every user objects

        :param user_request: The user who wants to view the list.
1117 1118
        :return: True if the user can view the list and an explanation
            message.
1119
        """
1120
        if user_request.has_perm('users.view_user'):
1121
            return True, None
1122 1123 1124 1125
        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()):
1126
                return True, None
1127
        return False, _("You don't have the right to view the list of users.")
1128

1129 1130
    @classmethod
    def get_instance(cls, clubid, *_args, **_kwargs):
1131
        """Try to find an instance of `Club` with the given id.
1132

1133 1134 1135
        :param clubid: The id of the adherent we are looking for.
        :return: A club.
        """
1136
        return cls.objects.get(pk=clubid)
1137

1138

1139 1140
@receiver(post_save, sender=Adherent)
@receiver(post_save, sender=Club)
1141
@receiver(post_save, sender=User)
1142
def user_post_save(**kwargs):
chirac's avatar
chirac committed
1143
    """ Synchronisation post_save : envoie le mail de bienvenue si creation
1144
    Synchronise le pseudo, en créant un alias mail correspondant
chirac's avatar
chirac committed
1145
    Synchronise le ldap"""
1146
    is_created = kwargs['created']
1147
    user = kwargs['instance']
1148 1149
    EMailAddress.objects.get_or_create(
        local_part=user.pseudo.lower(), user=user)
1150 1151
    if is_created:
        user.notif_inscription()
Gabriel Detraz's avatar
Gabriel Detraz committed
1152
    user.state_sync()
1153 1154 1155 1156 1157 1158
    user.ldap_sync(
        base=True,
        access_refresh=True,
        mac_refresh=False,
        group_refresh=True
    )
1159
    regen('mailing')
1160

chirac's avatar
chirac committed
1161

1162 1163 1164 1165 1166 1167 1168 1169 1170 1171
@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)

1172

1173 1174
@receiver(post_delete, sender=Adherent)
@receiver(post_delete, sender=Club)
1175
@receiver(post_delete, sender=User)
1176
def user_post_delete(**kwargs):
chirac's avatar
chirac committed
1177
    """Post delete d'un user, on supprime son instance ldap"""
1178
    user = kwargs['instance']
1179
    user.ldap_del()
1180
    regen('mailing')
1181

1182

1183
class ServiceUser(RevMixin, AclMixin, AbstractBaseUser):
chirac's avatar
chirac committed
1184
    """ Classe des users daemons, règle leurs accès au ldap"""
1185 1186
    readonly = 'readonly'
    ACCESS = (
chirac's avatar
chirac committed
1187 1188 1189 1190
        ('auth', 'auth'),
        ('readonly', 'readonly'),
        ('usermgmt', 'usermgmt'),
    )
1191