diff --git a/password_reset/forms.py b/password_reset/forms.py new file mode 100644 index 0000000000000000000000000000000000000000..48bf051b9b3b8a36a066f3058fdd6ac6e41b842f --- /dev/null +++ b/password_reset/forms.py @@ -0,0 +1,87 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Antoine BERNARD +# Authors: Antoine BERNARD +# +# 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 3 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, see . +""" + Formulaire de l'application password_reset +""" +#: Import des formulaires +from django import forms + +#: i18n de l'intranet +from django.utils.translation import ugettext_lazy as _ + +#: Fonctions de communication avec la base LDAP +from lc_ldap import shortcuts + +class EmailForm(forms.Form): + """ + Formulaire de demande d'une adresse e-mail. + """ + email = forms.EmailField( + label=_(u'Adresse e-mail'), + max_length=254, + required=True + ) + + def get_user(self): + """ + Renvoie l'objet LDAP à partir de l'e-mail + """ + try: + email = self.cleaned_data['email'] + conn = shortcuts.lc_ldap_readonly() + # On cherches les objets LDAP tels que : + # * le mail est dans le champ `mail` ou `mailExt` + # * et il a droit de se connecter + # * et c'est un adhérent + # * et il a un compte crans + res = conn.search( + u"(&(|(mail=%s)(mailExt=%s))\ + (!(shadowExpire=0))(aid=*)(uid=*))" + % (email, email) + ) + return res[0] + except IndexError: + return [] + + +class UsernameForm(forms.Form): + """ + Formulaire de demande d'un login Cr@ns. + """ + username = forms.CharField( + label=_(u"Nom d'utilisateur Cr@ns"), + max_length=254, + required=True + ) + + def get_user(self): + """ + Renvoie l'objet LDAP à partir du login + """ + try: + login = self.cleaned_data['username'] + conn = shortcuts.lc_ldap_readonly() + # On cherches les objets LDAP tels que : + # * le login est dans le champ `uid` + # * et il a droit de se connecter + # * et c'est un adhérent + res = conn.search(u"(&(uid=%s)(!(shadowExpire=0))(aid=*))" % login) + return res[0] + except IndexError: + return [] + diff --git a/password_reset/templates/password_reset/password_reset_done.html b/password_reset/templates/password_reset/password_reset_done.html new file mode 100644 index 0000000000000000000000000000000000000000..8ed0a55a412317802d0859b72a04aefe7f45db5f --- /dev/null +++ b/password_reset/templates/password_reset/password_reset_done.html @@ -0,0 +1,13 @@ +{% extends "template.html" %} +{% load i18n %} +{% block head %} +{% endblock head %} +{% block title %}{{ block.super }} : {% trans "Réinitialisation du mot de passe" %}{% endblock %} +{% block h1 %}{% trans "Mail envoyé" %}{% endblock %} +{% load staticfiles %} +{% block content %} + +

{% trans "Un mail a été envoyé à l'adresse de contact enregistré dans notre base de donnée si elle existe." %}

+ +

{% trans "Pensez à vérifier votre dossier spam." %}

+{% endblock %} diff --git a/password_reset/templates/password_reset/password_reset_form.html b/password_reset/templates/password_reset/password_reset_form.html new file mode 100644 index 0000000000000000000000000000000000000000..bac1e08a09331f45f6400f0e29a64f7a57a9ceef --- /dev/null +++ b/password_reset/templates/password_reset/password_reset_form.html @@ -0,0 +1,30 @@ +{% extends "template.html" %} +{% load i18n %} +{% block head %} +{% endblock head %} +{% block title %}{{ block.super }} : {% trans "Réinitialisation du mot de passe" %}{% endblock %} +{% block h1 %}{% trans "Réinitialisation du mot de passe" %}{% endblock %} +{% load staticfiles %} +{% block content %} + +

{% trans "Pour réinitialiser votre mot de passe, entrez au choix, votre nom d'utilisateur ou votre mèl de secours." %}

+ +{% if lform %} +
{% csrf_token %} +

{% trans "Par login" %}

+
+ {{ lform.as_table }} + +
+{% endif %} +{% if eform %} +
+
{% csrf_token %} +

{% trans "Par adresse e-mail" %}

+
+ {{ eform.as_table }} + +
+
+{% endif %} +{% endblock %} diff --git a/password_reset/urls.py b/password_reset/urls.py new file mode 100644 index 0000000000000000000000000000000000000000..a04756e94cdea8e4d2cee21340604ae3294d0fe1 --- /dev/null +++ b/password_reset/urls.py @@ -0,0 +1,33 @@ +# -*- coding: utf-8 -*- +# +# Copyright (C) 2017 Antoine BERNARD +# Authors: Antoine BERNARD +# +# 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 3 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, see . +""" + Urls de l'application password_reset +""" + +from django.conf.urls import patterns, url + +import views + +urlpatterns = [ + url(r'^$', + views.password_reset, + name="password_reset",), + url(r'^done/$', + views.password_reset_done, + name="password_reset_done"), +] diff --git a/password_reset/views.py b/password_reset/views.py new file mode 100644 index 0000000000000000000000000000000000000000..ef5aa74c511750cf924c58c046a52177fcb636e8 --- /dev/null +++ b/password_reset/views.py @@ -0,0 +1,133 @@ +# -*- encoding: utf-8 -*- +""" + Views de l'application password_reset +""" + +#: Import de settings de l'intranet +from intranet import settings + +#: Import de fonctions utiles +from django.shortcuts import render, redirect +from django.core.urlresolvers import reverse_lazy + +#: Import des views Django +from django.views.generic.detail import DetailView +from django.views.generic.edit import FormView + +#: i18n +from django.utils.translation import ugettext_lazy as _ + +#: Pour stocker des messages dans la session courante +from django.contrib import messages + +#: Import des formulaires +from password_reset.forms import EmailForm, UsernameForm + +#: Tokenization +from django.contrib.auth.tokens import PasswordResetTokenGenerator +from django.utils.encoding import force_bytes +from django.utils.http import urlsafe_base64_encode, int_to_base36 +from django.utils.crypto import salted_hmac + +#Copié depuis Django1.11 +class MyResetTokenGenerator(PasswordResetTokenGenerator): + """ + Classe pour la génération d'un token à partir de la + base LDAP. + """ + key_salt = "django.contrib.auth.tokens.PasswordResetTokenGenerator" + secret = settings.SECRET_KEY + + def _make_token_with_timestamp(self, user, timestamp): + ts_b36 = int_to_base36(timestamp) + + hash = salted_hmac( + self.key_salt, + self._make_hash_value(user, timestamp), + secret=self.secret, + ).hexdigest()[::2] + return "%s-%s" % (ts_b36, hash) + + def _make_hash_value(self, user, timestamp): + # On surchage la méthode pour rechercher les informations + # dans la base LDAP + login_timestamp = user['derniereConnexion'][0] or '' + return str(user["uidNumber"][0]) + str(user['userPassword'][0])\ + + str(login_timestamp) + str(timestamp) + +my_token_generator = MyResetTokenGenerator() + +class PasswordResetFormView(FormView): + template_name = "password_reset/password_reset_form.html" + success_url = reverse_lazy("password_reset:password_reset_done") + lform = UsernameForm + eform = EmailForm + token_generator = my_token_generator + + def form_valid(self, request, form, *args, **kwargs): + user = form.get_user() + if user: + from gestion import mail + c = { + 'from' : 'cableurs@crans.org', + 'to': str((user["mailExt"] or user["mail"])[0]), + 'name': str(user["cn"][0]), + 'protocol': 'https', + 'domain': request.META['HTTP_HOST'], + 'uid': urlsafe_base64_encode(force_bytes(user["aid"][0])), + 'token': self.token_generator.make_token(user), + 'username' : str(user["uid"][0]), + 'mailer' : u"reset_pw", + } + + # On utilise la class ServerConnection() dans /usr/scripts + with mail.ServerConnection() as conn_smtp: + conn_smtp.send_template('password_reset', c) + + return super(PasswordResetFormView, self).form_valid(form, *args, **kwargs) + + def post(self, request, *args, **kwargs): + req = request.POST + + # On récupere et on rempli le formulaire approrié + form = self.lform(req) if 'username' in req else self.eform(req) + + if form.is_valid(): + return self.form_valid(request, form, **kwargs) + else: + return redirect(self.success_url) + + def get(self, request, next=None, *args, **kwargs): + # On vérifie que l'utilisateur n'est pas connecté. + # Sinon, il ne chercherait pas à réinitialiser son mot de passe. + if not request.user.is_anonymous(): + messages.error(request, _(u"Vous devez vous déconnecter pour accédez à cette page")) + return redirect(reverse_lazy('index')) + return render( + request, + self.template_name, + {'lform': self.lform, + 'eform': self.eform, + }, + ) + +password_reset = PasswordResetFormView.as_view() + +class PasswordResetDoneView(DetailView): + """ + Vue pour la confirmation d'instructions de réinitialisation + de mot de passe. + + On confirme toujours sinon quoi on donnerait des informations sur l'existence + de login ou d'adresses e-mails. + """ + def get(self, request, next=None, *args, **kwargs): + if not request.user.is_anonymous(): + messages.error(request, _(u"Vous devez vous déconnecter pour accédez à cette page")) + return redirect(reverse_lazy('index')) + return render( + request, + "password_reset/password_reset_done.html", + ) + +password_reset_done = PasswordResetDoneView.as_view()