diff --git a/README.md b/README.md index ac5edbfe..023d186a 100644 --- a/README.md +++ b/README.md @@ -4,17 +4,28 @@ Gnu public license v2.0 ## Avant propos -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. +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. -Il utilise le framework django avec python3. Il permet de gérer les adhérents, les machines, les factures, les droits d'accès, les switchs et la topologie du réseau. -De cette manière, il est possible de pluguer très facilement des services dessus, qui accèdent à la base de donnée en passant par django (ex : dhcp), en chargeant la liste de toutes les mac-ip, ou la liste des mac-ip autorisées sur le réseau (adhérent à jour de cotisation). +Il utilise le framework django avec python3. Il permet de gérer les adhérents, +les machines, les factures, les droits d'accès, les switchs et la topologie du +réseau. +De cette manière, il est possible de pluguer très facilement des services +dessus, qui accèdent à la base de donnée en passant par django (ex : dhcp), en +chargeant la liste de toutes les mac-ip, ou la liste des mac-ip autorisées sur +le réseau (adhérent à jour de cotisation). #Installation ## Installation des dépendances -L'installation comporte 3 partie : le serveur web où se trouve le depot re2o ainsi que toutes ses dépendances, le serveur bdd (mysql ou pgsql) et le serveur ldap. Ces 3 serveurs peuvent en réalité être la même machine, ou séparés (recommandé en production). -Le serveur web sera nommé serveur A, le serveur bdd serveur B et le serveur ldap serveur C. +L'installation comporte 3 partie : le serveur web où se trouve le depot re2o +ainsi que toutes ses dépendances, le serveur bdd (mysql ou pgsql) et le +serveur ldap. Ces 3 serveurs peuvent en réalité être la même machine, ou séparés +(recommandé en production). +Le serveur web sera nommé serveur A, le serveur bdd serveur B et le serveur ldap +serveur C. ### Prérequis sur le serveur A @@ -66,49 +77,88 @@ Sur le serveur C (ldap), avec apt : ### Installation sur le serveur principal A Cloner le dépot re2o à partir du gitlab, par exemple dans /var/www/re2o. -Ensuite, il faut créer le fichier settings_local.py dans le sous dossier re2o, un settings_local.example.py est présent. Les options sont commentées, et des options par défaut existent. +Ensuite, il faut créer le fichier settings_local.py dans le sous dossier re2o, +un settings_local.example.py est présent. Les options sont commentées, et des +options par défaut existent. -En particulier, il est nécessaire de générer un login/mdp admin pour le ldap et un login/mdp pour l'utilisateur sql (cf ci-dessous), à mettre dans settings_local.py +En particulier, il est nécessaire de générer un login/mdp admin pour le ldap et +un login/mdp pour l'utilisateur sql (cf ci-dessous), à mettre dans +settings_local.py ### Installation du serveur mysql/postgresql sur B -Sur le serveur mysql ou postgresl, il est nécessaire de créer une base de donnée re2o, ainsi qu'un user re2o et un mot de passe associé. Ne pas oublier de faire écouter le serveur mysql ou postgresql avec les acl nécessaire pour que A puisse l'utiliser. +Sur le serveur mysql ou postgresl, il est nécessaire de créer une base de +donnée re2o, ainsi qu'un user re2o et un mot de passe associé. +Ne pas oublier de faire écouter le serveur mysql ou postgresql avec les acl +nécessaire pour que A puisse l'utiliser. +#### Mysql Voici les étapes à éxecuter pour mysql : * CREATE DATABASE re2o collate='utf8_general_ci'; * CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'password'; * GRANT ALL PRIVILEGES ON re2o.* TO 'newuser'@'localhost'; * FLUSH PRIVILEGES; -Si les serveurs A et B ne sont pas la même machine, il est nécessaire de remplacer localhost par l'ip avec laquelle A contacte B dans les commandes du dessus. -Une fois ces commandes effectuées, ne pas oublier de vérifier que newuser et password sont présents dans settings_local.py +#### Postgresql + * CREATE DATABASE re2o ENCODING 'UTF8' LC_COLLATE='fr_FR.UTF-8' +LC_CTYPE='fr_FR.UTF-8'; + * CREATE USER newuser with password 'password'; + * ALTER DATABASE re2o owner to newuser; + +Si les serveurs A et B ne sont pas la même machine, il est nécessaire de +remplacer localhost par l'ip avec laquelle A contacte B dans les commandes +du dessus. +Une fois ces commandes effectuées, ne pas oublier de vérifier que newuser et +password sont présents dans settings_local.py ### Installation du serveur ldap sur le serveur C Ceci se fait en plusieurs étapes : * générer un login/mdp administrateur (par example mkpasswd sous debian) - * Copier depuis re2o/install_utils (dans le dépot re2o) les fichiers db.ldiff et schema.ldiff (normalement sur le serveur A) sur le serveur C (par ex dans /tmp) - * Hasher le mot de passe généré en utilisant la commande slappasswd (installée par slapd) - * Remplacer toutes les sections FILL_IN par le hash dans schema.ldiff et db.ldiff - * Remplacer dans schema.ldiff et db.ldiff 'dc=example,dc=org' par le suffixe de l'organisation + * Copier depuis re2o/install_utils (dans le dépot re2o) les fichiers db.ldiff +et schema.ldiff (normalement sur le serveur A) sur le serveur C +(par ex dans /tmp) + * Hasher le mot de passe généré en utilisant la commande slappasswd +(installée par slapd) + * Remplacer toutes les sections FILL_IN par le hash dans schema.ldiff et +db.ldiff + * Remplacer dans schema.ldiff et db.ldiff 'dc=example,dc=org' par le +suffixe de l'organisation * Arréter slapd - * Supprimer les données existantes : '''rm -rf /etc/ldap/slapd.d/*''' et '''rm -rf /var/lib/ldap/*''' - * Injecter le nouveau schéma : '''slapadd -n 0 -l schema.ldiff -F /etc/ldap/slapd.d/''' et '''slapadd -n 1 -l db.ldiff''' - * Réparer les permissions (chown -R openldap:openldap /etc/ldap/slapd.d et chown -R openldap:openldap /var/lib/ldap) puis relancer slapd + * Supprimer les données existantes : '''rm -rf /etc/ldap/slapd.d/*''' et +'''rm -rf /var/lib/ldap/*''' + * Injecter le nouveau schéma : +'''slapadd -n 0 -l schema.ldiff -F /etc/ldap/slapd.d/''' et +'''slapadd -n 1 -l db.ldiff''' + * Réparer les permissions (chown -R openldap:openldap /etc/ldap/slapd.d et +chown -R openldap:openldap /var/lib/ldap) puis relancer slapd -Normalement le serveur ldap démare et est fonctionnel. Par défaut tls n'est pas activé, il faut pour cela modifier le schéma pour indiquer l'emplacement du certificat. -Pour visualiser et éditer le ldap, l'utilisation de shelldap est fortement recommandée, en utilisant en binddn cn=admin,dc=ldap,dc=example,dc=org et binddpw le mot de passe admin. +Pour visualiser et éditer le ldap, l'utilisation de shelldap est fortement +recommandée, en utilisant en binddn et basedn tous deux égaux à 'cn=config' et +binddpw le mot de passe admin. + +Rajouter (exemple de chemin de fichier avec un certif LE): +`olcTLSCertificateKeyFile: /etc/letsencrypt/live/HOSTNAME/privkey.pem +olcTLSCACertificateFile: /etc/letsencrypt/live/HOSTNAME/chain.pem +olcTLSCertificateFile: /etc/letsencrypt/live/HOSTNAME/cert.pem ` + +Mettre à jour la partie ldap du `settings_local.py` (mettre 'TLS' à True +si besoin, user cn=config,dc=example,dc=org et mot de passe +ldap choisi précédemment). ## Configuration initiale Normalement à cette étape, le ldap et la bdd sql sont configurées correctement. -Il faut alors lancer dans le dépot re2o '''python3 manage.py migrate''' qui va structurer initialement la base de données. -Les migrations sont normalement comitées au fur et à mesure, néanmoins cette étape peut crasher, merci de reporter les bugs. +Il faut alors lancer dans le dépot re2o '''python3 manage.py migrate''' qui +va structurer initialement la base de données. +Les migrations sont normalement comitées au fur et à mesure, néanmoins cette +étape peut crasher, merci de reporter les bugs. ## Démarer le site web -Il faut utiliser un moteur pour servir le site web. Nginx ou apache2 sont recommandés. +Il faut utiliser un moteur pour servir le site web. Nginx ou apache2 sont +recommandés. Pour apache2 : * apt install apache2 * apt install libapache2-mod-wsgi-py3 (pour le module wsgi) @@ -119,9 +169,12 @@ re2o/wsgi.py permet de fonctionner avec apache2 en production ## Configuration avancée Une fois démaré, le site web devrait être accessible. -Pour créer un premier user, faire '''python3 manage.py createsuperuser''' qui va alors créer un user admin. -Il est conseillé de créer alors les droits cableur, bureau, trésorier et infra, qui n'existent pas par défaut dans le menu adhérents. -Il est également conseillé de créer un user portant le nom de l'association/organisation, qui possedera l'ensemble des machines. +Pour créer un premier user, faire '''python3 manage.py createsuperuser''' +qui va alors créer un user admin. +Il est conseillé de créer alors les droits cableur, bureau, trésorier et infra, +qui n'existent pas par défaut dans le menu adhérents. +Il est également conseillé de créer un user portant le nom de +l'association/organisation, qui possedera l'ensemble des machines. ## Installations Optionnelles ### Générer le schéma des dépendances @@ -134,24 +187,37 @@ Pour cela : ## Fonctionnement général -Re2o est séparé entre les models, qui sont visible sur le schéma des dépendances. Il s'agit en réalité des tables sql, et les fields etant les colonnes. -Ceci dit il n'est jamais nécessaire de toucher directement au sql, django procédant automatiquement à tout cela. -On crée donc différents models (user, right pour les droits des users, interfaces, IpList pour l'ensemble des adresses ip, etc) +Re2o est séparé entre les models, qui sont visible sur le schéma des +dépendances. Il s'agit en réalité des tables sql, et les fields etant les +colonnes. +Ceci dit il n'est jamais nécessaire de toucher directement au sql, django +procédant automatiquement à tout cela. +On crée donc différents models (user, right pour les droits des users, +interfaces, IpList pour l'ensemble des adresses ip, etc) -Du coté des forms, il s'agit des formulaire d'édition des models. Il s'agit de ModelForms django, qui héritent des models très simplement, voir la documentation django models forms. +Du coté des forms, il s'agit des formulaire d'édition des models. Il +s'agit de ModelForms django, qui héritent des models très simplement, voir la +documentation django models forms. Enfin les views, générent les pages web à partir des forms et des templates. ## Fonctionnement avec les services Les services dhcp.py, dns.py etc accèdent aux données via des vues rest. -Celles-ci se trouvent dans machines/views.py. Elles sont générées via machines/serializers.py qui génère les vues. IL s'agit de vues en json utilisées par re2o-tools pour récupérer les données. -Il est nécessaire de créer un user dans re2o avec le droit serveur qui permet d'accéder à ces vues, utilisé par re2o-tools. +Celles-ci se trouvent dans machines/views.py. Elles sont générées via +machines/serializers.py qui génère les vues. IL s'agit de vues en json utilisées +par re2o-tools pour récupérer les données. +Il est nécessaire de créer un user dans re2o avec le droit serveur qui permet +d'accéder à ces vues, utilisé par re2o-tools. # Requète en base de donnée Pour avoir un shell, il suffit de lancer '''python3 manage.py shell''' -Pour charger des objets, example avec User, faire : ''' from users.models import User''' -Pour charger les objets django, il suffit de faire User.objects.all() pour tous les users par exemple. -Il est ensuite aisé de faire des requètes, par exemple User.objects.filter(pseudo='test') -Des exemples et la documentation complète sur les requètes django sont disponible sur le site officiel. +Pour charger des objets, example avec User, faire : +''' from users.models import User''' +Pour charger les objets django, il suffit de faire User.objects.all() +pour tous les users par exemple. +Il est ensuite aisé de faire des requètes, par exemple +User.objects.filter(pseudo='test') +Des exemples et la documentation complète sur les requètes django sont +disponible sur le site officiel. diff --git a/cotisations/views.py b/cotisations/views.py index 1e64e9e7..0a2a7648 100644 --- a/cotisations/views.py +++ b/cotisations/views.py @@ -109,7 +109,7 @@ def new_facture(request, userid): return form({'factureform': facture_form, 'venteform': article_formset, 'articlelist': article_list}, 'cotisations/new_facture.html', request) @login_required -@permission_required('trésorier') +@permission_required('tresorier') def new_facture_pdf(request): facture_form = NewFactureFormPdf(request.POST or None) if facture_form.is_valid(): @@ -156,7 +156,7 @@ def edit_facture(request, factureid): except Facture.DoesNotExist: messages.error(request, u"Facture inexistante" ) return redirect("/cotisations/") - if request.user.has_perms(['trésorier']): + if request.user.has_perms(['tresorier']): facture_form = TrezEditFactureForm(request.POST or None, instance=facture) elif facture.control or not facture.valid: messages.error(request, "Vous ne pouvez pas editer une facture controlée ou invalidée par le trésorier") @@ -223,7 +223,7 @@ def credit_solde(request, userid): @login_required -@permission_required('trésorier') +@permission_required('tresorier') def add_article(request): article = ArticleForm(request.POST or None) if article.is_valid(): @@ -236,7 +236,7 @@ def add_article(request): return form({'factureform': article}, 'cotisations/facture.html', request) @login_required -@permission_required('trésorier') +@permission_required('tresorier') def edit_article(request, articleid): try: article_instance = Article.objects.get(pk=articleid) @@ -254,7 +254,7 @@ def edit_article(request, articleid): return form({'factureform': article}, 'cotisations/facture.html', request) @login_required -@permission_required('trésorier') +@permission_required('tresorier') def del_article(request): article = DelArticleForm(request.POST or None) if article.is_valid(): @@ -267,7 +267,7 @@ def del_article(request): return form({'factureform': article}, 'cotisations/facture.html', request) @login_required -@permission_required('trésorier') +@permission_required('tresorier') def add_paiement(request): paiement = PaiementForm(request.POST or None) if paiement.is_valid(): @@ -280,7 +280,7 @@ def add_paiement(request): return form({'factureform': paiement}, 'cotisations/facture.html', request) @login_required -@permission_required('trésorier') +@permission_required('tresorier') def edit_paiement(request, paiementid): try: paiement_instance = Paiement.objects.get(pk=paiementid) @@ -298,7 +298,7 @@ def edit_paiement(request, paiementid): return form({'factureform': paiement}, 'cotisations/facture.html', request) @login_required -@permission_required('trésorier') +@permission_required('tresorier') def del_paiement(request): paiement = DelPaiementForm(request.POST or None) if paiement.is_valid(): @@ -329,7 +329,7 @@ def add_banque(request): return form({'factureform': banque}, 'cotisations/facture.html', request) @login_required -@permission_required('trésorier') +@permission_required('tresorier') def edit_banque(request, banqueid): try: banque_instance = Banque.objects.get(pk=banqueid) @@ -347,7 +347,7 @@ def edit_banque(request, banqueid): return form({'factureform': banque}, 'cotisations/facture.html', request) @login_required -@permission_required('trésorier') +@permission_required('tresorier') def del_banque(request): banque = DelBanqueForm(request.POST or None) if banque.is_valid(): @@ -365,7 +365,7 @@ def del_banque(request): return form({'factureform': banque}, 'cotisations/facture.html', request) @login_required -@permission_required('trésorier') +@permission_required('tresorier') def control(request): options, created = GeneralOption.objects.get_or_create() pagination_number = options.pagination_number diff --git a/freeradius_utils/auth.py b/freeradius_utils/auth.py index 64ddbc9d..5ea4e48c 100644 --- a/freeradius_utils/auth.py +++ b/freeradius_utils/auth.py @@ -257,12 +257,15 @@ def check_user_machine_and_register(nas_type, username, mac_address): return (True, u'Access Ok, Capture de la mac...', user.pwd_ntlm) else: return (False, u'Erreur dans le register mac %s' % reason, '') + else: + return (False, u'Machine inconnue', '') else: return (False, u"Machine inconnue", '') def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address): # Get port from switch and port number + extra_log = "" if not nas: return ('?', u'Nas inconnu', VLAN_OK) @@ -273,9 +276,15 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address): return (sw_name, u'Port inconnu', VLAN_OK) port = port.first() + # Si un vlan a été précisé, on l'utilise pour VLAN_OK + if port.vlan_force: + DECISION_VLAN = int(port.vlan_force.vlan_id) + extra_log = u"Force sur vlan " + str(DECISION_VLAN) + else: + DECISION_VLAN = VLAN_OK if port.radius == 'NO': - return (sw_name, u"Pas d'authentification sur ce port", VLAN_OK) + return (sw_name, u"Pas d'authentification sur ce port" + extra_log, DECISION_VLAN) if port.radius == 'BLOQ': return (sw_name, u'Port desactive', VLAN_NOK) @@ -309,16 +318,12 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address): else: result, reason = room_user.first().autoregister_machine(mac_address, nas_type) if result: - return (sw_name, u'Access Ok, Capture de la mac...', VLAN_OK) + return (sw_name, u'Access Ok, Capture de la mac...' + extra_log, DECISION_VLAN) else: return (sw_name, u'Erreur dans le register mac %s' % reason + unicode(mac_address), VLAN_NOK) elif not interface.first().is_active: return (sw_name, u'Machine non active / adherent non cotisant', VLAN_NOK) else: - return (sw_name, u'Machine OK', VLAN_OK) - - # On gere bien tous les autres états possibles, il ne reste que le VLAN en dur - return (sw_name, u'VLAN impose', int(port.radius)) - + return (sw_name, u'Machine OK' + extra_log, DECISION_VLAN) diff --git a/logs/views.py b/logs/views.py index 17b92947..d84a2f43 100644 --- a/logs/views.py +++ b/logs/views.py @@ -106,7 +106,7 @@ def index(request): 'user_id': v.revision.user_id, 'version': v } else : - to_remove.append(i) + to_remove.insert(0,i) # Remove all tagged invalid items for i in to_remove : versions.object_list.pop(i) diff --git a/machines/admin.py b/machines/admin.py index 5fe4c49b..49b02a7e 100644 --- a/machines/admin.py +++ b/machines/admin.py @@ -26,7 +26,9 @@ from __future__ import unicode_literals from django.contrib import admin from reversion.admin import VersionAdmin -from .models import IpType, Machine, MachineType, Domain, IpList, Interface, Extension, Mx, Ns, Vlan, Text, Nas, Service +from .models import IpType, Machine, MachineType, Domain, IpList, Interface +from .models import Extension, Mx, Ns, Vlan, Text, Nas, Service, OuverturePort +from .models import OuverturePortList class MachineAdmin(VersionAdmin): pass @@ -58,6 +60,12 @@ class NasAdmin(VersionAdmin): class IpListAdmin(VersionAdmin): pass +class OuverturePortAdmin(VersionAdmin): + pass + +class OuverturePortListAdmin(VersionAdmin): + pass + class InterfaceAdmin(VersionAdmin): list_display = ('machine','type','mac_address','ipv4','details') @@ -80,3 +88,7 @@ admin.site.register(Domain, DomainAdmin) admin.site.register(Service, ServiceAdmin) admin.site.register(Vlan, VlanAdmin) admin.site.register(Nas, NasAdmin) +admin.site.register(OuverturePort, OuverturePortAdmin) +admin.site.register(OuverturePortList, OuverturePortListAdmin) + + diff --git a/machines/forms.py b/machines/forms.py index c25ed1f3..ca94414e 100644 --- a/machines/forms.py +++ b/machines/forms.py @@ -24,9 +24,11 @@ from __future__ import unicode_literals +import re + from django.forms import ModelForm, Form, ValidationError from django import forms -from .models import Domain, Machine, Interface, IpList, MachineType, Extension, Mx, Text, Ns, Service, Vlan, Nas, IpType +from .models import Domain, Machine, Interface, IpList, MachineType, Extension, Mx, Text, Ns, Service, Vlan, Nas, IpType, OuverturePortList, OuverturePort from django.db.models import Q, F from django.core.validators import validate_email @@ -116,11 +118,11 @@ class DomainForm(AliasForm): fields = ['name'] def __init__(self, *args, **kwargs): - if 'name_user' in kwargs: - name_user = kwargs.pop('name_user') + if 'user' in kwargs: + user = kwargs.pop('user') nb_machine = kwargs.pop('nb_machine') initial = kwargs.get('initial', {}) - initial['name'] = name_user.lower()+str(nb_machine) + initial['name'] = user.get_next_domain_name() kwargs['initial'] = initial super(DomainForm, self).__init__(*args, **kwargs) @@ -148,7 +150,7 @@ class DelMachineTypeForm(Form): class IpTypeForm(ModelForm): class Meta: model = IpType - fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'vlan'] + fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'prefix_v6', 'vlan'] def __init__(self, *args, **kwargs): @@ -157,7 +159,7 @@ class IpTypeForm(ModelForm): class EditIpTypeForm(IpTypeForm): class Meta(IpTypeForm.Meta): - fields = ['extension','type','need_infra', 'vlan'] + fields = ['extension','type','need_infra', 'prefix_v6', 'vlan'] class DelIpTypeForm(Form): iptypes = forms.ModelMultipleChoiceField(queryset=IpType.objects.all(), label="Types d'ip actuelles", widget=forms.CheckboxSelectMultiple) @@ -238,5 +240,13 @@ class VlanForm(ModelForm): class DelVlanForm(Form): vlan = forms.ModelMultipleChoiceField(queryset=Vlan.objects.all(), label="Vlan actuels", widget=forms.CheckboxSelectMultiple) +class EditOuverturePortConfigForm(ModelForm): + class Meta: + model = Interface + fields = ['port_lists'] +class EditOuverturePortListForm(ModelForm): + class Meta: + model = OuverturePortList + fields = '__all__' diff --git a/machines/migrations/0058_auto_20171002_0350.py b/machines/migrations/0058_auto_20171002_0350.py new file mode 100644 index 00000000..bc6b2508 --- /dev/null +++ b/machines/migrations/0058_auto_20171002_0350.py @@ -0,0 +1,43 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-02 01:50 +from __future__ import unicode_literals + +from django.db import migrations, models +import django.db.models.deletion + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0057_nas_autocapture_mac'), + ] + + operations = [ + migrations.CreateModel( + name='OuverturePort', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('begin', models.IntegerField()), + ('end', models.IntegerField()), + ('protocole', models.CharField(choices=[('T', 'TCP'), ('U', 'UDP')], default='T', max_length=1)), + ('io', models.CharField(choices=[('I', 'IN'), ('O', 'OUT')], default='O', max_length=1)), + ], + ), + migrations.CreateModel( + name='OuverturePortList', + fields=[ + ('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')), + ('name', models.CharField(help_text='Nom de la configuration des ports.', max_length=255)), + ], + ), + migrations.AddField( + model_name='ouvertureport', + name='port_list', + field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='machines.OuverturePortList'), + ), + migrations.AddField( + model_name='interface', + name='port_lists', + field=models.ManyToManyField(blank=True, to='machines.OuverturePortList'), + ), + ] diff --git a/machines/migrations/0059_iptype_prefix_v6.py b/machines/migrations/0059_iptype_prefix_v6.py new file mode 100644 index 00000000..464fc5e6 --- /dev/null +++ b/machines/migrations/0059_iptype_prefix_v6.py @@ -0,0 +1,20 @@ +# -*- coding: utf-8 -*- +# Generated by Django 1.10.7 on 2017-10-02 16:33 +from __future__ import unicode_literals + +from django.db import migrations, models + + +class Migration(migrations.Migration): + + dependencies = [ + ('machines', '0058_auto_20171002_0350'), + ] + + operations = [ + migrations.AddField( + model_name='iptype', + name='prefix_v6', + field=models.GenericIPAddressField(blank=True, null=True, protocol='IPv6'), + ), + ] diff --git a/machines/models.py b/machines/models.py index 8eff89fc..698fdb90 100644 --- a/machines/models.py +++ b/machines/models.py @@ -56,6 +56,7 @@ class MachineType(models.Model): ip_type = models.ForeignKey('IpType', on_delete=models.PROTECT, blank=True, null=True) def all_interfaces(self): + """ Renvoie toutes les interfaces (cartes réseaux) de type machinetype""" return Interface.objects.filter(type=self) def __str__(self): @@ -70,27 +71,36 @@ class IpType(models.Model): need_infra = models.BooleanField(default=False) domaine_ip_start = models.GenericIPAddressField(protocol='IPv4') domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4') + prefix_v6 = models.GenericIPAddressField(protocol='IPv6', null=True, blank=True) vlan = models.ForeignKey('Vlan', on_delete=models.PROTECT, blank=True, null=True) @cached_property def ip_range(self): - return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop) + """ Renvoie un objet IPRange à partir de l'objet IpType""" + return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop) @cached_property def ip_set(self): + """ Renvoie une IPSet à partir de l'iptype""" return IPSet(self.ip_range) @cached_property def ip_set_as_str(self): + """ Renvoie une liste des ip en string""" return [str(x) for x in self.ip_set] def ip_objects(self): + """ Renvoie tous les objets ipv4 relié à ce type""" return IpList.objects.filter(ip_type=self) def free_ip(self): + """ Renvoie toutes les ip libres associées au type donné (self)""" return IpList.objects.filter(interface__isnull=True).filter(ip_type=self) def gen_ip_range(self): + """ 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""" # Creation du range d'ip dans les objets iplist networks = [] for net in self.ip_range.cidrs(): @@ -113,6 +123,11 @@ class IpType(models.Model): ip.delete() def clean(self): + """ 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""" 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 @@ -122,6 +137,9 @@ class IpType(models.Model): for element in IpType.objects.all().exclude(pk=self.pk): if not self.ip_set.isdisjoint(element.ip_set): raise ValidationError("Le range indiqué n'est pas disjoint des ranges existants") + # On formate le prefix v6 + if self.prefix_v6: + self.prefix_v6 = str(IPNetwork(self.prefix_v6 + '/64').network) return def save(self, *args, **kwargs): @@ -132,6 +150,7 @@ class IpType(models.Model): return self.type class Vlan(models.Model): + """ Un vlan : vlan_id et nom""" PRETTY_NAME = "Vlans" vlan_id = models.IntegerField() @@ -142,6 +161,9 @@ class Vlan(models.Model): return self.name class Nas(models.Model): + """ Les nas. Associé à un machine_type. + 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""" PRETTY_NAME = "Correspondance entre les nas et les machines connectées" default_mode = '802.1X' @@ -160,6 +182,8 @@ class Nas(models.Model): return self.name class Extension(models.Model): + """ Extension dns type example.org. Précise si tout le monde peut l'utiliser, + associé à un origin (ip d'origine)""" PRETTY_NAME = "Extensions dns" name = models.CharField(max_length=255, unique=True) @@ -168,12 +192,15 @@ class Extension(models.Model): @cached_property def dns_entry(self): + """ Une entrée DNS A""" return "@ IN A " + str(self.origin) def __str__(self): return self.name class Mx(models.Model): + """ Entrées des MX. Enregistre la zone (extension) associée et la priorité + Todo : pouvoir associer un MX à une interface """ PRETTY_NAME = "Enregistrements MX" zone = models.ForeignKey('Extension', on_delete=models.PROTECT) @@ -201,6 +228,7 @@ class Ns(models.Model): return str(self.zone) + ' ' + str(self.ns) class Text(models.Model): + """ Un enregistrement TXT associé à une extension""" PRETTY_NAME = "Enregistrement text" zone = models.ForeignKey('Extension', on_delete=models.PROTECT) @@ -215,14 +243,20 @@ class Text(models.Model): return str(self.field1) + " IN TXT " + str(self.field2) class Interface(models.Model): + """ Une interface. Objet clef de l'application machine : + - une address mac unique. Possibilité de la rendre unique avec le typemachine + - 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""" PRETTY_NAME = "Interface" ipv4 = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True) - #ipv6 = models.GenericIPAddressField(protocol='IPv6', null=True) mac_address = MACAddressField(integer=False, unique=True) machine = models.ForeignKey('Machine', on_delete=models.CASCADE) type = models.ForeignKey('MachineType', on_delete=models.PROTECT) details = models.CharField(max_length=255, blank=True) + port_lists = models.ManyToManyField('OuverturePortList', blank=True) @cached_property def is_active(self): @@ -231,16 +265,34 @@ class Interface(models.Model): user = self.machine.user return machine.active and user.has_access() + + @cached_property + def ipv6_object(self): + """ Renvoie un objet type ipv6 à partir du prefix associé à l'iptype parent""" + if self.type.ip_type.prefix_v6: + return EUI(self.mac_address).ipv6(IPNetwork(self.type.ip_type.prefix_v6).network) + else: + return None + + @cached_property + def ipv6(self): + """ Renvoie l'ipv6 en str. Mise en cache et propriété de l'objet""" + return str(self.ipv6_object) + def mac_bare(self): + """ Formatage de la mac type mac_bare""" return str(EUI(self.mac_address, dialect=mac_bare)).lower() def filter_macaddress(self): + """ Tente un formatage mac_bare, si échoue, lève une erreur de validation""" try: self.mac_address = str(EUI(self.mac_address)) except : raise ValidationError("La mac donnée est invalide") def clean(self, *args, **kwargs): + """ Formate l'addresse mac en mac_bare (fonction filter_mac) + et assigne une ipv4 dans le bon range si inexistante ou incohérente""" self.filter_macaddress() self.mac_address = str(EUI(self.mac_address)) or None if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type: @@ -257,6 +309,7 @@ class Interface(models.Model): return def unassign_ipv4(self): + """ Sans commentaire, désassigne une ipv4""" self.ipv4 = None def update_type(self): @@ -278,7 +331,21 @@ class Interface(models.Model): domain = None return str(domain) + def has_private_ip(self): + """ True si l'ip associée est privée""" + if self.ipv4: + return IPAddress(str(self.ipv4)).is_private() + else: + return False + + def may_have_port_open(self): + """ True si l'interface a une ip et une ip publique. + Permet de ne pas exporter des ouvertures sur des ip privées (useless)""" + return self.ipv4 and not self.has_private_ip() + class Domain(models.Model): + """ 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""" PRETTY_NAME = "Domaine dns" interface_parent = models.OneToOneField('Interface', on_delete=models.CASCADE, blank=True, null=True) @@ -290,6 +357,8 @@ class Domain(models.Model): unique_together = (("name", "extension"),) def get_extension(self): + """ Retourne l'extension de l'interface parente si c'est un A + Retourne l'extension propre si c'est un cname, renvoie None sinon""" if self.interface_parent: return self.interface_parent.type.ip_type.extension elif hasattr(self,'extension'): @@ -298,6 +367,11 @@ class Domain(models.Model): return None def clean(self): + """ Validation : + - l'objet est bien soit A soit CNAME + - le cname est pas pointé sur lui-même + - le nom contient bien les caractères autorisés par la norme dns et moins de 63 caractères au total + - le couple nom/extension est bien unique""" if self.get_extension(): self.extension=self.get_extension() """ Validation du nom de domaine, extensions dans type de machine, prefixe pas plus long que 63 caractères """ @@ -316,10 +390,12 @@ class Domain(models.Model): @cached_property def dns_entry(self): + """ Une entrée DNS""" if self.cname: return str(self.name) + " IN CNAME " + str(self.cname) + "." def save(self, *args, **kwargs): + """ Empèche le save sans extension valide. Force à avoir appellé clean avant""" if not self.get_extension(): raise ValidationError("Extension invalide") self.full_clean() @@ -336,9 +412,11 @@ class IpList(models.Model): @cached_property def need_infra(self): + """ Permet de savoir si un user basique peut assigner cette ip ou non""" return self.ip_type.need_infra def clean(self): + """ Erreur si l'ip_type est incorrect""" if not str(self.ipv4) in self.ip_type.ip_set_as_str: raise ValidationError("L'ipv4 et le range de l'iptype ne correspondent pas!") return @@ -406,6 +484,67 @@ class Service_link(models.Model): def __str__(self): return str(self.server) + " " + str(self.service) + +class OuverturePortList(models.Model): + """Liste des ports ouverts sur une interface.""" + name = models.CharField(help_text="Nom de la configuration des ports.", max_length=255) + + def __str__(self): + return self.name + + def tcp_ports_in(self): + return self.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.IN) + + def udp_ports_in(self): + return self.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.IN) + + def tcp_ports_out(self): + return self.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.OUT) + + def udp_ports_out(self): + return self.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.OUT) + + +class OuverturePort(models.Model): + """ + Représente un simple port ou une plage de ports. + + Les ports de la plage sont compris entre begin et en inclus. + Si begin == end alors on ne représente qu'un seul port. + """ + TCP = 'T' + UDP = 'U' + IN = 'I' + OUT = 'O' + begin = models.IntegerField() + end = models.IntegerField() + port_list = models.ForeignKey('OuverturePortList', on_delete=models.CASCADE) + protocole = models.CharField( + max_length=1, + choices=( + (TCP, 'TCP'), + (UDP, 'UDP'), + ), + default=TCP, + ) + io = models.CharField( + max_length=1, + choices=( + (IN, 'IN'), + (OUT, 'OUT'), + ), + default=OUT, + ) + + def __str__(self): + if self.begin == self.end : + return str(self.begin) + return '-'.join([str(self.begin), str(self.end)]) + + def show_port(self): + return str(self) + + @receiver(post_save, sender=Machine) def machine_post_save(sender, **kwargs): user = kwargs['instance'].user @@ -426,6 +565,9 @@ def interface_post_save(sender, **kwargs): interface = kwargs['instance'] user = interface.machine.user user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) + if not interface.may_have_port_open() and interface.port_lists.all(): + interface.port_lists.clear() + # Regen services regen('dhcp') regen('mac_ip_list') diff --git a/machines/serializers.py b/machines/serializers.py index fe59c4de..2cf3d3e8 100644 --- a/machines/serializers.py +++ b/machines/serializers.py @@ -56,6 +56,25 @@ class InterfaceSerializer(serializers.ModelSerializer): def get_macaddress(self, obj): return str(obj.mac_address) +class FullInterfaceSerializer(serializers.ModelSerializer): + ipv4 = IpListSerializer(read_only=True) + mac_address = serializers.SerializerMethodField('get_macaddress') + domain = serializers.SerializerMethodField('get_dns') + extension = serializers.SerializerMethodField('get_interface_extension') + + class Meta: + model = Interface + fields = ('ipv4', 'ipv6', 'mac_address', 'domain', 'extension') + + def get_dns(self, obj): + return obj.domain.name + + def get_interface_extension(self, obj): + return obj.domain.extension.name + + def get_macaddress(self, obj): + return str(obj.mac_address) + class ExtensionNameField(serializers.RelatedField): def to_representation(self, value): return value.name diff --git a/machines/templates/machines/aff_iptype.html b/machines/templates/machines/aff_iptype.html index 46318c83..aafc4c1d 100644 --- a/machines/templates/machines/aff_iptype.html +++ b/machines/templates/machines/aff_iptype.html @@ -30,6 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Nécessite l'autorisation infra Début Fin + Préfixe v6 Sur vlan @@ -42,6 +43,7 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ type.need_infra }} {{ type.domaine_ip_start }} {{ type.domaine_ip_stop }} + {{ type.prefix_v6 }} {{ type.vlan }} {% if is_infra %} diff --git a/machines/templates/machines/aff_machines.html b/machines/templates/machines/aff_machines.html index a09d8868..3e26f64b 100644 --- a/machines/templates/machines/aff_machines.html +++ b/machines/templates/machines/aff_machines.html @@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc., Nom dns Type Mac - Ipv4 + IP @@ -74,7 +74,12 @@ with this program; if not, write to the Free Software Foundation, Inc., {{ interface.type }} {{ interface.mac_address }} - {{ interface.ipv4 }} + IPv4 {{ interface.ipv4 }} + {% if ipv6_enabled %} +
+ IPv6 {{ interface.ipv6 }} + {% endif %} +