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 jonatan.schlag@ipfire.org --- 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 @@ <h1>{{ _("Forgot password") }}</h1> </div>
- <!-- XXX ---> - <div class="alert alert-warning"> - {{ _("Work in progress!") }} - </div> - <div class="row"> <div class="span6"> <p> @@ -29,7 +24,7 @@ {{ _("However, we allow to re-activate your account.") }} </p> <p> - {{ _("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.") }} </p> <hr> @@ -39,7 +34,7 @@
<fieldset> <div class="control-group"> - <label class="control-label" for="name">{{ _("Your username") }}</label> + <label class="control-label" for="name">{{ _("Your username or email address") }}</label> <div class="controls"> <input type="text" class="input-xlarge" id="name" name="name" /> </div> 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 %} + <div class="page-header"> + <h1>{{ _("Password recovery requested") }}</h1> + </div> + + <div class="row"> + <div class="span6"> + <p> + {{ _("An email with instructions how to recover your password was send to your primary email address.") }} + </p> + <hr> + </div> + </div> +{% 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 %} + <div class="page-header"> + <h1>{{ _("Password reset failed") }}</h1> + </div> + + <div class="row"> + <div class="span6"> + <p> + {{ message }} + </p> + <hr> + </div> + </div> +{% 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 %} + <div class="page-header"> + <h1>{{ _("Password reset succeeded") }}</h1> + </div> + + <div class="row"> + <div class="span6"> + <p> + {{ _("Successfully reset your password") }} + </p> + <hr> + </div> + </div> +{% 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 %} + <div class="page-header"> + <h2> + {{ _("Reset password") }} + </h2> + </div> + + <form class="form-horizontal" method="POST" action=""> + {% raw xsrf_form_html() %} + <input type="hidden" name="code" value="{{ user.password_recovery_code }}"> + + <fieldset> + <div class="control-group"> + <label class="control-label" for="password1">{{ _("Password") }}</label> + <div class="controls"> + <input type="password" class="input-xlarge" id="password1" name="password1"> + </div> + </div> + + <div class="control-group"> + <label class="control-label" for="password2">{{ _("Confirm password") }}</label> + <div class="controls"> + <input type="password" class="input-xlarge" id="password2" name="password2"> + </div> + </div> + </fieldset> + + <div class="form-actions"> + <button type="submit" class="btn btn-primary">{{ _("Reset password") }}</button> + </div> + </form> +{% 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):