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 64.1 KB
Newer Older
1
# -*- mode: python; coding: utf-8 -*-
2 3 4 5
# 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.
#
6
# Copyright © 2016-2018  Gabriel Détraz
7 8
# Copyright © 2017  Goulven Kermarec
# Copyright © 2017  Augustin Lemesle
9
# Copyright © 2018  Charlie Jacomme
10 11 12 13 14 15 16 17 18 19 20 21 22 23
#
# 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.
24 25 26
"""machines.models
The models definitions for the Machines app
"""
27

28 29
from __future__ import unicode_literals

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

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

46
from macaddress.fields import MACAddressField
47

48
from re2o.field_permissions import FieldPermissionModelMixin
49
from re2o.mixins import AclMixin, RevMixin
50

Maël Kervella's avatar
Maël Kervella committed
51 52 53
import users.models
import preferences.models

54

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

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

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

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

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

94
    @staticmethod
95
    def can_change_user(user_request, *_args, **_kwargs):
96 97 98 99 100 101 102 103 104 105
        """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.
        """
106 107
        return (user_request.has_perm('machines.change_machine_user'),
                "Vous ne pouvez pas modifier l'utilisateur de la machine.")
108

109 110
    @staticmethod
    def can_view_all(user_request, *_args, **_kwargs):
111 112 113 114 115
        """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'):
116 117
            return False, (u"Vous ne pouvez pas afficher l'ensemble des "
                           "machines sans permission")
118 119
        return True, None

120 121
    @staticmethod
    def can_create(user_request, userid, *_args, **_kwargs):
122 123 124 125 126
        """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
127
        try:
128
            user = users.models.User.objects.get(pk=userid)
Maël Kervella's avatar
Maël Kervella committed
129 130
        except users.models.User.DoesNotExist:
            return False, u"Utilisateur inexistant"
131 132 133 134
        max_lambdauser_interfaces = (preferences.models.OptionalMachine
                                     .get_cached_value(
                                         'max_lambdauser_interfaces'
                                     ))
135
        if not user_request.has_perm('machines.add_machine'):
136 137
            if not (preferences.models.OptionalMachine
                    .get_cached_value('create_machine')):
138
                return False, u"Vous ne pouvez pas ajouter une machine"
Maël Kervella's avatar
Maël Kervella committed
139
            if user != user_request:
140 141
                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
142
            if user.user_interfaces().count() >= max_lambdauser_interfaces:
143 144 145
                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
146 147
        return True, None

148
    def can_edit(self, user_request, *args, **kwargs):
149 150 151 152 153
        """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"""
154
        if self.user != user_request:
155 156 157 158 159 160 161 162 163
            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")
164 165
        return True, None

166
    def can_delete(self, user_request, *args, **kwargs):
167 168 169 170 171
        """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"""
172
        if self.user != user_request:
173 174 175 176 177 178 179 180 181
            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")
182 183
        return True, None

184
    def can_view(self, user_request, *_args, **_kwargs):
185 186 187 188 189
        """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"""
190 191 192 193
        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")
194
        return True, None
195

196 197 198
    def __init__(self, *args, **kwargs):
        super(Machine, self).__init__(*args, **kwargs)
        self.field_permissions = {
199
            'user': self.can_change_user,
200 201
        }

202
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
203 204
        return str(self.user) + ' - ' + str(self.id) + ' - ' + str(self.name)

205

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

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

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

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

230 231
    @staticmethod
    def can_use_all(user_request, *_args, **_kwargs):
232 233 234 235 236 237 238 239
        """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.
        """
240
        if not user_request.has_perm('machines.use_all_machinetype'):
241 242
            return False, (u"Vous n'avez pas le droit d'utiliser tout types "
                           "de machines")
243 244
        return True, None

245
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
246 247
        return self.type

248

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

253
    type = models.CharField(max_length=255)
254
    extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
255
    need_infra = models.BooleanField(default=False)
256 257
    domaine_ip_start = models.GenericIPAddressField(protocol='IPv4')
    domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4')
Gabriel Detraz's avatar
Gabriel Detraz committed
258 259 260 261 262 263 264 265 266 267 268 269 270 271 272 273
    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
    )
274

275 276 277 278 279 280
    class Meta:
        permissions = (
            ("view_iptype", "Peut voir un objet iptype"),
            ("use_all_iptype", "Peut utiliser tous les iptype"),
        )

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

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

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

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

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

    def gen_ip_range(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
307 308 309
        """ 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"""
310
        # Creation du range d'ip dans les objets iplist
311 312 313 314
        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
315 316 317
        listes_ip = IpList.objects.filter(
            ipv4__in=[str(ip) for ip in networks]
        )
318 319 320 321 322 323
        # 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)
324
        return
325 326

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

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

347
    def clean(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
348 349 350 351 352
        """ 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"""
353 354 355 356
        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
357 358
            raise ValidationError("Le range est trop gros, vous ne devez\
            pas créer plus grand qu'un /16")
359
        # On check que les / ne se recoupent pas
360
        for element in IpType.objects.all().exclude(pk=self.pk):
361
            if not self.ip_set.isdisjoint(element.ip_set):
Gabriel Detraz's avatar
Gabriel Detraz committed
362 363
                raise ValidationError("Le range indiqué n'est pas disjoint\
                des ranges existants")
364 365 366
        # On formate le prefix v6
        if self.prefix_v6:
            self.prefix_v6 = str(IPNetwork(self.prefix_v6 + '/64').network)
367 368 369 370 371 372
        return

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

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

381 382
    def __str__(self):
        return self.type
chirac's avatar
chirac committed
383

Gabriel Detraz's avatar
Gabriel Detraz committed
384

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

390
    vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)])
391 392 393
    name = models.CharField(max_length=256)
    comment = models.CharField(max_length=256, blank=True)

394 395 396 397 398
    class Meta:
        permissions = (
            ("view_vlan", "Peut voir un objet vlan"),
        )

399 400 401
    def __str__(self):
        return self.name

Gabriel Detraz's avatar
Gabriel Detraz committed
402

403
class Nas(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
404
    """ Les nas. Associé à un machine_type.
Gabriel Detraz's avatar
Gabriel Detraz committed
405 406
    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"""
407 408
    PRETTY_NAME = "Correspondance entre les nas et les machines connectées"

409 410 411 412 413 414
    default_mode = '802.1X'
    AUTH = (
        ('802.1X', '802.1X'),
        ('Mac-address', 'Mac-address'),
    )

415
    name = models.CharField(max_length=255, unique=True)
Gabriel Detraz's avatar
Gabriel Detraz committed
416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
    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
    )
431
    autocapture_mac = models.BooleanField(default=False)
432

433 434 435 436 437
    class Meta:
        permissions = (
            ("view_nas", "Peut voir un objet Nas"),
        )

438 439 440
    def __str__(self):
        return self.name

Gabriel Detraz's avatar
Gabriel Detraz committed
441

442
class SOA(RevMixin, AclMixin, models.Model):
443 444 445 446 447 448 449
    """
    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"

450
    name = models.CharField(max_length=255)
451 452 453 454
    mail = models.EmailField(
        help_text='Email du contact pour la zone'
    )
    refresh = models.PositiveIntegerField(
455
        default=86400,  # 24 hours
456 457 458 459
        help_text='Secondes avant que les DNS secondaires doivent demander le\
                   serial du DNS primaire pour détecter une modification'
    )
    retry = models.PositiveIntegerField(
460
        default=7200,  # 2 hours
461 462 463 464
        help_text='Secondes avant que les DNS secondaires fassent une nouvelle\
                   demande de serial en cas de timeout du DNS primaire'
    )
    expire = models.PositiveIntegerField(
465
        default=3600000,  # 1000 hours
466 467 468 469 470 471 472 473
        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'
    )

474 475 476 477 478
    class Meta:
        permissions = (
            ("view_soa", "Peut voir un objet soa"),
        )

479 480 481 482 483 484 485 486 487 488 489 490 491
    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 (
492 493 494 495
            '    {refresh}; refresh\n'
            '    {retry}; retry\n'
            '    {expire}; expire\n'
            '    {ttl}; TTL'
496
        ).format(
497 498 499 500
            refresh=str(self.refresh).ljust(12),
            retry=str(self.retry).ljust(12),
            expire=str(self.expire).ljust(12),
            ttl=str(self.ttl).ljust(12)
501 502 503 504 505 506
        )

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

    @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. """
515 516 517 518
        return cls.objects.get_or_create(
            name="SOA to edit",
            mail="postmaser@example.com"
        )[0].pk
519 520


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

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

550 551 552 553 554 555
    class Meta:
        permissions = (
            ("view_extension", "Peut voir un objet extension"),
            ("use_all_extension", "Peut utiliser toutes les extension"),
        )

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

568
    def get_associated_sshfp_records(self):
569 570 571
        from re2o.utils import all_active_assigned_interfaces
        return (all_active_assigned_interfaces()
                .filter(type__ip_type__extension=self)
572
                .filter(machine__id__in=SshFp.objects.values('machine')))
573

574
    def get_associated_a_records(self):
575 576
        from re2o.utils import all_active_assigned_interfaces
        return (all_active_assigned_interfaces()
577
                .filter(type__ip_type__extension=self)
578
                .filter(ipv4__isnull=False))
579 580

    def get_associated_aaaa_records(self):
581 582 583
        from re2o.utils import all_active_interfaces
        return (all_active_interfaces(full=True)
                .filter(type__ip_type__extension=self))
584 585

    def get_associated_cname_records(self):
586
        from re2o.utils import all_active_assigned_interfaces
587 588 589
        return (Domain.objects
                .filter(extension=self)
                .filter(cname__isnull=False)
590
                .filter(interface_parent__in=all_active_assigned_interfaces())
591
                .prefetch_related('cname'))
592

593 594
    @staticmethod
    def can_use_all(user_request, *_args, **_kwargs):
595 596
        """Superdroit qui permet d'utiliser toutes les extensions sans
        restrictions
597 598
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
599
        return user_request.has_perm('machines.use_all_extension'), None
600

601 602
    def __str__(self):
        return self.name
chirac's avatar
chirac committed
603

604
    def clean(self, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
605 606 607 608
        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
609

610
class Mx(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
611 612
    """ Entrées des MX. Enregistre la zone (extension) associée et la
    priorité
Gabriel Detraz's avatar
Gabriel Detraz committed
613
    Todo : pouvoir associer un MX à une interface """
614 615 616
    PRETTY_NAME = "Enregistrements MX"

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
617
    priority = models.PositiveIntegerField(unique=True)
chirac's avatar
chirac committed
618
    name = models.OneToOneField('Domain', on_delete=models.PROTECT)
619

620 621 622 623 624
    class Meta:
        permissions = (
            ("view_mx", "Peut voir un objet mx"),
        )

625 626
    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
627 628
        """Renvoie l'entrée DNS complète pour un MX à mettre dans les
        fichiers de zones"""
629 630
        return "@               IN  MX  {prior} {name}".format(
            prior=str(self.priority).ljust(3),
Maël Kervella's avatar
Maël Kervella committed
631
            name=str(self.name)
632
        )
633

634 635 636
    def __str__(self):
        return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name)

Gabriel Detraz's avatar
Gabriel Detraz committed
637

638
class Ns(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
639
    """Liste des enregistrements name servers par zone considéérée"""
640 641 642
    PRETTY_NAME = "Enregistrements NS"

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
643
    ns = models.ForeignKey('Domain', on_delete=models.PROTECT)
644

645 646
    class Meta:
        permissions = (
Gabriel Detraz's avatar
Gabriel Detraz committed
647
            ("view_ns", "Peut voir un objet ns"),
648 649
        )

650 651
    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
652
        """Renvoie un enregistrement NS complet pour les filezones"""
653
        return "@               IN  NS      " + str(self.ns)
654

655
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
656
        return str(self.zone) + ' ' + str(self.ns)
657

Gabriel Detraz's avatar
Gabriel Detraz committed
658

659
class Txt(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
660
    """ Un enregistrement TXT associé à une extension"""
661
    PRETTY_NAME = "Enregistrement TXT"
Gabriel Detraz's avatar
Gabriel Detraz committed
662 663 664

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

667 668 669 670 671
    class Meta:
        permissions = (
            ("view_txt", "Peut voir un objet txt"),
        )

Gabriel Detraz's avatar
Gabriel Detraz committed
672
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
673 674
        return str(self.zone) + " : " + str(self.field1) + " " +\
            str(self.field2)
Gabriel Detraz's avatar
Gabriel Detraz committed
675 676 677

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

Gabriel Detraz's avatar
Gabriel Detraz committed
681

Charlie Jacomme's avatar
Charlie Jacomme committed
682
class DName(RevMixin, AclMixin, models.Model):
Maël Kervella's avatar
Maël Kervella committed
683
    """A DNAME entry for the DNS."""
Charlie Jacomme's avatar
Charlie Jacomme committed
684 685 686 687 688
    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
    alias = models.CharField(max_length=255)

    class Meta:
        permissions = (
Maël Kervella's avatar
Maël Kervella committed
689
            ("view_dname", "Can see a dname object"),
Charlie Jacomme's avatar
Charlie Jacomme committed
690
        )
Maël Kervella's avatar
Maël Kervella committed
691 692
        verbose_name = "DNAME entry"
        verbose_name_plural = "DNAME entries"
Charlie Jacomme's avatar
Charlie Jacomme committed
693 694 695 696 697 698

    def __str__(self):
        return str(self.zone) + " : " + str(self.alias)

    @cached_property
    def dns_entry(self):
Maël Kervella's avatar
Maël Kervella committed
699
        """Returns the DNAME record for the DNS zone file."""
chirac's avatar
chirac committed
700
        return str(self.alias).ljust(15) + " IN  DNAME   " + str(self.zone)
Charlie Jacomme's avatar
Charlie Jacomme committed
701 702


703
class Srv(RevMixin, AclMixin, models.Model):
704
    """ A SRV record """
Gabriel Detraz's avatar
Gabriel Detraz committed
705 706 707 708 709
    PRETTY_NAME = "Enregistrement Srv"

    TCP = 'TCP'
    UDP = 'UDP'

710
    service = models.CharField(max_length=31)
Gabriel Detraz's avatar
Gabriel Detraz committed
711 712 713 714 715 716 717 718 719 720 721 722 723 724
    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(
725
        default=0,
Gabriel Detraz's avatar
Gabriel Detraz committed
726
        validators=[MaxValueValidator(65535)],
727 728 729
        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
730 731
    )
    weight = models.PositiveIntegerField(
732
        default=0,
Gabriel Detraz's avatar
Gabriel Detraz committed
733 734 735 736 737 738 739 740 741 742 743 744 745 746
        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"
    )

747 748
    class Meta:
        permissions = (
749
            ("view_srv", "Peut voir un objet srv"),
750 751
        )

Gabriel Detraz's avatar
Gabriel Detraz committed
752 753 754 755 756 757 758 759 760 761 762 763 764 765
    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) + '.'


766 767 768 769 770 771 772 773 774 775 776 777 778 779 780 781 782 783 784 785 786 787 788 789 790 791 792 793 794 795 796 797 798 799 800 801 802 803 804 805 806 807 808 809 810 811 812 813 814 815 816 817 818 819 820 821 822 823 824 825 826 827 828 829 830 831 832
class SshFp(RevMixin, AclMixin, models.Model):
    """A fingerprint of an SSH public key"""

    ALGO = (
        ("ssh-rsa", "ssh-rsa"),
        ("ssh-ed25519", "ssh-ed25519"),
        ("ecdsa-sha2-nistp256", "ecdsa-sha2-nistp256"),
        ("ecdsa-sha2-nistp384", "ecdsa-sha2-nistp384"),
        ("ecdsa-sha2-nistp521", "ecdsa-sha2-nistp521"),
    )

    machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
    pub_key_entry = models.TextField(
        help_text="SSH public key",
        max_length=2048
    )
    algo = models.CharField(
        choices=ALGO,
        max_length=32
    )
    comment = models.CharField(
        help_text="Comment",
        max_length=255,
        null=True,
        blank=True
    )

    @cached_property
    def algo_id(self):
        """Return the id of the algorithm for this key"""
        if "ecdsa" in self.algo:
            return 3
        elif "rsa" in self.algo:
            return 1
        else:
            return 2

    @cached_property
    def hash(self):
        """Return the hashess for the pub key with correct id
        cf RFC, 1 is sha1 , 2 sha256"""
        return {
            "1" : hashlib.sha1(base64.b64decode(self.pub_key_entry)).hexdigest(),
            "2" : hashlib.sha256(base64.b64decode(self.pub_key_entry)).hexdigest(),
        }

    class Meta:
        permissions = (
            ("view_sshfp", "Can see an SSHFP record"),
        )
        verbose_name = "SSHFP record"
        verbose_name_plural = "SSHFP records"

    def can_view(self, user_request, *_args, **_kwargs):
        return self.machine.can_view(user_request, *_args, **_kwargs)

    def can_edit(self, user_request, *args, **kwargs):
        return self.machine.can_edit(user_request, *args, **kwargs)

    def can_delete(self, user_request, *args, **kwargs):
        return self.machine.can_delete(user_request, *args, **kwargs)

    def __str__(self):
        return str(self.algo) + ' ' + str(self.comment)



833
class Interface(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
834 835 836
    """ 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
837 838 839 840
    - 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"""
841 842
    PRETTY_NAME = "Interface"

Gabriel Detraz's avatar
Gabriel Detraz committed
843 844 845 846 847 848
    ipv4 = models.OneToOneField(
        'IpList',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
849
    mac_address = MACAddressField(integer=False, unique=True)
850
    machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
851
    type = models.ForeignKey('MachineType', on_delete=models.PROTECT)
852
    details = models.CharField(max_length=255, blank=True)
853
    port_lists = models.ManyToManyField('OuverturePortList', blank=True)
chirac's avatar
chirac committed
854

855 856 857
    class Meta:
        permissions = (
            ("view_interface", "Peut voir un objet interface"),
858 859
            ("change_interface_machine",
             "Peut changer le propriétaire d'une interface"),
860 861
        )

862
    @cached_property
Dalahro's avatar
Dalahro committed
863 864 865 866
    def is_active(self):
        """ Renvoie si une interface doit avoir accès ou non """
        machine = self.machine
        user = self.machine.user
867
        return machine.active and user.has_access()
Dalahro's avatar
Dalahro committed
868

869
    @cached_property
870
    def ipv6_slaac(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
871 872
        """ Renvoie un objet type ipv6 à partir du prefix associé à
        l'iptype parent"""
873
        if self.type.ip_type.prefix_v6:
Gabriel Detraz's avatar
Gabriel Detraz committed
874 875 876
            return EUI(self.mac_address).ipv6(
                IPNetwork(self.type.ip_type.prefix_v6).network
            )
877 878 879
        else:
            return None

880 881 882 883 884 885
    @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
886 887 888 889
        return IPv6Address(
            IPv6Address(prefix_v6).exploded[:20] +
            IPv6Address(self.id).exploded[20:]
        )
890 891 892 893 894 895 896 897 898 899 900 901 902

    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

903 904 905 906 907 908 909 910
    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
911 912 913
        ipv6_object = (Ipv6List.objects
                       .filter(interface=self, slaac_ip=True)
                       .first())
914 915 916 917 918 919
        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()

920 921
    def sync_ipv6(self):
        """Cree et met à jour l'ensemble des ipv6 en fonction du mode choisi"""
922 923
        if (preferences.models.OptionalMachine
                .get_cached_value('ipv6_mode') == 'SLAAC'):
924
            self.sync_ipv6_slaac()
925 926
        elif (preferences.models.OptionalMachine
              .get_cached_value('ipv6_mode') == 'DHCPV6'):
927 928 929 930
            self.sync_ipv6_dhcpv6()
        else:
            return

931
    def ipv6(self):
932
        """ Renvoie le queryset de la liste des ipv6
933 934 935 936
        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'):
937
            return self.ipv6list.all()
938 939
        elif (preferences.models.OptionalMachine
              .get_cached_value('ipv6_mode') == 'DHCPV6'):
940
            return self.ipv6list.filter(slaac_ip=False)
941 942
        else:
            return None
943

944
    def mac_bare(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
945
        """ Formatage de la mac type mac_bare"""
946 947
        return str(EUI(self.mac_address, dialect=mac_bare)).lower()

948
    def filter_macaddress(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
949 950
        """ Tente un formatage mac_bare, si échoue, lève une erreur de
        validation"""
951 952
        try:
            self.mac_address = str(EUI(self.mac_address))
Gabriel Detraz's avatar
Gabriel Detraz committed
953
        except:
954 955
            raise ValidationError("La mac donnée est invalide")

956
    def clean(self, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
957 958
        """ Formate l'addresse mac en mac_bare (fonction filter_mac)
        et assigne une ipv4 dans le bon range si inexistante ou incohérente"""
959 960 961 962 963 964 965
        # 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.
966
        if not hasattr(self, 'type'):
967
            raise ValidationError("Le type d'ip choisi n'est pas valide")
968
        self.filter_macaddress()
969
        self.mac_address = str(EUI(self.mac_address)) or None
970
        if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
971
            self.assign_ipv4()
972
        super(Interface, self).clean(*args, **kwargs)
973 974 975 976 977 978 979

    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
980 981
            raise ValidationError("Il n'y a plus d'ip disponibles\
            dans le slash")
982 983 984
        return

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

988 989 990 991 992
    def update_type(self):
        """ Lorsque le machinetype est changé de type d'ip, on réassigne"""
        self.clean()
        self.save()

993
    def save(self, *args, **kwargs):
994
        self.filter_macaddress()
995
        # On verifie la cohérence en forçant l'extension par la méthode
996 997 998 999
        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")
1000 1001
        super(Interface, self).save(*args, **kwargs)

1002 1003
    @staticmethod
    def can_create(user_request, machineid, *_args, **_kwargs):
1004 1005 1006 1007 1008
        """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"""
1009
        try:
1010
            machine = Machine.objects.get(pk=machineid)
1011 1012
        except Machine.DoesNotExist:
            return False, u"Machine inexistante"
1013
        if not user_request.has_perm('machines.add_interface'):
1014 1015
            if not (preferences.models.OptionalMachine
                    .get_cached_value('create_machine')):
1016
                return False, u"Vous ne pouvez pas ajouter une machine"
1017 1018 1019 1020
            max_lambdauser_interfaces = (preferences.models.OptionalMachine
                                         .get_cached_value(
                                             'max_lambdauser_interfaces'
                                         ))
1021 1022 1023
            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"
1024 1025
            if (machine.user.user_interfaces().count() >=
                    max_lambdauser_interfaces):
1026 1027 1028 1029 1030
                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

1031
    @staticmethod
1032 1033 1034
    def can_change_machine(user_request, *_args, **_kwargs):
        """Check if a user can change the machine associated with an
        Interface object """
1035 1036
        return (user_request.has_perm('machines.change_interface_machine'),
                "Droit requis pour changer la machine")
1037

1038
    def can_edit(self, user_request, *args, **kwargs):
1039 1040 1041 1042 1043
        """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"""
1044
        if self.machine.user != user_request:
1045 1046 1047 1048 1049 1050 1051 1052
            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")
1053 1054
        return True, None

1055
    def can_delete(self, user_request, *args, **kwargs):
1056
        """Verifie que l'user a les bons droits delete object pour del
1057 1058 1059 1060
        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"""
1061
        if self.machine.user != user_request:
1062 1063 1064 1065 1066 1067 1068 1069
            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")
1070 1071
        return True, None

1072
    def can_view(self, user_request, *_args, **_kwargs):
1073
        """Vérifie qu'on peut bien voir cette instance particulière avec
1074
        droit view objet ou qu'elle appartient à l'user
1075 1076 1077
        :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"""
1078 1079 1080 1081
        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")
1082 1083
        return True, None

1084 1085 1086
    def __init__(self, *args, **kwargs):
        super(Interface, self).__init__(*args, **kwargs)
        self.field_permissions = {
1087
            'machine': self.can_change_machine,
1088 1089
        }

1090
    def __str__(self):
chirac's avatar
chirac committed
1091 1092 1093 1094 1095
        try:
            domain = self.domain
        except:
            domain = None
        return str(domain)
1096

1097
    def has_private_ip(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1098
        """ True si l'ip associée est privée"""
1099
        if self.ipv4:
1100 1101 1102
            return IPAddress(str(self.ipv4)).is_private()
        else:
            return False
1103

1104
    def may_have_port_open(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1105
        """ True si l'interface a une ip et une ip publique.
Gabriel Detraz's avatar
Gabriel Detraz committed
1106 1107
        Permet de ne pas exporter des ouvertures sur des ip privées
        (useless)"""
1108
        return self.ipv4 and not self.has_private_ip()
1109

Gabriel Detraz's avatar
Gabriel Detraz committed
1110

1111
class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
1112
    """ A list of IPv6 """
1113 1114 1115 1116 1117 1118
    PRETTY_NAME = 'Enregistrements Ipv6 des machines'

    ipv6 = models.GenericIPAddressField(
        protocol='IPv6',
        unique=True
    )
1119 1120 1121 1122 1123
    interface = models.ForeignKey(
        'Interface',
        on_delete=models.CASCADE,
        related_name='ipv6list'
    )
1124 1125 1126 1127 1128
    slaac_ip = models.BooleanField(default=False)

    class Meta:
        permissions = (
            ("view_ipv6list", "Peut voir un objet ipv6"),
1129 1130
            ("change_ipv6list_slaac_ip",
             "Peut changer la valeur slaac sur une ipv6"),
1131 1132
        )

1133 1134
    @staticmethod
    def can_create(user_request, interfaceid, *_args, **_kwargs):
1135 1136 1137 1138 1139 1140 1141 1142 1143 1144 1145 1146 1147 1148 1149 1150
        """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
1151 1152
    def can_change_slaac_ip(user_request, *_args, **_kwargs):
        """ Check if a user can change the slaac value """
1153 1154
        return (user_request.has_perm('machines.change_ipv6list_slaac_ip'),
                "Droit requis pour changer la valeur slaac ip")
1155 1156 1157 1158 1159 1160 1161 1162

    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:
1163 1164 1165 1166 1167 1168 1169 1170
            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")
1171 1172 1173 1174 1175 1176 1177 1178 1179
        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:
1180 1181 1182 1183 1184 1185 1186 1187
            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")
1188 1189
        return True, None

1190
    def can_view(self, user_request, *_args, **_kwargs):
1191 1192 1193 1194 1195
        """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"""
1196 1197 1198 1199
        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")
1200 1201 1202 1203 1204
        return True, None

    def __init__(self, *args, **kwargs):
        super(Ipv6List, self).__init__(*args, **kwargs)
        self.field_permissions = {
1205
            'slaac_ip': self.can_change_slaac_ip,
1206 1207
        }

1208 1209
    def check_and_replace_prefix(self, prefix=None):
        """Si le prefixe v6 est incorrect, on maj l'ipv6"""
1210 1211 1212
        prefix_v6 = prefix or self.interface.type.ip_type.prefix_v6
        if not prefix_v6:
            return
1213 1214 1215 1216 1217 1218
        if (IPv6Address(self.ipv6).exploded[:20] !=
                IPv6Address(prefix_v6).exploded[:20]):
            self.ipv6 = IPv6Address(
                IPv6Address(prefix_v6).exploded[:20] +
                IPv6Address(self.ipv6).exploded[20:]
            )
1219 1220
            self.save()

1221
    def clean(self, *args, **kwargs):
1222 1223 1224
        if self.slaac_ip and (Ipv6List.objects
                              .filter(interface=self.interface, slaac_ip=True)
                              .exclude(id=self.id)):
1225
            raise ValidationError("Une ip slaac est déjà enregistrée")
1226 1227
        prefix_v6 = self.interface.type.ip_type.prefix_v6
        if prefix_v6:
1228 1229 1230 1231 1232 1233
            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"
                )
1234 1235 1236 1237 1238 1239 1240
        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)

1241 1242 1243 1244
    def __str__(self):
        return str(self.ipv6)


1245
class Domain(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
1246 1247 1248
    """ 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
1249
    PRETTY_NAME = "Domaine dns"
1250

Gabriel Detraz's avatar
Gabriel Detraz committed
1251 1252 1253 1254 1255 1256 1257 1258 1259 1260
    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
1261
    extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
Gabriel Detraz's avatar
Gabriel Detraz committed
1262 1263 1264 1265 1266 1267
    cname = models.ForeignKey(
        'self',
        null=True,
        blank=True,
        related_name='related_domain'
    )
chirac's avatar
chirac committed
1268 1269

    class Meta:
1270
        unique_together = (("name", "extension"),)
1271 1272 1273
        permissions = (
            ("view_domain", "Peut voir un objet domain"),
        )
chirac's avatar
chirac committed
1274

1275
    def get_extension(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1276 1277
        """ Retourne l'extension de l'interface parente si c'est un A
         Retourne l'extension propre si c'est un cname, renvoie None sinon"""
1278 1279
        if self.interface_parent:
            return self.interface_parent.type.ip_type.extension
Gabriel Detraz's avatar
Gabriel Detraz committed
1280
        elif hasattr(self, 'extension'):
1281
            return self.extension
1282 1283
        else:
            return None
1284

chirac's avatar
chirac committed
1285
    def clean(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1286
        """ Validation :
Gabriel Detraz's avatar
Gabriel Detraz committed
1287 1288
        - l'objet est bien soit A soit CNAME
        - le cname est pas pointé sur lui-même
Gabriel Detraz's avatar
Gabriel Detraz committed
1289 1290
        - 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
1291
        - le couple nom/extension est bien unique"""
1292
        if self.get_extension():
Gabriel Detraz's avatar
Gabriel Detraz committed
1293
            self.extension = self.get_extension()
chirac's avatar
chirac committed
1294 1295
        if self.interface_parent