1
0
mirror of https://gitlab.crans.org/mediatek/med.git synced 2025-06-30 02:31:09 +02:00

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

This commit is contained in:
Gabriel Detraz
2017-06-12 01:34:13 +02:00
committed by root
commit 44f7e5060f
73 changed files with 12950 additions and 0 deletions

22
users/__init__.py Normal file
View File

@ -0,0 +1,22 @@
# 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.

83
users/admin.py Normal file
View File

@ -0,0 +1,83 @@
# 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 django.contrib import admin
from django.contrib.auth.models import Group
from django.contrib.auth.admin import UserAdmin as BaseUserAdmin
from reversion.admin import VersionAdmin
from .models import User, Machine, Request
from .forms import UserChangeForm, UserCreationForm
class UserAdmin(admin.ModelAdmin):
list_display = (
'name',
'surname',
'pseudo',
'email',
'state'
)
search_fields = ('name','surname','pseudo')
class RequestAdmin(admin.ModelAdmin):
list_display = ('user', 'type', 'created_at', 'expires_at')
class MachineAdmin(VersionAdmin):
list_display = ('mac_address','proprio')
class UserAdmin(VersionAdmin, BaseUserAdmin):
# The forms to add and change user instances
form = UserChangeForm
add_form = UserCreationForm
# The fields to be used in displaying the User model.
# These override the definitions on the base UserAdmin
# that reference specific fields on auth.User.
list_display = ('pseudo', 'name', 'surname', 'email', 'is_admin')
list_filter = ()
fieldsets = (
(None, {'fields': ('pseudo', 'password')}),
('Personal info', {'fields': ('name', 'surname', 'email')}),
('Permissions', {'fields': ('is_admin', )}),
)
# add_fieldsets is not a standard ModelAdmin attribute. UserAdmin
# overrides get_fieldsets to use this attribute when creating a user.
add_fieldsets = (
(None, {
'classes': ('wide',),
'fields': ('pseudo', 'name', 'surname', 'email', 'is_admin', 'password1', 'password2')}
),
)
search_fields = ('pseudo',)
ordering = ('pseudo',)
filter_horizontal = ()
admin.site.register(Machine, MachineAdmin)
admin.site.register(User, UserAdmin)
admin.site.register(Request, RequestAdmin)
# Now register the new UserAdmin...
admin.site.unregister(User)
admin.site.register(User, UserAdmin)
# ... and, since we're not using Django's built-in permissions,
# unregister the Group model from admin.
admin.site.unregister(Group)

100
users/forms.py Normal file
View File

@ -0,0 +1,100 @@
# 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.
# -*- coding: utf-8 -*-
from django import forms
from django.contrib.auth.forms import ReadOnlyPasswordHashField
from django.core.validators import MinLengthValidator
from django.utils import timezone
from .models import User
class PassForm(forms.Form):
passwd1 = forms.CharField(label=u'Nouveau mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput)
passwd2 = forms.CharField(label=u'Saisir à nouveau le mot de passe', max_length=255, validators=[MinLengthValidator(8)], widget=forms.PasswordInput)
class UserCreationForm(forms.ModelForm):
"""A form for creating new users. Includes all the required
fields, plus a repeated password."""
password1 = forms.CharField(label='Password', widget=forms.PasswordInput, validators=[MinLengthValidator(8)], max_length=255)
password2 = forms.CharField(label='Password confirmation', widget=forms.PasswordInput, validators=[MinLengthValidator(8)], max_length=255)
is_admin = forms.BooleanField(label='is admin')
class Meta:
model = User
fields = ('pseudo', 'name', 'surname', 'email')
def clean_password2(self):
# Check that the two password entries match
password1 = self.cleaned_data.get("password1")
password2 = self.cleaned_data.get("password2")
if password1 and password2 and password1 != password2:
raise forms.ValidationError("Passwords don't match")
return password2
def save(self, commit=True):
# Save the provided password in hashed format
user = super(UserCreationForm, self).save(commit=False)
user.set_password(self.cleaned_data["password1"])
user.save()
user.is_admin = self.cleaned_data.get("is_admin")
return user
class UserChangeForm(forms.ModelForm):
"""A form for updating users. Includes all the fields on
the user, but replaces the password field with admin's
password hash display field.
"""
password = ReadOnlyPasswordHashField()
is_admin = forms.BooleanField(label='is admin', required=False)
class Meta:
model = User
fields = ('pseudo', 'password', 'name', 'surname', 'email')
def __init__(self, *args, **kwargs):
super(UserChangeForm, self).__init__(*args, **kwargs)
print("User is admin : %s" % kwargs['instance'].is_admin)
self.initial['is_admin'] = kwargs['instance'].is_admin
def clean_password(self):
# Regardless of what the user provides, return the initial value.
# This is done here, rather than on the field, because the
# field does not have access to the initial value
return self.initial["password"]
def save(self, commit=True):
# Save the provided password in hashed format
user = super(UserChangeForm, self).save(commit=False)
user.is_admin = self.cleaned_data.get("is_admin")
if commit:
user.save()
return user
class ResetPasswordForm(forms.Form):
pseudo = forms.CharField(label=u'Pseudo', max_length=255)
email = forms.EmailField(max_length=255)

View File

@ -0,0 +1,44 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
]
operations = [
migrations.CreateModel(
name='User',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
('password', models.CharField(max_length=128, verbose_name='password')),
('last_login', models.DateTimeField(blank=True, verbose_name='last login', null=True)),
('name', models.CharField(max_length=255)),
('surname', models.CharField(max_length=255)),
('email', models.EmailField(max_length=254)),
('state', models.IntegerField(default=0, choices=[(0, 'STATE_ACTIVE'), (1, 'STATE_DISABLED'), (2, 'STATE_ARCHIVE')])),
('pseudo', models.CharField(max_length=32, unique=True)),
('registered', models.DateTimeField(auto_now_add=True)),
('admin', models.BooleanField(default=False)),
],
options={
'abstract': False,
},
),
migrations.CreateModel(
name='Request',
fields=[
('id', models.AutoField(serialize=False, verbose_name='ID', primary_key=True, auto_created=True)),
('type', models.CharField(max_length=2, choices=[('PW', 'Mot de passe'), ('EM', 'Email')])),
('token', models.CharField(max_length=32)),
('created_at', models.DateTimeField(auto_now_add=True)),
('expires_at', models.DateTimeField()),
('user', models.ForeignKey(on_delete=django.db.models.deletion.PROTECT, to=settings.AUTH_USER_MODEL)),
],
),
]

View File

@ -0,0 +1,24 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
class Migration(migrations.Migration):
dependencies = [
('users', '0001_initial'),
]
operations = [
migrations.AddField(
model_name='user',
name='comment',
field=models.CharField(blank=True, help_text='Commentaire, promo', max_length=255),
),
migrations.AlterField(
model_name='user',
name='pseudo',
field=models.CharField(unique=True, help_text='Doit contenir uniquement des lettres, chiffres, ou tirets. ', max_length=32),
),
]

View File

@ -0,0 +1,25 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import macaddress.fields
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('users', '0002_auto_20170610_1550'),
]
operations = [
migrations.CreateModel(
name='Machines',
fields=[
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
('mac_address', macaddress.fields.MACAddressField(unique=True, max_length=17, integer=False)),
('proprio', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=django.db.models.deletion.PROTECT)),
],
),
]

View File

@ -0,0 +1,32 @@
# -*- coding: utf-8 -*-
from __future__ import unicode_literals
from django.db import migrations, models
import django.db.models.deletion
import macaddress.fields
from django.conf import settings
class Migration(migrations.Migration):
dependencies = [
('users', '0003_machines'),
]
operations = [
migrations.CreateModel(
name='Machine',
fields=[
('id', models.AutoField(verbose_name='ID', primary_key=True, auto_created=True, serialize=False)),
('mac_address', macaddress.fields.MACAddressField(max_length=17, unique=True, integer=False)),
('proprio', models.ForeignKey(to=settings.AUTH_USER_MODEL, on_delete=django.db.models.deletion.PROTECT)),
],
),
migrations.RemoveField(
model_name='machines',
name='proprio',
),
migrations.DeleteModel(
name='Machines',
),
]

View File

281
users/models.py Normal file
View File

@ -0,0 +1,281 @@
# 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 django.db import models
from django.db.models import Q
from django.forms import ModelForm, Form
from django import forms
from django.db.models.signals import post_save, post_delete
from django.dispatch import receiver
from django.utils.functional import cached_property
from macaddress.fields import MACAddressField
from portail_captif.settings import GENERIC_IPSET_COMMAND, IPSET_NAME, REQ_EXPIRE_HRS
import re, uuid
import datetime
from django.utils import timezone
from django.contrib.auth.models import AbstractBaseUser, BaseUserManager
import subprocess
def apply(cmd):
return subprocess.Popen(cmd, shell=True, stdout=subprocess.PIPE)
def mac_from_ip(ip):
cmd = '/usr/sbin/arp -na %s' % ip
p = apply(cmd)
output, errors = p.communicate()
if output is not None :
mac_addr = output.decode().split()[3]
return str(mac_addr)
else:
return None
def create_ip_set():
command_to_execute = "sudo " + GENERIC_IPSET_COMMAND + " create " + IPSET_NAME + " hash:mac hashsize 1024 maxelem 65536"
apply(command_to_execute)
command_to_execute = "sudo " + GENERIC_IPSET_COMMAND + " flush " + IPSET_NAME
apply(command_to_execute)
return
def fill_ipset():
all_machines = Machine.objects.filter(proprio__in=User.objects.filter(state=User.STATE_ACTIVE))
file = open("/tmp/ipset_restore", 'w+')
file.write("%s\nCOMMIT\n" % '\n'.join(["add %s %s" % (IPSET_NAME, str(machine.mac_address)) for machine in all_machines]))
file.close()
command_to_execute = "sudo " + GENERIC_IPSET_COMMAND + " restore < /tmp/ipset_restore"
apply(command_to_execute)
return
class UserManager(BaseUserManager):
def _create_user(self, pseudo, name, surname, email, password=None, su=False):
if not pseudo:
raise ValueError('Users must have an username')
user = self.model(
pseudo=pseudo,
name=name,
surname=surname,
email=self.normalize_email(email),
)
user.set_password(password)
user.save(using=self._db)
if su:
user.make_admin()
return user
def create_user(self, pseudo, name, surname, email, password=None):
"""
Creates and saves a User with the given pseudo, name, surname, email,
and password.
"""
return self._create_user(pseudo, name, surname, email, password, False)
def create_superuser(self, pseudo, name, surname, email, password):
"""
Creates and saves a superuser with the given pseudo, name, surname,
email, and password.
"""
return self._create_user(pseudo, name, surname, email, password, True)
class User(AbstractBaseUser):
PRETTY_NAME = "Utilisateurs"
STATE_ACTIVE = 0
STATE_DISABLED = 1
STATE_ARCHIVE = 2
STATES = (
(0, 'STATE_ACTIVE'),
(1, 'STATE_DISABLED'),
(2, 'STATE_ARCHIVE'),
)
name = models.CharField(max_length=255)
surname = models.CharField(max_length=255)
email = models.EmailField()
state = models.IntegerField(choices=STATES, default=STATE_ACTIVE)
pseudo = models.CharField(max_length=32, unique=True, help_text="Doit contenir uniquement des lettres, chiffres, ou tirets. ")
comment = models.CharField(help_text="Commentaire, promo", max_length=255, blank=True)
registered = models.DateTimeField(auto_now_add=True)
admin = models.BooleanField(default=False)
USERNAME_FIELD = 'pseudo'
REQUIRED_FIELDS = ['name', 'surname', 'email']
objects = UserManager()
@property
def is_active(self):
return self.state == self.STATE_ACTIVE
@property
def is_staff(self):
return self.is_admin
@property
def is_admin(self):
return self.admin
@is_admin.setter
def is_admin(self, value):
if value and not self.is_admin:
self.make_admin()
elif not value and self.is_admin:
self.un_admin()
def has_perms(self, perms, obj=None):
for perm in perms:
if perm=="admin":
return self.is_admin
return False
def get_full_name(self):
return '%s %s' % (self.name, self.surname)
def get_short_name(self):
return self.name
def has_perm(self, perm, obj=None):
return True
def has_module_perms(self, app_label):
# Simplest version again
return True
def make_admin(self):
""" Make User admin """
self.admin = True
self.save()
def un_admin(self):
self.admin = False
self.save()
def machines(self):
return Machine.objects.filter(proprio=self)
def __str__(self):
return self.name + " " + self.surname
class Machine(models.Model):
proprio = models.ForeignKey('User', on_delete=models.PROTECT)
mac_address = MACAddressField(integer=False, unique=True)
def add_to_set(self):
command_to_execute = "sudo " + GENERIC_IPSET_COMMAND + " add " + IPSET_NAME + " " + str(self.mac_address)
apply(command_to_execute)
def del_to_set(self):
command_to_execute = "sudo " + GENERIC_IPSET_COMMAND + " del " + IPSET_NAME + " " + str(self.mac_address)
apply(command_to_execute)
@receiver(post_save, sender=Machine)
def machine_post_save(sender, **kwargs):
machine = kwargs['instance']
machine.add_to_set()
@receiver(post_delete, sender=Machine)
def machine_post_delete(sender, **kwargs):
machine = kwargs['instance']
machine.del_to_set()
class Request(models.Model):
PASSWD = 'PW'
EMAIL = 'EM'
TYPE_CHOICES = (
(PASSWD, 'Mot de passe'),
(EMAIL, 'Email'),
)
type = models.CharField(max_length=2, choices=TYPE_CHOICES)
token = models.CharField(max_length=32)
user = models.ForeignKey('User', on_delete=models.PROTECT)
created_at = models.DateTimeField(auto_now_add=True, editable=False)
expires_at = models.DateTimeField()
def save(self):
if not self.expires_at:
self.expires_at = timezone.now() \
+ datetime.timedelta(hours=REQ_EXPIRE_HRS)
if not self.token:
self.token = str(uuid.uuid4()).replace('-', '') # remove hyphens
super(Request, self).save()
class BaseInfoForm(ModelForm):
def __init__(self, *args, **kwargs):
super(BaseInfoForm, self).__init__(*args, **kwargs)
self.fields['name'].label = 'Prénom'
self.fields['surname'].label = 'Nom'
#self.fields['comment'].label = 'Commentaire'
class Meta:
model = User
fields = [
'name',
'pseudo',
'surname',
'email',
]
class EditInfoForm(BaseInfoForm):
class Meta(BaseInfoForm.Meta):
fields = [
'name',
'pseudo',
'surname',
'comment',
'email',
'admin',
]
class InfoForm(BaseInfoForm):
class Meta(BaseInfoForm.Meta):
fields = [
'name',
'pseudo',
'comment',
'surname',
'email',
'admin',
]
class UserForm(EditInfoForm):
class Meta(EditInfoForm.Meta):
fields = '__all__'
class PasswordForm(ModelForm):
class Meta:
model = User
fields = ['password']
class StateForm(ModelForm):
class Meta:
model = User
fields = ['state']
class MachineForm(ModelForm):
class Meta:
model = Machine
exclude = '__all__'

View File

@ -0,0 +1,52 @@
{% 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 users_list.paginator %}
{% include "pagination.html" with list=users_list %}
{% endif %}
<table class="table table-striped">
<thead>
<tr>
<th>Prénom</th>
<th>Nom</th>
<th>Pseudo</th>
<th>Mail</th>
<th>Machines</th>
<th>Profil</th>
</tr>
</thead>
{% for user in users_list %}
<tr>
<td>{{ user.name }}</td>
<td>{{ user.surname }}</td>
<td>{{ user.pseudo }}</td>
<td>{{ user.email }}</td>
<td><table class="table table-striped"><tr>{% for machine in user.machines %}<td>{{ machine.mac_address }}</td>{% endfor %}</tr></table></td>
<td><a href="{% url "users:profil" user.id%}" class="btn btn-primary btn-sm" role="button"><i class="glyphicon glyphicon-user"></i></a>
</td>
</tr>
{% endfor %}
</table>

View File

@ -0,0 +1,15 @@
Bonjour {{ name }},
Vous trouverez ci-dessous une url permetant d'initialiser ou de reinitialiser votre
compte {{ site_name }}. Celui-ci vous permet de gérer l'ensemble de vos équipements
connectés, votre compte, et les services proposés par le {{ asso }}.
{{ url }}
Contactez les administrateurs si vous n'êtes pas à l'origine de cette requête.
Ce lien expirera dans {{ expire_in }}.
Cordialement,
L'équipe de {{ asso }} {{ asso_mail }}.

View File

@ -0,0 +1,37 @@
{% extends "base.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 %}Utilisateurs{% endblock %}
{% block content %}
<h2>Users</h2>
{% include "users/aff_users.html" with users_list=users_list %}
<br />
<br />
<br />
{% endblock %}

View File

@ -0,0 +1,112 @@
{% extends "base.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 %}Profil{% endblock %}
{% block content %}
<h2>Compte</h2>
<div>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:edit-info' user.id %}">
<i class="glyphicon glyphicon-edit"></i>
Editer
</a>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:password' user.id %}">
<i class="glyphicon glyphicon-lock"></i>
Changer le mot de passe
</a>
{% if is_admin %}
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:state' user.id %}">
<i class="glyphicon glyphicon-flash"></i>
Changer le statut
</a>
{% endif %}
<a class="btn btn-info btn-sm" role="button" href="{% url 'users:history' 'user' user.id %}">
<i class="glyphicon glyphicon-time"></i>
Historique
</a>
</div>
<p>
<br />
</p>
<table class="table table-striped">
<tr>
<th>Prénom</th>
<td>{{ user.name }}</td>
<th>Nom</th>
<td>{{ user.surname }}</td>
</tr>
<tr>
<th>Pseudo</th>
<td>{{ user.pseudo }}</td>
<th>E-mail</th>
<td>{{ user.email }}</td>
</tr>
<tr>
<th>Commentaire</th>
<td>{{ user.comment }}</td>
<th>Date d'inscription</th>
<td>{{ user.registered }}</td>
</tr>
<th>Statut</th>
{% if user.state == 0 %}
<td><font color="green">Actif</font></td>
{% elif user.state == 1 %}
<td><font color="red">Désactivé</font></td>
{% else %}
<td><font color="orange">Archivé</font></td>
{% endif %}
<th>Dernière connexion</th>
<td>{{ user.last_login }}</td>
</tr>
</table>
<h2>Machines enregistrées</h2>
<a class="btn btn-primary btn-sm" role="button" href="{% url 'users:capture' %}">
<i class="glyphicon glyphicon-flash"></i>
Enregistrer la machine utilisée actuellement
</a>
<table class="table table-striped">
<thead>
<tr>
<th>Adresse mac</th>
<th>Historique</th>
</tr>
</thead>
{% for machine in machines_list %}
<tr>
<td>{{ machine.mac_address }}</td>
<td><a class="btn btn-info btn-sm" role="button" href="{% url 'users:history' 'machines' machine.id %}">
<i class="glyphicon glyphicon-time"></i>
</a>
</td>
</tr>
{% endfor %}
</table>
<br />
<br />
<br />
{% endblock %}

View File

@ -0,0 +1,41 @@
{% extends "base.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 d'utilisateur{% endblock %}
{% block content %}
{% bootstrap_form_errors userform %}
<form class="form" method="post">
{% csrf_token %}
{% bootstrap_form userform %}
{% bootstrap_button "Créer ou modifier" button_type="submit" icon="star" %}
</form>
<br />
<br />
<br />
{% endblock %}

25
users/tests.py Normal file
View File

@ -0,0 +1,25 @@
# 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 django.test import TestCase
# Create your tests here.

42
users/urls.py Normal file
View File

@ -0,0 +1,42 @@
# 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 django.conf.urls import url
from . import views
urlpatterns = [
url(r'^new_user/$', views.new_user, name='new-user'),
url(r'^capture/$', views.capture, name='capture'),
url(r'^edit_info/(?P<userid>[0-9]+)$', views.edit_info, name='edit-info'),
url(r'^state/(?P<userid>[0-9]+)$', views.state, name='state'),
url(r'^password/(?P<userid>[0-9]+)$', views.password, name='password'),
url(r'^profil/(?P<userid>[0-9]+)$', views.profil, name='profil'),
url(r'^mon_profil/$', views.mon_profil, name='mon-profil'),
url(r'^process/(?P<token>[a-z0-9]{32})/$', views.process, name='process'),
url(r'^reset_password/$', views.reset_password, name='reset-password'),
url(r'^history/(?P<object>user)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^history/(?P<object>machines)/(?P<id>[0-9]+)$', views.history, name='history'),
url(r'^$', views.index, name='index'),
]

331
users/views.py Normal file
View File

@ -0,0 +1,331 @@
# 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.
# App de gestion des users pour portail_captif
# Goulven Kermarec, Gabriel Détraz, Lemesle Augustin
# Gplv2
from django.shortcuts import get_object_or_404, render, redirect
from django.template.context_processors import csrf
from django.core.paginator import Paginator, EmptyPage, PageNotAnInteger
from django.template import Context, RequestContext, loader
from django.contrib import messages
from django.contrib.auth.decorators import login_required, permission_required
from django.contrib.auth.signals import user_logged_in
from django.db.models import Max, ProtectedError
from django.db import IntegrityError
from django.core.mail import send_mail
from django.utils import timezone
from django.core.urlresolvers import reverse
from django.db import transaction
from reversion.models import Version
from reversion import revisions as reversion
from users.models import User, MachineForm, Request
from users.models import EditInfoForm, InfoForm, BaseInfoForm, Machine, StateForm, mac_from_ip, apply
from users.forms import PassForm, ResetPasswordForm
import ipaddress
import subprocess
from portail_captif.settings import REQ_EXPIRE_STR, EMAIL_FROM, ASSO_NAME, ASSO_EMAIL, SITE_NAME, CAPTIVE_IP_RANGE, CAPTIVE_WIFI, PAGINATION_NUMBER
def form(ctx, template, request):
c = ctx
c.update(csrf(request))
return render(request, template, c)
def password_change_action(u_form, user, request, req=False):
""" Fonction qui effectue le changeemnt de mdp bdd"""
if u_form.cleaned_data['passwd1'] != u_form.cleaned_data['passwd2']:
messages.error(request, "Les 2 mots de passe différent")
return form({'userform': u_form}, 'users/user.html', request)
user.set_password(u_form.cleaned_data['passwd1'])
with transaction.atomic(), reversion.create_revision():
user.save()
reversion.set_comment("Réinitialisation du mot de passe")
messages.success(request, "Le mot de passe a changé")
if req:
req.delete()
return redirect("/")
return redirect("/users/profil/" + str(user.id))
def reset_passwd_mail(req, request):
""" Prend en argument un request, envoie un mail de réinitialisation de mot de pass """
t = loader.get_template('users/email_passwd_request')
c = Context({
'name': str(req.user.name) + ' ' + str(req.user.surname),
'asso': ASSO_NAME,
'asso_mail': ASSO_EMAIL,
'site_name': SITE_NAME,
'url': request.build_absolute_uri(
reverse('users:process', kwargs={'token': req.token})),
'expire_in': REQ_EXPIRE_STR,
})
send_mail('Changement de mot de passe', t.render(c),
EMAIL_FROM, [req.user.email], fail_silently=False)
return
def new_user(request):
""" Vue de création d'un nouvel utilisateur, envoie un mail pour le mot de passe"""
user = BaseInfoForm(request.POST or None)
if user.is_valid():
user = user.save(commit=False)
with transaction.atomic(), reversion.create_revision():
user.save()
reversion.set_comment("Création")
req = Request()
req.type = Request.PASSWD
req.user = user
req.save()
reset_passwd_mail(req, request)
messages.success(request, "L'utilisateur %s a été crée, un mail pour l'initialisation du mot de passe a été envoyé" % user.pseudo)
capture_mac(request, user)
return redirect("/users/profil/" + str(user.id))
return form({'userform': user}, 'users/user.html', request)
@login_required
def edit_info(request, userid):
""" Edite un utilisateur à partir de son id, si l'id est différent de request.user, vérifie la possession du droit admin """
try:
user = User.objects.get(pk=userid)
except User.DoesNotExist:
messages.error(request, "Utilisateur inexistant")
return redirect("/users/")
if not request.user.is_admin and user != request.user:
messages.error(request, "Vous ne pouvez pas modifier un autre user que vous sans droit admin")
return redirect("/users/profil/" + str(request.user.id))
if not request.user.is_admin:
user = BaseInfoForm(request.POST or None, instance=user)
else:
user = InfoForm(request.POST or None, instance=user)
if user.is_valid():
with transaction.atomic(), reversion.create_revision():
user.save()
reversion.set_user(request.user)
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in user.changed_data))
messages.success(request, "L'user a bien été modifié")
return redirect("/users/profil/" + userid)
return form({'userform': user}, 'users/user.html', request)
@login_required
@permission_required('admin')
def state(request, userid):
""" Changer l'etat actif/desactivé/archivé d'un user, need droit bureau """
try:
user = User.objects.get(pk=userid)
except User.DoesNotExist:
messages.error(request, "Utilisateur inexistant")
return redirect("/users/")
state = StateForm(request.POST or None, instance=user)
if state.is_valid():
with transaction.atomic(), reversion.create_revision():
state.save()
reversion.set_user(request.user)
reversion.set_comment("Champs modifié(s) : %s" % ', '.join(field for field in state.changed_data))
messages.success(request, "Etat changé avec succès")
return redirect("/users/profil/" + userid)
return form({'userform': state}, 'users/user.html', request)
@login_required
def password(request, userid):
""" Reinitialisation d'un mot de passe à partir de l'userid,
pour self par défaut, pour tous sans droit si droit admin,
pour tous si droit bureau """
try:
user = User.objects.get(pk=userid)
except User.DoesNotExist:
messages.error(request, "Utilisateur inexistant")
return redirect("/users/")
if not request.user.is_admin and user != request.user:
messages.error(request, "Vous ne pouvez pas modifier un autre user que vous sans droit admin")
return redirect("/users/profil/" + str(request.user.id))
u_form = PassForm(request.POST or None)
if u_form.is_valid():
return password_change_action(u_form, user, request)
return form({'userform': u_form}, 'users/user.html', request)
@login_required
@permission_required('admin')
def index(request):
""" Affiche l'ensemble des users, need droit admin """
users_list = User.objects.order_by('state', 'name')
paginator = Paginator(users_list, PAGINATION_NUMBER)
page = request.GET.get('page')
try:
users_list = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
users_list = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
users_list = paginator.page(paginator.num_pages)
return render(request, 'users/index.html', {'users_list': users_list})
@login_required
def history(request, object, id):
""" Affichage de l'historique : (acl, argument)
user : self, userid"""
if object == 'user':
try:
object_instance = User.objects.get(pk=id)
except User.DoesNotExist:
messages.error(request, "Utilisateur inexistant")
return redirect("/users/")
if not request.user.is_admin and object_instance != request.user:
messages.error(request, "Vous ne pouvez pas afficher l'historique d'un autre user que vous sans droit admin")
return redirect("/users/profil/" + str(request.user.id))
elif object == 'machines':
try:
object_instance = Machine.objects.get(pk=id)
except User.DoesNotExist:
messages.error(request, "Machine inexistante")
return redirect("/users/")
if not request.user.is_admin and object_instance.proprio != request.user:
messages.error(request, "Vous ne pouvez pas afficher l'historique d'un autre user que vous sans droit admin")
return redirect("/users/profil/" + str(request.user.id))
else:
messages.error(request, "Objet inconnu")
return redirect("/users/")
reversions = Version.objects.get_for_object(object_instance)
paginator = Paginator(reversions, PAGINATION_NUMBER)
page = request.GET.get('page')
try:
reversions = paginator.page(page)
except PageNotAnInteger:
# If page is not an integer, deliver first page.
reversions = paginator.page(1)
except EmptyPage:
# If page is out of range (e.g. 9999), deliver last page of results.
reversions = paginator.page(paginator.num_pages)
return render(request, 'portail_captif/history.html', {'reversions': reversions, 'object': object_instance})
@login_required
def mon_profil(request):
return redirect("/users/profil/" + str(request.user.id))
@login_required
def profil(request, userid):
try:
users = User.objects.get(pk=userid)
except User.DoesNotExist:
messages.error(request, "Utilisateur inexistant")
return redirect("/users/")
machines_list = Machine.objects.filter(proprio=users)
if not request.user.is_admin and users != request.user:
messages.error(request, "Vous ne pouvez pas afficher un autre user que vous sans droit admin")
return redirect("/users/profil/" + str(request.user.id))
return render(
request,
'users/profil.html',
{
'user': users,
'machines_list': machines_list,
}
)
def get_ip(request):
"""Returns the IP of the request, accounting for the possibility of being
behind a proxy.
"""
ip = request.META.get("HTTP_X_FORWARDED_FOR", None)
if ip:
# X_FORWARDED_FOR returns client1, proxy1, proxy2,...
ip = ip.split(", ")[0]
else:
ip = request.META.get("REMOTE_ADDR", "")
return ip
def capture_mac(request, users, verbose=True):
remote_ip = get_ip(request)
if ipaddress.ip_address(remote_ip) in ipaddress.ip_network(CAPTIVE_IP_RANGE):
mac_addr = mac_from_ip(remote_ip)
if mac_addr:
machine = Machine()
machine.proprio = users
machine.mac_address = str(mac_addr)
try:
with transaction.atomic(), reversion.create_revision():
machine.save()
reversion.set_comment("Enregistrement de la machine")
except:
if verbose:
messages.error(request, "Assurez-vous que la machine n'est pas déjà enregistrée")
else:
if verbose:
messages.error(request, "Impossible d'enregistrer la machine")
else:
if verbose:
messages.error(request, "Merci de vous connecter sur le réseau du portail captif pour capturer la machine (WiFi %s)" % CAPTIVE_WIFI)
def capture_mac_afterlogin(sender, user, request, **kwargs):
capture_mac(request, user, verbose=False)
# On récupère la mac après le login
user_logged_in.connect(capture_mac_afterlogin)
@login_required
def capture(request):
userid = str(request.user.id)
try:
users = User.objects.get(pk=userid)
except User.DoesNotExist:
messages.error(request, "Utilisateur inexistant")
return redirect("/users/")
capture_mac(request, users)
return redirect("/users/profil/" + str(users.id))
def reset_password(request):
userform = ResetPasswordForm(request.POST or None)
if userform.is_valid():
try:
user = User.objects.get(pseudo=userform.cleaned_data['pseudo'],email=userform.cleaned_data['email'])
except User.DoesNotExist:
messages.error(request, "Cet utilisateur n'existe pas")
return form({'userform': userform}, 'users/user.html', request)
req = Request()
req.type = Request.PASSWD
req.user = user
req.save()
reset_passwd_mail(req, request)
messages.success(request, "Un mail pour l'initialisation du mot de passe a été envoyé")
redirect("/")
return form({'userform': userform}, 'users/user.html', request)
def process(request, token):
valid_reqs = Request.objects.filter(expires_at__gt=timezone.now())
req = get_object_or_404(valid_reqs, token=token)
if req.type == Request.PASSWD:
return process_passwd(request, req)
elif req.type == Request.EMAIL:
return process_email(request, req=req)
else:
messages.error(request, "Entrée incorrecte, contactez un admin")
redirect("/")
def process_passwd(request, req):
u_form = PassForm(request.POST or None)
user = req.user
if u_form.is_valid():
return password_change_action(u_form, user, request, req=req)
return form({'userform': u_form}, 'users/user.html', request)