summaryrefslogtreecommitdiff
diff options
context:
space:
mode:
authorLV-426 <lv-426@taproot.org.il>2015-02-23 14:53:57 +0200
committerLV-426 <lv-426@taproot.org.il>2015-02-23 15:07:53 +0200
commitff7903bd5b7861dd729b88f347365140a2bee9d4 (patch)
tree38469e52b5952b02bdd8a3f575492ea12d07c890
parentc1fa9b6cd84340ae37bbc543c40a7566b14f5fe9 (diff)
rz_user.py: rest__pw_reset()
-rw-r--r--src/server/rz_server.py1
-rw-r--r--src/server/rz_user.py153
2 files changed, 154 insertions, 0 deletions
diff --git a/src/server/rz_server.py b/src/server/rz_server.py
index 95463164..3077f723 100644
--- a/src/server/rz_server.py
+++ b/src/server/rz_server.py
@@ -221,6 +221,7 @@ def init_rest_interface(cfg, flask_webapp):
rest_entry('/login', rz_user.rest__login, {'methods': ['GET', 'POST']}),
rest_entry('/logout', rz_user.rest__logout, {'methods': ['GET', 'POST']}),
rest_entry('/match/node-set', rz_api.match_node_set_by_attr_filter_map),
+ rest_entry('/pw-reset', rz_user.rest__pw_reset, {'methods': ['GET', 'POST']}),
rest_entry('/signup', rz_user.rest__user_signup, {'methods': ['GET', 'POST']}),
# server administration: access restricted to localhost
diff --git a/src/server/rz_user.py b/src/server/rz_user.py
index 6c649273..3808dbc9 100644
--- a/src/server/rz_user.py
+++ b/src/server/rz_user.py
@@ -166,6 +166,129 @@ def rest__logout():
log.debug('logout: success: user: %s' % (u))
return redirect(url_for('login'))
+def rest__pw_reset():
+
+ def get_or_init_pw_rst_req_map():
+ """
+ lazy pw reset request map getter. We map requests twice:
+ - once by email, to throttle reset requests per account
+ - second by security token, to allow lookup by supplied token on POST
+ """
+ if not hasattr(current_app, 'pw_rst_req_map'):
+ setattr(current_app, 'pw_rst_req_map', {})
+ return current_app.pw_rst_req_map
+
+ def sanitize_input(req):
+ """
+ here we expect a map of input-element id's to input-element values
+ """
+ req_json = request.get_json()
+ email_address = req_json.get('email_address') # None when collecting the new password
+ new_user_pw = req_json.get('new_user_password') # None when submitting initial reset request
+ pw_rst_tok = req_json.get('pw_rst_tok') # None unless accepting new pw
+
+ if None != email_address:
+ regex__email_address = r'[^@]+@[^@]+\.[^@]+'
+ if None == re.match(regex__email_address, email_address):
+ raise Exception('malformed signup request: regex match failure: regex: %s, input: %s' % (regex__email_address, email_address))
+
+ if None != pw_rst_tok:
+ tmp_tok = generate_security_token()
+ if len(tmp_tok) != len(pw_rst_tok):
+ raise Exception('malformed signup request: malformed security token: %s' % (pw_rst_tok))
+
+ return {'email_address': email_address,
+ 'new_user_pw': new_user_pw,
+ 'pw_rst_tok': pw_rst_tok }
+
+ html_ok__submitted = '<p>Your request has been received.<br>Please check your email to reset your password.</p>'
+ html_ok__pw_updated = '<p>Your password has been successfully updated.<br>Please head over to the <a href="/login">login page</a> to access your account.'
+ html_ok__already_pending = '<p>Your request has already been submitted.<br>please check your email to proceed.</p>'
+ html_err__tech_difficulty = '<p>We are experiencing technical difficulty processing your request,<br>please try again later.</p>'
+
+ pw_rst_req_map = get_or_init_pw_rst_req_map()
+ filter_expired__pw_rst_requests(pw_rst_req_map) # first do some housekeeping
+
+ if request.method == 'GET':
+
+ pw_rst_tok = request.args.get('pw_rst_tok')
+ if None == pw_rst_tok: # request submission
+ return render_template('pw_reset.html', pw_rst_step='step_0__reset_request_submission')
+
+ else: # token passed, perform request validation
+ pw_rst_req = pw_rst_req_map.get(pw_rst_tok)
+
+ if None == pw_rst_req:
+ # request expired & already removed OR bad token
+ log.warning('pw reset: request not found or bad token: remote-address: %s' % (request.remote_addr))
+ return render_template('pw_reset.html', pw_rst_step='general_error')
+
+ if pw_rst_req.has_expired(): # check again whether request has expired
+ log.warning('pw reset: attempt to activate expired reset request: email: %s' % (pw_rst_req.email_address))
+ return render_template('pw_reset.html', pw_rst_step='general_error')
+
+ return render_template('pw_reset.html', pw_rst_step='step_1__collect_new_password')
+
+ if request.method == 'POST':
+
+ req_data = sanitize_input(request)
+ user_db = current_app.user_db
+
+ pw_rst_tok = req_data.get('pw_rst_tok')
+ new_user_pw = req_data.get('new_user_pw')
+ email_address = req_data.get('email_address')
+
+ # handle according to passed req arguments
+ if pw_rst_tok and new_user_pw and None == email_address: # validate token & collect new pw
+
+ uid = None
+
+ pw_rst_req = pw_rst_req_map.get(pw_rst_tok)
+ if None == pw_rst_req:
+ # request expired & already removed OR bad token
+ log.warning('pw reset: request not found or bad token: remote-address: %s' % (request.remote_addr))
+ return make_response__json__html(status=500, html_str=html_err__tech_difficulty)
+
+ # perform pw update
+ uid, u_account = user_db.lookup_user__by_email_address(pw_rst_req.u_account.email_address)
+ user_db.update_user_password(uid, new_user_pw)
+ del pw_rst_req_map[pw_rst_tok] # rm request mapping: sec token
+ del pw_rst_req_map[u_account.email_address] # rm request mapping: email address
+
+ log.info('pw reset: pw reset complete: u_account: %s' % (u_account))
+ return make_response__json__html(html_str=html_ok__pw_updated)
+
+ elif email_address and None == pw_rst_tok and None == new_user_pw: # request submission
+
+ u_account = None
+
+ try:
+ uid, u_account = user_db.lookup_user__by_email_address(email_address)
+ except Exception as _:
+ log.exception('pw reset request for non-existing account: email_address: %s' % (email_address))
+ return make_response__json__html(status=500, html_str=html_err__tech_difficulty)
+
+ if None != pw_rst_req_map.get(email_address): # probe for pending requests
+ return make_response__json__html(html_str=html_ok__already_pending)
+
+ pw_rst_tok = generate_security_token()
+ pw_rst_req = User_Pw_Reset_Request(u_account, pw_rst_tok)
+
+ try:
+ pw_reset_link = send_user_pw_reset__email(u_account, pw_rst_tok)
+ pw_rst_req.submission_date = datetime.now() # mark submission date
+ pw_rst_req_map[pw_rst_tok] = pw_rst_req
+ pw_rst_req_map[email_address] = pw_rst_req
+
+ log.info('pw reset: req received, reset link sent via email: link: %s' % (pw_reset_link))
+ return make_response__json__html(html_str=html_ok__submitted)
+
+ except Exception as _:
+ return make_response__json__html(status=500, html_str=html_err__tech_difficulty)
+
+ else: # weird state: missing / unnecessary post fields
+ return make_response__json__html(status=500, html_str=html_err__tech_difficulty)
+
def rest__user_signup():
"""
REST API endpoint: user sign up
@@ -323,3 +446,33 @@ def send_user_activation_link__email(us_req):
subject="Rhizi sign up request",
body=msg_body)
return activation_link
+
+def send_user_pw_reset__email(u_account, pw_reset_token):
+ """
+ Send user pw reset email
+
+ @return: pw reset link
+ @raise exception: on send error
+ """
+
+ # FIXME: use HTTPS
+ pw_reset_link = 'http://%s/pw-reset?pw_rst_tok=%s' % (current_app.rz_config.SERVER_NAME,
+ pw_reset_token)
+
+ msg_body = ['Hello %s %s,' % (u_account.first_name,
+ u_account.last_name),
+ '',
+ 'A password reset request has been received for this email address.',
+ 'Please follow the link below to reset the password for your account:',
+ '',
+ pw_reset_link,
+ '',
+ 'Happy knowledge editing!',
+ 'The Rhizi team.'
+ ]
+ msg_body = '\n'.join(msg_body)
+
+ send_email_message(recipients=[u_account.email_address],
+ subject="Rhizi sign up request",
+ body=msg_body)
+ return pw_reset_link