diff options
Diffstat (limited to 'src/server')
| -rw-r--r-- | src/server/rz_server.py | 1 | ||||
| -rw-r--r-- | src/server/rz_user.py | 153 |
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 |
