models.py 64.4 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
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-Z][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(
Laouen Fernet's avatar
Laouen Fernet committed
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

Gabriel Detraz's avatar
Gabriel Detraz committed
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]
Gabriel Detraz's avatar
Gabriel Detraz committed
121 122
    return min(free_uids)

chirac's avatar
chirac committed
123

Gabriel Detraz's avatar
Gabriel Detraz committed
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]
Gabriel Detraz's avatar
Gabriel Detraz committed
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:
Laouen Fernet's avatar
Laouen Fernet committed
147
            raise ValueError(_("Users must have an username."))
148 149

        if not linux_user_check(pseudo):
Laouen Fernet's avatar
Laouen Fernet committed
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:
Maël Kervella's avatar
Maël Kervella committed
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

Maël Kervella's avatar
Maël Kervella committed
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"""
Laouen Fernet's avatar
Laouen Fernet committed
185

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

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

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

    objects = UserManager()

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

272 273 274 275 276 277 278 279
    @cached_property
    def name(self):
        """Si il s'agit d'un adhérent, on renvoie le prénom"""
        if self.is_class_adherent:
            return self.adherent.name
        else:
            return ''

280 281 282 283 284 285 286 287
    @cached_property
    def room(self):
        """Alias vers room """
        if self.is_class_adherent:
            return self.adherent.room
        elif self.is_class_club:
            return self.club.room
        else:
Laouen Fernet's avatar
Laouen Fernet committed
288
            raise NotImplementedError(_("Unknown type."))
289

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

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

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

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

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

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

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

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

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

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

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

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

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

Gabriel Detraz's avatar
Gabriel Detraz committed
371 372 373 374 375 376
    @property
    def get_shell(self):
        """ A utiliser de préférence, prend le shell par défaut
        si il n'est pas défini"""
        return self.shell or OptionalUser.get_cached_value('shell_default')

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

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

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

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

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

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

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

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

    def is_whitelisted(self):
        """ Renvoie si un user est whitelisté ou non """
465
        end = self.end_whitelist()
466 467
        if not end:
            return False
468
        elif end < timezone.now():
469 470 471 472 473 474
            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
475 476 477
        return (self.state == User.STATE_ACTIVE and
                not self.is_ban() and
                (self.is_connected() or self.is_whitelisted()))
478

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

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

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

527 528 529 530 531 532 533
    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
534
                    reversion.set_comment(_("IPv4 assigning"))
535 536 537
                    interface.save()

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

    def archive(self):
chirac's avatar
chirac committed
547
        """ Filling the user; no more active"""
548 549 550
        self.unassign_ips()

    def unarchive(self):
chirac's avatar
chirac committed
551
        """Unfilling the user"""
552
        self.assign_ips()
Gabriel Detraz's avatar
Gabriel Detraz committed
553 554 555 556 557 558 559

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

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

                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:
612
                user_ldap.macs = sorted([str(mac) for mac in Interface.objects.filter(
613
                    machine__user=self
614
                ).values_list('mac_address', flat=True).distinct()])
615 616 617 618 619 620 621 622
            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()
623 624

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

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

    def reset_passwd_mail(self, request):
chirac's avatar
chirac committed
657 658
        """ Prend en argument un request, envoie un mail de
        réinitialisation de mot de pass """
659 660 661 662
        req = Request()
        req.type = Request.PASSWD
        req.user = self
        req.save()
chirac's avatar
chirac committed
663 664
        template = loader.get_template('users/email_passwd_request')
        context = {
665
            'name': req.user.get_full_name(),
666 667
            'asso': AssoOption.get_cached_value('name'),
            'asso_mail': AssoOption.get_cached_value('contact'),
668
            'site_name': GeneralOption.get_cached_value('site_name'),
669
            'url': request.build_absolute_uri(
Maël Kervella's avatar
Maël Kervella committed
670 671 672 673 674 675
                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
676
        send_mail(
Maël Kervella's avatar
Maël Kervella committed
677 678
            '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
679 680 681 682 683
            template.render(context),
            GeneralOption.get_cached_value('email_from'),
            [req.user.email],
            fail_silently=False
        )
684 685
        return

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

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

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

748
    @cached_property
749
    def email_address(self):
750 751
        if (OptionalUser.get_cached_value('local_email_accounts_enabled')
                and self.local_email_enabled):
752 753
            return self.emailaddress_set.all()
        return EMailAddress.objects.none()
754

755 756 757
    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
758 759 760

        Recherche un nom disponible, pour une machine. Doit-être
        unique, concatène le nom, le pseudo et le numero de machine
761 762 763
        """

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

chirac's avatar
chirac committed
767 768 769
        def composed_pseudo(name):
            """Renvoie le resultat de simplepseudo et rajoute le nom"""
            return simple_pseudo() + str(name)
770 771

        num = 0
chirac's avatar
chirac committed
772
        while Domain.objects.filter(name=composed_pseudo(num)):
773 774 775
            num += 1
        return composed_pseudo(num)

Maël Kervella's avatar
Maël Kervella committed
776 777
    def can_edit(self, user_request, *_args, **_kwargs):
        """Check if a user can edit a user object.
778 779 780 781

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

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

Maël Kervella's avatar
Maël Kervella committed
842 843 844 845
    def check_selfpasswd(self, user_request, *_args, **_kwargs):
        """ Returns (True, None) if user_request is self, else returns
        (False, None)
        """
846 847
        return user_request == self, None

848
    def can_change_room(self, user_request, *_args, **_kwargs):
849 850 851 852 853 854 855 856 857 858 859 860
        """ Check if a user can change a room

        :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
        """
        if not ((self.pk == user_request.pk and OptionalUser.get_cached_value('self_change_room'))
            or user_request.has_perm('users.change_user')):
            return False, _("Permission required to change the room.")
        else:
            return True, None

861
    @staticmethod
Maël Kervella's avatar
Maël Kervella committed
862 863 864 865 866 867 868
    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
        """
Maël Kervella's avatar
Maël Kervella committed
869 870
        return (
            user_request.has_perm('users.change_user_state'),
Laouen Fernet's avatar
Laouen Fernet committed
871
            _("Permission required to change the state.")
Maël Kervella's avatar
Maël Kervella committed
872
        )
873

Gabriel Detraz's avatar
Gabriel Detraz committed
874
    def can_change_shell(self, user_request, *_args, **_kwargs):
Maël Kervella's avatar
Maël Kervella committed
875 876 877 878 879 880
        """ 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
        """
881
        if not ((self.pk == user_request.pk and OptionalUser.get_cached_value('self_change_shell'))
Gabriel Detraz's avatar
Gabriel Detraz committed
882
            or user_request.has_perm('users.change_user_shell')):
Laouen Fernet's avatar
Laouen Fernet committed
883
            return False, _("Permission required to change the shell.")
Gabriel Detraz's avatar
Gabriel Detraz committed
884 885
        else:
            return True, None
886

887
    @staticmethod
888 889
    def can_change_local_email_redirect(user_request, *_args, **_kwargs):
        """ Check if a user can change local_email_redirect.
890 891 892 893 894 895

        :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 (
896
            OptionalUser.get_cached_value('local_email_accounts_enabled'),
Laouen Fernet's avatar
Laouen Fernet committed
897
            _("Local email accounts must be enabled.")
898 899 900
        )

    @staticmethod
901
    def can_change_local_email_enabled(user_request, *_args, **_kwargs):
Laouen Fernet's avatar
Laouen Fernet committed
902
        """ Check if a user can change internal address.
903 904 905 906 907 908

        :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 (
909
            OptionalUser.get_cached_value('local_email_accounts_enabled'),
Laouen Fernet's avatar
Laouen Fernet committed
910
            _("Local email accounts must be enabled.")
911
        )
912

913
    @staticmethod
Maël Kervella's avatar
Maël Kervella committed
914 915 916 917 918 919 920
    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
        """
Maël Kervella's avatar
Maël Kervella committed
921 922
        return (
            user_request.has_perm('users.change_user_force'),
Laouen Fernet's avatar
Laouen Fernet committed
923
            _("Permission required to force the move.")
Maël Kervella's avatar
Maël Kervella committed
924
        )
925 926

    @staticmethod
Maël Kervella's avatar
Maël Kervella committed
927 928 929 930 931 932 933
    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
        """
Maël Kervella's avatar
Maël Kervella committed
934 935
        return (
            user_request.has_perm('users.change_user_groups'),
Laouen Fernet's avatar
Laouen Fernet committed
936
            _("Permission required to edit the user's groups of rights.")
Maël Kervella's avatar
Maël Kervella committed
937
        )
938

Hugo LEVY-FALK's avatar
ACL  
Hugo LEVY-FALK committed
939 940 941 942 943 944 945 946 947
    @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,