Commit 44f7e506 authored by Gabriel Detraz's avatar Gabriel Detraz Committed by root

Initial commit pour portail_captif, forké depuis re2o (https://gitlab.rezometz.org/rezo/re2o)

parents
settings_local.py
*.swp
*.pyc
re2o.png
__pycache__/*
static_files/*
[submodule "pypureomapi"]
path = pypureomapi
url = https://github.com/CygnusNetworks/pypureomapi.git
This diff is collapsed.
# Re2o
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.
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 portail_captif 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
Voici la liste des dépendances à installer sur le serveur principal (A).
### Avec apt :
#### Sous debian 8
Paquets obligatoires:
* python3-django (1.8, jessie-backports)
* python3-dateutil (jessie-backports)
* python3-django-reversion (stretch)
* python3-pip (jessie)
Paquet recommandés:
* python3-django-extensions (jessie)
Moteur de db conseillé (mysql), postgresql fonctionne également.
Pour mysql, il faut installer :
* python3-mysqldb (jessie-backports)
* mysql-client
### Prérequis sur le serveur B
Sur le serveur B, installer mysql ou postgresql, dans la version jessie ou stretch.
* mysql-server (jessie/stretch) ou postgresql (jessie-stretch)
### Prérequis sur le serveur C
Sur le serveur C (ldap), avec apt :
* slapd (jessie/stretch)
### Installation sur le serveur principal A
Cloner le dépot portail_captif à partir du gitlab, par exemple dans /var/www/portail_captif.
Ensuite, il faut créer le fichier settings_local.py dans le sous dossier portail_captif, 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
### 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 portail_captif, ainsi qu'un user portail_captif 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.
Voici les étapes à éxecuter pour mysql :
* CREATE DATABASE portail_captif;
* CREATE USER 'newuser'@'localhost' IDENTIFIED BY 'password';
* GRANT ALL PRIVILEGES ON portail_captif.* 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
### 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 portail_captif/install_utils (dans le dépot portail_captif) 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
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.
## Configuration initiale
Normalement à cette étape, le ldap et la bdd sql sont configurées correctement.
Il faut alors lancer dans le dépot portail_captif '''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.
Pour apache2 :
* apt install apache2
* apt install libapache2-mod-wsgi-py3 (pour le module wsgi)
Un example de site apache2 se trouve dans install_utils ( portail_captif.conf)
portail_captif/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.
## Installations Optionnelles
### Générer le schéma des dépendances
Pour cela :
* apt install python3-django-extensions
* python3 manage.py graph_models -a -g -o portail_captif.png
# Fonctionnement interne
## 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)
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.
# 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.
add allowed_guests 01:89:98:99:89:99 -q restore
#!/usr/bin/env python3
import os
import sys
if __name__ == "__main__":
os.environ.setdefault("DJANGO_SETTINGS_MODULE", "portail_captif.settings")
from django.core.management import execute_from_command_line
execute_from_command_line(sys.argv)
# 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.
# 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.
from .settings import SITE_NAME
def context_user(request):
user = request.user
is_admin = user.is_admin if hasattr(user,'is_admin') else False
return {
'is_admin' : is_admin,
'request_user': user,
'site_name': SITE_NAME,
}
# -*- coding: utf-8 -*-
# Module d'authentification
# David Sinquin, Gabriel Détraz, Goulven Kermarec
import hashlib
import binascii
import os
from base64 import encodestring
from base64 import decodestring
from collections import OrderedDict
from django.contrib.auth import hashers
ALGO_NAME = "{SSHA}"
ALGO_LEN = len(ALGO_NAME + "$")
DIGEST_LEN = 20
def makeSecret(password):
salt = os.urandom(4)
h = hashlib.sha1(password.encode())
h.update(salt)
return ALGO_NAME + "$" + encodestring(h.digest() + salt).decode()[:-1]
def checkPassword(challenge_password, password):
challenge_bytes = decodestring(challenge_password[ALGO_LEN:].encode())
digest = challenge_bytes[:DIGEST_LEN]
salt = challenge_bytes[DIGEST_LEN:]
hr = hashlib.sha1(password.encode())
hr.update(salt)
valid_password = True
# La comparaison est volontairement en temps constant
# (pour éviter les timing-attacks)
for i, j in zip(digest, hr.digest()):
valid_password &= i == j
return valid_password
class SSHAPasswordHasher(hashers.BasePasswordHasher):
"""
SSHA password hashing to allow for LDAP auth compatibility
"""
algorithm = ALGO_NAME
def encode(self, password, salt, iterations=None):
"""
Hash and salt the given password using SSHA algorithm
salt is overridden
"""
assert password is not None
return makeSecret(password)
def verify(self, password, encoded):
"""
Check password against encoded using SSHA algorithm
"""
assert encoded.startswith(self.algorithm)
return checkPassword(encoded, password)
def safe_summary(self, encoded):
"""
Provides a safe summary ofthe password
"""
assert encoded.startswith(self.algorithm)
hash = encoded[ALGO_LEN:]
hash = binascii.hexlify(decodestring(hash.encode())).decode()
return OrderedDict([
('algorithm', self.algorithm),
('iterations', 0),
('salt', hashers.mask_hash(hash[2*DIGEST_LEN:], show=2)),
('hash', hashers.mask_hash(hash[:2*DIGEST_LEN])),
])
def harden_runtime(self, password, encoded):
"""
Method implemented to shut up BasePasswordHasher warning
As we are not using multiple iterations the method is pretty useless
"""
pass
[Unit]
Description=Crans Portail Captif
Requires=nginx.service
Requires=portail_captif.socket
After=nginx.service
After=network-online.target
[Service]
Type=forking
User=root
Group=root
PIDFile=/run/portail_captif.pid
WorkingDirectory=/var/www/portail_captif/
ExecStart=/usr/bin/gunicorn3 portail_captif.wsgi:application --pid=/run/portail_captif.pid --name www-data --user www-data --group www-data --daemon --log-file /var/log/gunicorn/portail_captif.log --log-level=info --bind=unix:///tmp/gunicorn-portail_captif.sock --workers=1
ExecReload=/bin/kill -s HUP $MAINPID
ExecStop=/bin/kill -s TERM $MAINPID
Restart=on-failure
RestartSec=65
StartLimitInterval=60
StartLimitBurst=2
[Install]
WantedBy=multi-user.target
Also=portail_captif.socket
[Unit]
Description=Socket Portail Captif
[Socket]
ListenStream=/tmp/gunicorn-portail_captif.sock
[Install]
WantedBy=sockets.target
# 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.
"""
Django settings for portail_captif project.
Generated by 'django-admin startproject' using Django 1.8.13.
For more information on this file, see
https://docs.djangoproject.com/en/1.8/topics/settings/
For the full list of settings and their values, see
https://docs.djangoproject.com/en/1.8/ref/settings/
"""
# Build paths inside the project like this: os.path.join(BASE_DIR, ...)
import os
from .settings_local import *
BASE_DIR = os.path.dirname(os.path.dirname(os.path.abspath(__file__)))
# Quick-start development settings - unsuitable for production
# See https://docs.djangoproject.com/en/1.8/howto/deployment/checklist/
# Auth definition
PASSWORD_HASHERS = (
'portail_captif.login.SSHAPasswordHasher',
'django.contrib.auth.hashers.PBKDF2PasswordHasher',
)
AUTH_USER_MODEL = 'users.User'
LOGIN_URL = '/login'
LOGIN_REDIRECT_URL = '/'
# Application definition
INSTALLED_APPS = (
'django.contrib.admin',
'django.contrib.auth',
'django.contrib.contenttypes',
'django.contrib.sessions',
'django.contrib.messages',
'django.contrib.staticfiles',
'bootstrap3',
'users',
'portail_captif',
'reversion'
)
MIDDLEWARE_CLASSES = (
'django.contrib.sessions.middleware.SessionMiddleware',
'django.middleware.common.CommonMiddleware',
'django.middleware.csrf.CsrfViewMiddleware',
'django.contrib.auth.middleware.AuthenticationMiddleware',
'django.contrib.auth.middleware.SessionAuthenticationMiddleware',
'django.contrib.messages.middleware.MessageMiddleware',
'django.middleware.clickjacking.XFrameOptionsMiddleware',
'django.middleware.security.SecurityMiddleware',
)
ROOT_URLCONF = 'portail_captif.urls'
TEMPLATES = [
{
'BACKEND': 'django.template.backends.django.DjangoTemplates',
'DIRS': [
os.path.join(BASE_DIR, 'templates').replace('\\', '/'),
],
'APP_DIRS': True,
'OPTIONS': {
'context_processors': [
'django.template.context_processors.debug',
'django.template.context_processors.request',
'django.contrib.auth.context_processors.auth',
'django.contrib.messages.context_processors.messages',
'django.template.context_processors.request',
'portail_captif.context_processors.context_user',
],
},
},
]
WSGI_APPLICATION = 'portail_captif.wsgi.application'
# Internationalization
# https://docs.djangoproject.com/en/1.8/topics/i18n/
LANGUAGE_CODE = 'fr-fr'
TIME_ZONE = 'Europe/Paris'
USE_I18N = True
USE_L10N = True
USE_TZ = True
# django-bootstrap3 config dictionnary
BOOTSTRAP3 = {
'jquery_url': '/static/js/jquery-2.2.4.min.js',
'base_url': '/static/bootstrap/',
'include_jquery': True,
}
BOOTSTRAP_BASE_URL = '/static/bootstrap/'
STATICFILES_DIRS = (
# Put strings here, like "/home/html/static" or "C:/www/django/static".
# Always use forward slashes, even on Windows.
# Don't forget to use absolute paths, not relative paths.
os.path.join(
BASE_DIR,
'static',
),
)
STATIC_URL = '/static/'
STATIC_ROOT = os.path.join(BASE_DIR, 'static_files')
PAGINATION_NUMBER = 25
PAGINATION_LARGE_NUMBER = 8
GENERIC_IPSET_COMMAND = "/sbin/ipset -q "
GRAPH_MODELS = {
'all_applications': True,
'group_models': True,
}
# 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.
SECRET_KEY = 'SUPER_SECRET'
DB_PASSWORD = 'SUPER_SECRET'
# SECURITY WARNING: don't run with debug turned on in production!
DEBUG = False
ADMINS = [('Example', 'rezo-admin@example.org')]
SERVER_EMAIL = 'no-reply@example.org'
# Obligatoire, liste des host autorisés
ALLOWED_HOSTS = ['test.example.org']
DATABASES = {
'default': {
'ENGINE': 'django.db.backends.mysql',
'NAME': 're2o',
'USER': 're2o',
'PASSWORD': DB_PASSWORD,
'HOST': 'localhost',
},
'ldap': {
'ENGINE': 'ldapdb.backends.ldap',
'NAME': 'ldap://10.0.0.0/',
'USER': 'cn=admin,dc=ldap,dc=example,dc=org',
'PASSWORD': 'SUPER_SECRET',
}
}
# Security settings
SECURE_CONTENT_TYPE_NOSNIFF = True
SECURE_BROWSER_XSS_FILTER = True
SESSION_COOKIE_SECURE = True
CSRF_COOKIE_SECURE = True
CSRF_COOKIE_HTTPONLY = True
X_FRAME_OPTIONS = 'DENY'
SESSION_COOKIE_AGE = 60 * 60 * 3
# Association information
SITE_NAME = "Re2o.rez"
# Main extension used in asso
MAIN_EXTENSION = ".rez"
LOGO_PATH = "static_files/logo.png"
ASSO_NAME = "Asso reseau"
ASSO_ADDRESS_LINE1 = "2, rue Edouard Belin"
ASSO_ADDRESS_LINE2 = "57070 Metz"
ASSO_SIRET = ""
ASSO_EMAIL = "tresorier@ecole.fr"
ASSO_PHONE = "01 02 03 04 05"
ASSO_PSEUDO = "rezo"
services_urls = {
#Fill IT : ex : 'gitlab': {
# 'url': 'https://gitlab.rezometz.org',
# 'logo': 'gitlab.png',
# 'description': 'Gitlab is cool 8-)'},
}
# Number of hours a token remains valid after having been created. Numeric and string
# versions should have the same meaning.
REQ_EXPIRE_HRS = 48
REQ_EXPIRE_STR = '48 heures'
# Email `From` field
EMAIL_FROM = 'www-data@serveur.net'
EMAIL_HOST = 'smtp.example.org'
# Reglages pour la bdd ldap
LDAP = {
'base_user_dn' : 'cn=Utilisateurs,dc=ldap,dc=example,dc=org',
'base_userservice_dn' : 'ou=service-users,dc=ldap,dc=example,dc=org',
'base_usergroup_dn' : 'ou=posix,ou=groups,dc=ldap,dc=example,dc=org',
'user_gid' : 500,
}
UID_RANGES = {
'users' : [21001,30000],
'service-users' : [20000,21000],
}
# Chaque groupe a un gid assigné, voici la place libre pour assignation
GID_RANGES = {
'posix' : [501, 600],
}
# Affchage des résultats
SEARCH_RESULT = 15
# Max machines et max alias autorisés par personne
MAX_INTERFACES = 4
MAX_ALIAS = 4
# Liste des vlans id disponible sur un switch
VLAN_ID_LIST = [7,8,42,69]
# Décision radius à prendre
RADIUS_VLAN_DECISION = {
'VLAN_NOK' : 42,
'VLAN_OK' : 69,
}
OPTIONNAL_APPS = ()
{% 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 %}
{% if reversions.paginator %}
<ul class="pagination nav navbar-nav">
{% if reversions.has_previous %}
<li><a href="?page={{ reversions.previous_page_number }}">Suivants</a></li>
{% endif %}
{% for page in reversions.paginator.page_range %}
<li class="{% if reversions.number == page %}active{% endif %}"><a href="?page={{page }}">{{ page }}</a></li>
{% endfor %}
{% if reversions.has_next %}
<li> <a href="?page={{ reversions.next_page_number }}">Précédents</a></li>
{% endif %}
</ul>
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>Date</th>
<th>Cableur</th>
<th>Commentaire</th>
</tr>
</thead>
{% for rev in reversions %}
<tr>
<td>{{ rev.revision.date_created }}</td>
<td>{{ rev.revision.user }}</td>
<td>{{ rev.revision.comment }}</td>
</tr>
{% endfor %}
</table>
{% extends "portail_captif/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 %}Historique{% endblock %}
{% block content %}
<h2>Historique de {{ object }}</h2>
{% include "portail_captif/aff_history.html" with reversions=reversions %}
<br />
<br />
<br />
{% endblock %}
{% extends "portail_captif/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