From patchwork Thu Nov 2 05:26:46 2017 Content-Type: text/plain; charset="utf-8" MIME-Version: 1.0 Content-Transfer-Encoding: 7bit X-Patchwork-Submitter: Jonatan Schlag X-Patchwork-Id: 1513 Return-Path: Received: from mail01.ipfire.org (unknown [172.28.1.200]) by web02.ipfire.org (Postfix) with ESMTP id 87F2660DDF for ; Wed, 1 Nov 2017 19:26:52 +0100 (CET) Received: from mail01.ipfire.org (localhost [IPv6:::1]) by mail01.ipfire.org (Postfix) with ESMTP id 18D9511BB; Wed, 1 Nov 2017 19:26:52 +0100 (CET) Received: from bockland.local.familyschlag (unknown [10.172.1.10]) (using TLSv1.2 with cipher ECDHE-RSA-AES128-GCM-SHA256 (128/128 bits)) (Client did not present a certificate) by mail01.ipfire.org (Postfix) with ESMTPSA id DC13C11BB; Wed, 1 Nov 2017 19:26:49 +0100 (CET) From: Jonatan Schlag To: pakfire@lists.ipfire.org Subject: [PATCH] Add password recovery feature Date: Wed, 1 Nov 2017 19:26:46 +0100 Message-Id: <20171101182646.4109-1-jonatan.schlag@ipfire.org> X-Mailer: git-send-email 2.11.0 X-BeenThere: pakfire@lists.ipfire.org X-Mailman-Version: 2.1.21 Precedence: list List-Id: "Mailinglist for the Pakfire Build System." List-Unsubscribe: , List-Archive: List-Post: List-Help: List-Subscribe: , MIME-Version: 1.0 Errors-To: pakfire-bounces@lists.ipfire.org Sender: "Pakfire" It is now possible to reset the password, we only need to implement the mail feature. At the moment we cannot send a mail with the recovery code to the user. Fixes: #10095 Signed-off-by: Jonatan Schlag --- Makefile.am | 6 ++- src/buildservice/users.py | 35 +++++++++++++++++ src/templates/user-forgot-password.html | 9 +---- .../user-requested-password-recovery.html | 18 +++++++++ src/templates/user-reset-password-fail.html | 18 +++++++++ src/templates/user-reset-password-success.html | 18 +++++++++ src/templates/user-reset-password.html | 36 ++++++++++++++++++ src/web/__init__.py | 1 + src/web/auth.py | 44 ++++++++++++++++++++-- 9 files changed, 174 insertions(+), 11 deletions(-) create mode 100644 src/templates/user-requested-password-recovery.html create mode 100644 src/templates/user-reset-password-fail.html create mode 100644 src/templates/user-reset-password-success.html create mode 100644 src/templates/user-reset-password.html diff --git a/Makefile.am b/Makefile.am index bc5cd94..ccbf96c 100644 --- a/Makefile.am +++ b/Makefile.am @@ -214,7 +214,11 @@ dist_templates_DATA = \ src/templates/user-profile.html \ src/templates/user-profile-need-activation.html \ src/templates/user-profile-passwd.html \ - src/templates/user-profile-passwd-ok.html + src/templates/user-profile-passwd-ok.html \ + src/templates/user-requested-password-recovery.html \ + src/templates/user-reset-password.html \ + src/templates//user-reset-password-success.html \ + src/templates//user-reset-password-fail.html templatesdir = $(datadir)/templates diff --git a/src/buildservice/users.py b/src/buildservice/users.py index 7c98d4b..a4ce2b0 100644 --- a/src/buildservice/users.py +++ b/src/buildservice/users.py @@ -1,5 +1,6 @@ #!/usr/bin/python +import datetime import email.utils import hashlib import logging @@ -185,6 +186,10 @@ class Users(base.Object): LEFT JOIN users_emails ON users.id = users_emails.user_id \ WHERE users_emails.email = %s", email) + def get_by_password_recovery_code(self, code): + return self._get_user("SELECT * FROM users \ + WHERE password_recovery_code = %s AND password_recovery_code_expires_at > NOW()", code) + def find_maintainers(self, maintainers): email_addresses = [] @@ -297,6 +302,10 @@ class User(base.DataObject): """ Update the passphrase the users uses to log on. """ + # We cannot set the password for ldap users + if self.ldap_dn: + raise AttributeError("Cannot set passphrase for LDAP user") + self.db.execute("UPDATE users SET passphrase = %s WHERE id = %s", generate_password_hash(passphrase), self.id) @@ -437,6 +446,32 @@ class User(base.DataObject): timezone = property(get_timezone, set_timezone) + def get_password_recovery_code(self): + return self.data.password_recovery_code + + def set_password_recovery_code(self, code): + self._set_attribute("password_recovery_code", code) + + self._set_attribute("password_recovery_code_expires_at", + datetime.datetime.utcnow() + datetime.timedelta(days=1)) + + password_recovery_code = property(get_password_recovery_code, set_password_recovery_code) + + def forgot_password(self): + log.debug("User %s reqested password recovery" % self.name) + + # We cannot reset te password for ldap users + if self.ldap_dn: + # Maybe we should send an email with an explanation + return + + # Add a recovery code to the database and a timestamp when this code expires + self.password_recovery_code = generate_random_string(64) + + # XXX + # We should send an email with the activation code + + @property def activated(self): return self.data.activated diff --git a/src/templates/user-forgot-password.html b/src/templates/user-forgot-password.html index 2896ea4..3c21804 100644 --- a/src/templates/user-forgot-password.html +++ b/src/templates/user-forgot-password.html @@ -17,11 +17,6 @@

{{ _("Forgot password") }}

- -
- {{ _("Work in progress!") }} -
-

@@ -29,7 +24,7 @@ {{ _("However, we allow to re-activate your account.") }}

- {{ _("You need to enter your username below.") }} + {{ _("You need to enter your username or your email address below") }} {{ _("After that, you will receive an email with intructions how to go on.") }}


@@ -39,7 +34,7 @@
- +
diff --git a/src/templates/user-requested-password-recovery.html b/src/templates/user-requested-password-recovery.html new file mode 100644 index 0000000..29eb95a --- /dev/null +++ b/src/templates/user-requested-password-recovery.html @@ -0,0 +1,18 @@ +{% extends "base-form2.html" %} + +{% block title %}{{ _("Requested password recovery") }}{% end block %} + +{% block body %} + + +
+
+

+ {{ _("An email with instructions how to recover your password was send to your primary email address.") }} +

+
+
+
+{% end %} diff --git a/src/templates/user-reset-password-fail.html b/src/templates/user-reset-password-fail.html new file mode 100644 index 0000000..1daaef3 --- /dev/null +++ b/src/templates/user-reset-password-fail.html @@ -0,0 +1,18 @@ +{% extends "base-form2.html" %} + +{% block title %}{{ _("Password reset failed") }}{% end block %} + +{% block body %} + + +
+
+

+ {{ message }} +

+
+
+
+{% end %} diff --git a/src/templates/user-reset-password-success.html b/src/templates/user-reset-password-success.html new file mode 100644 index 0000000..f75b3a7 --- /dev/null +++ b/src/templates/user-reset-password-success.html @@ -0,0 +1,18 @@ +{% extends "base-form2.html" %} + +{% block title %}{{ _("Password reset succeeded") }}{% end block %} + +{% block body %} + + +
+
+

+ {{ _("Successfully reset your password") }} +

+
+
+
+{% end %} diff --git a/src/templates/user-reset-password.html b/src/templates/user-reset-password.html new file mode 100644 index 0000000..1fa07e4 --- /dev/null +++ b/src/templates/user-reset-password.html @@ -0,0 +1,36 @@ +{% extends "base.html" %} + +{% block title %}{{ _("Register a new account") }}{% end block %} + +{% block body %} + + +
+ {% raw xsrf_form_html() %} + + +
+
+ +
+ +
+
+ +
+ +
+ +
+
+
+ +
+ +
+
+{% end block %} diff --git a/src/web/__init__.py b/src/web/__init__.py index 5be08d8..f44a123 100644 --- a/src/web/__init__.py +++ b/src/web/__init__.py @@ -118,6 +118,7 @@ class Application(tornado.web.Application): (r"/logout", auth.LogoutHandler), (r"/register", auth.RegisterHandler), (r"/password-recovery", auth.PasswordRecoveryHandler), + (r"/password-reset", auth.PasswordResetHandler), # User profiles (r"/users", users.UsersHandler), diff --git a/src/web/auth.py b/src/web/auth.py index 4538db5..811b3e9 100644 --- a/src/web/auth.py +++ b/src/web/auth.py @@ -143,10 +143,48 @@ class PasswordRecoveryHandler(base.BaseHandler): def post(self): username = self.get_argument("name", None) - if not username: - return self.get() + with self.db.transaction(): + user = self.backend.users.get_by_email(username) \ + or self.backend.users.get_by_name(username) + + if user: + user.forgot_password() + + self.render("user-requested-password-recovery.html") + + +class PasswordResetHandler(base.BaseHandler): + def get(self): + code = self.get_argument("code") + + user = self.backend.users.get_by_password_recovery_code(code) + if not user: + raise tornado.web.HTTPError(400) + + self.render("user-reset-password.html", user=user) + + def post(self): + _ = self.locale.translate + + code = self.get_argument("code") + pass1 = self.get_argument("password1") + pass2 = self.get_argument("password2") + + user = self.backend.users.get_by_password_recovery_code(code) + if not user: + raise tornado.web.HTTPError(400) + + if not pass1 == pass2: + return self.render("user-reset-password-fail.html", + message=_("Second password does not match")) + + # XXX Check password strength + + with self.db.transaction(): + user.passphrase = pass1 + user.password_recovery_code = None - # XXX TODO + self.render("user-reset-password-success.html") class LogoutHandler(base.BaseHandler):