8
0
Fork 0
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:
Maël Kervella 2017-10-07 01:11:56 +00:00
commit af6df474ba
34 changed files with 923 additions and 108 deletions

136
README.md
View file

@ -4,17 +4,28 @@ Gnu public license v2.0
## Avant propos
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.

View file

@ -109,7 +109,7 @@ def new_facture(request, userid):
return form({'factureform': facture_form, 'venteform': article_formset, 'articlelist': article_list}, 'cotisations/new_facture.html', request)
@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

View file

@ -257,12 +257,15 @@ def check_user_machine_and_register(nas_type, username, mac_address):
return (True, u'Access Ok, Capture de la mac...', user.pwd_ntlm)
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)

View file

@ -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)

View file

@ -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)

View file

@ -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__'

View file

@ -0,0 +1,43 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-02 01:50
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
class Migration(migrations.Migration):
dependencies = [
('machines', '0057_nas_autocapture_mac'),
]
operations = [
migrations.CreateModel(
name='OuverturePort',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('begin', models.IntegerField()),
('end', models.IntegerField()),
('protocole', models.CharField(choices=[('T', 'TCP'), ('U', 'UDP')], default='T', max_length=1)),
('io', models.CharField(choices=[('I', 'IN'), ('O', 'OUT')], default='O', max_length=1)),
],
),
migrations.CreateModel(
name='OuverturePortList',
fields=[
('id', models.AutoField(auto_created=True, primary_key=True, serialize=False, verbose_name='ID')),
('name', models.CharField(help_text='Nom de la configuration des ports.', max_length=255)),
],
),
migrations.AddField(
model_name='ouvertureport',
name='port_list',
field=models.ForeignKey(on_delete=django.db.models.deletion.CASCADE, to='machines.OuverturePortList'),
),
migrations.AddField(
model_name='interface',
name='port_lists',
field=models.ManyToManyField(blank=True, to='machines.OuverturePortList'),
),
]

View file

@ -0,0 +1,20 @@
# -*- coding: utf-8 -*-
# Generated by Django 1.10.7 on 2017-10-02 16:33
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('machines', '0058_auto_20171002_0350'),
]
operations = [
migrations.AddField(
model_name='iptype',
name='prefix_v6',
field=models.GenericIPAddressField(blank=True, null=True, protocol='IPv6'),
),
]

View file

@ -56,6 +56,7 @@ class MachineType(models.Model):
ip_type = models.ForeignKey('IpType', on_delete=models.PROTECT, blank=True, null=True)
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')

View file

@ -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

View file

@ -30,6 +30,7 @@ with this program; if not, write to the Free Software Foundation, Inc.,
<th>Nécessite l'autorisation infra</th>
<th>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 %}

View file

@ -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

View file

@ -0,0 +1,69 @@
{% extends "machines/sidebar.html" %}
{% comment %}
Re2o est un logiciel d'administration développé initiallement au rezometz. Il
se veut agnostique au réseau considéré, de manière à être installable en
quelques clics.
Copyright © 2017 Gabriel Détraz
Copyright © 2017 Goulven Kermarec
Copyright © 2017 Augustin Lemesle
This program is free software; you can redistribute it and/or modify
it under the terms of the GNU General Public License as published by
the Free Software Foundation; either version 2 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU General Public License for more details.
You should have received a copy of the GNU General Public License along
with this program; if not, write to the Free Software Foundation, Inc.,
51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA.
{% endcomment %}
{% load bootstrap3 %}
{% block title %}Création et modification de machines{% endblock %}
{% block content %}
{% bootstrap_form_errors port_list %}
<form class="form" method="post">
{% csrf_token %}
{% bootstrap_form port_list %}
{{ ports.management_form }}
<div id="formset">
{% for form in ports.forms %}
<div class="port">
<p>
{{ form }}
</p>
</div>
{% endfor %}
</div>
<p>
<input class="btn btn-primary btn-sm" role="button" value="Ajouter un port" id="add_one">
</p>
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
</form>
<script type="text/javascript">
var template = `{{ports.empty_form}}`;
function add_port(){
var new_index = document.getElementsByClassName('port').length;
document.getElementById('id_form-TOTAL_FORMS').value =
parseInt(document.getElementById('id_form-TOTAL_FORMS').value) + 1;
var new_port = document.createElement('div');
new_port.className = 'port';
new_port.innerHTML = template.replace(/__prefix__/g, new_index);
document.getElementById('formset').appendChild(new_port);
}
document.addEventListener("DOMContentLoaded", function() {
document.getElementById("add_one").addEventListener("click", add_port, true);});
</script>
{% endblock %}

View file

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

View file

@ -55,4 +55,10 @@ with this program; if not, write to the Free Software Foundation, Inc.,
Services (dhcp, dns...)
</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 %}

View file

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

View file

@ -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

View file

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

View file

@ -45,6 +45,7 @@ class OptionalMachine(models.Model):
password_machine = models.BooleanField(default=False)
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"

View file

@ -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>

View file

@ -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,
}

View file

@ -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 = {

View file

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

View file

@ -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)

View file

@ -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:

View file

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

View file

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

View file

@ -34,17 +34,11 @@ import reversion
from machines.models import Vlan
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)

View file

@ -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 %}">

View file

@ -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:

View file

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

View file

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

View file

@ -48,13 +48,17 @@ from django.utils import timezone
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
from django.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 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 = (

View file

@ -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)