mirror of
https://gitlab2.federez.net/re2o/re2o
synced 2024-11-25 22:22:26 +00:00
Merge branch 'master' into faster_ipform
This commit is contained in:
commit
af6df474ba
34 changed files with 923 additions and 108 deletions
136
README.md
136
README.md
|
@ -4,17 +4,28 @@ Gnu public license v2.0
|
|||
|
||||
## Avant propos
|
||||
|
||||
Re2o est un logiciel d'administration développé initiallement au rezometz. Il se veut agnostique au réseau considéré, de manière à être installable en quelques clics.
|
||||
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
|
||||
se veut agnostique au réseau considéré, de manière à être installable en
|
||||
quelques clics.
|
||||
|
||||
Il utilise le framework django avec python3. Il permet de gérer les adhérents, les machines, les factures, les droits d'accès, les switchs et la topologie du réseau.
|
||||
De cette manière, il est possible de pluguer très facilement des services dessus, qui accèdent à la base de donnée en passant par django (ex : dhcp), en chargeant la liste de toutes les mac-ip, ou la liste des mac-ip autorisées sur le réseau (adhérent à jour de cotisation).
|
||||
Il utilise le framework django avec python3. Il permet de gérer les adhérents,
|
||||
les machines, les factures, les droits d'accès, les switchs et la topologie du
|
||||
réseau.
|
||||
De cette manière, il est possible de pluguer très facilement des services
|
||||
dessus, qui accèdent à la base de donnée en passant par django (ex : dhcp), en
|
||||
chargeant la liste de toutes les mac-ip, ou la liste des mac-ip autorisées sur
|
||||
le réseau (adhérent à jour de cotisation).
|
||||
|
||||
#Installation
|
||||
|
||||
## Installation des dépendances
|
||||
|
||||
L'installation comporte 3 partie : le serveur web où se trouve le depot re2o ainsi que toutes ses dépendances, le serveur bdd (mysql ou pgsql) et le serveur ldap. Ces 3 serveurs peuvent en réalité être la même machine, ou séparés (recommandé en production).
|
||||
Le serveur web sera nommé serveur A, le serveur bdd serveur B et le serveur ldap serveur C.
|
||||
L'installation comporte 3 partie : le serveur web où se trouve le depot re2o
|
||||
ainsi que toutes ses dépendances, le serveur bdd (mysql ou pgsql) et le
|
||||
serveur ldap. Ces 3 serveurs peuvent en réalité être la même machine, ou séparés
|
||||
(recommandé en production).
|
||||
Le serveur web sera nommé serveur A, le serveur bdd serveur B et le serveur ldap
|
||||
serveur C.
|
||||
|
||||
### Prérequis sur le serveur A
|
||||
|
||||
|
@ -66,49 +77,88 @@ Sur le serveur C (ldap), avec apt :
|
|||
### Installation sur le serveur principal A
|
||||
|
||||
Cloner le dépot re2o à partir du gitlab, par exemple dans /var/www/re2o.
|
||||
Ensuite, il faut créer le fichier settings_local.py dans le sous dossier re2o, un settings_local.example.py est présent. Les options sont commentées, et des options par défaut existent.
|
||||
Ensuite, il faut créer le fichier settings_local.py dans le sous dossier re2o,
|
||||
un settings_local.example.py est présent. Les options sont commentées, et des
|
||||
options par défaut existent.
|
||||
|
||||
En particulier, il est nécessaire de générer un login/mdp admin pour le ldap et un login/mdp pour l'utilisateur sql (cf ci-dessous), à mettre dans settings_local.py
|
||||
En particulier, il est nécessaire de générer un login/mdp admin pour le ldap et
|
||||
un login/mdp pour l'utilisateur sql (cf ci-dessous), à mettre dans
|
||||
settings_local.py
|
||||
|
||||
### Installation du serveur mysql/postgresql sur B
|
||||
|
||||
Sur le serveur mysql ou postgresl, il est nécessaire de créer une base de donnée re2o, ainsi qu'un user re2o et un mot de passe associé. Ne pas oublier de faire écouter le serveur mysql ou postgresql avec les acl nécessaire pour que A puisse l'utiliser.
|
||||
Sur le serveur mysql ou postgresl, il est nécessaire de créer une base de
|
||||
donnée re2o, ainsi qu'un user re2o et un mot de passe associé.
|
||||
Ne pas oublier de faire écouter le serveur mysql ou postgresql avec les acl
|
||||
nécessaire pour que A puisse l'utiliser.
|
||||
|
||||
#### Mysql
|
||||
Voici les étapes à éxecuter pour mysql :
|
||||
* CREATE DATABASE re2o collate='utf8_general_ci';
|
||||
* CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'password';
|
||||
* GRANT ALL PRIVILEGES ON re2o.* TO 'newuser'@'localhost';
|
||||
* FLUSH PRIVILEGES;
|
||||
|
||||
Si les serveurs A et B ne sont pas la même machine, il est nécessaire de remplacer localhost par l'ip avec laquelle A contacte B dans les commandes du dessus.
|
||||
Une fois ces commandes effectuées, ne pas oublier de vérifier que newuser et password sont présents dans settings_local.py
|
||||
#### Postgresql
|
||||
* CREATE DATABASE re2o ENCODING 'UTF8' LC_COLLATE='fr_FR.UTF-8'
|
||||
LC_CTYPE='fr_FR.UTF-8';
|
||||
* CREATE USER newuser with password 'password';
|
||||
* ALTER DATABASE re2o owner to newuser;
|
||||
|
||||
Si les serveurs A et B ne sont pas la même machine, il est nécessaire de
|
||||
remplacer localhost par l'ip avec laquelle A contacte B dans les commandes
|
||||
du dessus.
|
||||
Une fois ces commandes effectuées, ne pas oublier de vérifier que newuser et
|
||||
password sont présents dans settings_local.py
|
||||
|
||||
### Installation du serveur ldap sur le serveur C
|
||||
|
||||
Ceci se fait en plusieurs étapes :
|
||||
* générer un login/mdp administrateur (par example mkpasswd sous debian)
|
||||
* Copier depuis re2o/install_utils (dans le dépot re2o) les fichiers db.ldiff et schema.ldiff (normalement sur le serveur A) sur le serveur C (par ex dans /tmp)
|
||||
* Hasher le mot de passe généré en utilisant la commande slappasswd (installée par slapd)
|
||||
* Remplacer toutes les sections FILL_IN par le hash dans schema.ldiff et db.ldiff
|
||||
* Remplacer dans schema.ldiff et db.ldiff 'dc=example,dc=org' par le suffixe de l'organisation
|
||||
* Copier depuis re2o/install_utils (dans le dépot re2o) les fichiers db.ldiff
|
||||
et schema.ldiff (normalement sur le serveur A) sur le serveur C
|
||||
(par ex dans /tmp)
|
||||
* Hasher le mot de passe généré en utilisant la commande slappasswd
|
||||
(installée par slapd)
|
||||
* Remplacer toutes les sections FILL_IN par le hash dans schema.ldiff et
|
||||
db.ldiff
|
||||
* Remplacer dans schema.ldiff et db.ldiff 'dc=example,dc=org' par le
|
||||
suffixe de l'organisation
|
||||
* Arréter slapd
|
||||
* Supprimer les données existantes : '''rm -rf /etc/ldap/slapd.d/*''' et '''rm -rf /var/lib/ldap/*'''
|
||||
* Injecter le nouveau schéma : '''slapadd -n 0 -l schema.ldiff -F /etc/ldap/slapd.d/''' et '''slapadd -n 1 -l db.ldiff'''
|
||||
* Réparer les permissions (chown -R openldap:openldap /etc/ldap/slapd.d et chown -R openldap:openldap /var/lib/ldap) puis relancer slapd
|
||||
* Supprimer les données existantes : '''rm -rf /etc/ldap/slapd.d/*''' et
|
||||
'''rm -rf /var/lib/ldap/*'''
|
||||
* Injecter le nouveau schéma :
|
||||
'''slapadd -n 0 -l schema.ldiff -F /etc/ldap/slapd.d/''' et
|
||||
'''slapadd -n 1 -l db.ldiff'''
|
||||
* Réparer les permissions (chown -R openldap:openldap /etc/ldap/slapd.d et
|
||||
chown -R openldap:openldap /var/lib/ldap) puis relancer slapd
|
||||
|
||||
Normalement le serveur ldap démare et est fonctionnel. Par défaut tls n'est pas activé, il faut pour cela modifier le schéma pour indiquer l'emplacement du certificat.
|
||||
Pour visualiser et éditer le ldap, l'utilisation de shelldap est fortement recommandée, en utilisant en binddn cn=admin,dc=ldap,dc=example,dc=org et binddpw le mot de passe admin.
|
||||
Pour visualiser et éditer le ldap, l'utilisation de shelldap est fortement
|
||||
recommandée, en utilisant en binddn et basedn tous deux égaux à 'cn=config' et
|
||||
binddpw le mot de passe admin.
|
||||
|
||||
Rajouter (exemple de chemin de fichier avec un certif LE):
|
||||
`olcTLSCertificateKeyFile: /etc/letsencrypt/live/HOSTNAME/privkey.pem
|
||||
olcTLSCACertificateFile: /etc/letsencrypt/live/HOSTNAME/chain.pem
|
||||
olcTLSCertificateFile: /etc/letsencrypt/live/HOSTNAME/cert.pem `
|
||||
|
||||
Mettre à jour la partie ldap du `settings_local.py` (mettre 'TLS' à True
|
||||
si besoin, user cn=config,dc=example,dc=org et mot de passe
|
||||
ldap choisi précédemment).
|
||||
|
||||
## Configuration initiale
|
||||
|
||||
Normalement à cette étape, le ldap et la bdd sql sont configurées correctement.
|
||||
|
||||
Il faut alors lancer dans le dépot re2o '''python3 manage.py migrate''' qui va structurer initialement la base de données.
|
||||
Les migrations sont normalement comitées au fur et à mesure, néanmoins cette étape peut crasher, merci de reporter les bugs.
|
||||
Il faut alors lancer dans le dépot re2o '''python3 manage.py migrate''' qui
|
||||
va structurer initialement la base de données.
|
||||
Les migrations sont normalement comitées au fur et à mesure, néanmoins cette
|
||||
étape peut crasher, merci de reporter les bugs.
|
||||
|
||||
## Démarer le site web
|
||||
|
||||
Il faut utiliser un moteur pour servir le site web. Nginx ou apache2 sont recommandés.
|
||||
Il faut utiliser un moteur pour servir le site web. Nginx ou apache2 sont
|
||||
recommandés.
|
||||
Pour apache2 :
|
||||
* apt install apache2
|
||||
* apt install libapache2-mod-wsgi-py3 (pour le module wsgi)
|
||||
|
@ -119,9 +169,12 @@ re2o/wsgi.py permet de fonctionner avec apache2 en production
|
|||
## Configuration avancée
|
||||
|
||||
Une fois démaré, le site web devrait être accessible.
|
||||
Pour créer un premier user, faire '''python3 manage.py createsuperuser''' qui va alors créer un user admin.
|
||||
Il est conseillé de créer alors les droits cableur, bureau, trésorier et infra, qui n'existent pas par défaut dans le menu adhérents.
|
||||
Il est également conseillé de créer un user portant le nom de l'association/organisation, qui possedera l'ensemble des machines.
|
||||
Pour créer un premier user, faire '''python3 manage.py createsuperuser'''
|
||||
qui va alors créer un user admin.
|
||||
Il est conseillé de créer alors les droits cableur, bureau, trésorier et infra,
|
||||
qui n'existent pas par défaut dans le menu adhérents.
|
||||
Il est également conseillé de créer un user portant le nom de
|
||||
l'association/organisation, qui possedera l'ensemble des machines.
|
||||
|
||||
## Installations Optionnelles
|
||||
### Générer le schéma des dépendances
|
||||
|
@ -134,24 +187,37 @@ Pour cela :
|
|||
|
||||
## Fonctionnement général
|
||||
|
||||
Re2o est séparé entre les models, qui sont visible sur le schéma des dépendances. Il s'agit en réalité des tables sql, et les fields etant les colonnes.
|
||||
Ceci dit il n'est jamais nécessaire de toucher directement au sql, django procédant automatiquement à tout cela.
|
||||
On crée donc différents models (user, right pour les droits des users, interfaces, IpList pour l'ensemble des adresses ip, etc)
|
||||
Re2o est séparé entre les models, qui sont visible sur le schéma des
|
||||
dépendances. Il s'agit en réalité des tables sql, et les fields etant les
|
||||
colonnes.
|
||||
Ceci dit il n'est jamais nécessaire de toucher directement au sql, django
|
||||
procédant automatiquement à tout cela.
|
||||
On crée donc différents models (user, right pour les droits des users,
|
||||
interfaces, IpList pour l'ensemble des adresses ip, etc)
|
||||
|
||||
Du coté des forms, il s'agit des formulaire d'édition des models. Il s'agit de ModelForms django, qui héritent des models très simplement, voir la documentation django models forms.
|
||||
Du coté des forms, il s'agit des formulaire d'édition des models. Il
|
||||
s'agit de ModelForms django, qui héritent des models très simplement, voir la
|
||||
documentation django models forms.
|
||||
|
||||
Enfin les views, générent les pages web à partir des forms et des templates.
|
||||
|
||||
## Fonctionnement avec les services
|
||||
|
||||
Les services dhcp.py, dns.py etc accèdent aux données via des vues rest.
|
||||
Celles-ci se trouvent dans machines/views.py. Elles sont générées via machines/serializers.py qui génère les vues. IL s'agit de vues en json utilisées par re2o-tools pour récupérer les données.
|
||||
Il est nécessaire de créer un user dans re2o avec le droit serveur qui permet d'accéder à ces vues, utilisé par re2o-tools.
|
||||
Celles-ci se trouvent dans machines/views.py. Elles sont générées via
|
||||
machines/serializers.py qui génère les vues. IL s'agit de vues en json utilisées
|
||||
par re2o-tools pour récupérer les données.
|
||||
Il est nécessaire de créer un user dans re2o avec le droit serveur qui permet
|
||||
d'accéder à ces vues, utilisé par re2o-tools.
|
||||
|
||||
# Requète en base de donnée
|
||||
|
||||
Pour avoir un shell, il suffit de lancer '''python3 manage.py shell'''
|
||||
Pour charger des objets, example avec User, faire : ''' from users.models import User'''
|
||||
Pour charger les objets django, il suffit de faire User.objects.all() pour tous les users par exemple.
|
||||
Il est ensuite aisé de faire des requètes, par exemple User.objects.filter(pseudo='test')
|
||||
Des exemples et la documentation complète sur les requètes django sont disponible sur le site officiel.
|
||||
Pour charger des objets, example avec User, faire :
|
||||
''' from users.models import User'''
|
||||
Pour charger les objets django, il suffit de faire User.objects.all()
|
||||
pour tous les users par exemple.
|
||||
Il est ensuite aisé de faire des requètes, par exemple
|
||||
User.objects.filter(pseudo='test')
|
||||
Des exemples et la documentation complète sur les requètes django sont
|
||||
disponible sur le site officiel.
|
||||
|
|
|
@ -109,7 +109,7 @@ def new_facture(request, userid):
|
|||
return form({'factureform': facture_form, 'venteform': article_formset, 'articlelist': article_list}, 'cotisations/new_facture.html', request)
|
||||
|
||||
@login_required
|
||||
@permission_required('trésorier')
|
||||
@permission_required('tresorier')
|
||||
def new_facture_pdf(request):
|
||||
facture_form = NewFactureFormPdf(request.POST or None)
|
||||
if facture_form.is_valid():
|
||||
|
@ -156,7 +156,7 @@ def edit_facture(request, factureid):
|
|||
except Facture.DoesNotExist:
|
||||
messages.error(request, u"Facture inexistante" )
|
||||
return redirect("/cotisations/")
|
||||
if request.user.has_perms(['trésorier']):
|
||||
if request.user.has_perms(['tresorier']):
|
||||
facture_form = TrezEditFactureForm(request.POST or None, instance=facture)
|
||||
elif facture.control or not facture.valid:
|
||||
messages.error(request, "Vous ne pouvez pas editer une facture controlée ou invalidée par le trésorier")
|
||||
|
@ -223,7 +223,7 @@ def credit_solde(request, userid):
|
|||
|
||||
|
||||
@login_required
|
||||
@permission_required('trésorier')
|
||||
@permission_required('tresorier')
|
||||
def add_article(request):
|
||||
article = ArticleForm(request.POST or None)
|
||||
if article.is_valid():
|
||||
|
@ -236,7 +236,7 @@ def add_article(request):
|
|||
return form({'factureform': article}, 'cotisations/facture.html', request)
|
||||
|
||||
@login_required
|
||||
@permission_required('trésorier')
|
||||
@permission_required('tresorier')
|
||||
def edit_article(request, articleid):
|
||||
try:
|
||||
article_instance = Article.objects.get(pk=articleid)
|
||||
|
@ -254,7 +254,7 @@ def edit_article(request, articleid):
|
|||
return form({'factureform': article}, 'cotisations/facture.html', request)
|
||||
|
||||
@login_required
|
||||
@permission_required('trésorier')
|
||||
@permission_required('tresorier')
|
||||
def del_article(request):
|
||||
article = DelArticleForm(request.POST or None)
|
||||
if article.is_valid():
|
||||
|
@ -267,7 +267,7 @@ def del_article(request):
|
|||
return form({'factureform': article}, 'cotisations/facture.html', request)
|
||||
|
||||
@login_required
|
||||
@permission_required('trésorier')
|
||||
@permission_required('tresorier')
|
||||
def add_paiement(request):
|
||||
paiement = PaiementForm(request.POST or None)
|
||||
if paiement.is_valid():
|
||||
|
@ -280,7 +280,7 @@ def add_paiement(request):
|
|||
return form({'factureform': paiement}, 'cotisations/facture.html', request)
|
||||
|
||||
@login_required
|
||||
@permission_required('trésorier')
|
||||
@permission_required('tresorier')
|
||||
def edit_paiement(request, paiementid):
|
||||
try:
|
||||
paiement_instance = Paiement.objects.get(pk=paiementid)
|
||||
|
@ -298,7 +298,7 @@ def edit_paiement(request, paiementid):
|
|||
return form({'factureform': paiement}, 'cotisations/facture.html', request)
|
||||
|
||||
@login_required
|
||||
@permission_required('trésorier')
|
||||
@permission_required('tresorier')
|
||||
def del_paiement(request):
|
||||
paiement = DelPaiementForm(request.POST or None)
|
||||
if paiement.is_valid():
|
||||
|
@ -329,7 +329,7 @@ def add_banque(request):
|
|||
return form({'factureform': banque}, 'cotisations/facture.html', request)
|
||||
|
||||
@login_required
|
||||
@permission_required('trésorier')
|
||||
@permission_required('tresorier')
|
||||
def edit_banque(request, banqueid):
|
||||
try:
|
||||
banque_instance = Banque.objects.get(pk=banqueid)
|
||||
|
@ -347,7 +347,7 @@ def edit_banque(request, banqueid):
|
|||
return form({'factureform': banque}, 'cotisations/facture.html', request)
|
||||
|
||||
@login_required
|
||||
@permission_required('trésorier')
|
||||
@permission_required('tresorier')
|
||||
def del_banque(request):
|
||||
banque = DelBanqueForm(request.POST or None)
|
||||
if banque.is_valid():
|
||||
|
@ -365,7 +365,7 @@ def del_banque(request):
|
|||
return form({'factureform': banque}, 'cotisations/facture.html', request)
|
||||
|
||||
@login_required
|
||||
@permission_required('trésorier')
|
||||
@permission_required('tresorier')
|
||||
def control(request):
|
||||
options, created = GeneralOption.objects.get_or_create()
|
||||
pagination_number = options.pagination_number
|
||||
|
|
|
@ -257,12 +257,15 @@ def check_user_machine_and_register(nas_type, username, mac_address):
|
|||
return (True, u'Access Ok, Capture de la mac...', user.pwd_ntlm)
|
||||
else:
|
||||
return (False, u'Erreur dans le register mac %s' % reason, '')
|
||||
else:
|
||||
return (False, u'Machine inconnue', '')
|
||||
else:
|
||||
return (False, u"Machine inconnue", '')
|
||||
|
||||
|
||||
def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address):
|
||||
# Get port from switch and port number
|
||||
extra_log = ""
|
||||
if not nas:
|
||||
return ('?', u'Nas inconnu', VLAN_OK)
|
||||
|
||||
|
@ -273,9 +276,15 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address):
|
|||
return (sw_name, u'Port inconnu', VLAN_OK)
|
||||
|
||||
port = port.first()
|
||||
# Si un vlan a été précisé, on l'utilise pour VLAN_OK
|
||||
if port.vlan_force:
|
||||
DECISION_VLAN = int(port.vlan_force.vlan_id)
|
||||
extra_log = u"Force sur vlan " + str(DECISION_VLAN)
|
||||
else:
|
||||
DECISION_VLAN = VLAN_OK
|
||||
|
||||
if port.radius == 'NO':
|
||||
return (sw_name, u"Pas d'authentification sur ce port", VLAN_OK)
|
||||
return (sw_name, u"Pas d'authentification sur ce port" + extra_log, DECISION_VLAN)
|
||||
|
||||
if port.radius == 'BLOQ':
|
||||
return (sw_name, u'Port desactive', VLAN_NOK)
|
||||
|
@ -309,16 +318,12 @@ def decide_vlan_and_register_switch(nas, nas_type, port_number, mac_address):
|
|||
else:
|
||||
result, reason = room_user.first().autoregister_machine(mac_address, nas_type)
|
||||
if result:
|
||||
return (sw_name, u'Access Ok, Capture de la mac...', VLAN_OK)
|
||||
return (sw_name, u'Access Ok, Capture de la mac...' + extra_log, DECISION_VLAN)
|
||||
else:
|
||||
return (sw_name, u'Erreur dans le register mac %s' % reason + unicode(mac_address), VLAN_NOK)
|
||||
elif not interface.first().is_active:
|
||||
return (sw_name, u'Machine non active / adherent non cotisant', VLAN_NOK)
|
||||
else:
|
||||
return (sw_name, u'Machine OK', VLAN_OK)
|
||||
|
||||
# On gere bien tous les autres états possibles, il ne reste que le VLAN en dur
|
||||
return (sw_name, u'VLAN impose', int(port.radius))
|
||||
|
||||
return (sw_name, u'Machine OK' + extra_log, DECISION_VLAN)
|
||||
|
||||
|
||||
|
|
|
@ -106,7 +106,7 @@ def index(request):
|
|||
'user_id': v.revision.user_id,
|
||||
'version': v }
|
||||
else :
|
||||
to_remove.append(i)
|
||||
to_remove.insert(0,i)
|
||||
# Remove all tagged invalid items
|
||||
for i in to_remove :
|
||||
versions.object_list.pop(i)
|
||||
|
|
|
@ -26,7 +26,9 @@ from __future__ import unicode_literals
|
|||
from django.contrib import admin
|
||||
from reversion.admin import VersionAdmin
|
||||
|
||||
from .models import IpType, Machine, MachineType, Domain, IpList, Interface, Extension, Mx, Ns, Vlan, Text, Nas, Service
|
||||
from .models import IpType, Machine, MachineType, Domain, IpList, Interface
|
||||
from .models import Extension, Mx, Ns, Vlan, Text, Nas, Service, OuverturePort
|
||||
from .models import OuverturePortList
|
||||
|
||||
class MachineAdmin(VersionAdmin):
|
||||
pass
|
||||
|
@ -58,6 +60,12 @@ class NasAdmin(VersionAdmin):
|
|||
class IpListAdmin(VersionAdmin):
|
||||
pass
|
||||
|
||||
class OuverturePortAdmin(VersionAdmin):
|
||||
pass
|
||||
|
||||
class OuverturePortListAdmin(VersionAdmin):
|
||||
pass
|
||||
|
||||
class InterfaceAdmin(VersionAdmin):
|
||||
list_display = ('machine','type','mac_address','ipv4','details')
|
||||
|
||||
|
@ -80,3 +88,7 @@ admin.site.register(Domain, DomainAdmin)
|
|||
admin.site.register(Service, ServiceAdmin)
|
||||
admin.site.register(Vlan, VlanAdmin)
|
||||
admin.site.register(Nas, NasAdmin)
|
||||
admin.site.register(OuverturePort, OuverturePortAdmin)
|
||||
admin.site.register(OuverturePortList, OuverturePortListAdmin)
|
||||
|
||||
|
||||
|
|
|
@ -24,9 +24,11 @@
|
|||
|
||||
from __future__ import unicode_literals
|
||||
|
||||
import re
|
||||
|
||||
from django.forms import ModelForm, Form, ValidationError
|
||||
from django import forms
|
||||
from .models import Domain, Machine, Interface, IpList, MachineType, Extension, Mx, Text, Ns, Service, Vlan, Nas, IpType
|
||||
from .models import Domain, Machine, Interface, IpList, MachineType, Extension, Mx, Text, Ns, Service, Vlan, Nas, IpType, OuverturePortList, OuverturePort
|
||||
from django.db.models import Q, F
|
||||
from django.core.validators import validate_email
|
||||
|
||||
|
@ -116,11 +118,11 @@ class DomainForm(AliasForm):
|
|||
fields = ['name']
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
if 'name_user' in kwargs:
|
||||
name_user = kwargs.pop('name_user')
|
||||
if 'user' in kwargs:
|
||||
user = kwargs.pop('user')
|
||||
nb_machine = kwargs.pop('nb_machine')
|
||||
initial = kwargs.get('initial', {})
|
||||
initial['name'] = name_user.lower()+str(nb_machine)
|
||||
initial['name'] = user.get_next_domain_name()
|
||||
kwargs['initial'] = initial
|
||||
super(DomainForm, self).__init__(*args, **kwargs)
|
||||
|
||||
|
@ -148,7 +150,7 @@ class DelMachineTypeForm(Form):
|
|||
class IpTypeForm(ModelForm):
|
||||
class Meta:
|
||||
model = IpType
|
||||
fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'vlan']
|
||||
fields = ['type','extension','need_infra','domaine_ip_start','domaine_ip_stop', 'prefix_v6', 'vlan']
|
||||
|
||||
|
||||
def __init__(self, *args, **kwargs):
|
||||
|
@ -157,7 +159,7 @@ class IpTypeForm(ModelForm):
|
|||
|
||||
class EditIpTypeForm(IpTypeForm):
|
||||
class Meta(IpTypeForm.Meta):
|
||||
fields = ['extension','type','need_infra', 'vlan']
|
||||
fields = ['extension','type','need_infra', 'prefix_v6', 'vlan']
|
||||
|
||||
class DelIpTypeForm(Form):
|
||||
iptypes = forms.ModelMultipleChoiceField(queryset=IpType.objects.all(), label="Types d'ip actuelles", widget=forms.CheckboxSelectMultiple)
|
||||
|
@ -238,5 +240,13 @@ class VlanForm(ModelForm):
|
|||
class DelVlanForm(Form):
|
||||
vlan = forms.ModelMultipleChoiceField(queryset=Vlan.objects.all(), label="Vlan actuels", widget=forms.CheckboxSelectMultiple)
|
||||
|
||||
class EditOuverturePortConfigForm(ModelForm):
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ['port_lists']
|
||||
|
||||
class EditOuverturePortListForm(ModelForm):
|
||||
class Meta:
|
||||
model = OuverturePortList
|
||||
fields = '__all__'
|
||||
|
||||
|
|
43
machines/migrations/0058_auto_20171002_0350.py
Normal file
43
machines/migrations/0058_auto_20171002_0350.py
Normal 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'),
|
||||
),
|
||||
]
|
20
machines/migrations/0059_iptype_prefix_v6.py
Normal file
20
machines/migrations/0059_iptype_prefix_v6.py
Normal 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'),
|
||||
),
|
||||
]
|
|
@ -56,6 +56,7 @@ class MachineType(models.Model):
|
|||
ip_type = models.ForeignKey('IpType', on_delete=models.PROTECT, blank=True, null=True)
|
||||
|
||||
def all_interfaces(self):
|
||||
""" Renvoie toutes les interfaces (cartes réseaux) de type machinetype"""
|
||||
return Interface.objects.filter(type=self)
|
||||
|
||||
def __str__(self):
|
||||
|
@ -70,27 +71,36 @@ class IpType(models.Model):
|
|||
need_infra = models.BooleanField(default=False)
|
||||
domaine_ip_start = models.GenericIPAddressField(protocol='IPv4')
|
||||
domaine_ip_stop = models.GenericIPAddressField(protocol='IPv4')
|
||||
prefix_v6 = models.GenericIPAddressField(protocol='IPv6', null=True, blank=True)
|
||||
vlan = models.ForeignKey('Vlan', on_delete=models.PROTECT, blank=True, null=True)
|
||||
|
||||
@cached_property
|
||||
def ip_range(self):
|
||||
""" Renvoie un objet IPRange à partir de l'objet IpType"""
|
||||
return IPRange(self.domaine_ip_start, end=self.domaine_ip_stop)
|
||||
|
||||
@cached_property
|
||||
def ip_set(self):
|
||||
""" Renvoie une IPSet à partir de l'iptype"""
|
||||
return IPSet(self.ip_range)
|
||||
|
||||
@cached_property
|
||||
def ip_set_as_str(self):
|
||||
""" Renvoie une liste des ip en string"""
|
||||
return [str(x) for x in self.ip_set]
|
||||
|
||||
def ip_objects(self):
|
||||
""" Renvoie tous les objets ipv4 relié à ce type"""
|
||||
return IpList.objects.filter(ip_type=self)
|
||||
|
||||
def free_ip(self):
|
||||
""" Renvoie toutes les ip libres associées au type donné (self)"""
|
||||
return IpList.objects.filter(interface__isnull=True).filter(ip_type=self)
|
||||
|
||||
def gen_ip_range(self):
|
||||
""" Cree les IpList associées au type self. Parcours pédestrement et crée
|
||||
les ip une par une. Si elles existent déjà, met à jour le type associé
|
||||
à l'ip"""
|
||||
# Creation du range d'ip dans les objets iplist
|
||||
networks = []
|
||||
for net in self.ip_range.cidrs():
|
||||
|
@ -113,6 +123,11 @@ class IpType(models.Model):
|
|||
ip.delete()
|
||||
|
||||
def clean(self):
|
||||
""" Nettoyage. Vérifie :
|
||||
- Que ip_stop est après ip_start
|
||||
- Qu'on ne crée pas plus gros qu'un /16
|
||||
- Que le range crée ne recoupe pas un range existant
|
||||
- Formate l'ipv6 donnée en /64"""
|
||||
if IPAddress(self.domaine_ip_start) > IPAddress(self.domaine_ip_stop):
|
||||
raise ValidationError("Domaine end doit être après start...")
|
||||
# On ne crée pas plus grand qu'un /16
|
||||
|
@ -122,6 +137,9 @@ class IpType(models.Model):
|
|||
for element in IpType.objects.all().exclude(pk=self.pk):
|
||||
if not self.ip_set.isdisjoint(element.ip_set):
|
||||
raise ValidationError("Le range indiqué n'est pas disjoint des ranges existants")
|
||||
# On formate le prefix v6
|
||||
if self.prefix_v6:
|
||||
self.prefix_v6 = str(IPNetwork(self.prefix_v6 + '/64').network)
|
||||
return
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
|
@ -132,6 +150,7 @@ class IpType(models.Model):
|
|||
return self.type
|
||||
|
||||
class Vlan(models.Model):
|
||||
""" Un vlan : vlan_id et nom"""
|
||||
PRETTY_NAME = "Vlans"
|
||||
|
||||
vlan_id = models.IntegerField()
|
||||
|
@ -142,6 +161,9 @@ class Vlan(models.Model):
|
|||
return self.name
|
||||
|
||||
class Nas(models.Model):
|
||||
""" Les nas. Associé à un machine_type.
|
||||
Permet aussi de régler le port_access_mode (802.1X ou mac-address) pour
|
||||
le radius. Champ autocapture de la mac à true ou false"""
|
||||
PRETTY_NAME = "Correspondance entre les nas et les machines connectées"
|
||||
|
||||
default_mode = '802.1X'
|
||||
|
@ -160,6 +182,8 @@ class Nas(models.Model):
|
|||
return self.name
|
||||
|
||||
class Extension(models.Model):
|
||||
""" Extension dns type example.org. Précise si tout le monde peut l'utiliser,
|
||||
associé à un origin (ip d'origine)"""
|
||||
PRETTY_NAME = "Extensions dns"
|
||||
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
|
@ -168,12 +192,15 @@ class Extension(models.Model):
|
|||
|
||||
@cached_property
|
||||
def dns_entry(self):
|
||||
""" Une entrée DNS A"""
|
||||
return "@ IN A " + str(self.origin)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
class Mx(models.Model):
|
||||
""" Entrées des MX. Enregistre la zone (extension) associée et la priorité
|
||||
Todo : pouvoir associer un MX à une interface """
|
||||
PRETTY_NAME = "Enregistrements MX"
|
||||
|
||||
zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
|
||||
|
@ -201,6 +228,7 @@ class Ns(models.Model):
|
|||
return str(self.zone) + ' ' + str(self.ns)
|
||||
|
||||
class Text(models.Model):
|
||||
""" Un enregistrement TXT associé à une extension"""
|
||||
PRETTY_NAME = "Enregistrement text"
|
||||
|
||||
zone = models.ForeignKey('Extension', on_delete=models.PROTECT)
|
||||
|
@ -215,14 +243,20 @@ class Text(models.Model):
|
|||
return str(self.field1) + " IN TXT " + str(self.field2)
|
||||
|
||||
class Interface(models.Model):
|
||||
""" Une interface. Objet clef de l'application machine :
|
||||
- une address mac unique. Possibilité de la rendre unique avec le typemachine
|
||||
- une onetoone vers IpList pour attribution ipv4
|
||||
- le type parent associé au range ip et à l'extension
|
||||
- un objet domain associé contenant son nom
|
||||
- la liste des ports oiuvert"""
|
||||
PRETTY_NAME = "Interface"
|
||||
|
||||
ipv4 = models.OneToOneField('IpList', on_delete=models.PROTECT, blank=True, null=True)
|
||||
#ipv6 = models.GenericIPAddressField(protocol='IPv6', null=True)
|
||||
mac_address = MACAddressField(integer=False, unique=True)
|
||||
machine = models.ForeignKey('Machine', on_delete=models.CASCADE)
|
||||
type = models.ForeignKey('MachineType', on_delete=models.PROTECT)
|
||||
details = models.CharField(max_length=255, blank=True)
|
||||
port_lists = models.ManyToManyField('OuverturePortList', blank=True)
|
||||
|
||||
@cached_property
|
||||
def is_active(self):
|
||||
|
@ -231,16 +265,34 @@ class Interface(models.Model):
|
|||
user = self.machine.user
|
||||
return machine.active and user.has_access()
|
||||
|
||||
|
||||
@cached_property
|
||||
def ipv6_object(self):
|
||||
""" Renvoie un objet type ipv6 à partir du prefix associé à l'iptype parent"""
|
||||
if self.type.ip_type.prefix_v6:
|
||||
return EUI(self.mac_address).ipv6(IPNetwork(self.type.ip_type.prefix_v6).network)
|
||||
else:
|
||||
return None
|
||||
|
||||
@cached_property
|
||||
def ipv6(self):
|
||||
""" Renvoie l'ipv6 en str. Mise en cache et propriété de l'objet"""
|
||||
return str(self.ipv6_object)
|
||||
|
||||
def mac_bare(self):
|
||||
""" Formatage de la mac type mac_bare"""
|
||||
return str(EUI(self.mac_address, dialect=mac_bare)).lower()
|
||||
|
||||
def filter_macaddress(self):
|
||||
""" Tente un formatage mac_bare, si échoue, lève une erreur de validation"""
|
||||
try:
|
||||
self.mac_address = str(EUI(self.mac_address))
|
||||
except :
|
||||
raise ValidationError("La mac donnée est invalide")
|
||||
|
||||
def clean(self, *args, **kwargs):
|
||||
""" Formate l'addresse mac en mac_bare (fonction filter_mac)
|
||||
et assigne une ipv4 dans le bon range si inexistante ou incohérente"""
|
||||
self.filter_macaddress()
|
||||
self.mac_address = str(EUI(self.mac_address)) or None
|
||||
if not self.ipv4 or self.type.ip_type != self.ipv4.ip_type:
|
||||
|
@ -257,6 +309,7 @@ class Interface(models.Model):
|
|||
return
|
||||
|
||||
def unassign_ipv4(self):
|
||||
""" Sans commentaire, désassigne une ipv4"""
|
||||
self.ipv4 = None
|
||||
|
||||
def update_type(self):
|
||||
|
@ -278,7 +331,21 @@ class Interface(models.Model):
|
|||
domain = None
|
||||
return str(domain)
|
||||
|
||||
def has_private_ip(self):
|
||||
""" True si l'ip associée est privée"""
|
||||
if self.ipv4:
|
||||
return IPAddress(str(self.ipv4)).is_private()
|
||||
else:
|
||||
return False
|
||||
|
||||
def may_have_port_open(self):
|
||||
""" True si l'interface a une ip et une ip publique.
|
||||
Permet de ne pas exporter des ouvertures sur des ip privées (useless)"""
|
||||
return self.ipv4 and not self.has_private_ip()
|
||||
|
||||
class Domain(models.Model):
|
||||
""" Objet domain. Enregistrement A et CNAME en même temps : permet de stocker les
|
||||
alias et les nom de machines, suivant si interface_parent ou cname sont remplis"""
|
||||
PRETTY_NAME = "Domaine dns"
|
||||
|
||||
interface_parent = models.OneToOneField('Interface', on_delete=models.CASCADE, blank=True, null=True)
|
||||
|
@ -290,6 +357,8 @@ class Domain(models.Model):
|
|||
unique_together = (("name", "extension"),)
|
||||
|
||||
def get_extension(self):
|
||||
""" Retourne l'extension de l'interface parente si c'est un A
|
||||
Retourne l'extension propre si c'est un cname, renvoie None sinon"""
|
||||
if self.interface_parent:
|
||||
return self.interface_parent.type.ip_type.extension
|
||||
elif hasattr(self,'extension'):
|
||||
|
@ -298,6 +367,11 @@ class Domain(models.Model):
|
|||
return None
|
||||
|
||||
def clean(self):
|
||||
""" Validation :
|
||||
- l'objet est bien soit A soit CNAME
|
||||
- le cname est pas pointé sur lui-même
|
||||
- le nom contient bien les caractères autorisés par la norme dns et moins de 63 caractères au total
|
||||
- le couple nom/extension est bien unique"""
|
||||
if self.get_extension():
|
||||
self.extension=self.get_extension()
|
||||
""" Validation du nom de domaine, extensions dans type de machine, prefixe pas plus long que 63 caractères """
|
||||
|
@ -316,10 +390,12 @@ class Domain(models.Model):
|
|||
|
||||
@cached_property
|
||||
def dns_entry(self):
|
||||
""" Une entrée DNS"""
|
||||
if self.cname:
|
||||
return str(self.name) + " IN CNAME " + str(self.cname) + "."
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
""" Empèche le save sans extension valide. Force à avoir appellé clean avant"""
|
||||
if not self.get_extension():
|
||||
raise ValidationError("Extension invalide")
|
||||
self.full_clean()
|
||||
|
@ -336,9 +412,11 @@ class IpList(models.Model):
|
|||
|
||||
@cached_property
|
||||
def need_infra(self):
|
||||
""" Permet de savoir si un user basique peut assigner cette ip ou non"""
|
||||
return self.ip_type.need_infra
|
||||
|
||||
def clean(self):
|
||||
""" Erreur si l'ip_type est incorrect"""
|
||||
if not str(self.ipv4) in self.ip_type.ip_set_as_str:
|
||||
raise ValidationError("L'ipv4 et le range de l'iptype ne correspondent pas!")
|
||||
return
|
||||
|
@ -406,6 +484,67 @@ class Service_link(models.Model):
|
|||
def __str__(self):
|
||||
return str(self.server) + " " + str(self.service)
|
||||
|
||||
|
||||
class OuverturePortList(models.Model):
|
||||
"""Liste des ports ouverts sur une interface."""
|
||||
name = models.CharField(help_text="Nom de la configuration des ports.", max_length=255)
|
||||
|
||||
def __str__(self):
|
||||
return self.name
|
||||
|
||||
def tcp_ports_in(self):
|
||||
return self.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.IN)
|
||||
|
||||
def udp_ports_in(self):
|
||||
return self.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.IN)
|
||||
|
||||
def tcp_ports_out(self):
|
||||
return self.ouvertureport_set.filter(protocole=OuverturePort.TCP, io=OuverturePort.OUT)
|
||||
|
||||
def udp_ports_out(self):
|
||||
return self.ouvertureport_set.filter(protocole=OuverturePort.UDP, io=OuverturePort.OUT)
|
||||
|
||||
|
||||
class OuverturePort(models.Model):
|
||||
"""
|
||||
Représente un simple port ou une plage de ports.
|
||||
|
||||
Les ports de la plage sont compris entre begin et en inclus.
|
||||
Si begin == end alors on ne représente qu'un seul port.
|
||||
"""
|
||||
TCP = 'T'
|
||||
UDP = 'U'
|
||||
IN = 'I'
|
||||
OUT = 'O'
|
||||
begin = models.IntegerField()
|
||||
end = models.IntegerField()
|
||||
port_list = models.ForeignKey('OuverturePortList', on_delete=models.CASCADE)
|
||||
protocole = models.CharField(
|
||||
max_length=1,
|
||||
choices=(
|
||||
(TCP, 'TCP'),
|
||||
(UDP, 'UDP'),
|
||||
),
|
||||
default=TCP,
|
||||
)
|
||||
io = models.CharField(
|
||||
max_length=1,
|
||||
choices=(
|
||||
(IN, 'IN'),
|
||||
(OUT, 'OUT'),
|
||||
),
|
||||
default=OUT,
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
if self.begin == self.end :
|
||||
return str(self.begin)
|
||||
return '-'.join([str(self.begin), str(self.end)])
|
||||
|
||||
def show_port(self):
|
||||
return str(self)
|
||||
|
||||
|
||||
@receiver(post_save, sender=Machine)
|
||||
def machine_post_save(sender, **kwargs):
|
||||
user = kwargs['instance'].user
|
||||
|
@ -426,6 +565,9 @@ def interface_post_save(sender, **kwargs):
|
|||
interface = kwargs['instance']
|
||||
user = interface.machine.user
|
||||
user.ldap_sync(base=False, access_refresh=False, mac_refresh=True)
|
||||
if not interface.may_have_port_open() and interface.port_lists.all():
|
||||
interface.port_lists.clear()
|
||||
# Regen services
|
||||
regen('dhcp')
|
||||
regen('mac_ip_list')
|
||||
|
||||
|
|
|
@ -56,6 +56,25 @@ class InterfaceSerializer(serializers.ModelSerializer):
|
|||
def get_macaddress(self, obj):
|
||||
return str(obj.mac_address)
|
||||
|
||||
class FullInterfaceSerializer(serializers.ModelSerializer):
|
||||
ipv4 = IpListSerializer(read_only=True)
|
||||
mac_address = serializers.SerializerMethodField('get_macaddress')
|
||||
domain = serializers.SerializerMethodField('get_dns')
|
||||
extension = serializers.SerializerMethodField('get_interface_extension')
|
||||
|
||||
class Meta:
|
||||
model = Interface
|
||||
fields = ('ipv4', 'ipv6', 'mac_address', 'domain', 'extension')
|
||||
|
||||
def get_dns(self, obj):
|
||||
return obj.domain.name
|
||||
|
||||
def get_interface_extension(self, obj):
|
||||
return obj.domain.extension.name
|
||||
|
||||
def get_macaddress(self, obj):
|
||||
return str(obj.mac_address)
|
||||
|
||||
class ExtensionNameField(serializers.RelatedField):
|
||||
def to_representation(self, value):
|
||||
return value.name
|
||||
|
|
|
@ -30,6 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<th>Nécessite l'autorisation infra</th>
|
||||
<th>Début</th>
|
||||
<th>Fin</th>
|
||||
<th>Préfixe v6</th>
|
||||
<th>Sur vlan</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.domaine_ip_start }}</td>
|
||||
<td>{{ type.domaine_ip_stop }}</td>
|
||||
<td>{{ type.prefix_v6 }}</td>
|
||||
<td>{{ type.vlan }}</td>
|
||||
<td class="text-right">
|
||||
{% if is_infra %}
|
||||
|
|
|
@ -34,7 +34,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<th>Nom dns</th>
|
||||
<th>Type</th>
|
||||
<th>Mac</th>
|
||||
<th>Ipv4</th>
|
||||
<th>IP</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
</thead>
|
||||
|
@ -74,7 +74,12 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
</td>
|
||||
<td>{{ interface.type }}</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>
|
||||
<div class="dropdown">
|
||||
<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
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'machines:port-config' interface.id%}">
|
||||
<i class="glyphicon glyphicon-edit"></i> Gerer la configuration des ports
|
||||
</a>
|
||||
</li>
|
||||
<li>
|
||||
<a href="{% url 'machines:history' 'interface' interface.id %}">
|
||||
<i class="glyphicon glyphicon-time"></i> Historique
|
||||
|
|
69
machines/templates/machines/edit_portlist.html
Normal file
69
machines/templates/machines/edit_portlist.html
Normal 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 %}
|
57
machines/templates/machines/index_portlist.html
Normal file
57
machines/templates/machines/index_portlist.html
Normal 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 %}
|
|
@ -55,4 +55,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
Services (dhcp, dns...)
|
||||
</a>
|
||||
{% 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 %}
|
||||
|
|
|
@ -93,4 +93,10 @@ urlpatterns = [
|
|||
url(r'^rest/text/$', views.text, name='text'),
|
||||
url(r'^rest/zones/$', views.zones, name='zones'),
|
||||
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'),
|
||||
|
||||
]
|
||||
|
|
|
@ -37,20 +37,21 @@ from django.template import Context, RequestContext, loader
|
|||
from django.contrib import messages
|
||||
from django.contrib.auth.decorators import login_required, permission_required
|
||||
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.contrib.auth import authenticate, login
|
||||
from django.views.decorators.csrf import csrf_exempt
|
||||
|
||||
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.models import Version
|
||||
|
||||
import re
|
||||
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 .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 all_has_access
|
||||
from preferences.models import GeneralOption, OptionalMachine
|
||||
|
@ -132,7 +133,7 @@ def new_machine(request, userid):
|
|||
machine = NewMachineForm(request.POST or None)
|
||||
interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',)))
|
||||
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():
|
||||
new_machine = machine.save(commit=False)
|
||||
new_machine.user = user
|
||||
|
@ -957,6 +958,103 @@ def history(request, object, id):
|
|||
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 """
|
||||
|
||||
class JSONResponse(HttpResponse):
|
||||
|
@ -973,6 +1071,14 @@ def mac_ip_list(request):
|
|||
seria = InterfaceSerializer(interfaces, many=True)
|
||||
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
|
||||
@login_required
|
||||
@permission_required('serveur')
|
||||
|
@ -1032,7 +1138,7 @@ def mac_ip(request):
|
|||
@login_required
|
||||
@permission_required('serveur')
|
||||
def mac_ip_dns(request):
|
||||
seria = mac_ip_list(request)
|
||||
seria = full_mac_ip_list(request)
|
||||
return JSONResponse(seria)
|
||||
|
||||
@csrf_exempt
|
||||
|
|
20
preferences/migrations/0020_optionalmachine_ipv6.py
Normal file
20
preferences/migrations/0020_optionalmachine_ipv6.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -45,6 +45,7 @@ class OptionalMachine(models.Model):
|
|||
password_machine = models.BooleanField(default=False)
|
||||
max_lambdauser_interfaces = models.IntegerField(default=10)
|
||||
max_lambdauser_aliases = models.IntegerField(default=10)
|
||||
ipv6 = models.BooleanField(default=False)
|
||||
|
||||
class OptionalTopologie(models.Model):
|
||||
PRETTY_NAME = "Options topologie"
|
||||
|
|
|
@ -72,6 +72,8 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<tr>
|
||||
<th>Alias dns autorisé par utilisateur</th>
|
||||
<td>{{ machineoptions.max_lambdauser_aliases }}</td>
|
||||
<th>Support de l'ipv6</th>
|
||||
<td>{{ machineoptions.ipv6 }}</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h4>Préférences topologie</h4>
|
||||
|
|
|
@ -23,10 +23,11 @@
|
|||
from __future__ import unicode_literals
|
||||
|
||||
from machines.models import Interface, Machine
|
||||
from preferences.models import GeneralOption
|
||||
from preferences.models import GeneralOption, OptionalMachine
|
||||
|
||||
def context_user(request):
|
||||
general_options, created = GeneralOption.objects.get_or_create()
|
||||
machine_options, created = OptionalMachine.objects.get_or_create()
|
||||
user = request.user
|
||||
if user.is_authenticated():
|
||||
interfaces = user.user_interfaces()
|
||||
|
@ -54,4 +55,5 @@ def context_user(request):
|
|||
'is_admin' : is_admin,
|
||||
'interfaces': interfaces,
|
||||
'site_name': general_options.site_name,
|
||||
'ipv6_enabled' : machine_options.ipv6,
|
||||
}
|
||||
|
|
|
@ -157,8 +157,8 @@ STATIC_URL = '/static/'
|
|||
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
|
||||
|
||||
RIGHTS_LINK = {
|
||||
'cableur' : ['bureau','infra','bofh','trésorier'],
|
||||
'bofh' : ['bureau','trésorier'],
|
||||
'cableur' : ['bureau','infra','bofh','tresorier'],
|
||||
'bofh' : ['bureau','tresorier'],
|
||||
}
|
||||
|
||||
GRAPH_MODELS = {
|
||||
|
|
|
@ -48,6 +48,7 @@ DATABASES = {
|
|||
'ENGINE': 'ldapdb.backends.ldap',
|
||||
'NAME': 'ldap://ldap_host_ip/',
|
||||
'USER': 'ldap_dn',
|
||||
# 'TLS': True,
|
||||
'PASSWORD': 'SUPER_SECRET_LDAP',
|
||||
}
|
||||
}
|
||||
|
|
|
@ -29,16 +29,16 @@ from reversion.admin import VersionAdmin
|
|||
from .models import Port, Room, Switch, Stack
|
||||
|
||||
class StackAdmin(VersionAdmin):
|
||||
list_display = ('name', 'stack_id', 'details')
|
||||
pass
|
||||
|
||||
class SwitchAdmin(VersionAdmin):
|
||||
list_display = ('switch_interface','location','number','details')
|
||||
pass
|
||||
|
||||
class PortAdmin(VersionAdmin):
|
||||
list_display = ('switch', 'port','room','machine_interface','radius','details')
|
||||
pass
|
||||
|
||||
class RoomAdmin(VersionAdmin):
|
||||
list_display = ('name','details')
|
||||
pass
|
||||
|
||||
admin.site.register(Port, PortAdmin)
|
||||
admin.site.register(Room, RoomAdmin)
|
||||
|
|
|
@ -33,7 +33,7 @@ class PortForm(ModelForm):
|
|||
|
||||
class EditPortForm(ModelForm):
|
||||
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):
|
||||
super(EditPortForm, self).__init__(*args, **kwargs)
|
||||
|
@ -42,7 +42,7 @@ class EditPortForm(ModelForm):
|
|||
|
||||
class AddPortForm(ModelForm):
|
||||
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 Meta:
|
||||
|
|
20
topologie/migrations/0029_auto_20171002_0334.py
Normal file
20
topologie/migrations/0029_auto_20171002_0334.py
Normal 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),
|
||||
),
|
||||
]
|
26
topologie/migrations/0030_auto_20171004_0235.py
Normal file
26
topologie/migrations/0030_auto_20171004_0235.py
Normal 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),
|
||||
),
|
||||
]
|
|
@ -34,17 +34,11 @@ import reversion
|
|||
|
||||
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):
|
||||
""" 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"
|
||||
|
||||
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])
|
||||
|
||||
def save(self, *args, **kwargs):
|
||||
self.clean()
|
||||
if not self.name:
|
||||
self.name = self.stack_id
|
||||
super(Stack, self).save(*args, **kwargs)
|
||||
|
||||
def clean(self):
|
||||
""" Verification que l'id_max < 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"})
|
||||
|
||||
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"
|
||||
|
||||
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)
|
||||
|
||||
def clean(self):
|
||||
""" Verifie que l'id stack est dans le bon range"""
|
||||
if self.stack 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):
|
||||
|
@ -90,17 +95,27 @@ class Switch(models.Model):
|
|||
raise ValidationError({'stack_member_id': "L'id dans la stack ne peut être nul"})
|
||||
|
||||
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"
|
||||
STATES_BASE = (
|
||||
STATES = (
|
||||
('NO', 'NO'),
|
||||
('STRICT', 'STRICT'),
|
||||
('BLOQ', 'BLOQ'),
|
||||
('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")
|
||||
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)
|
||||
related = models.OneToOneField('self', null=True, blank=True, related_name='related_port')
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
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):
|
||||
""" 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 self.port > self.switch.number:
|
||||
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:
|
||||
raise ValidationError("Le port relié est déjà occupé, veuillez le libérer avant de créer une relation")
|
||||
else:
|
||||
make_port_related(self)
|
||||
self.make_port_related()
|
||||
elif hasattr(self, 'related_port'):
|
||||
clean_port_related(self)
|
||||
self.clean_port_related()
|
||||
|
||||
def __str__(self):
|
||||
return str(self.switch) + " - " + str(self.port)
|
||||
|
||||
class Room(models.Model):
|
||||
""" Une chambre/local contenant une prise murale"""
|
||||
PRETTY_NAME = "Chambre/ Prise murale"
|
||||
|
||||
name = models.CharField(max_length=255, unique=True)
|
||||
|
|
|
@ -30,6 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
<th>Interface machine</th>
|
||||
<th>Related</th>
|
||||
<th>Radius</th>
|
||||
<th>Vlan forcé</th>
|
||||
<th>Détails</th>
|
||||
<th></th>
|
||||
</tr>
|
||||
|
@ -53,6 +54,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
|
|||
{% endif %}
|
||||
</td>
|
||||
<td>{{ port.radius }}</td>
|
||||
<td>{% if not port.vlan_force %} Aucun{%else %}{{ port.vlan_force }}{% endif %}</td>
|
||||
<td>{{ port.details }}</td>
|
||||
<td class="text-right">
|
||||
<a class="btn btn-info btn-sm" role="button" title="Historique" href="{% url 'topologie:history' 'port' port.pk %}">
|
||||
|
|
|
@ -44,12 +44,14 @@ from preferences.models import AssoOption, GeneralOption
|
|||
@login_required
|
||||
@permission_required('cableur')
|
||||
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')
|
||||
return render(request, 'topologie/index.html', {'switch_list': switch_list})
|
||||
|
||||
@login_required
|
||||
@permission_required('cableur')
|
||||
def history(request, object, id):
|
||||
""" Vue générique pour afficher l'historique complet d'un objet"""
|
||||
if object == 'switch':
|
||||
try:
|
||||
object_instance = Switch.objects.get(pk=id)
|
||||
|
@ -95,6 +97,7 @@ def history(request, object, id):
|
|||
@login_required
|
||||
@permission_required('cableur')
|
||||
def index_port(request, switch_id):
|
||||
""" Affichage de l'ensemble des ports reliés à un switch particulier"""
|
||||
try:
|
||||
switch = Switch.objects.get(pk=switch_id)
|
||||
except Switch.DoesNotExist:
|
||||
|
@ -106,6 +109,7 @@ def index_port(request, switch_id):
|
|||
@login_required
|
||||
@permission_required('cableur')
|
||||
def index_room(request):
|
||||
""" Affichage de l'ensemble des chambres"""
|
||||
room_list = Room.objects.order_by('name')
|
||||
options, created = GeneralOption.objects.get_or_create()
|
||||
pagination_number = options.pagination_number
|
||||
|
@ -131,6 +135,7 @@ def index_stack(request):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def new_port(request, switch_id):
|
||||
""" Nouveau port"""
|
||||
try:
|
||||
switch = Switch.objects.get(pk=switch_id)
|
||||
except Switch.DoesNotExist:
|
||||
|
@ -154,6 +159,7 @@ def new_port(request, switch_id):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def edit_port(request, port_id):
|
||||
""" Edition d'un port. Permet de changer le switch parent et l'affectation du port"""
|
||||
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)
|
||||
except Port.DoesNotExist:
|
||||
|
@ -172,6 +178,7 @@ def edit_port(request, port_id):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def del_port(request,port_id):
|
||||
""" Supprime le port"""
|
||||
try:
|
||||
port = Port.objects.get(pk=port_id)
|
||||
except Port.DoesNotExist:
|
||||
|
@ -263,6 +270,9 @@ def edit_switchs_stack(request,stack_id):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
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)
|
||||
machine = NewMachineForm(request.POST or None)
|
||||
interface = AddInterfaceForm(request.POST or None, infra=request.user.has_perms(('infra',)))
|
||||
|
@ -304,6 +314,8 @@ def new_switch(request):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
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:
|
||||
switch = Switch.objects.get(pk=switch_id)
|
||||
except Switch.DoesNotExist:
|
||||
|
@ -341,6 +353,7 @@ def edit_switch(request, switch_id):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def new_room(request):
|
||||
"""Nouvelle chambre """
|
||||
room = EditRoomForm(request.POST or None)
|
||||
if room.is_valid():
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
|
@ -354,6 +367,7 @@ def new_room(request):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def edit_room(request, room_id):
|
||||
""" Edition numero et details de la chambre"""
|
||||
try:
|
||||
room = Room.objects.get(pk=room_id)
|
||||
except Room.DoesNotExist:
|
||||
|
@ -372,6 +386,7 @@ def edit_room(request, room_id):
|
|||
@login_required
|
||||
@permission_required('infra')
|
||||
def del_room(request, room_id):
|
||||
""" Suppression d'un chambre"""
|
||||
try:
|
||||
room = Room.objects.get(pk=room_id)
|
||||
except Room.DoesNotExist:
|
||||
|
|
|
@ -207,6 +207,7 @@ class EditInfoForm(BaseInfoForm):
|
|||
]
|
||||
|
||||
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)
|
||||
|
||||
def clean_force(self):
|
||||
|
@ -215,15 +216,18 @@ class InfoForm(EditInfoForm):
|
|||
return
|
||||
|
||||
class UserForm(InfoForm):
|
||||
""" Model form general"""
|
||||
class Meta(InfoForm.Meta):
|
||||
fields = '__all__'
|
||||
|
||||
class PasswordForm(ModelForm):
|
||||
""" Formulaire de changement brut de mot de passe. Ne pas utiliser sans traitement"""
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['password', 'pwd_ntlm']
|
||||
|
||||
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)
|
||||
|
||||
class Meta:
|
||||
|
@ -235,6 +239,7 @@ class EditServiceUserForm(ServiceUserForm):
|
|||
fields = ['access_group','comment']
|
||||
|
||||
class StateForm(ModelForm):
|
||||
""" Changement de l'état d'un user"""
|
||||
class Meta:
|
||||
model = User
|
||||
fields = ['state']
|
||||
|
|
21
users/migrations/0055_auto_20171003_0556.py
Normal file
21
users/migrations/0055_auto_20171003_0556.py
Normal 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')]),
|
||||
),
|
||||
]
|
111
users/models.py
111
users/models.py
|
@ -48,13 +48,17 @@ from django.utils import timezone
|
|||
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
|
||||
|
||||
from django.core.validators import MinLengthValidator
|
||||
from django.core.validators import RegexValidator
|
||||
from topologie.models import Room
|
||||
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
|
||||
|
||||
now = timezone.now()
|
||||
|
||||
|
||||
#### Utilitaires généraux
|
||||
|
||||
def remove_user_room(room):
|
||||
""" Déménage de force l'ancien locataire de la chambre """
|
||||
try:
|
||||
|
@ -72,6 +76,8 @@ def linux_user_check(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):
|
||||
raise forms.ValidationError(
|
||||
", ce pseudo ('%(label)s') contient des carractères interdits",
|
||||
|
@ -79,6 +85,7 @@ def linux_user_validator(login):
|
|||
)
|
||||
|
||||
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']))))
|
||||
try:
|
||||
used_uids = list(User.objects.values_list('uid_number', flat=True))
|
||||
|
@ -88,12 +95,15 @@ def get_fresh_user_uid():
|
|||
return min(free_uids)
|
||||
|
||||
def get_fresh_gid():
|
||||
""" Renvoie le plus petit gid libre """
|
||||
gids = list(range(int(min(GID_RANGES['posix'])),int(max(GID_RANGES['posix']))))
|
||||
used_gids = list(ListRight.objects.values_list('gid', flat=True))
|
||||
free_gids = [ id for id in gids if id not in used_gids]
|
||||
return min(free_gids)
|
||||
|
||||
def get_admin_right():
|
||||
""" Renvoie l'instance droit admin. La crée si elle n'existe pas
|
||||
Lui attribue un gid libre"""
|
||||
try:
|
||||
admin_right = ListRight.objects.get(listright="admin")
|
||||
except ListRight.DoesNotExist:
|
||||
|
@ -103,15 +113,20 @@ def get_admin_right():
|
|||
return admin_right
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
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()
|
||||
|
||||
class UserManager(BaseUserManager):
|
||||
|
@ -151,6 +166,9 @@ class UserManager(BaseUserManager):
|
|||
|
||||
|
||||
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"
|
||||
STATE_ACTIVE = 0
|
||||
STATE_DISABLED = 1
|
||||
|
@ -186,14 +204,17 @@ class User(AbstractBaseUser):
|
|||
|
||||
@property
|
||||
def is_active(self):
|
||||
""" Renvoie si l'user est à l'état actif"""
|
||||
return self.state == self.STATE_ACTIVE
|
||||
|
||||
@property
|
||||
def is_staff(self):
|
||||
""" Fonction de base django, renvoie si l'user est admin"""
|
||||
return self.is_admin
|
||||
|
||||
@property
|
||||
def is_admin(self):
|
||||
""" Renvoie si l'user est admin"""
|
||||
try:
|
||||
Right.objects.get(user=self, right__listright='admin')
|
||||
except Right.DoesNotExist:
|
||||
|
@ -202,18 +223,24 @@ class User(AbstractBaseUser):
|
|||
|
||||
@is_admin.setter
|
||||
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:
|
||||
self.make_admin()
|
||||
elif not value and self.is_admin:
|
||||
self.un_admin()
|
||||
|
||||
def get_full_name(self):
|
||||
""" Renvoie le nom complet de l'user formaté nom/prénom"""
|
||||
return '%s %s' % (self.name, self.surname)
|
||||
|
||||
def get_short_name(self):
|
||||
""" Renvoie seulement le nom"""
|
||||
return self.name
|
||||
|
||||
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:
|
||||
if perm in RIGHTS_LINK:
|
||||
query = Q()
|
||||
|
@ -232,6 +259,7 @@ class User(AbstractBaseUser):
|
|||
|
||||
|
||||
def has_right(self, right):
|
||||
""" Renvoie si un user a un right donné. Crée le right si il n'existe pas"""
|
||||
try:
|
||||
list_right = ListRight.objects.get(listright=right)
|
||||
except:
|
||||
|
@ -241,29 +269,38 @@ class User(AbstractBaseUser):
|
|||
|
||||
@cached_property
|
||||
def is_bureau(self):
|
||||
""" True si user a les droits bureau """
|
||||
return self.has_right('bureau')
|
||||
|
||||
@cached_property
|
||||
def is_bofh(self):
|
||||
""" True si l'user a les droits bofh"""
|
||||
return self.has_right('bofh')
|
||||
|
||||
@cached_property
|
||||
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')
|
||||
|
||||
@cached_property
|
||||
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
|
||||
def is_infra(self):
|
||||
""" True si a les droits infra"""
|
||||
return self.has_right('infra')
|
||||
|
||||
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']
|
||||
return date_max
|
||||
|
||||
def is_adherent(self):
|
||||
""" Renvoie True si l'user est adhérent : si self.end_adhesion()>now"""
|
||||
end = self.end_adhesion()
|
||||
if not end:
|
||||
return False
|
||||
|
@ -326,6 +363,8 @@ class User(AbstractBaseUser):
|
|||
|
||||
@cached_property
|
||||
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()
|
||||
user_solde = options.user_solde
|
||||
if user_solde:
|
||||
|
@ -336,8 +375,10 @@ class User(AbstractBaseUser):
|
|||
else:
|
||||
return 0
|
||||
|
||||
def user_interfaces(self):
|
||||
return Interface.objects.filter(machine__in=Machine.objects.filter(user=self, active=True))
|
||||
def user_interfaces(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):
|
||||
""" Assign une ipv4 aux machines d'un user """
|
||||
|
@ -350,6 +391,7 @@ class User(AbstractBaseUser):
|
|||
interface.save()
|
||||
|
||||
def unassign_ips(self):
|
||||
""" Désassigne les ipv4 aux machines de l'user"""
|
||||
interfaces = self.user_interfaces()
|
||||
for interface in interfaces:
|
||||
with transaction.atomic(), reversion.create_revision():
|
||||
|
@ -358,10 +400,12 @@ class User(AbstractBaseUser):
|
|||
interface.save()
|
||||
|
||||
def archive(self):
|
||||
""" Archive l'user : appelle unassign_ips() puis passe state à ARCHIVE"""
|
||||
self.unassign_ips()
|
||||
self.state = User.STATE_ARCHIVE
|
||||
|
||||
def unarchive(self):
|
||||
""" Désarchive l'user : réassigne ses ip et le passe en state ACTIVE"""
|
||||
self.assign_ips()
|
||||
self.state = User.STATE_ACTIVE
|
||||
|
||||
|
@ -382,6 +426,10 @@ class User(AbstractBaseUser):
|
|||
user_right.delete()
|
||||
|
||||
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()
|
||||
try:
|
||||
user_ldap = LdapUser.objects.get(uidNumber=self.uid_number)
|
||||
|
@ -410,6 +458,7 @@ class User(AbstractBaseUser):
|
|||
user_ldap.save()
|
||||
|
||||
def ldap_del(self):
|
||||
""" Supprime la version ldap de l'user"""
|
||||
try:
|
||||
user_ldap = LdapUser.objects.get(name=self.pseudo)
|
||||
user_ldap.delete()
|
||||
|
@ -457,9 +506,11 @@ class User(AbstractBaseUser):
|
|||
return
|
||||
|
||||
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()
|
||||
if all_machines.count() > options.max_lambdauser_interfaces:
|
||||
if all_interfaces.count() > options.max_lambdauser_interfaces:
|
||||
return False, "Maximum de machines enregistrees atteinte"
|
||||
if not nas_type:
|
||||
return False, "Re2o ne sait pas à quel machinetype affecter cette machine"
|
||||
|
@ -473,7 +524,7 @@ class User(AbstractBaseUser):
|
|||
interface_cible.clean()
|
||||
machine_parent.clean()
|
||||
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.clean()
|
||||
machine_parent.save()
|
||||
|
@ -486,19 +537,37 @@ class User(AbstractBaseUser):
|
|||
return False, e
|
||||
return True, "Ok"
|
||||
|
||||
def all_machines(self):
|
||||
return Interface.objects.filter(machine__in=Machine.objects.filter(user=self))
|
||||
|
||||
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.pwd_ntlm = hashNT(password)
|
||||
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):
|
||||
return self.pseudo
|
||||
|
||||
@receiver(post_save, sender=User)
|
||||
def user_post_save(sender, **kwargs):
|
||||
""" Synchronisation post_save : envoie le mail de bienvenue si creation
|
||||
Synchronise le ldap"""
|
||||
is_created = kwargs['created']
|
||||
user = kwargs['instance']
|
||||
if is_created:
|
||||
|
@ -513,6 +582,7 @@ def user_post_delete(sender, **kwargs):
|
|||
regen('mailing')
|
||||
|
||||
class ServiceUser(AbstractBaseUser):
|
||||
""" Classe des users daemons, règle leurs accès au ldap"""
|
||||
readonly = 'readonly'
|
||||
ACCESS = (
|
||||
('auth', 'auth'),
|
||||
|
@ -531,6 +601,7 @@ class ServiceUser(AbstractBaseUser):
|
|||
objects = UserManager()
|
||||
|
||||
def ldap_sync(self):
|
||||
""" Synchronisation du ServiceUser dans sa version ldap"""
|
||||
try:
|
||||
user_ldap = LdapServiceUser.objects.get(name=self.pseudo)
|
||||
except LdapServiceUser.DoesNotExist:
|
||||
|
@ -560,15 +631,19 @@ class ServiceUser(AbstractBaseUser):
|
|||
|
||||
@receiver(post_save, sender=ServiceUser)
|
||||
def service_user_post_save(sender, **kwargs):
|
||||
""" Synchronise un service user ldap après modification django"""
|
||||
service_user = kwargs['instance']
|
||||
service_user.ldap_sync()
|
||||
|
||||
@receiver(post_delete, sender=ServiceUser)
|
||||
def service_user_post_delete(sender, **kwargs):
|
||||
""" Supprime un service user ldap après suppression django"""
|
||||
service_user = kwargs['instance']
|
||||
service_user.ldap_del()
|
||||
|
||||
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"
|
||||
|
||||
user = models.ForeignKey('User', on_delete=models.PROTECT)
|
||||
|
@ -582,15 +657,18 @@ class Right(models.Model):
|
|||
|
||||
@receiver(post_save, sender=Right)
|
||||
def right_post_save(sender, **kwargs):
|
||||
""" Synchronise les users ldap groups avec les groupes de droits"""
|
||||
right = kwargs['instance'].right
|
||||
right.ldap_sync()
|
||||
|
||||
@receiver(post_delete, sender=Right)
|
||||
def right_post_delete(sender, **kwargs):
|
||||
""" Supprime l'user du groupe"""
|
||||
right = kwargs['instance'].right
|
||||
right.ldap_sync()
|
||||
|
||||
class School(models.Model):
|
||||
""" Etablissement d'enseignement"""
|
||||
PRETTY_NAME = "Etablissements enregistrés"
|
||||
|
||||
name = models.CharField(max_length=255)
|
||||
|
@ -600,9 +678,12 @@ class School(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 là il n'est plus modifiable après creation"""
|
||||
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)
|
||||
details = models.CharField(help_text="Description", max_length=255, blank=True)
|
||||
|
||||
|
@ -627,6 +708,7 @@ class ListRight(models.Model):
|
|||
|
||||
@receiver(post_save, sender=ListRight)
|
||||
def listright_post_save(sender, **kwargs):
|
||||
""" Synchronise le droit ldap quand il est modifié"""
|
||||
right = kwargs['instance']
|
||||
right.ldap_sync()
|
||||
|
||||
|
@ -644,6 +726,8 @@ class ListShell(models.Model):
|
|||
return self.shell
|
||||
|
||||
class Ban(models.Model):
|
||||
""" Bannissement. Actuellement a un effet tout ou rien.
|
||||
Gagnerait à être granulaire"""
|
||||
PRETTY_NAME = "Liste des bannissements"
|
||||
|
||||
STATE_HARD = 0
|
||||
|
@ -684,6 +768,7 @@ class Ban(models.Model):
|
|||
|
||||
@receiver(post_save, sender=Ban)
|
||||
def ban_post_save(sender, **kwargs):
|
||||
""" Regeneration de tous les services après modification d'un ban"""
|
||||
ban = kwargs['instance']
|
||||
is_created = kwargs['created']
|
||||
user = ban.user
|
||||
|
@ -699,6 +784,7 @@ def ban_post_save(sender, **kwargs):
|
|||
|
||||
@receiver(post_delete, sender=Ban)
|
||||
def ban_post_delete(sender, **kwargs):
|
||||
""" Regen de tous les services après suppression d'un ban"""
|
||||
user = kwargs['instance'].user
|
||||
user.ldap_sync(base=False, access_refresh=True, mac_refresh=False)
|
||||
regen('mailing')
|
||||
|
@ -742,6 +828,9 @@ def whitelist_post_delete(sender, **kwargs):
|
|||
regen('mac_ip_list')
|
||||
|
||||
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'
|
||||
EMAIL = 'EM'
|
||||
TYPE_CHOICES = (
|
||||
|
|
|
@ -716,6 +716,8 @@ class JSONResponse(HttpResponse):
|
|||
@login_required
|
||||
@permission_required('serveur')
|
||||
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()
|
||||
seria = MailSerializer(mails, many=True)
|
||||
return JSONResponse(seria.data)
|
||||
|
|
Loading…
Reference in a new issue