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

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

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

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

lhark's avatar
lhark committed
45

46 47
from __future__ import unicode_literals

chirac's avatar
chirac committed
48 49 50
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
Laouen Fernet's avatar
Laouen Fernet committed
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
84
from re2o.base import smtp_check
lhark's avatar
lhark committed
85

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

chirac's avatar
chirac committed
91

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

94 95

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


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

110

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

chirac's avatar
chirac committed
124

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

chirac's avatar
chirac committed
135

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

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

        if not linux_user_check(pseudo):
Laouen Fernet's avatar
Laouen Fernet committed
151
            raise ValueError(_("Username should only contain [a-z0-9-]."))
152

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

        user.set_password(password)
        if su:
Maël Kervella's avatar
Maël Kervella committed
162
            user.is_superuser = True
163
        user.save(using=self._db)
164 165
        return user

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

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

Maël Kervella's avatar
Maël Kervella committed
180 181 182

class User(RevMixin, FieldPermissionModelMixin, AbstractBaseUser,
           PermissionsMixin, AclMixin):
chirac's avatar
chirac committed
183 184 185
    """ 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"""
Laouen Fernet's avatar
Laouen Fernet committed
186

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

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

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

    objects = UserManager()

255 256
    class Meta:
        permissions = (
Maël Kervella's avatar
Maël Kervella committed
257
            ("change_user_password",
Laouen Fernet's avatar
Laouen Fernet committed
258 259 260 261
             _("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")),
Maël Kervella's avatar
Maël Kervella committed
262
            ("change_user_groups",
Laouen Fernet's avatar
Laouen Fernet committed
263 264
             _("Can edit the groups of rights of a user (critical"
               " permission)")),
Maël Kervella's avatar
Maël Kervella committed
265
            ("change_all_users",
Laouen Fernet's avatar
Laouen Fernet committed
266
             _("Can edit all users, including those with rights.")),
Maël Kervella's avatar
Maël Kervella committed
267
            ("view_user",
Laouen Fernet's avatar
Laouen Fernet committed
268
             _("Can view a user object")),
269
        )
Laouen Fernet's avatar
Laouen Fernet committed
270 271
        verbose_name = _("user (member or club)")
        verbose_name_plural = _("users (members or clubs)")
272

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

281 282 283 284 285 286 287 288
    @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:
Laouen Fernet's avatar
Laouen Fernet committed
289
            raise NotImplementedError(_("Unknown type."))
290

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

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

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

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

    @cached_property
    def is_class_adherent(self):
Maël Kervella's avatar
Maël Kervella committed
328 329
        """ Returns True if the object is a Adherent (subclassing User) """
        # TODO : change to isinstance (cleaner)
330 331
        return hasattr(self, 'adherent')

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

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

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

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

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

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

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

Gabriel Detraz's avatar
Gabriel Detraz committed
372 373 374 375 376 377
    @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')

378 379 380 381
    @cached_property
    def home_directory(self):
        return '/home/' + self.pseudo

Gabriel Detraz's avatar
Gabriel Detraz committed
382 383 384 385 386 387 388 389
    @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

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

    def is_adherent(self):
chirac's avatar
chirac committed
419 420
        """ Renvoie True si l'user est adhérent : si
        self.end_adhesion()>now"""
Gabriel Detraz's avatar
Gabriel Detraz committed
421
        end = self.end_adhesion()
422 423
        if not end:
            return False
424
        elif end < timezone.now():
425 426 427 428
            return False
        else:
            return True

Gabriel Detraz's avatar
Gabriel Detraz committed
429 430 431 432 433 434
    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
435
        elif end < timezone.now():
Gabriel Detraz's avatar
Gabriel Detraz committed
436 437 438 439
            return False
        else:
            return self.is_adherent()

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

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

    def is_ban(self):
        """ Renvoie si un user est banni ou non """
456
        end = self.end_ban()
457 458
        if not end:
            return False
459
        elif end < timezone.now():
460 461 462 463 464 465
            return False
        else:
            return True

    def is_whitelisted(self):
        """ Renvoie si un user est whitelisté ou non """
466
        end = self.end_whitelist()
467 468
        if not end:
            return False
469
        elif end < timezone.now():
470 471 472 473 474 475
            return False
        else:
            return True

    def has_access(self):
        """ Renvoie si un utilisateur a accès à internet """
Maël Kervella's avatar
Maël Kervella committed
476 477
        return (self.state == User.STATE_ACTIVE and
                not self.is_ban() and
478 479
                (self.is_connected() or self.is_whitelisted())) \
                or self == AssoOption.get_cached_value('utilisateur_asso')
480

481 482
    def end_access(self):
        """ Renvoie la date de fin normale d'accès (adhésion ou whiteliste)"""
Gabriel Detraz's avatar
Gabriel Detraz committed
483
        if not self.end_connexion():
484
            if not self.end_whitelist():
485 486
                return None
            else:
487
                return self.end_whitelist()
488
        else:
489
            if not self.end_whitelist():
Gabriel Detraz's avatar
Gabriel Detraz committed
490
                return self.end_connexion()
chirac's avatar
chirac committed
491
            else:
492
                return max(self.end_connexion(), self.end_whitelist())
493

chibrac's avatar
chibrac committed
494 495
    @cached_property
    def solde(self):
496
        """ Renvoie le solde d'un user.
chirac's avatar
chirac committed
497
        Somme les crédits de solde et retire les débit payés par solde"""
498
        solde_objects = Paiement.objects.filter(is_balance=True)
499 500 501 502 503 504 505 506 507
        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'),
edpibu's avatar
edpibu committed
508
                output_field=models.DecimalField()
509 510 511 512 513 514 515 516
            )
        )['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'),
edpibu's avatar
edpibu committed
517
                output_field=models.DecimalField()
518 519 520
            )
        )['total'] or 0
        return somme_credit - somme_debit
chibrac's avatar
chibrac committed
521

chirac's avatar
chirac committed
522
    def user_interfaces(self, active=True):
chirac's avatar
chirac committed
523 524 525 526 527
        """ 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')
528

529 530 531 532 533 534 535
    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()
Laouen Fernet's avatar
Laouen Fernet committed
536
                    reversion.set_comment(_("IPv4 assigning"))
537 538 539
                    interface.save()

    def unassign_ips(self):
chirac's avatar
chirac committed
540
        """ Désassigne les ipv4 aux machines de l'user"""
541 542 543 544
        interfaces = self.user_interfaces()
        for interface in interfaces:
            with transaction.atomic(), reversion.create_revision():
                interface.unassign_ipv4()
Laouen Fernet's avatar
Laouen Fernet committed
545
                reversion.set_comment(_("IPv4 unassigning"))
546 547
                interface.save()

548 549 550 551 552 553
    def disable_email(self):
        """Disable email account and redirection"""
        self.email = ""
        self.local_email_enabled = False
        self.local_email_redirect = False

554
    def archive(self):
chirac's avatar
chirac committed
555
        """ Filling the user; no more active"""
556
        self.unassign_ips()
557
        self.disable_email()
558 559

    def unarchive(self):
chirac's avatar
chirac committed
560
        """Unfilling the user"""
561
        self.assign_ips()
Gabriel Detraz's avatar
Gabriel Detraz committed
562 563 564 565 566 567 568

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

Maël Kervella's avatar
Maël Kervella committed
570 571
    def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True,
                  group_refresh=False):
chirac's avatar
chirac committed
572 573 574 575 576 577
        """ 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
578
        mac_refresh : synchronise les machines de l'user
579
        group_refresh : synchronise les group de l'user
580
        Si l'instance n'existe pas, on crée le ldapuser correspondant"""
581 582
        if sys.version_info[0] >= 3 and self.state != self.STATE_ARCHIVE and\
           self.state != self.STATE_DISABLED:
583 584 585 586
            self.refresh_from_db()
            try:
                user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
            except LdapUser.DoesNotExist:
Gabriel Detraz's avatar
Gabriel Detraz committed
587 588 589 590 591 592 593
                #  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
594 595 596 597
            if base:
                user_ldap.name = self.pseudo
                user_ldap.sn = self.pseudo
                user_ldap.dialupAccess = str(self.has_access())
598
                user_ldap.home_directory = self.home_directory
599 600 601 602 603 604
                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
605 606
                    user_ldap.user_password = self.password[:6] + \
                        self.password[7:]
607 608
                elif '{crypt}' in self.password:
                    # depending on the length, we need to remove or not a $
609
                    if len(self.password) == 41:
610
                        user_ldap.user_password = self.password
611
                    else:
612 613
                        user_ldap.user_password = self.password[:7] + \
                            self.password[8:]
614 615 616 617 618 619 620 621

                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:
622
                user_ldap.macs = [str(mac) for mac in Interface.objects.filter(
623
                    machine__user=self
624
                ).values_list('mac_address', flat=True).distinct()]
625 626 627 628 629 630 631 632
            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()
633 634

    def ldap_del(self):
chirac's avatar
chirac committed
635
        """ Supprime la version ldap de l'user"""
636 637 638 639 640 641
        try:
            user_ldap = LdapUser.objects.get(name=self.pseudo)
            user_ldap.delete()
        except LdapUser.DoesNotExist:
            pass

642 643
    def notif_inscription(self):
        """ Prend en argument un objet user, envoie un mail de bienvenue """
chirac's avatar
chirac committed
644 645 646 647
        template = loader.get_template('users/email_welcome')
        mailmessageoptions, _created = MailMessageOption\
            .objects.get_or_create()
        context = Context({
648
            'nom': self.get_full_name(),
649 650
            'asso_name': AssoOption.get_cached_value('name'),
            'asso_email': AssoOption.get_cached_value('contact'),
chirac's avatar
chirac committed
651 652 653
            'welcome_mail_fr': mailmessageoptions.welcome_mail_fr,
            'welcome_mail_en': mailmessageoptions.welcome_mail_en,
            'pseudo': self.pseudo,
654
        })
Gabriel Detraz's avatar
Gabriel Detraz committed
655 656 657
        send_mail(
            'Bienvenue au %(name)s / Welcome to %(name)s' % {
                'name': AssoOption.get_cached_value('name')
658
            },
Gabriel Detraz's avatar
Gabriel Detraz committed
659 660 661 662 663
            '',
            GeneralOption.get_cached_value('email_from'),
            [self.email],
            html_message=template.render(context)
        )
664 665 666
        return

    def reset_passwd_mail(self, request):
chirac's avatar
chirac committed
667 668
        """ Prend en argument un request, envoie un mail de
        réinitialisation de mot de pass """
669 670 671 672
        req = Request()
        req.type = Request.PASSWD
        req.user = self
        req.save()
chirac's avatar
chirac committed
673 674
        template = loader.get_template('users/email_passwd_request')
        context = {
675
            'name': req.user.get_full_name(),
676 677
            'asso': AssoOption.get_cached_value('name'),
            'asso_mail': AssoOption.get_cached_value('contact'),
678
            'site_name': GeneralOption.get_cached_value('site_name'),
679
            'url': request.build_absolute_uri(
Maël Kervella's avatar
Maël Kervella committed
680 681 682 683 684 685
                reverse('users:process', kwargs={'token': req.token})
            ),
            'expire_in': str(
                GeneralOption.get_cached_value('req_expire_hrs')
            ) + ' heures',
        }
Gabriel Detraz's avatar
Gabriel Detraz committed
686
        send_mail(
Maël Kervella's avatar
Maël Kervella committed
687 688
            'Changement de mot de passe du %(name)s / Password renewal for '
            '%(name)s' % {'name': AssoOption.get_cached_value('name')},
Gabriel Detraz's avatar
Gabriel Detraz committed
689 690 691 692 693
            template.render(context),
            GeneralOption.get_cached_value('email_from'),
            [req.user.email],
            fail_silently=False
        )
694 695
        return

696
    def autoregister_machine(self, mac_address, nas_type):
chirac's avatar
chirac committed
697 698
        """ Fonction appellée par freeradius. Enregistre la mac pour
        une machine inconnue sur le compte de l'user"""
detraz's avatar
detraz committed
699 700
        allowed, _message = Machine.can_create(self, self.id)
        if not allowed:
Laouen Fernet's avatar
Laouen Fernet committed
701
            return False, _("Maximum number of registered machines reached.")
702
        if not nas_type:
Laouen Fernet's avatar
Laouen Fernet committed
703
            return False, _("Re2o doesn't know wich machine type to assign.")
704
        machine_type_cible = nas_type.machine_type
705 706 707 708 709
        try:
            machine_parent = Machine()
            machine_parent.user = self
            interface_cible = Interface()
            interface_cible.mac_address = mac_address
710
            interface_cible.type = machine_type_cible
711 712 713
            interface_cible.clean()
            machine_parent.clean()
            domain = Domain()
714
            domain.name = self.get_next_domain_name()
715 716
            domain.interface_parent = interface_cible
            domain.clean()
717 718 719 720 721 722
            machine_parent.save()
            interface_cible.machine = machine_parent
            interface_cible.save()
            domain.interface_parent = interface_cible
            domain.clean()
            domain.save()
723
            self.notif_auto_newmachine(interface_cible)
chirac's avatar
chirac committed
724
        except Exception as error:
725
            return False,  traceback.format_exc()
726
        return interface_cible, "Ok"
727

728 729 730 731 732 733
    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(),
Maël Kervella's avatar
Maël Kervella committed
734
            'mac_address': interface.mac_address,
735
            'asso_name': AssoOption.get_cached_value('name'),
Maël Kervella's avatar
Maël Kervella committed
736
            'interface_name': interface.domain,
737
            'asso_email': AssoOption.get_cached_value('contact'),
738 739 740 741 742
            'pseudo': self.pseudo,
        })
        send_mail(
            "Ajout automatique d'une machine / New machine autoregistered",
            '',
743
            GeneralOption.get_cached_value('email_from'),
744 745 746 747 748
            [self.email],
            html_message=template.render(context)
        )
        return

749
    def set_password(self, password):
chirac's avatar
chirac committed
750
        """ A utiliser de préférence, set le password en hash courrant et
chirac's avatar
chirac committed
751
        dans la version ntlm"""
752
        super().set_password(password)
753 754 755
        self.pwd_ntlm = hashNT(password)
        return

756
    @cached_property
757
    def email_address(self):
758 759
        if (OptionalUser.get_cached_value('local_email_accounts_enabled')
                and self.local_email_enabled):
760 761
            return self.emailaddress_set.all()
        return EMailAddress.objects.none()
762

763 764 765
    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
766 767 768

        Recherche un nom disponible, pour une machine. Doit-être
        unique, concatène le nom, le pseudo et le numero de machine
769 770 771
        """

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

chirac's avatar
chirac committed
775 776 777
        def composed_pseudo(name):
            """Renvoie le resultat de simplepseudo et rajoute le nom"""
            return simple_pseudo() + str(name)
778 779

        num = 0
chirac's avatar
chirac committed
780
        while Domain.objects.filter(name=composed_pseudo(num)):
781 782 783
            num += 1
        return composed_pseudo(num)

Maël Kervella's avatar
Maël Kervella committed
784 785
    def can_edit(self, user_request, *_args, **_kwargs):
        """Check if a user can edit a user object.
786 787 788 789

        :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
Maël Kervella's avatar
Maël Kervella committed
790 791
            user_request one of its member, or if user_request is self, or if
            user_request has the 'cableur' right.
792
        """
793
        if self.is_class_club and user_request.is_class_adherent:
Maël Kervella's avatar
Maël Kervella committed
794 795 796
            if (self == user_request or
                    user_request.has_perm('users.change_user') or
                    user_request.adherent in self.club.administrators.all()):
797 798
                return True, None
            else:
Laouen Fernet's avatar
Laouen Fernet committed
799
                return False, _("You don't have the right to edit this club.")
800
        else:
801 802 803 804 805 806
            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):
Laouen Fernet's avatar
Laouen Fernet committed
807 808
                    return False, (_("User with critical rights, can't be"
                                     " edited."))
809
                elif self == AssoOption.get_cached_value('utilisateur_asso'):
Laouen Fernet's avatar
Laouen Fernet committed
810 811 812
                    return False, (_("Impossible to edit the organisation's"
                                     " user without the 'change_all_users'"
                                     " right."))
813 814 815
                else:
                    return True, None
            elif user_request.has_perm('users.change_all_users'):
816 817
                return True, None
            else:
Laouen Fernet's avatar
Laouen Fernet committed
818 819
                return False, (_("You don't have the right to edit another"
                                 " user."))
820

Maël Kervella's avatar
Maël Kervella committed
821 822 823 824 825 826 827 828 829
    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
        """
830
        if self.is_class_club and user_request.is_class_adherent:
Maël Kervella's avatar
Maël Kervella committed
831 832 833
            if (self == user_request or
                    user_request.has_perm('users.change_user_password') or
                    user_request.adherent in self.club.administrators.all()):
834 835
                return True, None
            else:
Laouen Fernet's avatar
Laouen Fernet committed
836
                return False, _("You don't have the right to edit this club.")
837
        else:
Maël Kervella's avatar
Maël Kervella committed
838 839 840 841
            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
842
                return True, None
Maël Kervella's avatar
Maël Kervella committed
843 844
            elif (user_request.has_perm('users.change_user') and
                  not self.groups.all()):
845 846
                return True, None
            else:
Laouen Fernet's avatar
Laouen Fernet committed
847 848
                return False, (_("You don't have the right to edit another"
                                 " user."))
849

Maël Kervella's avatar
Maël Kervella committed
850 851 852 853
    def check_selfpasswd(self, user_request, *_args, **_kwargs):
        """ Returns (True, None) if user_request is self, else returns
        (False, None)
        """