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 59.6 KB
Newer Older
1
# -*- mode: python; coding: utf-8 -*-
2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22
# 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.
#
# 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.
23 24 25
"""machines.models
The models definitions for the Machines app
"""
26

27 28
from __future__ import unicode_literals

Gabriel Detraz's avatar
Gabriel Detraz committed
29 30
from datetime import timedelta
import re
31
from ipaddress import IPv6Address
32
from itertools import chain
33
from netaddr import mac_bare, EUI, IPSet, IPRange, IPNetwork, IPAddress
Gabriel Detraz's avatar
Gabriel Detraz committed
34

35
from django.db import models
Gabriel Detraz's avatar
Gabriel Detraz committed
36
from django.db.models.signals import post_save, post_delete
37
from django.dispatch import receiver
38
from django.forms import ValidationError
39
from django.utils.functional import cached_property
40
from django.utils import timezone
Gabriel Detraz's avatar
Gabriel Detraz committed
41 42
from django.core.validators import MaxValueValidator

43
from macaddress.fields import MACAddressField
44

45
from re2o.field_permissions import FieldPermissionModelMixin
46
from re2o.mixins import AclMixin, RevMixin
47

Maël Kervella's avatar
Maël Kervella committed
48 49 50
import users.models
import preferences.models

51

52
class Machine(RevMixin, FieldPermissionModelMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
53 54
    """ Class définissant une machine, object parent user, objets fils
    interfaces"""
55
    PRETTY_NAME = "Machine"
Gabriel Detraz's avatar
Gabriel Detraz committed
56

57
    user = models.ForeignKey('users.User', on_delete=models.PROTECT)
Gabriel Detraz's avatar
Gabriel Detraz committed
58 59 60 61 62 63
    name = models.CharField(
        max_length=255,
        help_text="Optionnel",
        blank=True,
        null=True
    )
64
    active = models.BooleanField(default=True)
65

66 67 68
    class Meta:
        permissions = (
            ("view_machine", "Peut voir un objet machine quelquonque"),
69 70
            ("change_machine_user",
             "Peut changer le propriétaire d'une machine"),
71 72
        )

73 74
    @classmethod
    def get_instance(cls, machineid, *_args, **_kwargs):
75 76 77 78
        """Get the Machine instance with machineid.
        :param userid: The id
        :return: The user
        """
79
        return cls.objects.get(pk=machineid)
80

81 82 83
    def linked_objects(self):
        """Return linked objects : machine and domain.
        Usefull in history display"""
84 85 86 87 88 89
        return chain(
            self.interface_set.all(),
            Domain.objects.filter(
                interface_parent__in=self.interface_set.all()
            )
        )
90

91
    @staticmethod
92
    def can_change_user(user_request, *_args, **_kwargs):
93 94 95 96 97 98 99 100 101 102
        """Checks if an user is allowed to change the user who owns a
        Machine.

        Args:
            user_request: The user requesting to change owner.

        Returns:
            A tuple with a boolean stating if edition is allowed and an
            explanation message.
        """
103 104
        return (user_request.has_perm('machines.change_machine_user'),
                "Vous ne pouvez pas modifier l'utilisateur de la machine.")
105

106 107
    @staticmethod
    def can_view_all(user_request, *_args, **_kwargs):
108 109 110 111 112
        """Vérifie qu'on peut bien afficher l'ensemble des machines,
        droit particulier correspondant
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
        if not user_request.has_perm('machines.view_machine'):
113 114
            return False, (u"Vous ne pouvez pas afficher l'ensemble des "
                           "machines sans permission")
115 116
        return True, None

117 118
    @staticmethod
    def can_create(user_request, userid, *_args, **_kwargs):
119 120 121 122 123
        """Vérifie qu'un user qui fait la requète peut bien créer la machine
        et n'a pas atteint son quota, et crée bien une machine à lui
        :param user_request: Utilisateur qui fait la requête
        :param userid: id de l'user dont on va créer une machine
        :return: soit True, soit False avec la raison de l'échec"""
Maël Kervella's avatar
Maël Kervella committed
124
        try:
125
            user = users.models.User.objects.get(pk=userid)
Maël Kervella's avatar
Maël Kervella committed
126 127
        except users.models.User.DoesNotExist:
            return False, u"Utilisateur inexistant"
128 129 130 131
        max_lambdauser_interfaces = (preferences.models.OptionalMachine
                                     .get_cached_value(
                                         'max_lambdauser_interfaces'
                                     ))
132
        if not user_request.has_perm('machines.add_machine'):
133 134
            if not (preferences.models.OptionalMachine
                    .get_cached_value('create_machine')):
135
                return False, u"Vous ne pouvez pas ajouter une machine"
Maël Kervella's avatar
Maël Kervella committed
136
            if user != user_request:
137 138
                return False, (u"Vous ne pouvez pas ajouter une machine à un "
                               "autre user que vous sans droit")
Maël Kervella's avatar
Maël Kervella committed
139
            if user.user_interfaces().count() >= max_lambdauser_interfaces:
140 141 142
                return False, (u"Vous avez atteint le maximum d'interfaces "
                               "autorisées que vous pouvez créer vous même "
                               "(%s) " % max_lambdauser_interfaces)
Maël Kervella's avatar
Maël Kervella committed
143 144
        return True, None

145
    def can_edit(self, user_request, *args, **kwargs):
146 147 148 149 150
        """Vérifie qu'on peut bien éditer cette instance particulière (soit
        machine de soi, soit droit particulier
        :param self: instance machine à éditer
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison le cas échéant"""
151
        if self.user != user_request:
152 153 154 155 156 157 158 159 160
            if (not user_request.has_perm('machines.change_interface') or
                    not self.user.can_edit(
                        self.user,
                        user_request,
                        *args,
                        **kwargs
                    )[0]):
                return False, (u"Vous ne pouvez pas éditer une machine "
                               "d'un autre user que vous sans droit")
161 162
        return True, None

163
    def can_delete(self, user_request, *args, **kwargs):
164 165 166 167 168
        """Vérifie qu'on peut bien supprimer cette instance particulière (soit
        machine de soi, soit droit particulier
        :param self: instance machine à supprimer
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
169
        if self.user != user_request:
170 171 172 173 174 175 176 177 178
            if (not user_request.has_perm('machines.change_interface') or
                    not self.user.can_edit(
                        self.user,
                        user_request,
                        *args,
                        **kwargs
                    )[0]):
                return False, (u"Vous ne pouvez pas éditer une machine "
                               "d'un autre user que vous sans droit")
179 180
        return True, None

181
    def can_view(self, user_request, *_args, **_kwargs):
182 183 184 185 186
        """Vérifie qu'on peut bien voir cette instance particulière (soit
        machine de soi, soit droit particulier
        :param self: instance machine à éditer
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
187 188 189 190
        if (not user_request.has_perm('machines.view_machine') and
                self.user != user_request):
            return False, (u"Vous n'avez pas droit de voir les machines autre "
                           "que les vôtres")
191
        return True, None
192

193 194 195
    def __init__(self, *args, **kwargs):
        super(Machine, self).__init__(*args, **kwargs)
        self.field_permissions = {
196
            'user': self.can_change_user,
197 198
        }

199
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
200 201
        return str(self.user) + ' - ' + str(self.id) + ' - ' + str(self.name)

202

203
class MachineType(RevMixin, AclMixin, models.Model):
204
    """ Type de machine, relié à un type d'ip, affecté aux interfaces"""
205 206
    PRETTY_NAME = "Type de machine"

207
    type = models.CharField(max_length=255)
Gabriel Detraz's avatar
Gabriel Detraz committed
208 209 210 211 212 213
    ip_type = models.ForeignKey(
        'IpType',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
214

215 216 217
    class Meta:
        permissions = (
            ("view_machinetype", "Peut voir un objet machinetype"),
218 219
            ("use_all_machinetype",
             "Peut utiliser n'importe quel type de machine"),
220 221
        )

222
    def all_interfaces(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
223 224
        """ Renvoie toutes les interfaces (cartes réseaux) de type
        machinetype"""
225 226
        return Interface.objects.filter(type=self)

227 228
    @staticmethod
    def can_use_all(user_request, *_args, **_kwargs):
229 230 231 232 233 234 235 236
        """Check if an user can use every MachineType.

        Args:
            user_request: The user requesting edition.
        Returns:
            A tuple with a boolean stating if user can acces and an explanation
            message is acces is not allowed.
        """
237
        if not user_request.has_perm('machines.use_all_machinetype'):
238 239
            return False, (u"Vous n'avez pas le droit d'utiliser tout types "
                           "de machines")
240 241
        return True, None

242
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
243 244
        return self.type

245

246
class IpType(RevMixin, AclMixin, models.Model):
247
    """ Type d'ip, définissant un range d'ip, affecté aux machine types"""
248 249
    PRETTY_NAME = "Type d'ip"

250
    type = models.CharField(max_length=255)
251
    extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
252
    need_infra = models.BooleanField(default=False)
253 254
    domaine_ip_start = models.GenericIPAddressField(protocol='IPv4')
    domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4')
Gabriel Detraz's avatar
Gabriel Detraz committed
255 256 257 258 259 260 261 262 263 264 265 266 267 268 269 270
    prefix_v6 = models.GenericIPAddressField(
        protocol='IPv6',
        null=True,
        blank=True
    )
    vlan = models.ForeignKey(
        'Vlan',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
    ouverture_ports = models.ForeignKey(
        'OuverturePortList',
        blank=True,
        null=True
    )
271

272 273 274 275 276 277
    class Meta:
        permissions = (
            ("view_iptype", "Peut voir un objet iptype"),
            ("use_all_iptype", "Peut utiliser tous les iptype"),
        )

278
    @cached_property
279
    def ip_range(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
280 281
        """ Renvoie un objet IPRange à partir de l'objet IpType"""
        return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop)
282 283 284

    @cached_property
    def ip_set(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
285
        """ Renvoie une IPSet à partir de l'iptype"""
286
        return IPSet(self.ip_range)
287 288 289

    @cached_property
    def ip_set_as_str(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
290
        """ Renvoie une liste des ip en string"""
291 292 293
        return [str(x) for x in self.ip_set]

    def ip_objects(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
294
        """ Renvoie tous les objets ipv4 relié à ce type"""
295 296 297
        return IpList.objects.filter(ip_type=self)

    def free_ip(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
298
        """ Renvoie toutes les ip libres associées au type donné (self)"""
Gabriel Detraz's avatar
Gabriel Detraz committed
299 300 301
        return IpList.objects.filter(
            interface__isnull=True
        ).filter(ip_type=self)
302 303

    def gen_ip_range(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
304 305 306
        """ Cree les IpList associées au type self. Parcours pédestrement et
        crée les ip une par une. Si elles existent déjà, met à jour le type
        associé à l'ip"""
307
        # Creation du range d'ip dans les objets iplist
308 309 310 311
        networks = []
        for net in self.ip_range.cidrs():
            networks += net.iter_hosts()
        ip_obj = [IpList(ip_type=self, ipv4=str(ip)) for ip in networks]
Gabriel Detraz's avatar
Gabriel Detraz committed
312 313 314
        listes_ip = IpList.objects.filter(
            ipv4__in=[str(ip) for ip in networks]
        )
315 316 317 318 319 320
        # Si il n'y a pas d'ip, on les crée
        if not listes_ip:
            IpList.objects.bulk_create(ip_obj)
        # Sinon on update l'ip_type
        else:
            listes_ip.update(ip_type=self)
321
        return
322 323

    def del_ip_range(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
324 325
        """ Methode dépréciée, IpList est en mode cascade et supprimé
        automatiquement"""
326
        if Interface.objects.filter(ipv4__in=self.ip_objects()):
Gabriel Detraz's avatar
Gabriel Detraz committed
327 328
            raise ValidationError("Une ou plusieurs ip du range sont\
            affectées, impossible de supprimer le range")
329 330 331
        for ip in self.ip_objects():
            ip.delete()

332 333 334 335 336
    def check_replace_prefixv6(self):
        """Remplace les prefixv6 des interfaces liées à ce type d'ip"""
        if not self.prefix_v6:
            return
        else:
337
            for ipv6 in Ipv6List.objects.filter(
338 339 340 341
                    interface__in=Interface.objects.filter(
                        type__in=MachineType.objects.filter(ip_type=self)
                    )
                ):
342 343
                ipv6.check_and_replace_prefix(prefix=self.prefix_v6)

344
    def clean(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
345 346 347 348 349
        """ Nettoyage. Vérifie :
        - Que ip_stop est après ip_start
        - Qu'on ne crée pas plus gros qu'un /16
        - Que le range crée ne recoupe pas un range existant
        - Formate l'ipv6 donnée en /64"""
350 351 352 353
        if IPAddress(self.domaine_ip_start) > IPAddress(self.domaine_ip_stop):
            raise ValidationError("Domaine end doit être après start...")
        # On ne crée pas plus grand qu'un /16
        if self.ip_range.size > 65536:
Gabriel Detraz's avatar
Gabriel Detraz committed
354 355
            raise ValidationError("Le range est trop gros, vous ne devez\
            pas créer plus grand qu'un /16")
356
        # On check que les / ne se recoupent pas
357
        for element in IpType.objects.all().exclude(pk=self.pk):
358
            if not self.ip_set.isdisjoint(element.ip_set):
Gabriel Detraz's avatar
Gabriel Detraz committed
359 360
                raise ValidationError("Le range indiqué n'est pas disjoint\
                des ranges existants")
361 362 363
        # On formate le prefix v6
        if self.prefix_v6:
            self.prefix_v6 = str(IPNetwork(self.prefix_v6 + '/64').network)
364 365 366 367 368 369
        return

    def save(self, *args, **kwargs):
        self.clean()
        super(IpType, self).save(*args, **kwargs)

370 371
    @staticmethod
    def can_use_all(user_request, *_args, **_kwargs):
372 373
        """Superdroit qui permet d'utiliser toutes les extensions sans
        restrictions
374 375
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
376
        return user_request.has_perm('machines.use_all_iptype'), None
377

378 379
    def __str__(self):
        return self.type
chirac's avatar
chirac committed
380

Gabriel Detraz's avatar
Gabriel Detraz committed
381

382
class Vlan(RevMixin, AclMixin, models.Model):
383 384
    """ Un vlan : vlan_id et nom
    On limite le vlan id entre 0 et 4096, comme défini par la norme"""
385 386
    PRETTY_NAME = "Vlans"

387
    vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)])
388 389 390
    name = models.CharField(max_length=256)
    comment = models.CharField(max_length=256, blank=True)

391 392 393 394 395
    class Meta:
        permissions = (
            ("view_vlan", "Peut voir un objet vlan"),
        )

396 397 398
    def __str__(self):
        return self.name

Gabriel Detraz's avatar
Gabriel Detraz committed
399

400
class Nas(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
401
    """ Les nas. Associé à un machine_type.
Gabriel Detraz's avatar
Gabriel Detraz committed
402 403
    Permet aussi de régler le port_access_mode (802.1X ou mac-address) pour
    le radius. Champ autocapture de la mac à true ou false"""
404 405
    PRETTY_NAME = "Correspondance entre les nas et les machines connectées"

406 407 408 409 410 411
    default_mode = '802.1X'
    AUTH = (
        ('802.1X', '802.1X'),
        ('Mac-address', 'Mac-address'),
    )

412
    name = models.CharField(max_length=255, unique=True)
Gabriel Detraz's avatar
Gabriel Detraz committed
413 414 415 416 417 418 419 420 421 422 423 424 425 426 427
    nas_type = models.ForeignKey(
        'MachineType',
        on_delete=models.PROTECT,
        related_name='nas_type'
    )
    machine_type = models.ForeignKey(
        'MachineType',
        on_delete=models.PROTECT,
        related_name='machinetype_on_nas'
    )
    port_access_mode = models.CharField(
        choices=AUTH,
        default=default_mode,
        max_length=32
    )
428
    autocapture_mac = models.BooleanField(default=False)
429

430 431 432 433 434
    class Meta:
        permissions = (
            ("view_nas", "Peut voir un objet Nas"),
        )

435 436 437
    def __str__(self):
        return self.name

Gabriel Detraz's avatar
Gabriel Detraz committed
438

439
class SOA(RevMixin, AclMixin, models.Model):
440 441 442 443 444 445 446
    """
    Un enregistrement SOA associé à une extension
    Les valeurs par défault viennent des recommandations RIPE :
    https://www.ripe.net/publications/docs/ripe-203
    """
    PRETTY_NAME = "Enregistrement SOA"

447
    name = models.CharField(max_length=255)
448 449 450 451
    mail = models.EmailField(
        help_text='Email du contact pour la zone'
    )
    refresh = models.PositiveIntegerField(
452
        default=86400,  # 24 hours
453 454 455 456
        help_text='Secondes avant que les DNS secondaires doivent demander le\
                   serial du DNS primaire pour détecter une modification'
    )
    retry = models.PositiveIntegerField(
457
        default=7200,  # 2 hours
458 459 460 461
        help_text='Secondes avant que les DNS secondaires fassent une nouvelle\
                   demande de serial en cas de timeout du DNS primaire'
    )
    expire = models.PositiveIntegerField(
462
        default=3600000,  # 1000 hours
463 464 465 466 467 468 469 470
        help_text='Secondes après lesquelles les DNS secondaires arrêtent de\
                   de répondre aux requêtes en cas de timeout du DNS primaire'
    )
    ttl = models.PositiveIntegerField(
        default=172800,  # 2 days
        help_text='Time To Live'
    )

471 472 473 474 475
    class Meta:
        permissions = (
            ("view_soa", "Peut voir un objet soa"),
        )

476 477 478 479 480 481 482 483 484 485 486 487 488
    def __str__(self):
        return str(self.name)

    @cached_property
    def dns_soa_param(self):
        """
        Renvoie la partie de l'enregistrement SOA correspondant aux champs :
            <refresh>   ; refresh
            <retry>     ; retry
            <expire>    ; expire
            <ttl>       ; TTL
        """
        return (
489 490 491 492
            '    {refresh}; refresh\n'
            '    {retry}; retry\n'
            '    {expire}; expire\n'
            '    {ttl}; TTL'
493
        ).format(
494 495 496 497
            refresh=str(self.refresh).ljust(12),
            retry=str(self.retry).ljust(12),
            expire=str(self.expire).ljust(12),
            ttl=str(self.ttl).ljust(12)
498 499 500 501 502 503
        )

    @cached_property
    def dns_soa_mail(self):
        """ Renvoie le mail dans l'enregistrement SOA """
        mail_fields = str(self.mail).split('@')
504
        return mail_fields[0].replace('.', '\\.') + '.' + mail_fields[1] + '.'
505 506 507 508 509 510 511

    @classmethod
    def new_default_soa(cls):
        """ Fonction pour créer un SOA par défaut, utile pour les nouvelles
        extensions .
        /!\ Ne jamais supprimer ou renommer cette fonction car elle est
        utilisée dans les migrations de la BDD. """
512 513 514 515
        return cls.objects.get_or_create(
            name="SOA to edit",
            mail="postmaser@example.com"
        )[0].pk
516 517


518
class Extension(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
519 520
    """ Extension dns type example.org. Précise si tout le monde peut
    l'utiliser, associé à un origin (ip d'origine)"""
521 522
    PRETTY_NAME = "Extensions dns"

Gabriel Detraz's avatar
Gabriel Detraz committed
523 524 525 526 527
    name = models.CharField(
        max_length=255,
        unique=True,
        help_text="Nom de la zone, doit commencer par un point (.example.org)"
    )
528
    need_infra = models.BooleanField(default=False)
529
    origin = models.ForeignKey(
Gabriel Detraz's avatar
Gabriel Detraz committed
530 531 532
        'IpList',
        on_delete=models.PROTECT,
        blank=True,
Gabriel Detraz's avatar
Gabriel Detraz committed
533 534
        null=True,
        help_text="Enregistrement A associé à la zone"
Gabriel Detraz's avatar
Gabriel Detraz committed
535
    )
536 537 538
    origin_v6 = models.GenericIPAddressField(
        protocol='IPv6',
        null=True,
Gabriel Detraz's avatar
Gabriel Detraz committed
539
        blank=True,
lhark's avatar
lhark committed
540
        help_text="Enregistrement AAAA associé à la zone"
541
    )
542 543
    soa = models.ForeignKey(
        'SOA',
544
        on_delete=models.CASCADE
545
    )
546

547 548 549 550 551 552
    class Meta:
        permissions = (
            ("view_extension", "Peut voir un objet extension"),
            ("use_all_extension", "Peut utiliser toutes les extension"),
        )

553 554
    @cached_property
    def dns_entry(self):
555 556 557
        """ Une entrée DNS A et AAAA sur origin (zone self)"""
        entry = ""
        if self.origin:
558
            entry += "@               IN  A       " + str(self.origin)
559 560 561
        if self.origin_v6:
            if entry:
                entry += "\n"
562
            entry += "@               IN  AAAA    " + str(self.origin_v6)
563
        return entry
564

565 566
    @staticmethod
    def can_use_all(user_request, *_args, **_kwargs):
567 568
        """Superdroit qui permet d'utiliser toutes les extensions sans
        restrictions
569 570
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
571
        return user_request.has_perm('machines.use_all_extension'), None
572

573 574
    def __str__(self):
        return self.name
chirac's avatar
chirac committed
575

576
    def clean(self, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
577 578 579 580
        if self.name and self.name[0] != '.':
            raise ValidationError("Une extension doit commencer par un point")
        super(Extension, self).clean(*args, **kwargs)

Gabriel Detraz's avatar
Gabriel Detraz committed
581

582
class Mx(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
583 584
    """ Entrées des MX. Enregistre la zone (extension) associée et la
    priorité
Gabriel Detraz's avatar
Gabriel Detraz committed
585
    Todo : pouvoir associer un MX à une interface """
586 587 588
    PRETTY_NAME = "Enregistrements MX"

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
589
    priority = models.PositiveIntegerField(unique=True)
chirac's avatar
chirac committed
590
    name = models.OneToOneField('Domain', on_delete=models.PROTECT)
591

592 593 594 595 596
    class Meta:
        permissions = (
            ("view_mx", "Peut voir un objet mx"),
        )

597 598
    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
599 600
        """Renvoie l'entrée DNS complète pour un MX à mettre dans les
        fichiers de zones"""
601 602
        return "@               IN  MX  {prior} {name}".format(
            prior=str(self.priority).ljust(3),
Maël Kervella's avatar
Maël Kervella committed
603
            name=str(self.name)
604
        )
605

606 607 608
    def __str__(self):
        return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name)

Gabriel Detraz's avatar
Gabriel Detraz committed
609

610
class Ns(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
611
    """Liste des enregistrements name servers par zone considéérée"""
612 613 614
    PRETTY_NAME = "Enregistrements NS"

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
615
    ns = models.ForeignKey('Domain', on_delete=models.PROTECT)
616

617 618
    class Meta:
        permissions = (
Gabriel Detraz's avatar
Gabriel Detraz committed
619
            ("view_ns", "Peut voir un objet ns"),
620 621
        )

622 623
    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
624
        """Renvoie un enregistrement NS complet pour les filezones"""
625
        return "@               IN  NS      " + str(self.ns)
626

627
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
628
        return str(self.zone) + ' ' + str(self.ns)
629

Gabriel Detraz's avatar
Gabriel Detraz committed
630

631
class Txt(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
632
    """ Un enregistrement TXT associé à une extension"""
633
    PRETTY_NAME = "Enregistrement TXT"
Gabriel Detraz's avatar
Gabriel Detraz committed
634 635 636

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
    field1 = models.CharField(max_length=255)
637
    field2 = models.TextField(max_length=2047)
Gabriel Detraz's avatar
Gabriel Detraz committed
638

639 640 641 642 643
    class Meta:
        permissions = (
            ("view_txt", "Peut voir un objet txt"),
        )

Gabriel Detraz's avatar
Gabriel Detraz committed
644
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
645 646
        return str(self.zone) + " : " + str(self.field1) + " " +\
            str(self.field2)
Gabriel Detraz's avatar
Gabriel Detraz committed
647 648 649

    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
650
        """Renvoie l'enregistrement TXT complet pour le fichier de zone"""
651
        return str(self.field1).ljust(15) + " IN  TXT     " + str(self.field2)
Gabriel Detraz's avatar
Gabriel Detraz committed
652

Gabriel Detraz's avatar
Gabriel Detraz committed
653

654
class Srv(RevMixin, AclMixin, models.Model):
655
    """ A SRV record """
Gabriel Detraz's avatar
Gabriel Detraz committed
656 657 658 659 660
    PRETTY_NAME = "Enregistrement Srv"

    TCP = 'TCP'
    UDP = 'UDP'

661
    service = models.CharField(max_length=31)
Gabriel Detraz's avatar
Gabriel Detraz committed
662 663 664 665 666 667 668 669 670 671 672 673 674 675
    protocole = models.CharField(
        max_length=3,
        choices=(
            (TCP, 'TCP'),
            (UDP, 'UDP'),
            ),
        default=TCP,
    )
    extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
    ttl = models.PositiveIntegerField(
        default=172800,  # 2 days
        help_text='Time To Live'
    )
    priority = models.PositiveIntegerField(
676
        default=0,
Gabriel Detraz's avatar
Gabriel Detraz committed
677
        validators=[MaxValueValidator(65535)],
678 679 680
        help_text=("La priorité du serveur cible (valeur entière non "
                   "négative, plus elle est faible, plus ce serveur sera "
                   "utilisé s'il est disponible)")
Gabriel Detraz's avatar
Gabriel Detraz committed
681 682
    )
    weight = models.PositiveIntegerField(
683
        default=0,
Gabriel Detraz's avatar
Gabriel Detraz committed
684 685 686 687 688 689 690 691 692 693 694 695 696 697
        validators=[MaxValueValidator(65535)],
        help_text="Poids relatif pour les enregistrements de même priorité\
            (valeur entière de 0 à 65535)"
    )
    port = models.PositiveIntegerField(
        validators=[MaxValueValidator(65535)],
        help_text="Port (tcp/udp)"
    )
    target = models.ForeignKey(
        'Domain',
        on_delete=models.PROTECT,
        help_text="Serveur cible"
    )

698 699
    class Meta:
        permissions = (
700
            ("view_srv", "Peut voir un objet srv"),
701 702
        )

Gabriel Detraz's avatar
Gabriel Detraz committed
703 704 705 706 707 708 709 710 711 712 713 714 715 716
    def __str__(self):
        return str(self.service) + ' ' + str(self.protocole) + ' ' +\
            str(self.extension) + ' ' + str(self.priority) +\
            ' ' + str(self.weight) + str(self.port) + str(self.target)

    @cached_property
    def dns_entry(self):
        """Renvoie l'enregistrement SRV complet pour le fichier de zone"""
        return str(self.service) + '._' + str(self.protocole).lower() +\
            str(self.extension) + '. ' + str(self.ttl) + ' IN SRV ' +\
            str(self.priority) + ' ' + str(self.weight) + ' ' +\
            str(self.port) + ' ' + str(self.target) + '.'


717
class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
718 719 720
    """ Une interface. Objet clef de l'application machine :
    - une address mac unique. Possibilité de la rendre unique avec le
    typemachine
Gabriel Detraz's avatar
Gabriel Detraz committed
721 722 723 724
    - une onetoone vers IpList pour attribution ipv4
    - le type parent associé au range ip et à l'extension
    - un objet domain associé contenant son nom
    - la liste des ports oiuvert"""
725 726
    PRETTY_NAME = "Interface"

Gabriel Detraz's avatar
Gabriel Detraz committed
727 728 729 730 731 732
    ipv4 = models.OneToOneField(
        'IpList',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
733
    mac_address = MACAddressField(integer=False, unique=True)
734
    machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
735
    type = models.ForeignKey('MachineType', on_delete=models.PROTECT)
736
    details = models.CharField(max_length=255, blank=True)
737
    port_lists = models.ManyToManyField('OuverturePortList', blank=True)
chirac's avatar
chirac committed
738

739 740 741
    class Meta:
        permissions = (
            ("view_interface", "Peut voir un objet interface"),
742 743
            ("change_interface_machine",
             "Peut changer le propriétaire d'une interface"),
744 745
        )

746
    @cached_property
Dalahro's avatar
Dalahro committed
747 748 749 750
    def is_active(self):
        """ Renvoie si une interface doit avoir accès ou non """
        machine = self.machine
        user = self.machine.user
751
        return machine.active and user.has_access()
Dalahro's avatar
Dalahro committed
752

753
    @cached_property
754
    def ipv6_slaac(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
755 756
        """ Renvoie un objet type ipv6 à partir du prefix associé à
        l'iptype parent"""
757
        if self.type.ip_type.prefix_v6:
Gabriel Detraz's avatar
Gabriel Detraz committed
758 759 760
            return EUI(self.mac_address).ipv6(
                IPNetwork(self.type.ip_type.prefix_v6).network
            )
761 762 763
        else:
            return None

764 765 766 767 768 769
    @cached_property
    def gen_ipv6_dhcpv6(self):
        """Cree une ip, à assigner avec dhcpv6 sur une machine"""
        prefix_v6 = self.type.ip_type.prefix_v6
        if not prefix_v6:
            return None
770 771 772 773
        return IPv6Address(
            IPv6Address(prefix_v6).exploded[:20] +
            IPv6Address(self.id).exploded[20:]
        )
774 775 776 777 778 779 780 781 782 783 784 785 786

    def sync_ipv6_dhcpv6(self):
        """Affecte une ipv6 dhcpv6 calculée à partir de l'id de la machine"""
        ipv6_dhcpv6 = self.gen_ipv6_dhcpv6
        if not ipv6_dhcpv6:
            return
        ipv6 = Ipv6List.objects.filter(ipv6=str(ipv6_dhcpv6)).first()
        if not ipv6:
            ipv6 = Ipv6List(ipv6=str(ipv6_dhcpv6))
        ipv6.interface = self
        ipv6.save()
        return

787 788 789 790 791 792 793 794
    def sync_ipv6_slaac(self):
        """Cree, mets à jour et supprime si il y a lieu l'ipv6 slaac associée
        à la machine
        Sans prefixe ipv6, on return
        Si l'ip slaac n'est pas celle qu'elle devrait être, on maj"""
        ipv6_slaac = self.ipv6_slaac
        if not ipv6_slaac:
            return
795 796 797
        ipv6_object = (Ipv6List.objects
                       .filter(interface=self, slaac_ip=True)
                       .first())
798 799 800 801 802 803
        if not ipv6_object:
            ipv6_object = Ipv6List(interface=self, slaac_ip=True)
        if ipv6_object.ipv6 != str(ipv6_slaac):
            ipv6_object.ipv6 = str(ipv6_slaac)
            ipv6_object.save()

804 805
    def sync_ipv6(self):
        """Cree et met à jour l'ensemble des ipv6 en fonction du mode choisi"""
806 807
        if (preferences.models.OptionalMachine
                .get_cached_value('ipv6_mode') == 'SLAAC'):
808
            self.sync_ipv6_slaac()
809 810
        elif (preferences.models.OptionalMachine
              .get_cached_value('ipv6_mode') == 'DHCPV6'):
811 812 813 814
            self.sync_ipv6_dhcpv6()
        else:
            return

815
    def ipv6(self):
816
        """ Renvoie le queryset de la liste des ipv6
817 818 819 820
        On renvoie l'ipv6 slaac que si le mode slaac est activé
        (et non dhcpv6)"""
        if (preferences.models.OptionalMachine
                .get_cached_value('ipv6_mode') == 'SLAAC'):
821
            return self.ipv6list.all()
822 823
        elif (preferences.models.OptionalMachine
              .get_cached_value('ipv6_mode') == 'DHCPV6'):
824
            return self.ipv6list.filter(slaac_ip=False)
825 826
        else:
            return None
827

828
    def mac_bare(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
829
        """ Formatage de la mac type mac_bare"""
830 831
        return str(EUI(self.mac_address, dialect=mac_bare)).lower()

832
    def filter_macaddress(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
833 834
        """ Tente un formatage mac_bare, si échoue, lève une erreur de
        validation"""
835 836
        try:
            self.mac_address = str(EUI(self.mac_address))
Gabriel Detraz's avatar
Gabriel Detraz committed
837
        except:
838 839
            raise ValidationError("La mac donnée est invalide")

840
    def clean(self, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
841 842
        """ Formate l'addresse mac en mac_bare (fonction filter_mac)
        et assigne une ipv4 dans le bon range si inexistante ou incohérente"""
843 844 845 846 847 848 849
        # If type was an invalid value, django won't create an attribute type
        # but try clean() as we may be able to create it from another value
        # so even if the error as yet been detected at this point, django
        # continues because the error might not prevent us from creating the
        # instance.
        # But in our case, it's impossible to create a type value so we raise
        # the error.
850
        if not hasattr(self, 'type'):
851
            raise ValidationError("Le type d'ip choisi n'est pas valide")
852
        self.filter_macaddress()
853
        self.mac_address = str(EUI(self.mac_address)) or None
854
        if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
855
            self.assign_ipv4()
856
        super(Interface, self).clean(*args, **kwargs)
857 858 859 860 861 862 863

    def assign_ipv4(self):
        """ Assigne une ip à l'interface """
        free_ips = self.type.ip_type.free_ip()
        if free_ips:
            self.ipv4 = free_ips[0]
        else:
Gabriel Detraz's avatar
Gabriel Detraz committed
864 865
            raise ValidationError("Il n'y a plus d'ip disponibles\
            dans le slash")
866 867 868
        return

    def unassign_ipv4(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
869
        """ Sans commentaire, désassigne une ipv4"""
870
        self.ipv4 = None
871

872 873 874 875 876
    def update_type(self):
        """ Lorsque le machinetype est changé de type d'ip, on réassigne"""
        self.clean()
        self.save()

877
    def save(self, *args, **kwargs):
878
        self.filter_macaddress()
879
        # On verifie la cohérence en forçant l'extension par la méthode
880 881 882 883
        if self.ipv4:
            if self.type.ip_type != self.ipv4.ip_type:
                raise ValidationError("L'ipv4 et le type de la machine ne\
                correspondent pas")
884 885
        super(Interface, self).save(*args, **kwargs)

886 887
    @staticmethod
    def can_create(user_request, machineid, *_args, **_kwargs):
888 889 890 891 892
        """Verifie que l'user a les bons droits infra pour créer
        une interface, ou bien que la machine appartient bien à l'user
        :param macineid: Id de la machine parente de l'interface
        :param user_request: instance utilisateur qui fait la requête
        :return: soit True, soit False avec la raison de l'échec"""
893
        try:
894
            machine = Machine.objects.get(pk=machineid)
895 896
        except Machine.DoesNotExist:
            return False, u"Machine inexistante"
897
        if not user_request.has_perm('machines.add_interface'):
898 899
            if not (preferences.models.OptionalMachine
                    .get_cached_value('create_machine')):
900
                return False, u"Vous ne pouvez pas ajouter une machine"
901 902 903 904
            max_lambdauser_interfaces = (preferences.models.OptionalMachine
                                         .get_cached_value(
                                             'max_lambdauser_interfaces'
                                         ))
905 906 907
            if machine.user != user_request:
                return False, u"Vous ne pouvez pas ajouter une interface à une\
                        machine d'un autre user que vous sans droit"
908 909
            if (machine.user.user_interfaces().count() >=
                    max_lambdauser_interfaces):
910 911 912 913 914
                return False, u"Vous avez atteint le maximum d'interfaces\
                        autorisées que vous pouvez créer vous même (%s) "\
                        % max_lambdauser_interfaces
        return True, None

915
    @staticmethod
916 917 918
    def can_change_machine(user_request, *_args, **_kwargs):
        """Check if a user can change the machine associated with an
        Interface object """
919 920
        return (user_request.has_perm('machines.change_interface_machine'),
                "Droit requis pour changer la machine")
921

922
    def can_edit(self, user_request, *args, **kwargs):
923 924 925 926 927
        """Verifie que l'user a les bons droits infra pour editer
        cette instance interface, ou qu'elle lui appartient
        :param self: Instance interface à editer
        :param user_request: Utilisateur qui fait la requête
        :return: soit True, soit False avec la raison de l'échec"""
928
        if self.machine.user != user_request:
929 930 931 932 933 934 935 936
            if (not user_request.has_perm('machines.change_interface') or
                    not self.machine.user.can_edit(
                        user_request,
                        *args,
                        **kwargs
                    )[0]):
                return False, (u"Vous ne pouvez pas éditer une machine "
                               "d'un autre user que vous sans droit")
937 938
        return True, None

939
    def can_delete(self, user_request, *args, **kwargs):
940
        """Verifie que l'user a les bons droits delete object pour del
941 942 943 944
        cette instance interface, ou qu'elle lui appartient
        :param self: Instance interface à del
        :param user_request: Utilisateur qui fait la requête
        :return: soit True, soit False avec la raison de l'échec"""
945
        if self.machine.user != user_request:
946 947 948 949 950 951 952 953
            if (not user_request.has_perm('machines.change_interface') or
                    not self.machine.user.can_edit(
                        user_request,
                        *args,
                        **kwargs
                    )[0]):
                return False, (u"Vous ne pouvez pas éditer une machine "
                               "d'un autre user que vous sans droit")
954 955
        return True, None

956
    def can_view(self, user_request, *_args, **_kwargs):
957
        """Vérifie qu'on peut bien voir cette instance particulière avec
958
        droit view objet ou qu'elle appartient à l'user
959 960 961
        :param self: instance interface à voir
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
962 963 964 965
        if (not user_request.has_perm('machines.view_interface') and
                self.machine.user != user_request):
            return False, (u"Vous n'avez pas le droit de voir des machines "
                           "autre que les vôtres")
966 967
        return True, None

968 969 970
    def __init__(self, *args, **kwargs):
        super(Interface, self).__init__(*args, **kwargs)
        self.field_permissions = {
971
            'machine': self.can_change_machine,
972 973
        }

974
    def __str__(self):
chirac's avatar
chirac committed
975 976 977 978 979
        try:
            domain = self.domain
        except:
            domain = None
        return str(domain)
980

981
    def has_private_ip(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
982
        """ True si l'ip associée est privée"""
983
        if self.ipv4:
984 985 986
            return IPAddress(str(self.ipv4)).is_private()
        else:
            return False
987

988
    def may_have_port_open(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
989
        """ True si l'interface a une ip et une ip publique.
Gabriel Detraz's avatar
Gabriel Detraz committed
990 991
        Permet de ne pas exporter des ouvertures sur des ip privées
        (useless)"""
992
        return self.ipv4 and not self.has_private_ip()
993

Gabriel Detraz's avatar
Gabriel Detraz committed
994

995
class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
996
    """ A list of IPv6 """
997 998 999 1000 1001 1002
    PRETTY_NAME = 'Enregistrements Ipv6 des machines'

    ipv6 = models.GenericIPAddressField(
        protocol='IPv6',
        unique=True
    )
1003 1004 1005 1006 1007
    interface = models.ForeignKey(
        'Interface',
        on_delete=models.CASCADE,
        related_name='ipv6list'
    )
1008 1009 1010 1011 1012
    slaac_ip = models.BooleanField(default=False)

    class Meta:
        permissions = (
            ("view_ipv6list", "Peut voir un objet ipv6"),
1013 1014
            ("change_ipv6list_slaac_ip",
             "Peut changer la valeur slaac sur une ipv6"),
1015 1016
        )

1017 1018
    @staticmethod
    def can_create(user_request, interfaceid, *_args, **_kwargs):
1019 1020 1021 1022 1023 1024 1025 1026 1027 1028 1029 1030 1031 1032 1033 1034
        """Verifie que l'user a les bons droits infra pour créer
        une ipv6, ou possède l'interface associée
        :param interfaceid: Id de l'interface associée à cet objet domain
        :param user_request: instance utilisateur qui fait la requête
        :return: soit True, soit False avec la raison de l'échec"""
        try:
            interface = Interface.objects.get(pk=interfaceid)
        except Interface.DoesNotExist:
            return False, u"Interface inexistante"
        if not user_request.has_perm('machines.add_ipv6list'):
            if interface.machine.user != user_request:
                return False, u"Vous ne pouvez pas ajouter un alias à une\
                        machine d'un autre user que vous sans droit"
        return True, None

    @staticmethod
1035 1036
    def can_change_slaac_ip(user_request, *_args, **_kwargs):
        """ Check if a user can change the slaac value """
1037 1038
        return (user_request.has_perm('machines.change_ipv6list_slaac_ip'),
                "Droit requis pour changer la valeur slaac ip")
1039 1040 1041 1042 1043 1044 1045 1046

    def can_edit(self, user_request, *args, **kwargs):
        """Verifie que l'user a les bons droits infra pour editer
        cette instance interface, ou qu'elle lui appartient
        :param self: Instance interface à editer
        :param user_request: Utilisateur qui fait la requête
        :return: soit True, soit False avec la raison de l'échec"""
        if self.interface.machine.user != user_request:
1047 1048 1049 1050 1051 1052 1053 1054
            if (not user_request.has_perm('machines.change_ipv6list') or
                    not self.interface.machine.user.can_edit(
                        user_request,
                        *args,
                        **kwargs
                    )[0]):
                return False, (u"Vous ne pouvez pas éditer une machine "
                               "d'un autre user que vous sans droit")
1055 1056 1057 1058 1059 1060 1061 1062 1063
        return True, None

    def can_delete(self, user_request, *args, **kwargs):
        """Verifie que l'user a les bons droits delete object pour del
        cette instance interface, ou qu'elle lui appartient
        :param self: Instance interface à del
        :param user_request: Utilisateur qui fait la requête
        :return: soit True, soit False avec la raison de l'échec"""
        if self.interface.machine.user != user_request:
1064 1065 1066 1067 1068 1069 1070 1071
            if (not user_request.has_perm('machines.change_ipv6list') or
                    not self.interface.machine.user.can_edit(
                        user_request,
                        *args,
                        **kwargs
                    )[0]):
                return False, (u"Vous ne pouvez pas éditer une machine "
                               "d'un autre user que vous sans droit")
1072 1073
        return True, None

1074
    def can_view(self, user_request, *_args, **_kwargs):
1075 1076 1077 1078 1079
        """Vérifie qu'on peut bien voir cette instance particulière avec
        droit view objet ou qu'elle appartient à l'user
        :param self: instance interface à voir
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
1080 1081 1082 1083
        if (not user_request.has_perm('machines.view_ipv6list') and
                self.interface.machine.user != user_request):
            return False, (u"Vous n'avez pas le droit de voir des machines "
                           "autre que les vôtres")
1084 1085 1086 1087 1088
        return True, None

    def __init__(self, *args, **kwargs):
        super(Ipv6List, self).__init__(*args, **kwargs)
        self.field_permissions = {
1089
            'slaac_ip': self.can_change_slaac_ip,
1090 1091
        }

1092 1093
    def check_and_replace_prefix(self, prefix=None):
        """Si le prefixe v6 est incorrect, on maj l'ipv6"""
1094 1095 1096
        prefix_v6 = prefix or self.interface.type.ip_type.prefix_v6
        if not prefix_v6:
            return
1097 1098 1099 1100 1101 1102
        if (IPv6Address(self.ipv6).exploded[:20] !=
                IPv6Address(prefix_v6).exploded[:20]):
            self.ipv6 = IPv6Address(
                IPv6Address(prefix_v6).exploded[:20] +
                IPv6Address(self.ipv6).exploded[20:]
            )
1103 1104
            self.save()

1105
    def clean(self, *args, **kwargs):
1106 1107 1108
        if self.slaac_ip and (Ipv6List.objects
                              .filter(interface=self.interface, slaac_ip=True)
                              .exclude(id=self.id)):
1109
            raise ValidationError("Une ip slaac est déjà enregistrée")
1110 1111
        prefix_v6 = self.interface.type.ip_type.prefix_v6
        if prefix_v6:
1112 1113 1114 1115 1116 1117
            if (IPv6Address(self.ipv6).exploded[:20] !=
                    IPv6Address(prefix_v6).exploded[:20]):
                raise ValidationError(
                    "Le prefixv6 est incorrect et ne correspond pas au type "
                    "associé à la machine"
                )
1118 1119 1120 1121 1122 1123 1124
        super(Ipv6List, self).clean(*args, **kwargs)

    def save(self, *args, **kwargs):
        """Force à avoir appellé clean avant"""
        self.full_clean()
        super(Ipv6List, self).save(*args, **kwargs)

1125 1126 1127 1128
    def __str__(self):
        return str(self.ipv6)


1129
class Domain(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
1130 1131 1132
    """ Objet domain. Enregistrement A et CNAME en même temps : permet de
    stocker les alias et les nom de machines, suivant si interface_parent
    ou cname sont remplis"""
chirac's avatar
chirac committed
1133
    PRETTY_NAME = "Domaine dns"
1134

Gabriel Detraz's avatar
Gabriel Detraz committed
1135 1136 1137 1138 1139 1140 1141 1142 1143 1144
    interface_parent = models.OneToOneField(
        'Interface',
        on_delete=models.CASCADE,
        blank=True,
        null=True
    )
    name = models.CharField(
        help_text="Obligatoire et unique, ne doit pas comporter de points",
        max_length=255
    )
chirac's avatar
chirac committed
1145
    extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
Gabriel Detraz's avatar
Gabriel Detraz committed
1146 1147 1148 1149 1150 1151
    cname = models.ForeignKey(
        'self',
        null=True,
        blank=True,
        related_name='related_domain'
    )
chirac's avatar
chirac committed
1152 1153

    class Meta:
1154
        unique_together = (("name", "extension"),)
1155 1156 1157
        permissions = (
            ("view_domain", "Peut voir un objet domain"),
        )
chirac's avatar
chirac committed
1158

1159
    def get_extension(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1160 1161
        """ Retourne l'extension de l'interface parente si c'est un A
         Retourne l'extension propre si c'est un cname, renvoie None sinon"""
1162 1163
        if self.interface_parent:
            return self.interface_parent.type.ip_type.extension
Gabriel Detraz's avatar
Gabriel Detraz committed
1164
        elif hasattr(self, 'extension'):
1165
            return self.extension
1166 1167
        else:
            return None
1168

chirac's avatar
chirac committed
1169
    def clean(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1170
        """ Validation :
Gabriel Detraz's avatar
Gabriel Detraz committed
1171 1172
        - l'objet est bien soit A soit CNAME
        - le cname est pas pointé sur lui-même
Gabriel Detraz's avatar
Gabriel Detraz committed
1173 1174
        - le nom contient bien les caractères autorisés par la norme
        dns et moins de 63 caractères au total
Gabriel Detraz's avatar
Gabriel Detraz committed
1175
        - le couple nom/extension est bien unique"""
1176
        if self.get_extension():
Gabriel Detraz's avatar
Gabriel Detraz committed
1177
            self.extension = self.get_extension()
chirac's avatar
chirac committed
1178 1179
        if self.interface_parent and self.cname:
            raise ValidationError("On ne peut créer à la fois A et CNAME")
Gabriel Detraz's avatar
Gabriel Detraz committed
1180
        if self.cname == self:
chirac's avatar
chirac committed
1181
            raise ValidationError("On ne peut créer un cname sur lui même")
Gabriel Detraz's avatar
Gabriel Detraz committed
1182
        HOSTNAME_LABEL_PATTERN = re.compile(
1183
            r"(?!-)[A-Z\d-]+(?<!-)$",
Gabriel Detraz's avatar
Gabriel Detraz committed
1184 1185
            re.IGNORECASE
        )
1186 1187
        dns = self.name.lower()
        if len(dns) > 63:
Gabriel Detraz's avatar
Gabriel Detraz committed
1188 1189
            raise ValidationError("Le nom de domaine %s est trop long\
            (maximum de 63 caractères)." % dns)
1190
        if not HOSTNAME_LABEL_PATTERN.match(dns):
Gabriel Detraz's avatar
Gabriel Detraz committed
1191 1192
            raise ValidationError("Ce nom de domaine %s contient des\
            carractères interdits." % dns)
1193 1194 1195
        self.validate_unique()
        super(Domain, self).clean()

1196 1197
    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1198
        """ Une entrée DNS"""
1199
        if self.cname:
1200 1201 1202 1203
            return "{name} IN CNAME   {cname}.".format(
                name=str(self.name).ljust(15),
                cname=str(self.cname)
            )
1204

1205
    def save(self, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
1206 1207
        """ Empèche le save sans extension valide.
        Force à avoir appellé clean avant"""
1208 1209 1210
        if not self.get_extension():
            raise ValidationError("Extension invalide")
        self.full_clean()
1211 1212
        super(Domain, self).save(*args, **kwargs)

1213 1214 1215 1216 1217 1218 1219 1220 1221 1222 1223 1224
    @cached_property
    def get_source_interface(self):
        """Renvoie l'interface source :
        - l'interface reliée si c'est un A
        - si c'est un cname, suit le cname jusqu'à atteindre le A
        et renvoie l'interface parente
        Fonction récursive"""
        if self.interface_parent:
            return self.interface_parent
        else:
            return self.cname.get_parent_interface()

1225 1226
    @staticmethod
    def can_create(user_request, interfaceid, *_args, **_kwargs):
1227 1228 1229 1230 1231
        """Verifie que l'user a les bons droits infra pour créer
        un domain, ou possède l'interface associée
        :param interfaceid: Id de l'interface associée à cet objet domain
        :param user_request: instance utilisateur qui fait la requête
        :return: soit True, soit False avec la raison de l'échec"""
1232
        try:
1233
            interface = Interface.objects.get(pk=interfaceid)
1234 1235
        except Interface.DoesNotExist:
            return False, u"Interface inexistante"
1236
        if not user_request.has_perm('machines.add_domain'):
1237 1238 1239 1240
            max_lambdauser_aliases = (preferences.models.OptionalMachine
                                      .get_cached_value(
                                          'max_lambdauser_aliases'
                                      ))
1241
            if interface.machine.user != user_request:
1242 1243
                return False, (u"Vous ne pouvez pas ajouter un alias à une "
                               "machine d'un autre user que vous sans droit")
1244
            if Domain.objects.filter(
1245 1246 1247 1248 1249
                    cname__in=Domain.objects.filter(
                        interface_parent__in=(interface.machine.user
                                              .user_interfaces())
                    )
                ).count() >= max_lambdauser_aliases:
1250 1251 1252
                return False, (u"Vous avez atteint le maximum d'alias "
                               "autorisés que vous pouvez créer vous même "
                               "(%s) " % max_lambdauser_aliases)
1253 1254
        return True, None

1255
    def can_edit(self, user_request, *_args, **_kwargs):
1256
        """Verifie que l'user a les bons droits pour editer
1257 1258 1259 1260
        cette instance domain
        :param self: Instance domain à editer
        :param user_request: Utilisateur qui fait la requête
        :return: soit True, soit False avec la raison de l'échec"""
1261 1262 1263 1264
        if (not user_request.has_perm('machines.change_domain') and
                self.get_source_interface.machine.user != user_request):
            return False, (u"Vous ne pouvez pas editer un alias à une machine "
                           "d'un autre user que vous sans droit")
1265 1266
        return True, None

1267
    def can_delete(self, user_request, *_args, **_kwargs):