models.py 56.9 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 23
# 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.

24 25
from __future__ import unicode_literals

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

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

40
from macaddress.fields import MACAddressField
41

42
from re2o.field_permissions import FieldPermissionModelMixin
43
from re2o.mixins import AclMixin, RevMixin 
44

Maël Kervella's avatar
Maël Kervella committed
45 46 47
import users.models
import preferences.models

48

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

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

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

69 70 71 72 73 74 75
    def get_instance(machineid, *args, **kwargs):
        """Get the Machine instance with machineid.
        :param userid: The id
        :return: The user
        """
        return Machine.objects.get(pk=machineid)

76 77 78 79 80
    def linked_objects(self):
        """Return linked objects : machine and domain.
        Usefull in history display"""
        return chain(self.interface_set.all(), Domain.objects.filter(interface_parent__in=self.interface_set.all()))

81 82 83 84 85 86 87 88 89 90 91 92
    @staticmethod
    def can_change_user(user_request, *args, **kwargs):
        """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.
        """
93
        return user_request.has_perm('machines.change_machine_user'), "Vous ne pouvez pas modifier l'utilisateur de la machine."
94

95 96 97 98 99 100 101 102 103
    def can_view_all(user_request, *args, **kwargs):
        """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'):
            return False, u"Vous ne pouvez pas afficher l'ensemble des machines sans permission"
        return True, None

104
    def can_create(user_request, userid, *args, **kwargs):
105 106 107 108 109
        """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
110
        try:
111
            user = users.models.User.objects.get(pk=userid)
Maël Kervella's avatar
Maël Kervella committed
112 113
        except users.models.User.DoesNotExist:
            return False, u"Utilisateur inexistant"
114
        max_lambdauser_interfaces = preferences.models.OptionalMachine.get_cached_value('max_lambdauser_interfaces')
115
        if not user_request.has_perm('machines.add_machine'):
116 117
            if not preferences.models.OptionalMachine.get_cached_value('create_machine'):
                return False, u"Vous ne pouvez pas ajouter une machine"
Maël Kervella's avatar
Maël Kervella committed
118 119 120 121 122 123 124 125 126
            if user != user_request:
                return False, u"Vous ne pouvez pas ajouter une machine à un\
                        autre user que vous sans droit"
            if user.user_interfaces().count() >= max_lambdauser_interfaces:
                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

127
    def can_edit(self, user_request, *args, **kwargs):
128 129 130 131 132
        """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"""
133 134 135 136
        if self.user != user_request:
            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"
137 138
        return True, None

139
    def can_delete(self, user_request, *args, **kwargs):
140 141 142 143 144
        """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"""
145 146 147 148
        if self.user != user_request:
            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"
149 150
        return True, None

151
    def can_view(self, user_request, *args, **kwargs):
152 153 154 155 156
        """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"""
157
        if not user_request.has_perm('machines.view_machine') and self.user != user_request:
158 159
            return False, u"Vous n'avez pas droit de voir les machines autre\
                que les vôtres"
160
        return True, None
161

162 163 164 165 166 167
    def __init__(self, *args, **kwargs):
        super(Machine, self).__init__(*args, **kwargs)
        self.field_permissions = {
            'user' : self.can_change_user,
        }

168
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
169 170
        return str(self.user) + ' - ' + str(self.id) + ' - ' + str(self.name)

171

172
class MachineType(RevMixin, AclMixin, models.Model):
173
    """ Type de machine, relié à un type d'ip, affecté aux interfaces"""
174 175
    PRETTY_NAME = "Type de machine"

176
    type = models.CharField(max_length=255)
Gabriel Detraz's avatar
Gabriel Detraz committed
177 178 179 180 181 182
    ip_type = models.ForeignKey(
        'IpType',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
183

184 185 186 187 188 189
    class Meta:
        permissions = (
            ("view_machinetype", "Peut voir un objet machinetype"),
            ("use_all_machinetype", "Peut utiliser n'importe quel type de machine"),
        )

190
    def all_interfaces(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
191 192
        """ Renvoie toutes les interfaces (cartes réseaux) de type
        machinetype"""
193 194
        return Interface.objects.filter(type=self)

195 196 197 198 199 200 201 202 203
    def can_use_all(user_request, *args, **kwargs):
        """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.
        """
204
        if not user_request.has_perm('machines.use_all_machinetype'):
205 206 207
            return False, u"Vous n'avez pas le droit d'utiliser tout types de machines"
        return True, None

208
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
209 210
        return self.type

211

212
class IpType(RevMixin, AclMixin, models.Model):
213
    """ Type d'ip, définissant un range d'ip, affecté aux machine types"""
214 215
    PRETTY_NAME = "Type d'ip"

216
    type = models.CharField(max_length=255)
217
    extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
218
    need_infra = models.BooleanField(default=False)
219 220
    domaine_ip_start = models.GenericIPAddressField(protocol='IPv4')
    domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4')
Gabriel Detraz's avatar
Gabriel Detraz committed
221 222 223 224 225 226 227 228 229 230 231 232 233 234 235 236
    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
    )
237

238 239 240 241 242 243
    class Meta:
        permissions = (
            ("view_iptype", "Peut voir un objet iptype"),
            ("use_all_iptype", "Peut utiliser tous les iptype"),
        )

244
    @cached_property
245
    def ip_range(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
246 247
        """ Renvoie un objet IPRange à partir de l'objet IpType"""
        return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop)
248 249 250

    @cached_property
    def ip_set(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
251
        """ Renvoie une IPSet à partir de l'iptype"""
252
        return IPSet(self.ip_range)
253 254 255

    @cached_property
    def ip_set_as_str(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
256
        """ Renvoie une liste des ip en string"""
257 258 259
        return [str(x) for x in self.ip_set]

    def ip_objects(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
260
        """ Renvoie tous les objets ipv4 relié à ce type"""
261 262 263
        return IpList.objects.filter(ip_type=self)

    def free_ip(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
264
        """ Renvoie toutes les ip libres associées au type donné (self)"""
Gabriel Detraz's avatar
Gabriel Detraz committed
265 266 267
        return IpList.objects.filter(
            interface__isnull=True
        ).filter(ip_type=self)
268 269

    def gen_ip_range(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
270 271 272
        """ 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"""
273
        # Creation du range d'ip dans les objets iplist
274 275 276 277
        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
278 279 280
        listes_ip = IpList.objects.filter(
            ipv4__in=[str(ip) for ip in networks]
        )
281 282 283 284 285 286
        # 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)
287
        return
288 289

    def del_ip_range(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
290 291
        """ Methode dépréciée, IpList est en mode cascade et supprimé
        automatiquement"""
292
        if Interface.objects.filter(ipv4__in=self.ip_objects()):
Gabriel Detraz's avatar
Gabriel Detraz committed
293 294
            raise ValidationError("Une ou plusieurs ip du range sont\
            affectées, impossible de supprimer le range")
295 296 297
        for ip in self.ip_objects():
            ip.delete()

298 299 300 301 302 303 304 305
    def check_replace_prefixv6(self):
        """Remplace les prefixv6 des interfaces liées à ce type d'ip"""
        if not self.prefix_v6:
            return
        else:
            for ipv6 in Ipv6List.objects.filter(interface__in=Interface.objects.filter(type__in=MachineType.objects.filter(ip_type=self))):
                ipv6.check_and_replace_prefix(prefix=self.prefix_v6)

306
    def clean(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
307 308 309 310 311
        """ 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"""
312 313 314 315
        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
316 317
            raise ValidationError("Le range est trop gros, vous ne devez\
            pas créer plus grand qu'un /16")
318
        # On check que les / ne se recoupent pas
319
        for element in IpType.objects.all().exclude(pk=self.pk):
320
            if not self.ip_set.isdisjoint(element.ip_set):
Gabriel Detraz's avatar
Gabriel Detraz committed
321 322
                raise ValidationError("Le range indiqué n'est pas disjoint\
                des ranges existants")
323 324 325
        # On formate le prefix v6
        if self.prefix_v6:
            self.prefix_v6 = str(IPNetwork(self.prefix_v6 + '/64').network)
326 327 328 329 330 331
        return

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

332 333 334 335
    def can_use_all(user_request, *args, **kwargs):
        """Superdroit qui permet d'utiliser toutes les extensions sans restrictions
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
336
        return user_request.has_perm('machines.use_all_iptype'), None
337

338 339
    def __str__(self):
        return self.type
chirac's avatar
chirac committed
340

Gabriel Detraz's avatar
Gabriel Detraz committed
341

342
class Vlan(RevMixin, AclMixin, models.Model):
343 344
    """ Un vlan : vlan_id et nom
    On limite le vlan id entre 0 et 4096, comme défini par la norme"""
345 346
    PRETTY_NAME = "Vlans"

347
    vlan_id = models.PositiveIntegerField(validators=[MaxValueValidator(4095)])
348 349 350
    name = models.CharField(max_length=256)
    comment = models.CharField(max_length=256, blank=True)

351 352 353 354 355
    class Meta:
        permissions = (
            ("view_vlan", "Peut voir un objet vlan"),
        )

356 357 358
    def __str__(self):
        return self.name

Gabriel Detraz's avatar
Gabriel Detraz committed
359

360
class Nas(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
361
    """ Les nas. Associé à un machine_type.
Gabriel Detraz's avatar
Gabriel Detraz committed
362 363
    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"""
364 365
    PRETTY_NAME = "Correspondance entre les nas et les machines connectées"

366 367 368 369 370 371
    default_mode = '802.1X'
    AUTH = (
        ('802.1X', '802.1X'),
        ('Mac-address', 'Mac-address'),
    )

372
    name = models.CharField(max_length=255, unique=True)
Gabriel Detraz's avatar
Gabriel Detraz committed
373 374 375 376 377 378 379 380 381 382 383 384 385 386 387
    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
    )
388
    autocapture_mac = models.BooleanField(default=False)
389

390 391 392 393 394
    class Meta:
        permissions = (
            ("view_nas", "Peut voir un objet Nas"),
        )

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

Gabriel Detraz's avatar
Gabriel Detraz committed
398

399
class SOA(RevMixin, AclMixin, models.Model):
400 401 402 403 404 405 406
    """
    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"

407
    name = models.CharField(max_length=255)
408 409 410 411 412 413 414 415 416 417 418 419 420 421 422 423 424 425 426 427 428 429 430
    mail = models.EmailField(
        help_text='Email du contact pour la zone'
    )
    refresh = models.PositiveIntegerField(
        default=86400,    # 24 hours
        help_text='Secondes avant que les DNS secondaires doivent demander le\
                   serial du DNS primaire pour détecter une modification'
    )
    retry = models.PositiveIntegerField(
        default=7200,    # 2 hours
        help_text='Secondes avant que les DNS secondaires fassent une nouvelle\
                   demande de serial en cas de timeout du DNS primaire'
    )
    expire = models.PositiveIntegerField(
        default=3600000, # 1000 hours
        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'
    )

431 432 433 434 435
    class Meta:
        permissions = (
            ("view_soa", "Peut voir un objet soa"),
        )

436 437 438 439 440 441 442 443 444 445 446 447 448
    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 (
449 450 451 452
            '    {refresh}; refresh\n'
            '    {retry}; retry\n'
            '    {expire}; expire\n'
            '    {ttl}; TTL'
453
        ).format(
454 455 456 457
            refresh=str(self.refresh).ljust(12),
            retry=str(self.retry).ljust(12),
            expire=str(self.expire).ljust(12),
            ttl=str(self.ttl).ljust(12)
458 459 460 461 462 463
        )

    @cached_property
    def dns_soa_mail(self):
        """ Renvoie le mail dans l'enregistrement SOA """
        mail_fields = str(self.mail).split('@')
464
        return mail_fields[0].replace('.', '\\.') + '.' + mail_fields[1] + '.'
465 466 467 468 469 470 471 472 473 474 475

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



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

Gabriel Detraz's avatar
Gabriel Detraz committed
481 482 483 484 485
    name = models.CharField(
        max_length=255,
        unique=True,
        help_text="Nom de la zone, doit commencer par un point (.example.org)"
    )
486
    need_infra = models.BooleanField(default=False)
487
    origin = models.ForeignKey(
Gabriel Detraz's avatar
Gabriel Detraz committed
488 489 490
        'IpList',
        on_delete=models.PROTECT,
        blank=True,
Gabriel Detraz's avatar
Gabriel Detraz committed
491 492
        null=True,
        help_text="Enregistrement A associé à la zone"
Gabriel Detraz's avatar
Gabriel Detraz committed
493
    )
494 495 496
    origin_v6 = models.GenericIPAddressField(
        protocol='IPv6',
        null=True,
Gabriel Detraz's avatar
Gabriel Detraz committed
497
        blank=True,
lhark's avatar
lhark committed
498
        help_text="Enregistrement AAAA associé à la zone"
499
    )
500 501 502 503 504
    soa = models.ForeignKey(
        'SOA',
        on_delete=models.CASCADE,
        default=SOA.new_default_soa
    )
505

506 507 508 509 510 511
    class Meta:
        permissions = (
            ("view_extension", "Peut voir un objet extension"),
            ("use_all_extension", "Peut utiliser toutes les extension"),
        )

512 513
    @cached_property
    def dns_entry(self):
514 515 516
        """ Une entrée DNS A et AAAA sur origin (zone self)"""
        entry = ""
        if self.origin:
517
            entry += "@               IN  A       " + str(self.origin)
518 519 520
        if self.origin_v6:
            if entry:
                entry += "\n"
521
            entry += "@               IN  AAAA    " + str(self.origin_v6)
522
        return entry
523

524 525 526 527
    def can_use_all(user_request, *args, **kwargs):
        """Superdroit qui permet d'utiliser toutes les extensions sans restrictions
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
528
        return user_request.has_perm('machines.use_all_extension'), None
529

530 531
    def __str__(self):
        return self.name
chirac's avatar
chirac committed
532

533
    def clean(self, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
534 535 536 537
        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
538

539
class Mx(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
540 541
    """ Entrées des MX. Enregistre la zone (extension) associée et la
    priorité
Gabriel Detraz's avatar
Gabriel Detraz committed
542
    Todo : pouvoir associer un MX à une interface """
543 544 545
    PRETTY_NAME = "Enregistrements MX"

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
546
    priority = models.PositiveIntegerField(unique=True)
chirac's avatar
chirac committed
547
    name = models.OneToOneField('Domain', on_delete=models.PROTECT)
548

549 550 551 552 553
    class Meta:
        permissions = (
            ("view_mx", "Peut voir un objet mx"),
        )

554 555
    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
556 557
        """Renvoie l'entrée DNS complète pour un MX à mettre dans les
        fichiers de zones"""
558
        return "@               IN  MX  " + str(self.priority).ljust(3) + " " + str(self.name)
559

560 561 562
    def __str__(self):
        return str(self.zone) + ' ' + str(self.priority) + ' ' + str(self.name)

Gabriel Detraz's avatar
Gabriel Detraz committed
563

564
class Ns(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
565
    """Liste des enregistrements name servers par zone considéérée"""
566 567 568
    PRETTY_NAME = "Enregistrements NS"

    zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
Gabriel Detraz's avatar
Gabriel Detraz committed
569
    ns = models.OneToOneField('Domain', on_delete=models.PROTECT)
570

571 572
    class Meta:
        permissions = (
Gabriel Detraz's avatar
Gabriel Detraz committed
573
            ("view_ns", "Peut voir un objet ns"),
574 575
        )

576 577
    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
578
        """Renvoie un enregistrement NS complet pour les filezones"""
579
        return "@               IN  NS      " + str(self.ns)
580

581
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
582
        return str(self.zone) + ' ' + str(self.ns)
583

Gabriel Detraz's avatar
Gabriel Detraz committed
584

585
class Txt(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
586
    """ Un enregistrement TXT associé à une extension"""
587
    PRETTY_NAME = "Enregistrement TXT"
Gabriel Detraz's avatar
Gabriel Detraz committed
588 589 590

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

593 594 595 596 597
    class Meta:
        permissions = (
            ("view_txt", "Peut voir un objet txt"),
        )

Gabriel Detraz's avatar
Gabriel Detraz committed
598
    def __str__(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
599 600
        return str(self.zone) + " : " + str(self.field1) + " " +\
            str(self.field2)
Gabriel Detraz's avatar
Gabriel Detraz committed
601 602 603

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

Gabriel Detraz's avatar
Gabriel Detraz committed
607

608
class Srv(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
609 610 611 612 613 614 615 616 617 618 619 620 621 622 623 624 625 626 627 628
    PRETTY_NAME = "Enregistrement Srv"

    TCP = 'TCP'
    UDP = 'UDP'

    service =  models.CharField(max_length=31)
    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(
629
        default=0,
Gabriel Detraz's avatar
Gabriel Detraz committed
630 631 632
        validators=[MaxValueValidator(65535)],
        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)"
lhark's avatar
lhark committed
633

Gabriel Detraz's avatar
Gabriel Detraz committed
634 635
    )
    weight = models.PositiveIntegerField(
636
        default=0,
Gabriel Detraz's avatar
Gabriel Detraz committed
637 638 639 640 641 642 643 644 645 646 647 648 649 650
        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"
    )

651 652 653 654 655
    class Meta:
        permissions = (
            ("view_soa", "Peut voir un objet soa"),
        )

Gabriel Detraz's avatar
Gabriel Detraz committed
656 657 658 659 660 661 662 663 664 665 666 667 668 669
    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) + '.'


670
class Interface(RevMixin, AclMixin, FieldPermissionModelMixin,models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
671 672 673
    """ 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
674 675 676 677
    - 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"""
678 679
    PRETTY_NAME = "Interface"

Gabriel Detraz's avatar
Gabriel Detraz committed
680 681 682 683 684 685
    ipv4 = models.OneToOneField(
        'IpList',
        on_delete=models.PROTECT,
        blank=True,
        null=True
    )
686
    mac_address = MACAddressField(integer=False, unique=True)
687
    machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
688
    type = models.ForeignKey('MachineType', on_delete=models.PROTECT)
689
    details = models.CharField(max_length=255, blank=True)
690
    port_lists = models.ManyToManyField('OuverturePortList', blank=True)
chirac's avatar
chirac committed
691

692 693 694
    class Meta:
        permissions = (
            ("view_interface", "Peut voir un objet interface"),
695
            ("change_interface_machine", "Peut changer le propriétaire d'une interface"),
696 697
        )

698
    @cached_property
Dalahro's avatar
Dalahro committed
699 700 701 702
    def is_active(self):
        """ Renvoie si une interface doit avoir accès ou non """
        machine = self.machine
        user = self.machine.user
703
        return machine.active and user.has_access()
Dalahro's avatar
Dalahro committed
704

705
    @cached_property
706
    def ipv6_slaac(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
707 708
        """ Renvoie un objet type ipv6 à partir du prefix associé à
        l'iptype parent"""
709
        if self.type.ip_type.prefix_v6:
Gabriel Detraz's avatar
Gabriel Detraz committed
710 711 712
            return EUI(self.mac_address).ipv6(
                IPNetwork(self.type.ip_type.prefix_v6).network
            )
713 714 715
        else:
            return None

716 717 718 719 720 721 722 723 724 725 726 727 728 729 730 731 732 733 734 735
    @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
        return IPv6Address(IPv6Address(prefix_v6).exploded[:20] + IPv6Address(self.id).exploded[20:])

    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

736 737 738 739 740 741 742 743 744 745 746 747 748 749 750
    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
        ipv6_object = Ipv6List.objects.filter(interface=self, slaac_ip=True).first()
        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()

751 752
    def sync_ipv6(self):
        """Cree et met à jour l'ensemble des ipv6 en fonction du mode choisi"""
753
        if preferences.models.OptionalMachine.get_cached_value('ipv6_mode') == 'SLAAC':
754
            self.sync_ipv6_slaac()
755
        elif preferences.models.OptionalMachine.get_cached_value('ipv6_mode') == 'DHCPV6':
756 757 758 759
            self.sync_ipv6_dhcpv6()
        else:
            return

760
    def ipv6(self):
761 762
        """ Renvoie le queryset de la liste des ipv6
        On renvoie l'ipv6 slaac que si le mode slaac est activé (et non dhcpv6)"""
763 764 765 766
        if preferences.models.OptionalMachine.get_cached_value('ipv6_mode') == 'SLAAC':
            return self.ipv6list.all()
        elif preferences.models.OptionalMachine.get_cached_value('ipv6_mode') == 'DHCPV6':
            return self.ipv6list.filter(slaac_ip=False)
767 768
        else:
            return None
769

770
    def mac_bare(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
771
        """ Formatage de la mac type mac_bare"""
772 773
        return str(EUI(self.mac_address, dialect=mac_bare)).lower()

774
    def filter_macaddress(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
775 776
        """ Tente un formatage mac_bare, si échoue, lève une erreur de
        validation"""
777 778
        try:
            self.mac_address = str(EUI(self.mac_address))
Gabriel Detraz's avatar
Gabriel Detraz committed
779
        except:
780 781
            raise ValidationError("La mac donnée est invalide")

782
    def clean(self, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
783 784
        """ Formate l'addresse mac en mac_bare (fonction filter_mac)
        et assigne une ipv4 dans le bon range si inexistante ou incohérente"""
785 786 787 788 789 790 791 792 793
        # 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.
        if not hasattr(self, 'type') :
            raise ValidationError("Le type d'ip choisi n'est pas valide")
794
        self.filter_macaddress()
795
        self.mac_address = str(EUI(self.mac_address)) or None
796
        if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
797
            self.assign_ipv4()
798
        super(Interface, self).clean(*args, **kwargs)
799 800 801 802 803 804 805

    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
806 807
            raise ValidationError("Il n'y a plus d'ip disponibles\
            dans le slash")
808 809 810
        return

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

814 815 816 817 818
    def update_type(self):
        """ Lorsque le machinetype est changé de type d'ip, on réassigne"""
        self.clean()
        self.save()

819
    def save(self, *args, **kwargs):
820
        self.filter_macaddress()
821
        # On verifie la cohérence en forçant l'extension par la méthode
822 823 824 825
        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")
826 827
        super(Interface, self).save(*args, **kwargs)

828
    def can_create(user_request, machineid, *args, **kwargs):
829 830 831 832 833
        """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"""
834
        try:
835
            machine = Machine.objects.get(pk=machineid)
836 837
        except Machine.DoesNotExist:
            return False, u"Machine inexistante"
838
        if not user_request.has_perm('machines.add_interface'):
839 840
            if not preferences.models.OptionalMachine.get_cached_value('create_machine'):
                return False, u"Vous ne pouvez pas ajouter une machine"
841
            max_lambdauser_interfaces = preferences.models.OptionalMachine.get_cached_value('max_lambdauser_interfaces')
842 843 844 845 846 847 848 849 850
            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"
            if machine.user.user_interfaces().count() >= max_lambdauser_interfaces:
                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

851 852 853 854
    @staticmethod
    def can_change_machine(user_request, *args, **kwargs):
        return user_request.has_perm('machines.change_interface_machine'), "Droit requis pour changer la machine"

855
    def can_edit(self, user_request, *args, **kwargs):
856 857 858 859 860
        """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"""
861 862 863 864
        if self.machine.user != user_request:
            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"
865 866
        return True, None

867
    def can_delete(self, user_request, *args, **kwargs):
868
        """Verifie que l'user a les bons droits delete object pour del
869 870 871 872
        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"""
873 874 875 876
        if self.machine.user != user_request:
            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"
877 878
        return True, None

879
    def can_view(self, user_request, *args, **kwargs):
880
        """Vérifie qu'on peut bien voir cette instance particulière avec
881
        droit view objet ou qu'elle appartient à l'user
882 883 884
        :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"""
885
        if not user_request.has_perm('machines.view_interface') and self.machine.user != user_request:
886 887 888 889
            return False, u"Vous n'avez pas le droit de voir des machines autre\
                que les vôtres"
        return True, None

890 891 892 893 894 895
    def __init__(self, *args, **kwargs):
        super(Interface, self).__init__(*args, **kwargs)
        self.field_permissions = {
            'machine' : self.can_change_machine,
        }

896
    def __str__(self):
chirac's avatar
chirac committed
897 898 899 900 901
        try:
            domain = self.domain
        except:
            domain = None
        return str(domain)
902

903
    def has_private_ip(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
904
        """ True si l'ip associée est privée"""
905
        if self.ipv4:
906 907 908
            return IPAddress(str(self.ipv4)).is_private()
        else:
            return False
909

910
    def may_have_port_open(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
911
        """ True si l'interface a une ip et une ip publique.
Gabriel Detraz's avatar
Gabriel Detraz committed
912 913
        Permet de ne pas exporter des ouvertures sur des ip privées
        (useless)"""
914
        return self.ipv4 and not self.has_private_ip()
915

Gabriel Detraz's avatar
Gabriel Detraz committed
916

917
class Ipv6List(RevMixin, AclMixin, FieldPermissionModelMixin, models.Model):
918 919 920 921 922 923
    PRETTY_NAME = 'Enregistrements Ipv6 des machines'

    ipv6 = models.GenericIPAddressField(
        protocol='IPv6',
        unique=True
    )
924
    interface = models.ForeignKey('Interface', on_delete=models.CASCADE, related_name='ipv6list')
925 926 927 928 929 930 931 932 933 934 935 936 937 938 939 940 941 942 943 944 945 946 947 948 949 950 951 952 953 954 955 956 957 958 959 960 961 962 963 964 965 966 967 968 969 970 971 972 973 974 975 976 977 978 979 980 981 982 983 984 985 986 987 988 989 990 991 992 993
    slaac_ip = models.BooleanField(default=False)

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

    def can_create(user_request, interfaceid, *args, **kwargs):
        """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
    def can_change_slaac_ip(user_request, *args, **kwargs):
        return user_request.has_perm('machines.change_ipv6list_slaac_ip'), "Droit requis pour changer la valeur slaac ip"

    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:
            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"
        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:
            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"
        return True, None

    def can_view(self, user_request, *args, **kwargs):
        """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"""
        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"
        return True, None

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

994 995
    def check_and_replace_prefix(self, prefix=None):
        """Si le prefixe v6 est incorrect, on maj l'ipv6"""
996 997 998 999 1000
        prefix_v6 = prefix or self.interface.type.ip_type.prefix_v6
        if not prefix_v6:
            return
        if IPv6Address(self.ipv6).exploded[:20] != IPv6Address(prefix_v6).exploded[:20]:
            self.ipv6 = IPv6Address(IPv6Address(prefix_v6).exploded[:20] + IPv6Address(self.ipv6).exploded[20:])
1001 1002
            self.save()

1003 1004 1005
    def clean(self, *args, **kwargs):
        if self.slaac_ip and Ipv6List.objects.filter(interface=self.interface, slaac_ip=True).exclude(id=self.id):
            raise ValidationError("Une ip slaac est déjà enregistrée")
1006 1007 1008 1009
        prefix_v6 = self.interface.type.ip_type.prefix_v6
        if prefix_v6:
            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")
1010 1011 1012 1013 1014 1015 1016
        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)

1017 1018 1019 1020
    def __str__(self):
        return str(self.ipv6)


1021
class Domain(RevMixin, AclMixin, models.Model):
Gabriel Detraz's avatar
Gabriel Detraz committed
1022 1023 1024
    """ 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
1025
    PRETTY_NAME = "Domaine dns"
1026

Gabriel Detraz's avatar
Gabriel Detraz committed
1027 1028 1029 1030 1031 1032 1033 1034 1035 1036
    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
1037
    extension = models.ForeignKey('Extension', on_delete=models.PROTECT)
Gabriel Detraz's avatar
Gabriel Detraz committed
1038 1039 1040 1041 1042 1043
    cname = models.ForeignKey(
        'self',
        null=True,
        blank=True,
        related_name='related_domain'
    )
chirac's avatar
chirac committed
1044 1045

    class Meta:
1046
        unique_together = (("name", "extension"),)
1047 1048 1049
        permissions = (
            ("view_domain", "Peut voir un objet domain"),
        )
chirac's avatar
chirac committed
1050

1051
    def get_extension(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1052 1053
        """ Retourne l'extension de l'interface parente si c'est un A
         Retourne l'extension propre si c'est un cname, renvoie None sinon"""
1054 1055
        if self.interface_parent:
            return self.interface_parent.type.ip_type.extension
Gabriel Detraz's avatar
Gabriel Detraz committed
1056
        elif hasattr(self, 'extension'):
1057
            return self.extension
1058 1059
        else:
            return None
1060

chirac's avatar
chirac committed
1061
    def clean(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1062
        """ Validation :
Gabriel Detraz's avatar
Gabriel Detraz committed
1063 1064
        - l'objet est bien soit A soit CNAME
        - le cname est pas pointé sur lui-même
Gabriel Detraz's avatar
Gabriel Detraz committed
1065 1066
        - 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
1067
        - le couple nom/extension est bien unique"""
1068
        if self.get_extension():
Gabriel Detraz's avatar
Gabriel Detraz committed
1069
            self.extension = self.get_extension()
chirac's avatar
chirac committed
1070 1071
        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
1072
        if self.cname == self:
chirac's avatar
chirac committed
1073
            raise ValidationError("On ne peut créer un cname sur lui même")
Gabriel Detraz's avatar
Gabriel Detraz committed
1074 1075 1076 1077
        HOSTNAME_LABEL_PATTERN = re.compile(
            "(?!-)[A-Z\d-]+(?<!-)$",
            re.IGNORECASE
        )
1078 1079
        dns = self.name.lower()
        if len(dns) > 63:
Gabriel Detraz's avatar
Gabriel Detraz committed
1080 1081
            raise ValidationError("Le nom de domaine %s est trop long\
            (maximum de 63 caractères)." % dns)
1082
        if not HOSTNAME_LABEL_PATTERN.match(dns):
Gabriel Detraz's avatar
Gabriel Detraz committed
1083 1084
            raise ValidationError("Ce nom de domaine %s contient des\
            carractères interdits." % dns)
1085 1086 1087
        self.validate_unique()
        super(Domain, self).clean()

1088 1089
    @cached_property
    def dns_entry(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1090
        """ Une entrée DNS"""
1091
        if self.cname:
1092
            return str(self.name).ljust(15) + " IN CNAME   " + str(self.cname) + "."
1093

1094
    def save(self, *args, **kwargs):
Gabriel Detraz's avatar
Gabriel Detraz committed
1095 1096
        """ Empèche le save sans extension valide.
        Force à avoir appellé clean avant"""
1097 1098 1099
        if not self.get_extension():
            raise ValidationError("Extension invalide")
        self.full_clean()
1100 1101
        super(Domain, self).save(*args, **kwargs)

1102 1103 1104 1105 1106 1107 1108 1109 1110 1111 1112 1113
    @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()

1114
    def can_create(user_request, interfaceid, *args, **kwargs):
1115 1116 1117 1118 1119
        """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"""
1120
        try:
1121
            interface = Interface.objects.get(pk=interfaceid)
1122 1123
        except Interface.DoesNotExist:
            return False, u"Interface inexistante"
1124
        if not user_request.has_perm('machines.add_domain'):
1125
            max_lambdauser_aliases = preferences.models.OptionalMachine.get_cached_value('max_lambdauser_aliases')
1126 1127 1128 1129 1130 1131 1132 1133 1134 1135 1136 1137 1138
            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"
            if Domain.objects.filter(
                    cname__in=Domain.objects.filter(
                        interface_parent__in=interface.machine.user.user_interfaces()
                    )
                ).count() >= max_lambdauser_aliases:
                return False, u"Vous avez atteint le maximum d'alias\
                        autorisés que vous pouvez créer vous même (%s) "\
                        % max_lambdauser_aliases
        return True, None

1139
    def can_edit(self, user_request, *args, **kwargs):
1140
        """Verifie que l'user a les bons droits pour editer
1141 1142 1143 1144
        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"""
1145
        if not user_request.has_perm('machines.change_domain') and\
1146
            self.get_source_interface.machine.user != user_request:
1147
            return False, u"Vous ne pouvez pas editer un alias à une machine\
1148 1149 1150
                    d'un autre user que vous sans droit"
        return True, None

1151
    def can_delete(self, user_request, *args, **kwargs):
1152
        """Verifie que l'user a les bons droits delete object pour del
1153 1154 1155 1156
        cette instance domain, ou qu'elle lui appartient
        :param self: Instance domain à del
        :param user_request: Utilisateur qui fait la requête
        :return: soit True, soit False avec la raison de l'échec"""
1157
        if not user_request.has_perm('machines.delete_domain') and\
1158
            self.get_source_interface.machine.user != user_request:
1159 1160 1161 1162
            return False, u"Vous ne pouvez pas supprimer un alias à une machine\
                d'un autre user que vous sans droit"
        return True, None

1163
    def can_view(self, user_request, *args, **kwargs):
1164
        """Vérifie qu'on peut bien voir cette instance particulière avec
1165
        droit view objet ou qu'elle appartient à l'user
1166 1167 1168
        :param self: instance domain à voir
        :param user_request: instance user qui fait l'edition
        :return: True ou False avec la raison de l'échec le cas échéant"""
1169
        if not user_request.has_perm('machines.view_domain') and\
1170
            self.get_source_interface.machine.user != user_request:
1171 1172 1173 1174
            return False, u"Vous n'avez pas le droit de voir des machines autre\
                que les vôtres"
        return True, None

1175
    def __str__(self):
1176
        return str(self.name) + str(self.extension)
1177

Gabriel Detraz's avatar
Gabriel Detraz committed
1178

1179
class IpList(RevMixin, AclMixin, models.Model):
1180 1181
    PRETTY_NAME = "Addresses ipv4"

1182
    ipv4 = models.GenericIPAddressField(protocol='IPv4', unique=True)
1183 1184
    ip_type = models.ForeignKey('IpType', on_delete=models.CASCADE)

1185 1186 1187 1188 1189
    class Meta:
        permissions = (
            ("view_iplist", "Peut voir un objet iplist"),
        )

1190 1191
    @cached_property
    def need_infra(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1192 1193
        """ Permet de savoir si un user basique peut assigner cette ip ou
        non"""
1194 1195 1196
        return self.ip_type.need_infra

    def clean(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1197
        """ Erreur si l'ip_type est incorrect"""
1198
        if not str(self.ipv4) in self.ip_type.ip_set_as_str:
Gabriel Detraz's avatar
Gabriel Detraz committed
1199 1200
            raise ValidationError("L'ipv4 et le range de l'iptype ne\
            correspondent pas!")
1201 1202 1203 1204 1205
        return

    def save(self, *args, **kwargs):
        self.clean()
        super(IpList, self).save(*args, **kwargs)
1206 1207

    def __str__(self):
1208 1209
        return self.ipv4

Gabriel Detraz's avatar
Gabriel Detraz committed
1210

1211
class Service(RevMixin, AclMixin, models.Model):
1212
    """ Definition d'un service (dhcp, dns, etc)"""
Gabriel Detraz's avatar
Gabriel Detraz committed
1213 1214
    PRETTY_NAME = "Services à générer (dhcp, dns, etc)"

1215
    service_type = models.CharField(max_length=255, blank=True, unique=True)
Gabriel Detraz's avatar
Gabriel Detraz committed
1216 1217 1218 1219 1220 1221 1222 1223
    min_time_regen = models.DurationField(
        default=timedelta(minutes=1),
        help_text="Temps minimal avant nouvelle génération du service"
    )
    regular_time_regen = models.DurationField(
        default=timedelta(hours=1),
        help_text="Temps maximal avant nouvelle génération du service"
    )
1224 1225
    servers = models.ManyToManyField('Interface', through='Service_link')

1226 1227 1228 1229 1230
    class Meta:
        permissions = (
            ("view_service", "Peut voir un objet service"),
        )

1231
    def ask_regen(self):
1232
        """ Marque à True la demande de régénération pour un service x """
Gabriel Detraz's avatar
Gabriel Detraz committed
1233 1234
        Service_link.objects.filter(service=self).exclude(asked_regen=True)\
            .update(asked_regen=True)
1235
        return
1236 1237

    def process_link(self, servers):
Gabriel Detraz's avatar
Gabriel Detraz committed
1238 1239 1240 1241 1242
        """ Django ne peut créer lui meme les relations manytomany avec table
        intermediaire explicite"""
        for serv in servers.exclude(
                pk__in=Interface.objects.filter(service=self)
            ):
1243 1244
            link = Service_link(service=self, server=serv)
            link.save()
Gabriel Detraz's avatar
Gabriel Detraz committed
1245 1246
        Service_link.objects.filter(service=self).exclude(server__in=servers)\
            .delete()
1247 1248 1249 1250 1251 1252 1253 1254
        return

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

    def __str__(self):
        return str(self.service_type)

Gabriel Detraz's avatar
Gabriel Detraz committed
1255

1256
def regen(service):
Gabriel Detraz's avatar
Gabriel Detraz committed
1257 1258
    """ Fonction externe pour régérération d'un service, prend un objet service
    en arg"""
1259 1260 1261
    obj = Service.objects.filter(service_type=service)
    if obj:
        obj[0].ask_regen()
1262 1263
    return

Gabriel Detraz's avatar
Gabriel Detraz committed
1264

1265
class Service_link(RevMixin, AclMixin, models.Model):
1266
    """ Definition du lien entre serveurs et services"""
Gabriel Detraz's avatar
Gabriel Detraz committed
1267 1268
    PRETTY_NAME = "Relation entre service et serveur"

1269 1270 1271 1272 1273
    service = models.ForeignKey('Service', on_delete=models.CASCADE)
    server = models.ForeignKey('Interface', on_delete=models.CASCADE)
    last_regen = models.DateTimeField(auto_now_add=True)
    asked_regen = models.BooleanField(default=False)

1274 1275 1276 1277 1278
    def done_regen(self):
        """ Appellé lorsqu'un serveur a regénéré son service"""
        self.last_regen = timezone.now()
        self.asked_regen = False
        self.save()
1279 1280

    def need_regen(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1281 1282 1283 1284 1285 1286 1287 1288 1289 1290
        """ Décide si le temps minimal écoulé est suffisant pour provoquer une
        régénération de service"""
        return bool(
            (self.asked_regen and (
                self.last_regen + self.service.min_time_regen
            ) < timezone.now()
            ) or (
                self.last_regen + self.service.regular_time_regen
            ) < timezone.now()
        )
1291 1292 1293 1294

    def __str__(self):
        return str(self.server) + " " + str(self.service)

1295

1296
class OuverturePortList(RevMixin, AclMixin, models.Model):
1297
    """Liste des ports ouverts sur une interface."""
Gabriel Detraz's avatar
Gabriel Detraz committed
1298 1299
    PRETTY_NAME = "Profil d'ouverture de ports"

Gabriel Detraz's avatar
Gabriel Detraz committed
1300 1301 1302 1303
    name = models.CharField(
        help_text="Nom de la configuration des ports.",
        max_length=255
    )
1304

1305 1306 1307 1308 1309
    class Meta:
        permissions = (
            ("view_ouvertureportlist", "Peut voir un objet ouvertureport"),
        )

1310
    def can_delete(self, user_request, *args, **kwargs):
1311 1312 1313 1314 1315
        """Verifie que l'user a les bons droits bureau pour delete
        cette instance ouvertureportlist
        :param self: Instance ouvertureportlist à delete
        :param user_request: Utilisateur qui fait la requête
        :return: soit True, soit False avec la raison de l'échec"""
1316
        if not user_request.has_perm('machines.delete_ouvertureportlist'):
1317 1318 1319 1320 1321
            return False, u"Vous n'avez pas le droit de supprimer une ouverture\
                de port"
        if self.interface_set.all():
            return False, u"Cette liste de ports est utilisée"
        return True, None
1322

1323
    def __str__(self):
1324
        return self.name
1325

1326
    def tcp_ports_in(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1327 1328 1329 1330 1331 1332
        """Renvoie la liste des ports ouverts en TCP IN pour ce profil"""
        return self.ouvertureport_set.filter(
            protocole=OuverturePort.TCP,
            io=OuverturePort.IN
        )

1333
    def udp_ports_in(self):
Gabriel Detraz's avatar
Gabriel Detraz committed
1334 1335 1336 1337 1338
        """Renvoie la liste des ports ouverts en UDP IN pour ce profil"""
        return self.ouvertureport_set.filter(
            protocole=OuverturePort.UDP,
            io=OuverturePort.IN
        )