Add password recovery feature

Message ID 20171101182646.4109-1-jonatan.schlag@ipfire.org
State New
Headers show
Series
  • Add password recovery feature
Related show

Commit Message

Jonatan Schlag Nov. 2, 2017, 5:26 a.m. UTC
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

Patch

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):