Skip to content

Commit 1afc2c5

Browse files
author
Richard Jones
committed
Added password reset facility for forgotten passwords.
Uses similar mechanism to PyPI.
1 parent 554c110 commit 1afc2c5

File tree

5 files changed

+200
-45
lines changed

5 files changed

+200
-45
lines changed

roundup/cgi/client.py

Lines changed: 144 additions & 41 deletions
Original file line numberDiff line numberDiff line change
@@ -1,19 +1,20 @@
1-
# $Id: client.py,v 1.100 2003-02-26 04:57:49 richard Exp $
1+
# $Id: client.py,v 1.101 2003-02-27 05:43:01 richard Exp $
22

33
__doc__ = """
44
WWW request handler (also used in the stand-alone server).
55
"""
66

77
import os, os.path, cgi, StringIO, urlparse, re, traceback, mimetypes, urllib
88
import binascii, Cookie, time, random, MimeWriter, smtplib, socket, quopri
9-
import stat, rfc822
9+
import stat, rfc822, string
1010

1111
from roundup import roundupdb, date, hyperdb, password
1212
from roundup.i18n import _
1313
from roundup.cgi.templating import Templates, HTMLRequest, NoTemplate
1414
from roundup.cgi import cgitb
1515
from roundup.cgi.PageTemplates import PageTemplate
1616
from roundup.rfc2822 import encode_header
17+
from roundup.mailgw import uidFromAddress
1718

1819
class HTTPException(Exception):
1920
pass
@@ -257,20 +258,31 @@ def inner_main(self):
257258
self.write(cgitb.html())
258259

259260
def clean_sessions(self):
260-
'''age sessions, remove when they haven't been used for a week.
261-
Do it only once an hour'''
261+
''' Age sessions, remove when they haven't been used for a week.
262+
263+
Do it only once an hour.
264+
265+
Note: also cleans One Time Keys, and other "session" based
266+
stuff.
267+
'''
262268
sessions = self.db.sessions
263269
last_clean = sessions.get('last_clean', 'last_use') or 0
264270

265271
week = 60*60*24*7
266272
hour = 60*60
267273
now = time.time()
268274
if now - last_clean > hour:
269-
# remove age sessions
275+
# remove aged sessions
270276
for sessid in sessions.list():
271277
interval = now - sessions.get(sessid, 'last_use')
272278
if interval > week:
273279
sessions.destroy(sessid)
280+
# remove aged otks
281+
otks = self.db.otks
282+
for sessid in otks.list():
283+
interval = now - okts.get(sessid, '__time')
284+
if interval > week:
285+
otk.destroy(sessid)
274286
sessions.set('last_clean', last_use=time.time())
275287

276288
def determine_user(self):
@@ -479,6 +491,7 @@ def renderContext(self):
479491
('new', 'newItemAction'),
480492
('register', 'registerAction'),
481493
('confrego', 'confRegoAction'),
494+
('passrst', 'passResetAction'),
482495
('login', 'loginAction'),
483496
('logout', 'logout_action'),
484497
('search', 'searchAction'),
@@ -489,17 +502,8 @@ def handle_action(self):
489502
''' Determine whether there should be an Action called.
490503
491504
The action is defined by the form variable :action which
492-
identifies the method on this object to call. The four basic
493-
actions are defined in the "actions" sequence on this class:
494-
"edit" -> self.editItemAction
495-
"editcsv" -> self.editCSVAction
496-
"new" -> self.newItemAction
497-
"register" -> self.registerAction
498-
"confrego" -> self.confRegoAction
499-
"login" -> self.loginAction
500-
"logout" -> self.logout_action
501-
"search" -> self.searchAction
502-
"retire" -> self.retireAction
505+
identifies the method on this object to call. The actions
506+
are defined in the "actions" sequence on this class.
503507
'''
504508
if self.form.has_key(':action'):
505509
action = self.form[':action'].value.lower()
@@ -675,7 +679,7 @@ def logout_action(self):
675679
# Let the user know what's going on
676680
self.ok_message.append(_('You are logged out'))
677681

678-
chars = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789'
682+
chars = string.letters+string.digits
679683
def registerAction(self):
680684
'''Attempt to create a new user based on the contents of the form
681685
and then set the cookie.
@@ -713,15 +717,35 @@ def registerAction(self):
713717
props[propname] = str(value)
714718
elif isinstance(proptype, hyperdb.Password):
715719
props[propname] = str(value)
720+
props['__time'] = time.time()
716721
self.db.otks.set(otk, **props)
717722

723+
# send the email
724+
tracker_name = self.db.config.TRACKER_NAME
725+
subject = 'Complete your registration to %s'%tracker_name
726+
body = '''
727+
To complete your registration of the user "%(name)s" with %(tracker)s,
728+
please visit the following URL:
729+
730+
%(url)s?@action=confrego&otk=%(otk)s
731+
'''%{'name': props['username'], 'tracker': tracker_name, 'url': self.base,
732+
'otk': otk}
733+
if not self.sendEmail(props['address'], subject, body):
734+
return
735+
736+
# commit changes to the database
737+
self.db.commit()
738+
739+
# redirect to the "you're almost there" page
740+
raise Redirect, '%suser?@template=rego_progress'%self.base
741+
742+
def sendEmail(self, to, subject, content):
718743
# send email to the user's email address
719744
message = StringIO.StringIO()
720745
writer = MimeWriter.MimeWriter(message)
721746
tracker_name = self.db.config.TRACKER_NAME
722-
s = 'Complete your registration to %s'%tracker_name
723-
writer.addheader('Subject', encode_header(s))
724-
writer.addheader('To', props['address'])
747+
writer.addheader('Subject', encode_header(subject))
748+
writer.addheader('To', to)
725749
writer.addheader('From', roundupdb.straddr((tracker_name,
726750
self.db.config.ADMIN_EMAIL)))
727751
writer.addheader('Date', time.strftime("%a, %d %b %Y %H:%M:%S +0000",
@@ -734,36 +758,23 @@ def registerAction(self):
734758
body = writer.startbody('text/plain; charset=utf-8')
735759

736760
# message body, encoded quoted-printable
737-
content = StringIO.StringIO('''
738-
To complete your registration of the user "%(name)s" with %(tracker)s,
739-
please visit the following URL:
740-
741-
http://localhost:8001/test/?@action=confrego&otk=%(otk)s
742-
'''%{'name': props['username'], 'tracker': tracker_name, 'url': self.base,
743-
'otk': otk})
761+
content = StringIO.StringIO(content)
744762
quopri.encode(content, body, 0)
745763

746764
# now try to send the message
747765
try:
748766
# send the message as admin so bounces are sent there
749767
# instead of to roundup
750768
smtp = smtplib.SMTP(self.db.config.MAILHOST)
751-
smtp.sendmail(self.db.config.ADMIN_EMAIL, [props['address']],
752-
message.getvalue())
769+
smtp.sendmail(self.db.config.ADMIN_EMAIL, [to], message.getvalue())
753770
except socket.error, value:
754-
self.error_message.append("Error: couldn't send "
755-
"confirmation email: mailhost %s"%value)
756-
return
771+
self.error_message.append("Error: couldn't send email: "
772+
"mailhost %s"%value)
773+
return 0
757774
except smtplib.SMTPException, value:
758-
self.error_message.append("Error: couldn't send "
759-
"confirmation email: %s"%value)
760-
return
761-
762-
# commit changes to the database
763-
self.db.commit()
764-
765-
# redirect to the "you're almost there" page
766-
raise Redirect, '%s?:template=rego_step1_done'%self.base
775+
self.error_message.append("Error: couldn't send email: %s"%value)
776+
return 0
777+
return 1
767778

768779
def registerPermission(self, props):
769780
''' Determine whether the user has permission to register
@@ -805,6 +816,7 @@ def confRegoAction(self):
805816
# try:
806817
if 1:
807818
props['roles'] = self.instance.config.NEW_WEB_USER_ROLES
819+
del props['__time']
808820
self.userid = cl.create(**props)
809821
# clear the props from the otk database
810822
self.db.otks.destroy(otk)
@@ -833,6 +845,97 @@ def confRegoAction(self):
833845
raise Redirect, '%suser%s?@ok_message=%s'%(
834846
self.base, self.userid, urllib.quote(message))
835847

848+
def passResetAction(self):
849+
''' Handle password reset requests.
850+
851+
Presence of either "name" or "address" generate email.
852+
Presense of "otk" performs the reset.
853+
'''
854+
if self.form.has_key('otk'):
855+
# pull the rego information out of the otk database
856+
otk = self.form['otk'].value
857+
uid = self.db.otks.get(otk, 'uid')
858+
859+
# re-open the database as "admin"
860+
if self.user != 'admin':
861+
self.opendb('admin')
862+
863+
# change the password
864+
newpw = ''.join([random.choice(self.chars) for x in range(8)])
865+
866+
cl = self.db.user
867+
# XXX we need to make the "default" page be able to display errors!
868+
# try:
869+
if 1:
870+
# set the password
871+
cl.set(uid, password=password.Password(newpw))
872+
# clear the props from the otk database
873+
self.db.otks.destroy(otk)
874+
self.db.commit()
875+
# except (ValueError, KeyError), message:
876+
# self.error_message.append(str(message))
877+
# return
878+
879+
# user info
880+
address = self.db.user.get(uid, 'address')
881+
name = self.db.user.get(uid, 'username')
882+
883+
# send the email
884+
tracker_name = self.db.config.TRACKER_NAME
885+
subject = 'Password reset for %s'%tracker_name
886+
body = '''
887+
The password has been reset for username "%(name)s".
888+
889+
Your password is now: %(password)s
890+
'''%{'name': name, 'password': newpw}
891+
if not self.sendEmail(address, subject, body):
892+
return
893+
894+
self.ok_message.append('Password reset and email sent to %s'%address)
895+
return
896+
897+
# no OTK, so now figure the user
898+
if self.form.has_key('username'):
899+
name = self.form['username'].value
900+
try:
901+
uid = self.db.user.lookup(name)
902+
except KeyError:
903+
self.error_message.append('Unknown username')
904+
return
905+
address = self.db.user.get(uid, 'address')
906+
elif self.form.has_key('address'):
907+
address = self.form['address'].value
908+
uid = uidFromAddress(self.db, ('', address), create=0)
909+
if not uid:
910+
self.error_message.append('Unknown email address')
911+
return
912+
name = self.db.user.get(uid, 'username')
913+
else:
914+
self.error_message.append('You need to specify a username '
915+
'or address')
916+
return
917+
918+
# generate the one-time-key and store the props for later
919+
otk = ''.join([random.choice(self.chars) for x in range(32)])
920+
self.db.otks.set(otk, uid=uid, __time=time.time())
921+
922+
# send the email
923+
tracker_name = self.db.config.TRACKER_NAME
924+
subject = 'Confirm reset of password for %s'%tracker_name
925+
body = '''
926+
Someone, perhaps you, has requested that the password be changed for your
927+
username, "%(name)s". If you wish to proceed with the change, please follow
928+
the link below:
929+
930+
%(url)suser?@template=forgotten&@action=passrst&otk=%(otk)s
931+
932+
You should then receive another email with the new password.
933+
'''%{'name': name, 'tracker': tracker_name, 'url': self.base, 'otk': otk}
934+
if not self.sendEmail(address, subject, body):
935+
return
936+
937+
self.ok_message.append('Email sent to %s'%address)
938+
836939
def editItemAction(self):
837940
''' Perform an edit of an item in the database.
838941

roundup/mailgw.py

Lines changed: 5 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -73,7 +73,7 @@ class node. Any parts of other types are each stored in separate files
7373
an exception, the original message is bounced back to the sender with the
7474
explanatory message given in the exception.
7575
76-
$Id: mailgw.py,v 1.110 2003-02-22 06:47:04 richard Exp $
76+
$Id: mailgw.py,v 1.111 2003-02-27 05:43:01 richard Exp $
7777
'''
7878

7979
import string, re, os, mimetools, cStringIO, smtplib, socket, binascii, quopri
@@ -999,14 +999,16 @@ def uidFromAddress(db, address, create=1, **user_props):
999999

10001000
# try a straight match of the address
10011001
user = extractUserFromList(db.user, db.user.stringFind(address=address))
1002-
if user is not None: return user
1002+
if user is not None:
1003+
return user
10031004

10041005
# try the user alternate addresses if possible
10051006
props = db.user.getprops()
10061007
if props.has_key('alternate_addresses'):
10071008
users = db.user.filter(None, {'alternate_addresses': address})
10081009
user = extractUserFromList(db.user, users)
1009-
if user is not None: return user
1010+
if user is not None:
1011+
return user
10101012

10111013
# try to match the username to the address (for local
10121014
# submissions where the address is empty)

roundup/templates/classic/html/page

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -71,7 +71,8 @@
7171
<input size="10" type="password" name="__login_password"><br>
7272
<input type="submit" name=":action" value="login">
7373
<span tal:replace="structure request/indexargs_form" />
74-
<a href="user?:template=register">Register</a>
74+
<a href="user?:template=register">Register</a><br>
75+
<a href="user?:template=forgotten">Forgotten your password?</a><br>
7576
</p>
7677
</form>
7778

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
<!-- dollarId: user.item,v 1.7 2002/08/16 04:29:04 richard Exp dollar-->
2+
<tal:block metal:use-macro="templates/page/macros/icing">
3+
<title metal:fill-slot="head_title">Password reset request</title>
4+
<td class="page-header-top" metal:fill-slot="body_title">
5+
<h2>Password reset request</h2>
6+
</td>
7+
<td class="content" metal:fill-slot="content">
8+
9+
<p>You have two options if you have forgotten your password. If you
10+
know the email address you registered with, enter it below.</p>
11+
12+
<form method="POST" onSubmit="return submit_once()">
13+
<input type="hidden" name="@action" value="passrst">
14+
<input type="hidden" name="@template" value="forgotten">
15+
<table class="form">
16+
<tr><th>Email Address:</th> <td><input name="address"></td> </tr>
17+
<tr><td></td><td><input type="submit" value="Request password reset"></td></tr>
18+
</table>
19+
20+
<p>Or, if you know your username, then enter it below.</p>
21+
22+
<table class="form">
23+
<tr><th>Username:</th> <td><input name="username"></td> </tr>
24+
<tr><td></td><td><input type="submit" value="Request password reset"></td></tr>
25+
</table>
26+
</form>
27+
28+
<p>A confirmation email will be sent to you - please follow the
29+
instructions
30+
within it to complete the reset process.</p>
31+
</td>
32+
33+
</tal:block>
Lines changed: 16 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,16 @@
1+
<!-- dollarId: issue.index,v 1.2 2001/07/29 04:07:37 richard Exp dollar-->
2+
<tal:block metal:use-macro="templates/page/macros/icing">
3+
<title metal:fill-slot="head_title">List of issues</title>
4+
<td class="page-header-top" metal:fill-slot="body_title">
5+
<h1>Registration in progress...</h1>
6+
</td>
7+
<td class="content" metal:fill-slot="content">
8+
9+
<p>You will shortly receive an email to confirm your registration. To
10+
complete the registration process, visit the link indicated in the
11+
email.
12+
</p>
13+
14+
</td>
15+
</tal:block>
16+

0 commit comments

Comments
 (0)