8
0
Fork 0
mirror of https://gitlab2.federez.net/re2o/re2o synced 2024-11-22 19:33:11 +00:00

Merge branch 'master' into faster_ipform

This commit is contained in:
Maël Kervella 2017-10-07 01:11:56 +00:00
commit af6df474ba
34 changed files with 923 additions and 108 deletions

136
README.md
View file

@ -4,17 +4,28 @@ Gnu public license v2.0
## Avant propos ## 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. Il utilise le framework django avec python3. Il permet de gérer les adhérents,
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). 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
## Installation des dépendances ## 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). L'installation comporte 3 partie : le serveur web où se trouve le depot re2o
Le serveur web sera nommé serveur A, le serveur bdd serveur B et le serveur ldap serveur C. 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 ### Prérequis sur le serveur A
@ -66,49 +77,88 @@ Sur le serveur C (ldap), avec apt :
### Installation sur le serveur principal A ### Installation sur le serveur principal A
Cloner le dépot re2o à partir du gitlab, par exemple dans /var/www/re2o. 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 ### 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 : Voici les étapes à éxecuter pour mysql :
* CREATE DATABASE re2o collate='utf8_general_ci'; * CREATE DATABASE re2o collate='utf8_general_ci';
* CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'password'; * CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'password';
* GRANT ALL PRIVILEGES ON re2o.* TO 'newuser'@'localhost'; * GRANT ALL PRIVILEGES ON re2o.* TO 'newuser'@'localhost';
* FLUSH PRIVILEGES; * 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. #### Postgresql
Une fois ces commandes effectuées, ne pas oublier de vérifier que newuser et password sont présents dans settings_local.py * 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 ### Installation du serveur ldap sur le serveur C
Ceci se fait en plusieurs étapes : Ceci se fait en plusieurs étapes :
* générer un login/mdp administrateur (par example mkpasswd sous debian) * 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) * Copier depuis re2o/install_utils (dans le dépot re2o) les fichiers db.ldiff
* Hasher le mot de passe généré en utilisant la commande slappasswd (installée par slapd) et schema.ldiff (normalement sur le serveur A) sur le serveur C
* Remplacer toutes les sections FILL_IN par le hash dans schema.ldiff et db.ldiff (par ex dans /tmp)
* Remplacer dans schema.ldiff et db.ldiff 'dc=example,dc=org' par le suffixe de l'organisation * 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 * Arréter slapd
* Supprimer les données existantes : '''rm -rf /etc/ldap/slapd.d/*''' et '''rm -rf /var/lib/ldap/*''' * Supprimer les données existantes : '''rm -rf /etc/ldap/slapd.d/*''' et
* Injecter le nouveau schéma : '''slapadd -n 0 -l schema.ldiff -F /etc/ldap/slapd.d/''' et '''slapadd -n 1 -l db.ldiff''' '''rm -rf /var/lib/ldap/*'''
* Réparer les permissions (chown -R openldap:openldap /etc/ldap/slapd.d et chown -R openldap:openldap /var/lib/ldap) puis relancer slapd * 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
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. 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 ## Configuration initiale
Normalement à cette étape, le ldap et la bdd sql sont configurées correctement. 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. Il faut alors lancer dans le dépot re2o '''python3 manage.py migrate''' qui
Les migrations sont normalement comitées au fur et à mesure, néanmoins cette étape peut crasher, merci de reporter les bugs. 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 ## 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 : Pour apache2 :
* apt install apache2 * apt install apache2
* apt install libapache2-mod-wsgi-py3 (pour le module wsgi) * 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 ## Configuration avancée
Une fois démaré, le site web devrait être accessible. 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. Pour créer un premier user, faire '''python3 manage.py createsuperuser'''
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. qui va alors créer un user admin.
Il est également conseillé de créer un user portant le nom de l'association/organisation, qui possedera l'ensemble des machines. 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 ## Installations Optionnelles
### Générer le schéma des dépendances ### Générer le schéma des dépendances
@ -134,24 +187,37 @@ Pour cela :
## Fonctionnement général ## 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. Re2o est séparé entre les models, qui sont visible sur le schéma des
Ceci dit il n'est jamais nécessaire de toucher directement au sql, django procédant automatiquement à tout cela. dépendances. Il s'agit en réalité des tables sql, et les fields etant les
On crée donc différents models (user, right pour les droits des users, interfaces, IpList pour l'ensemble des adresses ip, etc) 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. Enfin les views, générent les pages web à partir des forms et des templates.
## Fonctionnement avec les services ## Fonctionnement avec les services
Les services dhcp.py, dns.py etc accèdent aux données via des vues rest. 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. Celles-ci se trouvent dans machines/views.py. Elles sont générées via
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. 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 # Requète en base de donnée
Pour avoir un shell, il suffit de lancer '''python3 manage.py shell''' 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 des objets, example avec User, faire :
Pour charger les objets django, il suffit de faire User.objects.all() pour tous les users par exemple. ''' from users.models import User'''
Il est ensuite aisé de faire des requètes, par exemple User.objects.filter(pseudo='test') Pour charger les objets django, il suffit de faire User.objects.all()
Des exemples et la documentation complète sur les requètes django sont disponible sur le site officiel. 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.

View file

@ -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) return form({'factureform': facture_form, 'venteform': article_formset, 'articlelist': article_list}, 'cotisations/new_facture.html', request)
@login_required @login_required
@permission_required('trésorier') @permission_required('tresorier')
def new_facture_pdf(request): def new_facture_pdf(request):
facture_form = NewFactureFormPdf(request.POST or None) facture_form = NewFactureFormPdf(request.POST or None)
if facture_form.is_valid(): if facture_form.is_valid():
@ -156,7 +156,7 @@ def edit_facture(request, factureid):
except Facture.DoesNotExist: except Facture.DoesNotExist:
messages.error(request, u"Facture inexistante" ) messages.error(request, u"Facture inexistante" )
return redirect("/cotisations/") 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) facture_form = TrezEditFactureForm(request.POST or None, instance=facture)
elif facture.control or not facture.valid: 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") 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 @login_required
@permission_required('trésorier') @permission_required('tresorier')
def add_article(request): def add_article(request):
article = ArticleForm(request.POST or None) article = ArticleForm(request.POST or None)
if article.is_valid(): if article.is_valid():
@ -236,7 +236,7 @@ def add_article(request):
return form({'factureform': article}, 'cotisations/facture.html', request) return form({'factureform': article}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('trésorier') @permission_required('tresorier')
def edit_article(request, articleid): def edit_article(request, articleid):
try: try:
article_instance = Article.objects.get(pk=articleid) article_instance = Article.objects.get(pk=articleid)
@ -254,7 +254,7 @@ def edit_article(request, articleid):
return form({'factureform': article}, 'cotisations/facture.html', request) return form({'factureform': article}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('trésorier') @permission_required('tresorier')
def del_article(request): def del_article(request):
article = DelArticleForm(request.POST or None) article = DelArticleForm(request.POST or None)
if article.is_valid(): if article.is_valid():
@ -267,7 +267,7 @@ def del_article(request):
return form({'factureform': article}, 'cotisations/facture.html', request) return form({'factureform': article}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('trésorier') @permission_required('tresorier')
def add_paiement(request): def add_paiement(request):
paiement = PaiementForm(request.POST or None) paiement = PaiementForm(request.POST or None)
if paiement.is_valid(): if paiement.is_valid():
@ -280,7 +280,7 @@ def add_paiement(request):
return form({'factureform': paiement}, 'cotisations/facture.html', request) return form({'factureform': paiement}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('trésorier') @permission_required('tresorier')
def edit_paiement(request, paiementid): def edit_paiement(request, paiementid):
try: try:
paiement_instance = Paiement.objects.get(pk=paiementid) paiement_instance = Paiement.objects.get(pk=paiementid)
@ -298,7 +298,7 @@ def edit_paiement(request, paiementid):
return form({'factureform': paiement}, 'cotisations/facture.html', request) return form({'factureform': paiement}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('trésorier') @permission_required('tresorier')
def del_paiement(request): def del_paiement(request):
paiement = DelPaiementForm(request.POST or None) paiement = DelPaiementForm(request.POST or None)
if paiement.is_valid(): if paiement.is_valid():
@ -329,7 +329,7 @@ def add_banque(request):
return form({'factureform': banque}, 'cotisations/facture.html', request) return form({'factureform': banque}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('trésorier') @permission_required('tresorier')
def edit_banque(request, banqueid): def edit_banque(request, banqueid):
try: try:
banque_instance = Banque.objects.get(pk=banqueid) banque_instance = Banque.objects.get(pk=banqueid)
@ -347,7 +347,7 @@ def edit_banque(request, banqueid):
return form({'factureform': banque}, 'cotisations/facture.html', request) return form({'factureform': banque}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('trésorier') @permission_required('tresorier')
def del_banque(request): def del_banque(request):
banque = DelBanqueForm(request.POST or None) banque = DelBanqueForm(request.POST or None)
if banque.is_valid(): if banque.is_valid():
@ -365,7 +365,7 @@ def del_banque(request):
return form({'factureform': banque}, 'cotisations/facture.html', request) return form({'factureform': banque}, 'cotisations/facture.html', request)
@login_required @login_required
@permission_required('trésorier') @permission_required('tresorier')
def control(request): def control(request):
options, created = GeneralOption.objects.get_or_create() options, created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number pagination_number = options.pagination_number

View file

@ -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) return (True, u'Access Ok, Capture de la mac...', user.pwd_ntlm)
else: else:
return (False, u'Erreur dans le register mac %s' % reason, '') return (False, u'Erreur dans le register mac %s' % reason, '')
else:
return (False, u'Machine inconnue', '')
else: else:
return (False, u"Machine inconnue", '') return (False, u"Machine inconnue", '')
def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address): def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address):
# Get port from switch and port number # Get port from switch and port number
extra_log = ""
if not nas: if not nas:
return ('?', u'Nas inconnu', VLAN_OK) 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) return (sw_name, u'Port inconnu', VLAN_OK)
port = port.first() 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': 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': if port.radius == 'BLOQ':
return (sw_name, u'Port desactive', VLAN_NOK) 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: else:
result, reason = room_user.first().autoregister_machine(mac_address, nas_type) result, reason = room_user.first().autoregister_machine(mac_address, nas_type)
if result: 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: else:
return (sw_name, u'Erreur dans le register mac %s' % reason + unicode(mac_address), VLAN_NOK) return (sw_name, u'Erreur dans le register mac %s' % reason + unicode(mac_address), VLAN_NOK)
elif not interface.first().is_active: elif not interface.first().is_active:
return (sw_name, u'Machine non active / adherent non cotisant', VLAN_NOK) return (sw_name, u'Machine non active / adherent non cotisant', VLAN_NOK)
else: else:
return (sw_name, u'Machine OK', VLAN_OK) return (sw_name, u'Machine OK' + extra_log, DECISION_VLAN)
# 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))

View file

@ -106,7 +106,7 @@ def index(request):
'user_id': v.revision.user_id, 'user_id': v.revision.user_id,
'version': v } 'version': v }
else : else :
to_remove.append(i) to_remove.insert(0,i)
# Remove all tagged invalid items # Remove all tagged invalid items
for i in to_remove : for i in to_remove :
versions.object_list.pop(i) versions.object_list.pop(i)

View file

@ -26,7 +26,9 @@ from __future__ import unicode_literals
from django.contrib import admin from django.contrib import admin
from reversion.admin import VersionAdmin 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): class MachineAdmin(VersionAdmin):
pass pass
@ -58,6 +60,12 @@ class NasAdmin(VersionAdmin):
class IpListAdmin(VersionAdmin): class IpListAdmin(VersionAdmin):
pass pass
class OuverturePortAdmin(VersionAdmin):
pass
class OuverturePortListAdmin(VersionAdmin):
pass
class InterfaceAdmin(VersionAdmin): class InterfaceAdmin(VersionAdmin):
list_display = ('machine','type','mac_address','ipv4','details') 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(Service, ServiceAdmin)
admin.site.register(Vlan, VlanAdmin) admin.site.register(Vlan, VlanAdmin)
admin.site.register(Nas, NasAdmin) admin.site.register(Nas, NasAdmin)
admin.site.register(OuverturePort, OuverturePortAdmin)
admin.site.register(OuverturePortList, OuverturePortListAdmin)

View file

@ -24,9 +24,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
import re
from django.forms import ModelForm, Form, ValidationError from django.forms import ModelForm, Form, ValidationError
from django import forms 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.db.models import Q, F
from django.core.validators import validate_email from django.core.validators import validate_email
@ -116,11 +118,11 @@ class DomainForm(AliasForm):
fields = ['name'] fields = ['name']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
if 'name_user' in kwargs: if 'user' in kwargs:
name_user = kwargs.pop('name_user') user = kwargs.pop('user')
nb_machine = kwargs.pop('nb_machine') nb_machine = kwargs.pop('nb_machine')
initial = kwargs.get('initial', {}) initial = kwargs.get('initial', {})
initial['name'] = name_user.lower()+str(nb_machine) initial['name'] = user.get_next_domain_name()
kwargs['initial'] = initial kwargs['initial'] = initial
super(DomainForm, self).__init__(*args, **kwargs) super(DomainForm, self).__init__(*args, **kwargs)
@ -148,7 +150,7 @@ class DelMachineTypeForm(Form):
class IpTypeForm(ModelForm): class IpTypeForm(ModelForm):
class Meta: class Meta:
model = IpType 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): def __init__(self, *args, **kwargs):
@ -157,7 +159,7 @@ class IpTypeForm(ModelForm):
class EditIpTypeForm(IpTypeForm): class EditIpTypeForm(IpTypeForm):
class Meta(IpTypeForm.Meta): class Meta(IpTypeForm.Meta):
fields = ['extension','type','need_infra', 'vlan'] fields = ['extension','type','need_infra', 'prefix_v6', 'vlan']
class DelIpTypeForm(Form): class DelIpTypeForm(Form):
iptypes = forms.ModelMultipleChoiceField(queryset=IpType.objects.all(), label="Types d'ip actuelles", widget=forms.CheckboxSelectMultiple) 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): class DelVlanForm(Form):
vlan = forms.ModelMultipleChoiceField(queryset=Vlan.objects.all(), label="Vlan actuels", widget=forms.CheckboxSelectMultiple) 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__'

View file

@ -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'),
),
]

View file

@ -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'),
),
]

View file

@ -56,6 +56,7 @@ class MachineType(models.Model):
ip_type = models.ForeignKey('IpType', on_delete=models.PROTECT, blank=True, null=True) ip_type = models.ForeignKey('IpType', on_delete=models.PROTECT, blank=True, null=True)
def all_interfaces(self): def all_interfaces(self):
""" Renvoie toutes les interfaces (cartes réseaux) de type machinetype"""
return Interface.objects.filter(type=self) return Interface.objects.filter(type=self)
def __str__(self): def __str__(self):
@ -70,27 +71,36 @@ class IpType(models.Model):
need_infra = models.BooleanField(default=False) need_infra = models.BooleanField(default=False)
domaine_ip_start = models.GenericIPAddressField(protocol='IPv4') domaine_ip_start = models.GenericIPAddressField(protocol='IPv4')
domaine_ip_stop = 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) vlan = models.ForeignKey('Vlan', on_delete=models.PROTECT, blank=True, null=True)
@cached_property @cached_property
def ip_range(self): def ip_range(self):
""" Renvoie un objet IPRange à partir de l'objet IpType"""
return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop) return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop)
@cached_property @cached_property
def ip_set(self): def ip_set(self):
""" Renvoie une IPSet à partir de l'iptype"""
return IPSet(self.ip_range) return IPSet(self.ip_range)
@cached_property @cached_property
def ip_set_as_str(self): def ip_set_as_str(self):
""" Renvoie une liste des ip en string"""
return [str(x) for x in self.ip_set] return [str(x) for x in self.ip_set]
def ip_objects(self): def ip_objects(self):
""" Renvoie tous les objets ipv4 relié à ce type"""
return IpList.objects.filter(ip_type=self) return IpList.objects.filter(ip_type=self)
def free_ip(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) return IpList.objects.filter(interface__isnull=True).filter(ip_type=self)
def gen_ip_range(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 # Creation du range d'ip dans les objets iplist
networks = [] networks = []
for net in self.ip_range.cidrs(): for net in self.ip_range.cidrs():
@ -113,6 +123,11 @@ class IpType(models.Model):
ip.delete() ip.delete()
def clean(self): 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): if IPAddress(self.domaine_ip_start) > IPAddress(self.domaine_ip_stop):
raise ValidationError("Domaine end doit être après start...") raise ValidationError("Domaine end doit être après start...")
# On ne crée pas plus grand qu'un /16 # 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): for element in IpType.objects.all().exclude(pk=self.pk):
if not self.ip_set.isdisjoint(element.ip_set): if not self.ip_set.isdisjoint(element.ip_set):
raise ValidationError("Le range indiqué n'est pas disjoint des ranges existants") 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 return
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
@ -132,6 +150,7 @@ class IpType(models.Model):
return self.type return self.type
class Vlan(models.Model): class Vlan(models.Model):
""" Un vlan : vlan_id et nom"""
PRETTY_NAME = "Vlans" PRETTY_NAME = "Vlans"
vlan_id = models.IntegerField() vlan_id = models.IntegerField()
@ -142,6 +161,9 @@ class Vlan(models.Model):
return self.name return self.name
class Nas(models.Model): 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" PRETTY_NAME = "Correspondance entre les nas et les machines connectées"
default_mode = '802.1X' default_mode = '802.1X'
@ -160,6 +182,8 @@ class Nas(models.Model):
return self.name return self.name
class Extension(models.Model): 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" PRETTY_NAME = "Extensions dns"
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)
@ -168,12 +192,15 @@ class Extension(models.Model):
@cached_property @cached_property
def dns_entry(self): def dns_entry(self):
""" Une entrée DNS A"""
return "@ IN A " + str(self.origin) return "@ IN A " + str(self.origin)
def __str__(self): def __str__(self):
return self.name return self.name
class Mx(models.Model): 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" PRETTY_NAME = "Enregistrements MX"
zone = models.ForeignKey('Extension', on_delete=models.PROTECT) zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
@ -201,6 +228,7 @@ class Ns(models.Model):
return str(self.zone) + ' ' + str(self.ns) return str(self.zone) + ' ' + str(self.ns)
class Text(models.Model): class Text(models.Model):
""" Un enregistrement TXT associé à une extension"""
PRETTY_NAME = "Enregistrement text" PRETTY_NAME = "Enregistrement text"
zone = models.ForeignKey('Extension', on_delete=models.PROTECT) 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) return str(self.field1) + " IN TXT " + str(self.field2)
class Interface(models.Model): 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" PRETTY_NAME = "Interface"
ipv4 = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True) 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) mac_address = MACAddressField(integer=False, unique=True)
machine = models.ForeignKey('Machine', on_delete=models.CASCADE) machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
type = models.ForeignKey('MachineType', on_delete=models.PROTECT) type = models.ForeignKey('MachineType', on_delete=models.PROTECT)
details = models.CharField(max_length=255, blank=True) details = models.CharField(max_length=255, blank=True)
port_lists = models.ManyToManyField('OuverturePortList', blank=True)
@cached_property @cached_property
def is_active(self): def is_active(self):
@ -231,16 +265,34 @@ class Interface(models.Model):
user = self.machine.user user = self.machine.user
return machine.active and user.has_access() 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): def mac_bare(self):
""" Formatage de la mac type mac_bare"""
return str(EUI(self.mac_address, dialect=mac_bare)).lower() return str(EUI(self.mac_address, dialect=mac_bare)).lower()
def filter_macaddress(self): def filter_macaddress(self):
""" Tente un formatage mac_bare, si échoue, lève une erreur de validation"""
try: try:
self.mac_address = str(EUI(self.mac_address)) self.mac_address = str(EUI(self.mac_address))
except : except :
raise ValidationError("La mac donnée est invalide") raise ValidationError("La mac donnée est invalide")
def clean(self, *args, **kwargs): 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.filter_macaddress()
self.mac_address = str(EUI(self.mac_address)) or None self.mac_address = str(EUI(self.mac_address)) or None
if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type: if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
@ -257,6 +309,7 @@ class Interface(models.Model):
return return
def unassign_ipv4(self): def unassign_ipv4(self):
""" Sans commentaire, désassigne une ipv4"""
self.ipv4 = None self.ipv4 = None
def update_type(self): def update_type(self):
@ -278,7 +331,21 @@ class Interface(models.Model):
domain = None domain = None
return str(domain) 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): 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" PRETTY_NAME = "Domaine dns"
interface_parent = models.OneToOneField('Interface', on_delete=models.CASCADE, blank=True, null=True) 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"),) unique_together = (("name", "extension"),)
def get_extension(self): 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: if self.interface_parent:
return self.interface_parent.type.ip_type.extension return self.interface_parent.type.ip_type.extension
elif hasattr(self,'extension'): elif hasattr(self,'extension'):
@ -298,6 +367,11 @@ class Domain(models.Model):
return None return None
def clean(self): 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(): if self.get_extension():
self.extension=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 """ """ 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 @cached_property
def dns_entry(self): def dns_entry(self):
""" Une entrée DNS"""
if self.cname: if self.cname:
return str(self.name) + " IN CNAME " + str(self.cname) + "." return str(self.name) + " IN CNAME " + str(self.cname) + "."
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
""" Empèche le save sans extension valide. Force à avoir appellé clean avant"""
if not self.get_extension(): if not self.get_extension():
raise ValidationError("Extension invalide") raise ValidationError("Extension invalide")
self.full_clean() self.full_clean()
@ -336,9 +412,11 @@ class IpList(models.Model):
@cached_property @cached_property
def need_infra(self): def need_infra(self):
""" Permet de savoir si un user basique peut assigner cette ip ou non"""
return self.ip_type.need_infra return self.ip_type.need_infra
def clean(self): def clean(self):
""" Erreur si l'ip_type est incorrect"""
if not str(self.ipv4) in self.ip_type.ip_set_as_str: 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!") raise ValidationError("L'ipv4 et le range de l'iptype ne correspondent pas!")
return return
@ -406,6 +484,67 @@ class Service_link(models.Model):
def __str__(self): def __str__(self):
return str(self.server) + " " + str(self.service) 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) @receiver(post_save, sender=Machine)
def machine_post_save(sender, **kwargs): def machine_post_save(sender, **kwargs):
user = kwargs['instance'].user user = kwargs['instance'].user
@ -426,6 +565,9 @@ def interface_post_save(sender, **kwargs):
interface = kwargs['instance'] interface = kwargs['instance']
user = interface.machine.user user = interface.machine.user
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True) 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('dhcp')
regen('mac_ip_list') regen('mac_ip_list')

View file

@ -56,6 +56,25 @@ class InterfaceSerializer(serializers.ModelSerializer):
def get_macaddress(self, obj): def get_macaddress(self, obj):
return str(obj.mac_address) 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): class ExtensionNameField(serializers.RelatedField):
def to_representation(self, value): def to_representation(self, value):
return value.name return value.name

View file

@ -30,6 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>Nécessite l'autorisation infra</th> <th>Nécessite l'autorisation infra</th>
<th>Début</th> <th>Début</th>
<th>Fin</th> <th>Fin</th>
<th>Préfixe v6</th>
<th>Sur vlan</th> <th>Sur vlan</th>
<th></th> <th></th>
<th></th> <th></th>
@ -42,6 +43,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<td>{{ type.need_infra }}</td> <td>{{ type.need_infra }}</td>
<td>{{ type.domaine_ip_start }}</td> <td>{{ type.domaine_ip_start }}</td>
<td>{{ type.domaine_ip_stop }}</td> <td>{{ type.domaine_ip_stop }}</td>
<td>{{ type.prefix_v6 }}</td>
<td>{{ type.vlan }}</td> <td>{{ type.vlan }}</td>
<td class="text-right"> <td class="text-right">
{% if is_infra %} {% if is_infra %}

View file

@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>Nom dns</th> <th>Nom dns</th>
<th>Type</th> <th>Type</th>
<th>Mac</th> <th>Mac</th>
<th>Ipv4</th> <th>IP</th>
<th></th> <th></th>
</tr> </tr>
</thead> </thead>
@ -74,7 +74,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
</td> </td>
<td>{{ interface.type }}</td> <td>{{ interface.type }}</td>
<td>{{ interface.mac_address }}</td> <td>{{ interface.mac_address }}</td>
<td>{{ interface.ipv4 }}</td> <td><b>IPv4</b> {{ interface.ipv4 }}
{% if ipv6_enabled %}
<br>
<b>IPv6</b> {{ interface.ipv6 }}
{% endif %}
</td>
<td> <td>
<div class="dropdown"> <div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" id="editioninterface" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true"> <button class="btn btn-default dropdown-toggle" type="button" id="editioninterface" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
@ -91,6 +96,11 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<i class="glyphicon glyphicon-edit"></i> Gerer les alias <i class="glyphicon glyphicon-edit"></i> Gerer les alias
</a> </a>
</li> </li>
<li>
<a href="{% url 'machines:port-config' interface.id%}">
<i class="glyphicon glyphicon-edit"></i> Gerer la configuration des ports
</a>
</li>
<li> <li>
<a href="{% url 'machines:history' 'interface' interface.id %}"> <a href="{% url 'machines:history' 'interface' interface.id %}">
<i class="glyphicon glyphicon-time"></i> Historique <i class="glyphicon glyphicon-time"></i> Historique

View file

@ -0,0 +1,69 @@
{% extends "machines/sidebar.html" %}
{% comment %}
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.
{% endcomment %}
{% load bootstrap3 %}
{% block title %}Création et modification de machines{% endblock %}
{% block content %}
{% bootstrap_form_errors port_list %}
<form class="form" method="post">
{% csrf_token %}
{% bootstrap_form port_list %}
{{ ports.management_form }}
<div id="formset">
{% for form in ports.forms %}
<div class="port">
<p>
{{ form }}
</p>
</div>
{% endfor %}
</div>
<p>
<input class="btn btn-primary btn-sm" role="button" value="Ajouter un port" id="add_one">
</p>
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
</form>
<script type="text/javascript">
var template = `{{ports.empty_form}}`;
function add_port(){
var new_index = document.getElementsByClassName('port').length;
document.getElementById('id_form-TOTAL_FORMS').value =
parseInt(document.getElementById('id_form-TOTAL_FORMS').value) + 1;
var new_port = document.createElement('div');
new_port.className = 'port';
new_port.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('formset').appendChild(new_port);
}
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("add_one").addEventListener("click", add_port, true);});
</script>
{% endblock %}

View file

@ -0,0 +1,57 @@
{% extends "machines/sidebar.html" %}
{% load bootstrap3 %}
{% block title %}Configuration de ports{% endblock %}
{% block content %}
<h2>Liste des configurations de ports</h2>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'machines:add-portlist' %}"><i class="glyphicon glyphicon-plus"></i>Ajouter une configuration</a>
<table class="table table-striped">
<thead>
<tr>
<th>Nom</th>
<th>TCP (entrée)</th>
<th>TCP (sortie)</th>
<th>UDP (entrée)</th>
<th>UDP (sortie)</th>
<th>Machines</th>
<th></th>
</tr>
</thead>
{% for pl in port_list %}
<tr>
<td>{{pl.name}}</td>
<td>{% for p in pl.tcp_ports_in %}{{p.show_port}}, {%endfor%}</td>
<td>{% for p in pl.tcp_ports_out %}{{p.show_port}}, {%endfor%}</td>
<td>{% for p in pl.udp_ports_in %}{{p.show_port}}, {%endfor%}</td>
<td>{% for p in pl.udp_ports_out %}{{p.show_port}}, {%endfor%}</td>
<td>
{% if pl.interface_set.all %}
<div class="dropdown">
<button class="btn btn-default dropdown-toggle" type="button" id="editioninterface" data-toggle="dropdown" aria-haspopup="true" aria-expanded="true">
<span class="caret"></span>
</button>
<ul class="dropdown-menu" aria-labelledby="editioninterface">
{% for interface in pl.interface_set.all %}
<li>
<a href="{% url 'users:profil' userid=interface.machine.user.id %}">
{{ interface }}
</a>
</li>
{% endfor %}
</ul>
</div>
{% endif %}
<td class="text-right">
{% include 'buttons/suppr.html' with href='machines:del-portlist' id=pl.id %}
{% include 'buttons/edit.html' with href='machines:edit-portlist' id=pl.id %}
</td>
</tr>
{%endfor%}
</table>
<br />
<br />
<br />
{% endblock %}

View file

@ -55,4 +55,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Services (dhcp, dns...) Services (dhcp, dns...)
</a> </a>
{% endif %} {% endif %}
{% if is_cableur %}
<a class="list-group-item list-group-item-info" href="{% url "machines:index-portlist" %}">
<i class="glyphicon glyphicon-list"></i>
Configuration de ports
</a>
{%endif%}
{% endblock %} {% endblock %}

View file

@ -93,4 +93,10 @@ urlpatterns = [
url(r'^rest/text/$', views.text, name='text'), url(r'^rest/text/$', views.text, name='text'),
url(r'^rest/zones/$', views.zones, name='zones'), url(r'^rest/zones/$', views.zones, name='zones'),
url(r'^rest/service_servers/$', views.service_servers, name='service-servers'), url(r'^rest/service_servers/$', views.service_servers, name='service-servers'),
url(r'index_portlist/$', views.index_portlist, name='index-portlist'),
url(r'^edit_portlist/(?P<pk>[0-9]+)$', views.edit_portlist, name='edit-portlist'),
url(r'^del_portlist/(?P<pk>[0-9]+)$', views.del_portlist, name='del-portlist'),
url(r'^add_portlist/$', views.add_portlist, name='add-portlist'),
url(r'^port_config/(?P<pk>[0-9]+)$', views.configure_ports, name='port-config'),
] ]

View file

@ -37,20 +37,21 @@ from django.template import Context, RequestContext, loader
from django.contrib import messages from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required from django.contrib.auth.decorators import login_required, permission_required
from django.db.models import ProtectedError from django.db.models import ProtectedError
from django.forms import ValidationError from django.forms import ValidationError, modelformset_factory
from django.db import transaction from django.db import transaction
from django.contrib.auth import authenticate, login from django.contrib.auth import authenticate, login
from django.views.decorators.csrf import csrf_exempt from django.views.decorators.csrf import csrf_exempt
from rest_framework.renderers import JSONRenderer from rest_framework.renderers import JSONRenderer
from machines.serializers import InterfaceSerializer, TypeSerializer, DomainSerializer, TextSerializer, MxSerializer, ExtensionSerializer, ServiceServersSerializer, NsSerializer from machines.serializers import FullInterfaceSerializer, InterfaceSerializer, TypeSerializer, DomainSerializer, TextSerializer, MxSerializer, ExtensionSerializer, ServiceServersSerializer, NsSerializer
from reversion import revisions as reversion from reversion import revisions as reversion
from reversion.models import Version from reversion.models import Version
import re import re
from .forms import NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm, MachineTypeForm, DelMachineTypeForm, ExtensionForm, DelExtensionForm, BaseEditInterfaceForm, BaseEditMachineForm from .forms import NewMachineForm, EditMachineForm, EditInterfaceForm, AddInterfaceForm, MachineTypeForm, DelMachineTypeForm, ExtensionForm, DelExtensionForm, BaseEditInterfaceForm, BaseEditMachineForm
from .forms import EditIpTypeForm, IpTypeForm, DelIpTypeForm, DomainForm, AliasForm, DelAliasForm, NsForm, DelNsForm, TextForm, DelTextForm, MxForm, DelMxForm, VlanForm, DelVlanForm, ServiceForm, DelServiceForm, NasForm, DelNasForm from .forms import EditIpTypeForm, IpTypeForm, DelIpTypeForm, DomainForm, AliasForm, DelAliasForm, NsForm, DelNsForm, TextForm, DelTextForm, MxForm, DelMxForm, VlanForm, DelVlanForm, ServiceForm, DelServiceForm, NasForm, DelNasForm
from .models import IpType, Machine, Interface, IpList, MachineType, Extension, Mx, Ns, Domain, Service, Service_link, Vlan, Nas, Text from .forms import EditOuverturePortListForm, EditOuverturePortConfigForm
from .models import IpType, Machine, Interface, IpList, MachineType, Extension, Mx, Ns, Domain, Service, Service_link, Vlan, Nas, Text, OuverturePortList, OuverturePort
from users.models import User from users.models import User
from users.models import all_has_access from users.models import all_has_access
from preferences.models import GeneralOption, OptionalMachine from preferences.models import GeneralOption, OptionalMachine
@ -132,7 +133,7 @@ def new_machine(request, userid):
machine = NewMachineForm(request.POST or None) machine = NewMachineForm(request.POST or None)
interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',))) interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',)))
nb_machine = Interface.objects.filter(machine__user=userid).count() nb_machine = Interface.objects.filter(machine__user=userid).count()
domain = DomainForm(request.POST or None, name_user=user.pseudo.replace('_','-'), nb_machine=nb_machine) domain = DomainForm(request.POST or None, user=user, nb_machine=nb_machine)
if machine.is_valid() and interface.is_valid(): if machine.is_valid() and interface.is_valid():
new_machine = machine.save(commit=False) new_machine = machine.save(commit=False)
new_machine.user = user new_machine.user = user
@ -957,6 +958,103 @@ def history(request, object, id):
return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance}) return render(request, 're2o/history.html', {'reversions': reversions, 'object': object_instance})
@login_required
@permission_required('cableur')
def index_portlist(request):
port_list = OuverturePortList.objects.all().order_by('name')
return render(request, "machines/index_portlist.html", {'port_list':port_list})
@login_required
@permission_required('bureau')
def edit_portlist(request, pk):
try:
port_list_instance = OuverturePortList.objects.get(pk=pk)
except OuverturePortList.DoesNotExist:
messages.error(request, "Liste de ports inexistante")
return redirect("/machines/index_portlist/")
port_list = EditOuverturePortListForm(request.POST or None, instance=port_list_instance)
port_formset = modelformset_factory(
OuverturePort,
fields=('begin','end','protocole','io'),
extra=0,
can_delete=True,
min_num=1,
validate_min=True,
)(request.POST or None, queryset=port_list_instance.ouvertureport_set.all())
if port_list.is_valid() and port_formset.is_valid():
pl = port_list.save()
instances = port_formset.save(commit=False)
for to_delete in port_formset.deleted_objects:
to_delete.delete()
for port in instances:
port.port_list = pl
port.save()
messages.success(request, "Liste de ports modifiée")
return redirect("/machines/index_portlist/")
return form({'port_list' : port_list, 'ports' : port_formset}, 'machines/edit_portlist.html', request)
@login_required
@permission_required('bureau')
def del_portlist(request, pk):
try:
port_list_instance = OuverturePortList.objects.get(pk=pk)
except OuverturePortList.DoesNotExist:
messages.error(request, "Liste de ports inexistante")
return redirect("/machines/index_portlist/")
if port_list_instance.interface_set.all():
messages.error(request, "Cette liste de ports est utilisée")
return redirect("/machines/index_portlist/")
port_list_instance.delete()
messages.success(request, "La liste de ports a été supprimée")
return redirect("/machines/index_portlist/")
@login_required
@permission_required('bureau')
def add_portlist(request):
port_list = EditOuverturePortListForm(request.POST or None)
port_formset = modelformset_factory(
OuverturePort,
fields=('begin','end','protocole','io'),
extra=0,
can_delete=True,
min_num=1,
validate_min=True,
)(request.POST or None, queryset=OuverturePort.objects.none())
if port_list.is_valid() and port_formset.is_valid():
pl = port_list.save()
instances = port_formset.save(commit=False)
for to_delete in port_formset.deleted_objects:
to_delete.delete()
for port in instances:
port.port_list = pl
port.save()
messages.success(request, "Liste de ports créée")
return redirect("/machines/index_portlist/")
return form({'port_list' : port_list, 'ports' : port_formset}, 'machines/edit_portlist.html', request)
port_list = EditOuverturePortListForm(request.POST or None)
if port_list.is_valid():
port_list.save()
messages.success(request, "Liste de ports créée")
return redirect("/machines/index_portlist/")
return form({'machineform' : port_list}, 'machines/machine.html', request)
@login_required
@permission_required('cableur')
def configure_ports(request, pk):
try:
interface_instance = Interface.objects.get(pk=pk)
except Interface.DoesNotExist:
messages.error(request, u"Interface inexistante" )
return redirect("/machines")
if not interface_instance.may_have_port_open():
messages.error(request, "Attention, l'ipv4 n'est pas publique, l'ouverture n'aura pas d'effet en v4")
interface = EditOuverturePortConfigForm(request.POST or None, instance=interface_instance)
if interface.is_valid():
interface.save()
messages.success(request, "Configuration des ports mise à jour.")
return redirect("/machines/")
return form({'interfaceform' : interface}, 'machines/machine.html', request)
""" Framework Rest """ """ Framework Rest """
class JSONResponse(HttpResponse): class JSONResponse(HttpResponse):
@ -973,6 +1071,14 @@ def mac_ip_list(request):
seria = InterfaceSerializer(interfaces, many=True) seria = InterfaceSerializer(interfaces, many=True)
return seria.data return seria.data
@csrf_exempt
@login_required
@permission_required('serveur')
def full_mac_ip_list(request):
interfaces = all_active_assigned_interfaces()
seria = FullInterfaceSerializer(interfaces, many=True)
return seria.data
@csrf_exempt @csrf_exempt
@login_required @login_required
@permission_required('serveur') @permission_required('serveur')
@ -1032,7 +1138,7 @@ def mac_ip(request):
@login_required @login_required
@permission_required('serveur') @permission_required('serveur')
def mac_ip_dns(request): def mac_ip_dns(request):
seria = mac_ip_list(request) seria = full_mac_ip_list(request)
return JSONResponse(seria) return JSONResponse(seria)
@csrf_exempt @csrf_exempt

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-02 16:14
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('preferences', '0019_remove_optionaltopologie_mac_autocapture'),
]
operations = [
migrations.AddField(
model_name='optionalmachine',
name='ipv6',
field=models.BooleanField(default=False),
),
]

View file

@ -45,6 +45,7 @@ class OptionalMachine(models.Model):
password_machine = models.BooleanField(default=False) password_machine = models.BooleanField(default=False)
max_lambdauser_interfaces = models.IntegerField(default=10) max_lambdauser_interfaces = models.IntegerField(default=10)
max_lambdauser_aliases = models.IntegerField(default=10) max_lambdauser_aliases = models.IntegerField(default=10)
ipv6 = models.BooleanField(default=False)
class OptionalTopologie(models.Model): class OptionalTopologie(models.Model):
PRETTY_NAME = "Options topologie" PRETTY_NAME = "Options topologie"

View file

@ -72,6 +72,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<tr> <tr>
<th>Alias dns autorisé par utilisateur</th> <th>Alias dns autorisé par utilisateur</th>
<td>{{ machineoptions.max_lambdauser_aliases }}</td> <td>{{ machineoptions.max_lambdauser_aliases }}</td>
<th>Support de l'ipv6</th>
<td>{{ machineoptions.ipv6 }}</td>
</tr> </tr>
</table> </table>
<h4>Préférences topologie</h4> <h4>Préférences topologie</h4>

View file

@ -23,10 +23,11 @@
from __future__ import unicode_literals from __future__ import unicode_literals
from machines.models import Interface, Machine from machines.models import Interface, Machine
from preferences.models import GeneralOption from preferences.models import GeneralOption, OptionalMachine
def context_user(request): def context_user(request):
general_options, created = GeneralOption.objects.get_or_create() general_options, created = GeneralOption.objects.get_or_create()
machine_options, created = OptionalMachine.objects.get_or_create()
user = request.user user = request.user
if user.is_authenticated(): if user.is_authenticated():
interfaces = user.user_interfaces() interfaces = user.user_interfaces()
@ -54,4 +55,5 @@ def context_user(request):
'is_admin' : is_admin, 'is_admin' : is_admin,
'interfaces': interfaces, 'interfaces': interfaces,
'site_name': general_options.site_name, 'site_name': general_options.site_name,
'ipv6_enabled' : machine_options.ipv6,
} }

View file

@ -157,8 +157,8 @@ STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files') STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
RIGHTS_LINK = { RIGHTS_LINK = {
'cableur' : ['bureau','infra','bofh','trésorier'], 'cableur' : ['bureau','infra','bofh','tresorier'],
'bofh' : ['bureau','trésorier'], 'bofh' : ['bureau','tresorier'],
} }
GRAPH_MODELS = { GRAPH_MODELS = {

View file

@ -48,6 +48,7 @@ DATABASES = {
'ENGINE': 'ldapdb.backends.ldap', 'ENGINE': 'ldapdb.backends.ldap',
'NAME': 'ldap://ldap_host_ip/', 'NAME': 'ldap://ldap_host_ip/',
'USER': 'ldap_dn', 'USER': 'ldap_dn',
# 'TLS': True,
'PASSWORD': 'SUPER_SECRET_LDAP', 'PASSWORD': 'SUPER_SECRET_LDAP',
} }
} }

View file

@ -29,16 +29,16 @@ from reversion.admin import VersionAdmin
from .models import Port, Room, Switch, Stack from .models import Port, Room, Switch, Stack
class StackAdmin(VersionAdmin): class StackAdmin(VersionAdmin):
list_display = ('name', 'stack_id', 'details') pass
class SwitchAdmin(VersionAdmin): class SwitchAdmin(VersionAdmin):
list_display = ('switch_interface','location','number','details') pass
class PortAdmin(VersionAdmin): class PortAdmin(VersionAdmin):
list_display = ('switch', 'port','room','machine_interface','radius','details') pass
class RoomAdmin(VersionAdmin): class RoomAdmin(VersionAdmin):
list_display = ('name','details') pass
admin.site.register(Port, PortAdmin) admin.site.register(Port, PortAdmin)
admin.site.register(Room, RoomAdmin) admin.site.register(Room, RoomAdmin)

View file

@ -33,7 +33,7 @@ class PortForm(ModelForm):
class EditPortForm(ModelForm): class EditPortForm(ModelForm):
class Meta(PortForm.Meta): class Meta(PortForm.Meta):
fields = ['room', 'related', 'machine_interface', 'radius', 'details'] fields = ['room', 'related', 'machine_interface', 'radius', 'vlan_force', 'details']
def __init__(self, *args, **kwargs): def __init__(self, *args, **kwargs):
super(EditPortForm, self).__init__(*args, **kwargs) super(EditPortForm, self).__init__(*args, **kwargs)
@ -42,7 +42,7 @@ class EditPortForm(ModelForm):
class AddPortForm(ModelForm): class AddPortForm(ModelForm):
class Meta(PortForm.Meta): class Meta(PortForm.Meta):
fields = ['port', 'room', 'machine_interface', 'related', 'radius', 'details'] fields = ['port', 'room', 'machine_interface', 'related', 'radius', 'vlan_force', 'details']
class StackForm(ModelForm): class StackForm(ModelForm):
class Meta: class Meta:

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-02 01:34
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('topologie', '0028_auto_20170913_1503'),
]
operations = [
migrations.AlterField(
model_name='port',
name='radius',
field=models.CharField(choices=[('NO', 'NO'), ('STRICT', 'STRICT'), ('BLOQ', 'BLOQ'), ('COMMON', 'COMMON'), ('3', '3'), ('7', '7'), ('8', '8'), ('13', '13'), ('20', '20'), ('42', '42'), ('69', '69')], default='NO', max_length=32),
),
]

View file

@ -0,0 +1,26 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-04 00:35
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('topologie', '0029_auto_20171002_0334'),
]
operations = [
migrations.AddField(
model_name='port',
name='vlan_force',
field=models.ForeignKey(blank=True, null=True, on_delete=django.db.models.deletion.SET_NULL, to='machines.Vlan'),
),
migrations.AlterField(
model_name='port',
name='radius',
field=models.CharField(choices=[('NO', 'NO'), ('STRICT', 'STRICT'), ('BLOQ', 'BLOQ'), ('COMMON', 'COMMON')], default='NO', max_length=32),
),
]

View file

@ -34,17 +34,11 @@ import reversion
from machines.models import Vlan from machines.models import Vlan
def make_port_related(port):
related_port = port.related
related_port.related = port
related_port.save()
def clean_port_related(port):
related_port = port.related_port
related_port.related = None
related_port.save()
class Stack(models.Model): class Stack(models.Model):
""" Un objet stack. Regrouppe des switchs en foreign key
, contient une id de stack, un switch id min et max dans
le stack"""
PRETTY_NAME = "Stack de switchs" PRETTY_NAME = "Stack de switchs"
name = models.CharField(max_length=32, blank=True, null=True) name = models.CharField(max_length=32, blank=True, null=True)
@ -57,15 +51,25 @@ class Stack(models.Model):
return " ".join([self.name, self.stack_id]) return " ".join([self.name, self.stack_id])
def save(self, *args, **kwargs): def save(self, *args, **kwargs):
self.clean()
if not self.name: if not self.name:
self.name = self.stack_id self.name = self.stack_id
super(Stack, self).save(*args, **kwargs) super(Stack, self).save(*args, **kwargs)
def clean(self): def clean(self):
""" Verification que l'id_max < id_min"""
if self.member_id_max < self.member_id_min: if self.member_id_max < self.member_id_min:
raise ValidationError({'member_id_max':"L'id maximale est inférieure à l'id minimale"}) raise ValidationError({'member_id_max':"L'id maximale est inférieure à l'id minimale"})
class Switch(models.Model): class Switch(models.Model):
""" Definition d'un switch. Contient un nombre de ports (number),
un emplacement (location), un stack parent (optionnel, stack)
et un id de membre dans le stack (stack_member_id)
relié en onetoone à une interface
Pourquoi ne pas avoir fait hériter switch de interface ?
Principalement par méconnaissance de la puissance de cette façon de faire.
Ceci étant entendu, django crée en interne un onetoone, ce qui a un
effet identique avec ce que l'on fait ici"""
PRETTY_NAME = "Switch / Commutateur" PRETTY_NAME = "Switch / Commutateur"
switch_interface = models.OneToOneField('machines.Interface', on_delete=models.CASCADE) switch_interface = models.OneToOneField('machines.Interface', on_delete=models.CASCADE)
@ -82,6 +86,7 @@ class Switch(models.Model):
return str(self.location) + ' ' + str(self.switch_interface) return str(self.location) + ' ' + str(self.switch_interface)
def clean(self): def clean(self):
""" Verifie que l'id stack est dans le bon range"""
if self.stack is not None: if self.stack is not None:
if self.stack_member_id is not None: if self.stack_member_id is not None:
if (self.stack_member_id > self.stack.member_id_max) or (self.stack_member_id < self.stack.member_id_min): if (self.stack_member_id > self.stack.member_id_max) or (self.stack_member_id < self.stack.member_id_min):
@ -90,17 +95,27 @@ class Switch(models.Model):
raise ValidationError({'stack_member_id': "L'id dans la stack ne peut être nul"}) raise ValidationError({'stack_member_id': "L'id dans la stack ne peut être nul"})
class Port(models.Model): class Port(models.Model):
""" Definition d'un port. Relié à un switch(foreign_key),
un port peut etre relié de manière exclusive à :
- une chambre (room)
- une machine (serveur etc) (machine_interface)
- un autre port (uplink) (related)
Champs supplémentaires :
- RADIUS (mode STRICT : connexion sur port uniquement si machine
d'un adhérent à jour de cotisation et que la chambre est également à jour de cotisation
mode COMMON : vérification uniquement du statut de la machine
mode NO : accepte toute demande venant du port et place sur le vlan normal
mode BLOQ : rejet de toute authentification
- vlan_force : override la politique générale de placement vlan, permet
de forcer un port sur un vlan particulier. S'additionne à la politique
RADIUS"""
PRETTY_NAME = "Port de switch" PRETTY_NAME = "Port de switch"
STATES_BASE = ( STATES = (
('NO', 'NO'), ('NO', 'NO'),
('STRICT', 'STRICT'), ('STRICT', 'STRICT'),
('BLOQ', 'BLOQ'), ('BLOQ', 'BLOQ'),
('COMMON', 'COMMON'), ('COMMON', 'COMMON'),
) )
try:
STATES = STATES_BASE + tuple([(str(id), str(id)) for id in list(Vlan.objects.values_list('vlan_id', flat=True).order_by('vlan_id'))])
except:
STATES = STATES_BASE
switch = models.ForeignKey('Switch', related_name="ports") switch = models.ForeignKey('Switch', related_name="ports")
port = models.IntegerField() port = models.IntegerField()
@ -108,12 +123,32 @@ class Port(models.Model):
machine_interface = models.ForeignKey('machines.Interface', on_delete=models.SET_NULL, blank=True, null=True) machine_interface = models.ForeignKey('machines.Interface', on_delete=models.SET_NULL, blank=True, null=True)
related = models.OneToOneField('self', null=True, blank=True, related_name='related_port') related = models.OneToOneField('self', null=True, blank=True, related_name='related_port')
radius = models.CharField(max_length=32, choices=STATES, default='NO') radius = models.CharField(max_length=32, choices=STATES, default='NO')
vlan_force = models.ForeignKey('machines.Vlan', on_delete=models.SET_NULL, blank=True, null=True)
details = models.CharField(max_length=255, blank=True) details = models.CharField(max_length=255, blank=True)
class Meta: class Meta:
unique_together = ('switch', 'port') unique_together = ('switch', 'port')
def make_port_related(self):
""" Synchronise le port distant sur self"""
related_port = self.related
related_port.related = self
related_port.save()
def clean_port_related(self):
""" Supprime la relation related sur self"""
related_port = self.related_port
related_port.related = None
related_port.save()
def clean(self): def clean(self):
""" Verifie que un seul de chambre, interface_parent et related_port est rempli.
Verifie que le related n'est pas le port lui-même....
Verifie que le related n'est pas déjà occupé par une machine ou une chambre. Si
ce n'est pas le cas, applique la relation related
Si un port related point vers self, on nettoie la relation
A priori pas d'autre solution que de faire ça à la main. A priori tout cela est dans
un bloc transaction, donc pas de problème de cohérence"""
if hasattr(self, 'switch'): if hasattr(self, 'switch'):
if self.port > self.switch.number: if self.port > self.switch.number:
raise ValidationError("Ce port ne peut exister, numero trop élevé") raise ValidationError("Ce port ne peut exister, numero trop élevé")
@ -125,14 +160,15 @@ class Port(models.Model):
if self.related.machine_interface or self.related.room: if self.related.machine_interface or self.related.room:
raise ValidationError("Le port relié est déjà occupé, veuillez le libérer avant de créer une relation") raise ValidationError("Le port relié est déjà occupé, veuillez le libérer avant de créer une relation")
else: else:
make_port_related(self) self.make_port_related()
elif hasattr(self, 'related_port'): elif hasattr(self, 'related_port'):
clean_port_related(self) self.clean_port_related()
def __str__(self): def __str__(self):
return str(self.switch) + " - " + str(self.port) return str(self.switch) + " - " + str(self.port)
class Room(models.Model): class Room(models.Model):
""" Une chambre/local contenant une prise murale"""
PRETTY_NAME = "Chambre/ Prise murale" PRETTY_NAME = "Chambre/ Prise murale"
name = models.CharField(max_length=255, unique=True) name = models.CharField(max_length=255, unique=True)

View file

@ -30,6 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>Interface machine</th> <th>Interface machine</th>
<th>Related</th> <th>Related</th>
<th>Radius</th> <th>Radius</th>
<th>Vlan forcé</th>
<th>Détails</th> <th>Détails</th>
<th></th> <th></th>
</tr> </tr>
@ -53,6 +54,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
{% endif %} {% endif %}
</td> </td>
<td>{{ port.radius }}</td> <td>{{ port.radius }}</td>
<td>{% if not port.vlan_force %} Aucun{%else %}{{ port.vlan_force }}{% endif %}</td>
<td>{{ port.details }}</td> <td>{{ port.details }}</td>
<td class="text-right"> <td class="text-right">
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'port' port.pk %}"> <a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'port' port.pk %}">

View file

@ -44,12 +44,14 @@ from preferences.models import AssoOption, GeneralOption
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index(request): def index(request):
""" Vue d'affichage de tous les swicthes"""
switch_list = Switch.objects.order_by('stack','stack_member_id','location').select_related('switch_interface__domain__extension').select_related('switch_interface__ipv4').select_related('switch_interface__domain') switch_list = Switch.objects.order_by('stack','stack_member_id','location').select_related('switch_interface__domain__extension').select_related('switch_interface__ipv4').select_related('switch_interface__domain')
return render(request, 'topologie/index.html', {'switch_list': switch_list}) return render(request, 'topologie/index.html', {'switch_list': switch_list})
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def history(request, object, id): def history(request, object, id):
""" Vue générique pour afficher l'historique complet d'un objet"""
if object == 'switch': if object == 'switch':
try: try:
object_instance = Switch.objects.get(pk=id) object_instance = Switch.objects.get(pk=id)
@ -95,6 +97,7 @@ def history(request, object, id):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_port(request, switch_id): def index_port(request, switch_id):
""" Affichage de l'ensemble des ports reliés à un switch particulier"""
try: try:
switch = Switch.objects.get(pk=switch_id) switch = Switch.objects.get(pk=switch_id)
except Switch.DoesNotExist: except Switch.DoesNotExist:
@ -106,6 +109,7 @@ def index_port(request, switch_id):
@login_required @login_required
@permission_required('cableur') @permission_required('cableur')
def index_room(request): def index_room(request):
""" Affichage de l'ensemble des chambres"""
room_list = Room.objects.order_by('name') room_list = Room.objects.order_by('name')
options, created = GeneralOption.objects.get_or_create() options, created = GeneralOption.objects.get_or_create()
pagination_number = options.pagination_number pagination_number = options.pagination_number
@ -131,6 +135,7 @@ def index_stack(request):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def new_port(request, switch_id): def new_port(request, switch_id):
""" Nouveau port"""
try: try:
switch = Switch.objects.get(pk=switch_id) switch = Switch.objects.get(pk=switch_id)
except Switch.DoesNotExist: except Switch.DoesNotExist:
@ -154,6 +159,7 @@ def new_port(request, switch_id):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def edit_port(request, port_id): def edit_port(request, port_id):
""" Edition d'un port. Permet de changer le switch parent et l'affectation du port"""
try: try:
port_object = Port.objects.select_related('switch__switch_interface__domain__extension').select_related('machine_interface__domain__extension').select_related('machine_interface__switch').select_related('room').select_related('related').get(pk=port_id) port_object = Port.objects.select_related('switch__switch_interface__domain__extension').select_related('machine_interface__domain__extension').select_related('machine_interface__switch').select_related('room').select_related('related').get(pk=port_id)
except Port.DoesNotExist: except Port.DoesNotExist:
@ -172,6 +178,7 @@ def edit_port(request, port_id):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def del_port(request,port_id): def del_port(request,port_id):
""" Supprime le port"""
try: try:
port = Port.objects.get(pk=port_id) port = Port.objects.get(pk=port_id)
except Port.DoesNotExist: except Port.DoesNotExist:
@ -263,6 +270,9 @@ def edit_switchs_stack(request,stack_id):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def new_switch(request): def new_switch(request):
""" Creation d'un switch. Cree en meme temps l'interface et la machine associée.
Vue complexe. Appelle successivement les 4 models forms adaptés : machine,
interface, domain et switch"""
switch = NewSwitchForm(request.POST or None) switch = NewSwitchForm(request.POST or None)
machine = NewMachineForm(request.POST or None) machine = NewMachineForm(request.POST or None)
interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',))) interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',)))
@ -304,6 +314,8 @@ def new_switch(request):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def edit_switch(request, switch_id): def edit_switch(request, switch_id):
""" Edition d'un switch. Permet de chambre nombre de ports, place dans le stack,
interface et machine associée"""
try: try:
switch = Switch.objects.get(pk=switch_id) switch = Switch.objects.get(pk=switch_id)
except Switch.DoesNotExist: except Switch.DoesNotExist:
@ -341,6 +353,7 @@ def edit_switch(request, switch_id):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def new_room(request): def new_room(request):
"""Nouvelle chambre """
room = EditRoomForm(request.POST or None) room = EditRoomForm(request.POST or None)
if room.is_valid(): if room.is_valid():
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -354,6 +367,7 @@ def new_room(request):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def edit_room(request, room_id): def edit_room(request, room_id):
""" Edition numero et details de la chambre"""
try: try:
room = Room.objects.get(pk=room_id) room = Room.objects.get(pk=room_id)
except Room.DoesNotExist: except Room.DoesNotExist:
@ -372,6 +386,7 @@ def edit_room(request, room_id):
@login_required @login_required
@permission_required('infra') @permission_required('infra')
def del_room(request, room_id): def del_room(request, room_id):
""" Suppression d'un chambre"""
try: try:
room = Room.objects.get(pk=room_id) room = Room.objects.get(pk=room_id)
except Room.DoesNotExist: except Room.DoesNotExist:

View file

@ -207,6 +207,7 @@ class EditInfoForm(BaseInfoForm):
] ]
class InfoForm(EditInfoForm): class InfoForm(EditInfoForm):
""" Utile pour forcer un déménagement quand il y a déjà un user en place"""
force = forms.BooleanField(label="Forcer le déménagement ?", initial=False, required=False) force = forms.BooleanField(label="Forcer le déménagement ?", initial=False, required=False)
def clean_force(self): def clean_force(self):
@ -215,15 +216,18 @@ class InfoForm(EditInfoForm):
return return
class UserForm(InfoForm): class UserForm(InfoForm):
""" Model form general"""
class Meta(InfoForm.Meta): class Meta(InfoForm.Meta):
fields = '__all__' fields = '__all__'
class PasswordForm(ModelForm): class PasswordForm(ModelForm):
""" Formulaire de changement brut de mot de passe. Ne pas utiliser sans traitement"""
class Meta: class Meta:
model = User model = User
fields = ['password', 'pwd_ntlm'] fields = ['password', 'pwd_ntlm']
class ServiceUserForm(ModelForm): class ServiceUserForm(ModelForm):
""" Modification d'un service user"""
password = forms.CharField(label=u'Nouveau mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput, required=False) password = forms.CharField(label=u'Nouveau mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput, required=False)
class Meta: class Meta:
@ -235,6 +239,7 @@ class EditServiceUserForm(ServiceUserForm):
fields = ['access_group','comment'] fields = ['access_group','comment']
class StateForm(ModelForm): class StateForm(ModelForm):
""" Changement de l'état d'un user"""
class Meta: class Meta:
model = User model = User
fields = ['state'] fields = ['state']

View file

@ -0,0 +1,21 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-03 03:56
from __future__ import unicode_literals
import django.core.validators
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0054_auto_20170626_2219'),
]
operations = [
migrations.AlterField(
model_name='listright',
name='listright',
field=models.CharField(max_length=255, unique=True, validators=[django.core.validators.RegexValidator('^[a-z]+$', message='Les groupes unix ne peuvent contenir que des lettres minuscules')]),
),
]

View file

@ -48,13 +48,17 @@ from django.utils import timezone
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.core.validators import MinLengthValidator from django.core.validators import MinLengthValidator
from django.core.validators import RegexValidator
from topologie.models import Room from topologie.models import Room
from cotisations.models import Cotisation, Facture, Paiement, Vente from cotisations.models import Cotisation, Facture, Paiement, Vente
from machines.models import Domain, Interface, MachineType, Machine, Nas, MachineType, regen from machines.models import Domain, Interface, MachineType, Machine, Nas, MachineType, Extension, regen
from preferences.models import GeneralOption, AssoOption, OptionalUser, OptionalMachine, MailMessageOption from preferences.models import GeneralOption, AssoOption, OptionalUser, OptionalMachine, MailMessageOption
now = timezone.now() now = timezone.now()
#### Utilitaires généraux
def remove_user_room(room): def remove_user_room(room):
""" Déménage de force l'ancien locataire de la chambre """ """ Déménage de force l'ancien locataire de la chambre """
try: try:
@ -72,6 +76,8 @@ def linux_user_check(login):
def linux_user_validator(login): def linux_user_validator(login):
""" Retourne une erreur de validation si le login ne respecte
pas les contraintes unix (maj, min, chiffres ou tiret)"""
if not linux_user_check(login): if not linux_user_check(login):
raise forms.ValidationError( raise forms.ValidationError(
", ce pseudo ('%(label)s') contient des carractères interdits", ", ce pseudo ('%(label)s') contient des carractères interdits",
@ -79,6 +85,7 @@ def linux_user_validator(login):
) )
def get_fresh_user_uid(): def get_fresh_user_uid():
""" Renvoie le plus petit uid non pris. Fonction très paresseuse """
uids = list(range(int(min(UID_RANGES['users'])),int(max(UID_RANGES['users'])))) uids = list(range(int(min(UID_RANGES['users'])),int(max(UID_RANGES['users']))))
try: try:
used_uids = list(User.objects.values_list('uid_number', flat=True)) used_uids = list(User.objects.values_list('uid_number', flat=True))
@ -88,12 +95,15 @@ def get_fresh_user_uid():
return min(free_uids) return min(free_uids)
def get_fresh_gid(): def get_fresh_gid():
""" Renvoie le plus petit gid libre """
gids = list(range(int(min(GID_RANGES['posix'])),int(max(GID_RANGES['posix'])))) gids = list(range(int(min(GID_RANGES['posix'])),int(max(GID_RANGES['posix']))))
used_gids = list(ListRight.objects.values_list('gid', flat=True)) used_gids = list(ListRight.objects.values_list('gid', flat=True))
free_gids = [ id for id in gids if id not in used_gids] free_gids = [ id for id in gids if id not in used_gids]
return min(free_gids) return min(free_gids)
def get_admin_right(): def get_admin_right():
""" Renvoie l'instance droit admin. La crée si elle n'existe pas
Lui attribue un gid libre"""
try: try:
admin_right = ListRight.objects.get(listright="admin") admin_right = ListRight.objects.get(listright="admin")
except ListRight.DoesNotExist: except ListRight.DoesNotExist:
@ -103,15 +113,20 @@ def get_admin_right():
return admin_right return admin_right
def all_adherent(search_time=now): def all_adherent(search_time=now):
""" Fonction renvoyant tous les users adherents. Optimisee pour n'est qu'une seule requete sql
Inspecte les factures de l'user et ses cotisation, regarde si elles sont posterieur à now (end_time)"""
return User.objects.filter(facture__in=Facture.objects.filter(vente__in=Vente.objects.filter(cotisation__in=Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.all().exclude(valid=False))).filter(date_end__gt=search_time)))).distinct() return User.objects.filter(facture__in=Facture.objects.filter(vente__in=Vente.objects.filter(cotisation__in=Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.all().exclude(valid=False))).filter(date_end__gt=search_time)))).distinct()
def all_baned(search_time=now): def all_baned(search_time=now):
""" Fonction renvoyant tous les users bannis """
return User.objects.filter(ban__in=Ban.objects.filter(date_end__gt=search_time)).distinct() return User.objects.filter(ban__in=Ban.objects.filter(date_end__gt=search_time)).distinct()
def all_whitelisted(search_time=now): def all_whitelisted(search_time=now):
""" Fonction renvoyant tous les users whitelistes """
return User.objects.filter(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)).distinct() return User.objects.filter(whitelist__in=Whitelist.objects.filter(date_end__gt=search_time)).distinct()
def all_has_access(search_time=now): def all_has_access(search_time=now):
""" Renvoie tous les users beneficiant d'une connexion : user adherent ou whiteliste et non banni """
return User.objects.filter(Q(state=User.STATE_ACTIVE) & ~Q(ban__in=Ban.objects.filter(date_end__gt=timezone.now())) & (Q(whitelist__in=Whitelist.objects.filter(date_end__gt=timezone.now())) | Q(facture__in=Facture.objects.filter(vente__in=Vente.objects.filter(cotisation__in=Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.all().exclude(valid=False))).filter(date_end__gt=search_time)))))).distinct() return User.objects.filter(Q(state=User.STATE_ACTIVE) & ~Q(ban__in=Ban.objects.filter(date_end__gt=timezone.now())) & (Q(whitelist__in=Whitelist.objects.filter(date_end__gt=timezone.now())) | Q(facture__in=Facture.objects.filter(vente__in=Vente.objects.filter(cotisation__in=Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.all().exclude(valid=False))).filter(date_end__gt=search_time)))))).distinct()
class UserManager(BaseUserManager): class UserManager(BaseUserManager):
@ -151,6 +166,9 @@ class UserManager(BaseUserManager):
class User(AbstractBaseUser): class User(AbstractBaseUser):
""" Definition de l'utilisateur de base.
Champs principaux : name, surnname, pseudo, email, room, password
Herite du django BaseUser et du système d'auth django"""
PRETTY_NAME = "Utilisateurs" PRETTY_NAME = "Utilisateurs"
STATE_ACTIVE = 0 STATE_ACTIVE = 0
STATE_DISABLED = 1 STATE_DISABLED = 1
@ -186,14 +204,17 @@ class User(AbstractBaseUser):
@property @property
def is_active(self): def is_active(self):
""" Renvoie si l'user est à l'état actif"""
return self.state == self.STATE_ACTIVE return self.state == self.STATE_ACTIVE
@property @property
def is_staff(self): def is_staff(self):
""" Fonction de base django, renvoie si l'user est admin"""
return self.is_admin return self.is_admin
@property @property
def is_admin(self): def is_admin(self):
""" Renvoie si l'user est admin"""
try: try:
Right.objects.get(user=self, right__listright='admin') Right.objects.get(user=self, right__listright='admin')
except Right.DoesNotExist: except Right.DoesNotExist:
@ -202,18 +223,24 @@ class User(AbstractBaseUser):
@is_admin.setter @is_admin.setter
def is_admin(self, value): def is_admin(self, value):
""" Change la valeur de admin à true ou false suivant la valeur de value"""
if value and not self.is_admin: if value and not self.is_admin:
self.make_admin() self.make_admin()
elif not value and self.is_admin: elif not value and self.is_admin:
self.un_admin() self.un_admin()
def get_full_name(self): def get_full_name(self):
""" Renvoie le nom complet de l'user formaté nom/prénom"""
return '%s %s' % (self.name, self.surname) return '%s %s' % (self.name, self.surname)
def get_short_name(self): def get_short_name(self):
""" Renvoie seulement le nom"""
return self.name return self.name
def has_perms(self, perms, obj=None): def has_perms(self, perms, obj=None):
""" Renvoie true si l'user dispose de la permission.
Prend en argument une liste de permissions.
TODO : Arranger cette fonction"""
for perm in perms: for perm in perms:
if perm in RIGHTS_LINK: if perm in RIGHTS_LINK:
query = Q() query = Q()
@ -232,6 +259,7 @@ class User(AbstractBaseUser):
def has_right(self, right): def has_right(self, right):
""" Renvoie si un user a un right donné. Crée le right si il n'existe pas"""
try: try:
list_right = ListRight.objects.get(listright=right) list_right = ListRight.objects.get(listright=right)
except: except:
@ -241,29 +269,38 @@ class User(AbstractBaseUser):
@cached_property @cached_property
def is_bureau(self): def is_bureau(self):
""" True si user a les droits bureau """
return self.has_right('bureau') return self.has_right('bureau')
@cached_property @cached_property
def is_bofh(self): def is_bofh(self):
""" True si l'user a les droits bofh"""
return self.has_right('bofh') return self.has_right('bofh')
@cached_property @cached_property
def is_cableur(self): def is_cableur(self):
""" True si l'user a les droits cableur
(également true si bureau, infra ou bofh)"""
return self.has_right('cableur') or self.has_right('bureau') or self.has_right('infra') or self.has_right('bofh') return self.has_right('cableur') or self.has_right('bureau') or self.has_right('infra') or self.has_right('bofh')
@cached_property @cached_property
def is_trez(self): def is_trez(self):
return self.has_right('trésorier') """ Renvoie true si droits trésorier pour l'user"""
return self.has_right('tresorier')
@cached_property @cached_property
def is_infra(self): def is_infra(self):
""" True si a les droits infra"""
return self.has_right('infra') return self.has_right('infra')
def end_adhesion(self): def end_adhesion(self):
""" Renvoie la date de fin d'adhésion d'un user. Examine les objets
cotisation"""
date_max = Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.filter(user=self).exclude(valid=False))).aggregate(models.Max('date_end'))['date_end__max'] date_max = Cotisation.objects.filter(vente__in=Vente.objects.filter(facture__in=Facture.objects.filter(user=self).exclude(valid=False))).aggregate(models.Max('date_end'))['date_end__max']
return date_max return date_max
def is_adherent(self): def is_adherent(self):
""" Renvoie True si l'user est adhérent : si self.end_adhesion()>now"""
end = self.end_adhesion() end = self.end_adhesion()
if not end: if not end:
return False return False
@ -326,6 +363,8 @@ class User(AbstractBaseUser):
@cached_property @cached_property
def solde(self): def solde(self):
""" Renvoie le solde d'un user. Vérifie que l'option solde est activé, retourne 0 sinon.
Somme les crédits de solde et retire les débit payés par solde"""
options, created = OptionalUser.objects.get_or_create() options, created = OptionalUser.objects.get_or_create()
user_solde = options.user_solde user_solde = options.user_solde
if user_solde: if user_solde:
@ -336,8 +375,10 @@ class User(AbstractBaseUser):
else: else:
return 0 return 0
def user_interfaces(self): def user_interfaces(self, active=True):
return Interface.objects.filter(machine__in=Machine.objects.filter(user=self, active=True)) """ Renvoie toutes les interfaces dont les machines appartiennent à self
Par defaut ne prend que les interfaces actives"""
return Interface.objects.filter(machine__in=Machine.objects.filter(user=self, active=active))
def assign_ips(self): def assign_ips(self):
""" Assign une ipv4 aux machines d'un user """ """ Assign une ipv4 aux machines d'un user """
@ -350,6 +391,7 @@ class User(AbstractBaseUser):
interface.save() interface.save()
def unassign_ips(self): def unassign_ips(self):
""" Désassigne les ipv4 aux machines de l'user"""
interfaces = self.user_interfaces() interfaces = self.user_interfaces()
for interface in interfaces: for interface in interfaces:
with transaction.atomic(), reversion.create_revision(): with transaction.atomic(), reversion.create_revision():
@ -358,10 +400,12 @@ class User(AbstractBaseUser):
interface.save() interface.save()
def archive(self): def archive(self):
""" Archive l'user : appelle unassign_ips() puis passe state à ARCHIVE"""
self.unassign_ips() self.unassign_ips()
self.state = User.STATE_ARCHIVE self.state = User.STATE_ARCHIVE
def unarchive(self): def unarchive(self):
""" Désarchive l'user : réassigne ses ip et le passe en state ACTIVE"""
self.assign_ips() self.assign_ips()
self.state = User.STATE_ACTIVE self.state = User.STATE_ACTIVE
@ -382,6 +426,10 @@ class User(AbstractBaseUser):
user_right.delete() user_right.delete()
def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True): def ldap_sync(self, base=True, access_refresh=True, mac_refresh=True):
""" Synchronisation du ldap. Synchronise dans le ldap les attributs de self
Options : base : synchronise tous les attributs de base - nom, prenom, mail, password, shell, home
access_refresh : synchronise le dialup_access notant si l'user a accès aux services
mac_refresh : synchronise les machines de l'user"""
self.refresh_from_db() self.refresh_from_db()
try: try:
user_ldap = LdapUser.objects.get(uidNumber=self.uid_number) user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
@ -410,6 +458,7 @@ class User(AbstractBaseUser):
user_ldap.save() user_ldap.save()
def ldap_del(self): def ldap_del(self):
""" Supprime la version ldap de l'user"""
try: try:
user_ldap = LdapUser.objects.get(name=self.pseudo) user_ldap = LdapUser.objects.get(name=self.pseudo)
user_ldap.delete() user_ldap.delete()
@ -457,9 +506,11 @@ class User(AbstractBaseUser):
return return
def autoregister_machine(self, mac_address, nas_type): def autoregister_machine(self, mac_address, nas_type):
all_machines = self.all_machines() """ Fonction appellée par freeradius. Enregistre la mac pour une machine inconnue
sur le compte de l'user"""
all_interfaces = self.user_interfaces(active=False)
options, created = OptionalMachine.objects.get_or_create() options, created = OptionalMachine.objects.get_or_create()
if all_machines.count() > options.max_lambdauser_interfaces: if all_interfaces.count() > options.max_lambdauser_interfaces:
return False, "Maximum de machines enregistrees atteinte" return False, "Maximum de machines enregistrees atteinte"
if not nas_type: if not nas_type:
return False, "Re2o ne sait pas à quel machinetype affecter cette machine" return False, "Re2o ne sait pas à quel machinetype affecter cette machine"
@ -473,7 +524,7 @@ class User(AbstractBaseUser):
interface_cible.clean() interface_cible.clean()
machine_parent.clean() machine_parent.clean()
domain = Domain() domain = Domain()
domain.name = self.pseudo.replace('_','-').lower() + str(all_machines.count()) domain.name = self.get_next_domain_name()
domain.interface_parent = interface_cible domain.interface_parent = interface_cible
domain.clean() domain.clean()
machine_parent.save() machine_parent.save()
@ -486,19 +537,37 @@ class User(AbstractBaseUser):
return False, e return False, e
return True, "Ok" return True, "Ok"
def all_machines(self):
return Interface.objects.filter(machine__in=Machine.objects.filter(user=self))
def set_user_password(self, password): def set_user_password(self, password):
""" A utiliser de préférence, set le password en hash courrant et
dans la version ntlm"""
self.set_password(password) self.set_password(password)
self.pwd_ntlm = hashNT(password) self.pwd_ntlm = hashNT(password)
return return
def get_next_domain_name(self):
"""Look for an available name for a new interface for
this user by trying "pseudo0", "pseudo1", "pseudo2", ...
"""
def simple_pseudo():
return self.pseudo.replace('_', '-').lower()
def composed_pseudo( n ):
return simple_pseudo() + str(n)
num = 0
while Domain.objects.filter(name=composed_pseudo(num)) :
num += 1
return composed_pseudo(num)
def __str__(self): def __str__(self):
return self.pseudo return self.pseudo
@receiver(post_save, sender=User) @receiver(post_save, sender=User)
def user_post_save(sender, **kwargs): def user_post_save(sender, **kwargs):
""" Synchronisation post_save : envoie le mail de bienvenue si creation
Synchronise le ldap"""
is_created = kwargs['created'] is_created = kwargs['created']
user = kwargs['instance'] user = kwargs['instance']
if is_created: if is_created:
@ -513,6 +582,7 @@ def user_post_delete(sender, **kwargs):
regen('mailing') regen('mailing')
class ServiceUser(AbstractBaseUser): class ServiceUser(AbstractBaseUser):
""" Classe des users daemons, règle leurs accès au ldap"""
readonly = 'readonly' readonly = 'readonly'
ACCESS = ( ACCESS = (
('auth', 'auth'), ('auth', 'auth'),
@ -531,6 +601,7 @@ class ServiceUser(AbstractBaseUser):
objects = UserManager() objects = UserManager()
def ldap_sync(self): def ldap_sync(self):
""" Synchronisation du ServiceUser dans sa version ldap"""
try: try:
user_ldap = LdapServiceUser.objects.get(name=self.pseudo) user_ldap = LdapServiceUser.objects.get(name=self.pseudo)
except LdapServiceUser.DoesNotExist: except LdapServiceUser.DoesNotExist:
@ -560,15 +631,19 @@ class ServiceUser(AbstractBaseUser):
@receiver(post_save, sender=ServiceUser) @receiver(post_save, sender=ServiceUser)
def service_user_post_save(sender, **kwargs): def service_user_post_save(sender, **kwargs):
""" Synchronise un service user ldap après modification django"""
service_user = kwargs['instance'] service_user = kwargs['instance']
service_user.ldap_sync() service_user.ldap_sync()
@receiver(post_delete, sender=ServiceUser) @receiver(post_delete, sender=ServiceUser)
def service_user_post_delete(sender, **kwargs): def service_user_post_delete(sender, **kwargs):
""" Supprime un service user ldap après suppression django"""
service_user = kwargs['instance'] service_user = kwargs['instance']
service_user.ldap_del() service_user.ldap_del()
class Right(models.Model): class Right(models.Model):
""" Couple droit/user. Peut-être aurait-on mieux fait ici d'utiliser un manytomany
Ceci dit le résultat aurait été le même avec une table intermediaire"""
PRETTY_NAME = "Droits affectés à des users" PRETTY_NAME = "Droits affectés à des users"
user = models.ForeignKey('User', on_delete=models.PROTECT) user = models.ForeignKey('User', on_delete=models.PROTECT)
@ -582,15 +657,18 @@ class Right(models.Model):
@receiver(post_save, sender=Right) @receiver(post_save, sender=Right)
def right_post_save(sender, **kwargs): def right_post_save(sender, **kwargs):
""" Synchronise les users ldap groups avec les groupes de droits"""
right = kwargs['instance'].right right = kwargs['instance'].right
right.ldap_sync() right.ldap_sync()
@receiver(post_delete, sender=Right) @receiver(post_delete, sender=Right)
def right_post_delete(sender, **kwargs): def right_post_delete(sender, **kwargs):
""" Supprime l'user du groupe"""
right = kwargs['instance'].right right = kwargs['instance'].right
right.ldap_sync() right.ldap_sync()
class School(models.Model): class School(models.Model):
""" Etablissement d'enseignement"""
PRETTY_NAME = "Etablissements enregistrés" PRETTY_NAME = "Etablissements enregistrés"
name = models.CharField(max_length=255) name = models.CharField(max_length=255)
@ -600,9 +678,12 @@ class School(models.Model):
class ListRight(models.Model): class ListRight(models.Model):
""" Ensemble des droits existants. Chaque droit crée un groupe ldap synchronisé, avec gid.
Permet de gérer facilement les accès serveurs et autres
La clef de recherche est le gid, pour cette raison il n'est plus modifiable après creation"""
PRETTY_NAME = "Liste des droits existants" PRETTY_NAME = "Liste des droits existants"
listright = models.CharField(max_length=255, unique=True) listright = models.CharField(max_length=255, unique=True, validators=[RegexValidator('^[a-z]+$', message="Les groupes unix ne peuvent contenir que des lettres minuscules")])
gid = models.IntegerField(unique=True, null=True) gid = models.IntegerField(unique=True, null=True)
details = models.CharField(help_text="Description", max_length=255, blank=True) details = models.CharField(help_text="Description", max_length=255, blank=True)
@ -627,6 +708,7 @@ class ListRight(models.Model):
@receiver(post_save, sender=ListRight) @receiver(post_save, sender=ListRight)
def listright_post_save(sender, **kwargs): def listright_post_save(sender, **kwargs):
""" Synchronise le droit ldap quand il est modifié"""
right = kwargs['instance'] right = kwargs['instance']
right.ldap_sync() right.ldap_sync()
@ -644,6 +726,8 @@ class ListShell(models.Model):
return self.shell return self.shell
class Ban(models.Model): class Ban(models.Model):
""" Bannissement. Actuellement a un effet tout ou rien.
Gagnerait à être granulaire"""
PRETTY_NAME = "Liste des bannissements" PRETTY_NAME = "Liste des bannissements"
STATE_HARD = 0 STATE_HARD = 0
@ -684,6 +768,7 @@ class Ban(models.Model):
@receiver(post_save, sender=Ban) @receiver(post_save, sender=Ban)
def ban_post_save(sender, **kwargs): def ban_post_save(sender, **kwargs):
""" Regeneration de tous les services après modification d'un ban"""
ban = kwargs['instance'] ban = kwargs['instance']
is_created = kwargs['created'] is_created = kwargs['created']
user = ban.user user = ban.user
@ -699,6 +784,7 @@ def ban_post_save(sender, **kwargs):
@receiver(post_delete, sender=Ban) @receiver(post_delete, sender=Ban)
def ban_post_delete(sender, **kwargs): def ban_post_delete(sender, **kwargs):
""" Regen de tous les services après suppression d'un ban"""
user = kwargs['instance'].user user = kwargs['instance'].user
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False) user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
regen('mailing') regen('mailing')
@ -742,6 +828,9 @@ def whitelist_post_delete(sender, **kwargs):
regen('mac_ip_list') regen('mac_ip_list')
class Request(models.Model): class Request(models.Model):
""" Objet request, générant une url unique de validation.
Utilisé par exemple pour la generation du mot de passe et
sa réinitialisation"""
PASSWD = 'PW' PASSWD = 'PW'
EMAIL = 'EM' EMAIL = 'EM'
TYPE_CHOICES = ( TYPE_CHOICES = (

View file

@ -716,6 +716,8 @@ class JSONResponse(HttpResponse):
@login_required @login_required
@permission_required('serveur') @permission_required('serveur')
def mailing(request): def mailing(request):
""" Fonction de serialisation des addresses mail de tous les users
Pour generation de ml all users"""
mails = all_has_access().values('email').distinct() mails = all_has_access().values('email').distinct()
seria = MailSerializer(mails, many=True) seria = MailSerializer(mails, many=True)
return JSONResponse(seria.data) return JSONResponse(seria.data)