Hi, Unlike flask login-manager flask-security does not provide facility to pass custom view function to any of callbacks like change/reset/forgot password. So we cannot handle any exceptions occurred during changing/resetting password. Only way we can handle such exceptions is writing our own routes for these callbacks and add addition code to handle such exceptions.
-- *Harshal Dhumal* *Sr. Software Engineer* EnterpriseDB India: http://www.enterprisedb.com The Enterprise PostgreSQL Company
diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index a1d10b8..0196852 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -174,6 +174,23 @@ def create_app(app_name=None): if not app_name: app_name = config.APP_NAME + # Only enable password related functionality in server mode. + if config.SERVER_MODE is True: + # Some times we need to access these config params where application + # context is not available (we can't use current_app.config in those + # cases even with current_app.app_context()) + # So update these params in config itself. + # And also these updated config values will picked up by application + # since we are updating config before the application instance is + # created. + + config.SECURITY_RECOVERABLE = True + config.SECURITY_CHANGEABLE = True + # Now we'll open change password page in alertify dialog + # we don't want it to redirect to main page after password + # change operation so we will open the same password change page again. + config.SECURITY_POST_CHANGE_VIEW = 'browser.change_password' + """Create the Flask application, startup logging and dynamically load additional modules (blueprints) that are found in this directory.""" app = PgAdmin(__name__, static_url_path='/static') @@ -276,18 +293,6 @@ def create_app(app_name=None): getattr(config, 'SQLITE_TIMEOUT', 500) ) - # Only enable password related functionality in server mode. - if config.SERVER_MODE is True: - # TODO: Figure out how to disable /logout and /login - app.config['SECURITY_RECOVERABLE'] = True - app.config['SECURITY_CHANGEABLE'] = True - # Now we'll open change password page in alertify dialog - # we don't want it to redirect to main page after password - # change operation so we will open the same password change page again. - app.config.update( - dict(SECURITY_POST_CHANGE_VIEW='security.change_password') - ) - # Create database connection object and mailer db.init_app(app) diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py index a70a751..b54441b 100644 --- a/web/pgadmin/browser/__init__.py +++ b/web/pgadmin/browser/__init__.py @@ -8,19 +8,32 @@ ########################################################################## import json +import logging from abc import ABCMeta, abstractmethod, abstractproperty - import six +from socket import error as SOCKETErrorException +from smtplib import SMTPConnectError, SMTPResponseException,\ + SMTPServerDisconnected, SMTPDataError,SMTPHeloError, SMTPException, \ + SMTPAuthenticationError, SMTPSenderRefused, SMTPRecipientsRefused from flask import current_app, render_template, url_for, make_response, flash,\ - Response + Response, request, after_this_request, redirect from flask_babel import gettext -from flask_login import current_user -from flask_security import login_required +from flask_login import current_user, login_required +from flask_security.decorators import anonymous_user_required from flask_gravatar import Gravatar from pgadmin.settings import get_setting from pgadmin.utils import PgAdminModule from pgadmin.utils.ajax import make_json_response from pgadmin.utils.preferences import Preferences +from werkzeug.datastructures import MultiDict +from flask_security.views import _security, _commit, _render_json, _ctx +from flask_security.changeable import change_user_password +from flask_security.recoverable import reset_password_token_status, \ + generate_reset_password_token, update_password +from flask_security.utils import config_value, do_flash, get_url, get_message,\ + slash_url_suffix, login_user, send_mail +from flask_security.signals import reset_password_instructions_sent + import config from pgadmin import current_blueprint @@ -528,6 +541,7 @@ def index(): return response + @blueprint.route("/js/utils.js") @login_required def utils(): @@ -677,3 +691,188 @@ def get_nodes(): nodes.extend(submodule.get_nodes()) return make_json_response(data=nodes) + +# Only register route if SECURITY_CHANGEABLE is set to True +# We can't access app context here so cannot +# use app.config['SECURITY_CHANGEABLE'] +if hasattr(config, 'SECURITY_CHANGEABLE') and config.SECURITY_CHANGEABLE: + @blueprint.route("/change_password", endpoint="change_password", + methods=['GET', 'POST']) + @login_required + def change_password(): + """View function which handles a change password request.""" + + has_error = False + form_class = _security.change_password_form + + if request.json: + form = form_class(MultiDict(request.json)) + else: + form = form_class() + + if form.validate_on_submit(): + try: + change_user_password(current_user, form.new_password.data) + except SOCKETErrorException as e: + # Handle socket errors which are not covered by SMTPExceptions. + logging.exception(str(e), exc_info=True) + flash(u'SMTP Socket error: {}'.format(e), 'danger') + has_error = True + except (SMTPConnectError, SMTPResponseException, + SMTPServerDisconnected, SMTPDataError, SMTPHeloError, + SMTPException, SMTPAuthenticationError, SMTPSenderRefused, + SMTPRecipientsRefused) as e: + # Handle smtp specific exceptions. + logging.exception(str(e), exc_info=True) + flash(u'SMTP error: {}'.format(e), 'danger') + has_error = True + except Exception as e: + # Handle other exceptions. + logging.exception(str(e), exc_info=True) + flash(u'Error: {}'.format(e), 'danger') + has_error = True + + if request.json is None and not has_error: + after_this_request(_commit) + do_flash(*get_message('PASSWORD_CHANGE')) + return redirect(get_url(_security.post_change_view) or + get_url(_security.post_login_view)) + + if request.json and not has_error: + form.user = current_user + return _render_json(form) + + return _security.render_template( + config_value('CHANGE_PASSWORD_TEMPLATE'), + change_password_form=form, + **_ctx('change_password')) + + +# Only register route if SECURITY_RECOVERABLE is set to True +if hasattr(config, 'SECURITY_RECOVERABLE') and config.SECURITY_RECOVERABLE: + + def send_reset_password_instructions(user): + """Sends the reset password instructions email for the specified user. + + :param user: The user to send the instructions to + """ + token = generate_reset_password_token(user) + reset_link = url_for('browser.reset_password', token=token, + _external=True) + + send_mail(config_value('EMAIL_SUBJECT_PASSWORD_RESET'), user.email, + 'reset_instructions', + user=user, reset_link=reset_link) + + reset_password_instructions_sent.send( + current_app._get_current_object(), + user=user, token=token) + + + @blueprint.route("/forgot_password", endpoint="forgot_password", + methods=['GET', 'POST']) + @anonymous_user_required + def forgot_password(): + """View function that handles a forgotten password request.""" + has_error = False + form_class = _security.forgot_password_form + + if request.json: + form = form_class(MultiDict(request.json)) + else: + form = form_class() + + if form.validate_on_submit(): + try: + send_reset_password_instructions(form.user) + except SOCKETErrorException as e: + # Handle socket errors which are not covered by SMTPExceptions. + logging.exception(str(e), exc_info=True) + flash(u'SMTP Socket error: {}'.format(e), 'danger') + has_error = True + except (SMTPConnectError, SMTPResponseException, + SMTPServerDisconnected, SMTPDataError, SMTPHeloError, + SMTPException, SMTPAuthenticationError, SMTPSenderRefused, + SMTPRecipientsRefused) as e: + + # Handle smtp specific exceptions. + logging.exception(str(e), exc_info=True) + flash(u'SMTP error: {}'.format(e), 'danger') + has_error = True + except Exception as e: + # Handle other exceptions. + logging.exception(str(e), exc_info=True) + flash(u'Error: {}'.format(e), 'danger') + has_error = True + + if request.json is None and not has_error: + do_flash(*get_message('PASSWORD_RESET_REQUEST', + email=form.user.email)) + + if request.json and not has_error: + return _render_json(form, include_user=False) + + return _security.render_template( + config_value('FORGOT_PASSWORD_TEMPLATE'), + forgot_password_form=form, + **_ctx('forgot_password')) + + + # We are not in app context so cannot use url_for('browser.forgot_password') + # So hard code the url '/browser/forgot_password' while passing as + # parameter to slash_url_suffix function. + @blueprint.route('/forgot_password' + slash_url_suffix( + '/browser/forgot_password', '<token>'), + methods=['GET', 'POST'], + endpoint='reset_password') + @anonymous_user_required + def reset_password(token): + """View function that handles a reset password request.""" + + expired, invalid, user = reset_password_token_status(token) + + if invalid: + do_flash(*get_message('INVALID_RESET_PASSWORD_TOKEN')) + if expired: + do_flash(*get_message('PASSWORD_RESET_EXPIRED', email=user.email, + within=_security.reset_password_within)) + if invalid or expired: + return redirect(url_for('browser.forgot_password')) + has_error = False + form = _security.reset_password_form() + + if form.validate_on_submit(): + try: + update_password(user, form.password.data) + except SOCKETErrorException as e: + # Handle socket errors which are not covered by SMTPExceptions. + logging.exception(str(e), exc_info=True) + flash(u'SMTP Socket error: {}'.format(e), 'danger') + has_error = True + except (SMTPConnectError, SMTPResponseException, + SMTPServerDisconnected, SMTPDataError, SMTPHeloError, + SMTPException, SMTPAuthenticationError, SMTPSenderRefused, + SMTPRecipientsRefused) as e: + + # Handle smtp specific exceptions. + logging.exception(str(e), exc_info=True) + flash(u'SMTP error: {}'.format(e), 'danger') + has_error = True + except Exception as e: + # Handle other exceptions. + logging.exception(str(e), exc_info=True) + flash(u'Error: {}'.format(e), 'danger') + has_error = True + + if not has_error: + after_this_request(_commit) + do_flash(*get_message('PASSWORD_RESET')) + login_user(user) + return redirect(get_url(_security.post_reset_view) or + get_url(_security.post_login_view)) + + return _security.render_template( + config_value('RESET_PASSWORD_TEMPLATE'), + reset_password_form=form, + reset_password_token=token, + **_ctx('reset_password')) diff --git a/web/pgadmin/browser/templates/browser/index.html b/web/pgadmin/browser/templates/browser/index.html index 97dd495..88ea27f 100644 --- a/web/pgadmin/browser/templates/browser/index.html +++ b/web/pgadmin/browser/templates/browser/index.html @@ -172,7 +172,7 @@ window.onload = function(e){ <ul class="dropdown-menu navbar-inverse"> <li> <a href="#" onclick="pgAdmin.Browser.UserManagement.change_password( - '{{ url_for('security.change_password') }}' + '{{ url_for('browser.change_password') }}' )"> {{ _('Change Password') }} </a> diff --git a/web/pgadmin/templates/security/change_password.html b/web/pgadmin/templates/security/change_password.html index 980a372..33bb834 100644 --- a/web/pgadmin/templates/security/change_password.html +++ b/web/pgadmin/templates/security/change_password.html @@ -1,7 +1,7 @@ {% extends "security/panel.html" %} {% block panel_title %}{{ _('%(appname)s Password Change', appname=config.APP_NAME) }}{% endblock %} {% block panel_body %} -<form action="{{ url_for_security('change_password') }}" method="POST" name="change_password_form"> +<form action="{{ url_for('browser.change_password') }}" method="POST" name="change_password_form"> {{ change_password_form.hidden_tag() }} <fieldset> {{ render_field_with_errors(change_password_form.password, "password") }} diff --git a/web/pgadmin/templates/security/email/change_notice.html b/web/pgadmin/templates/security/email/change_notice.html new file mode 100644 index 0000000..00ccebe --- /dev/null +++ b/web/pgadmin/templates/security/email/change_notice.html @@ -0,0 +1,4 @@ +<p>Your password has been changed.</p> +{% if security.recoverable %} +<p>If you did not change your password, <a href="{{ url_for('browser.forgot_password', _external=True) }}">click here to reset it</a>.</p> +{% endif %} diff --git a/web/pgadmin/templates/security/email/change_notice.txt b/web/pgadmin/templates/security/email/change_notice.txt new file mode 100644 index 0000000..69fae68 --- /dev/null +++ b/web/pgadmin/templates/security/email/change_notice.txt @@ -0,0 +1,5 @@ +Your password has been changed +{% if security.recoverable %} +If you did not change your password, click the link below to reset it. +{{ url_for('browser.forgot_password', _external=True) }} +{% endif %} diff --git a/web/pgadmin/templates/security/forgot_password.html b/web/pgadmin/templates/security/forgot_password.html index d05c5c5..3b90b3c 100644 --- a/web/pgadmin/templates/security/forgot_password.html +++ b/web/pgadmin/templates/security/forgot_password.html @@ -2,7 +2,7 @@ {% block panel_title %}{{ _('Recover %(appname)s Password', appname=config.APP_NAME) }}{% endblock %} {% block panel_body %} <p>{{ _('Enter the email address for the user account you wish to recover the password for:') }}</p> -<form action="{{ url_for_security('forgot_password') }}" method="POST" name="forgot_password_form"> +<form action="{{ url_for('browser.forgot_password') }}" method="POST" name="forgot_password_form"> {{ forgot_password_form.hidden_tag() }} <fieldset> {{ render_field_with_errors(forgot_password_form.email, "text") }} diff --git a/web/pgadmin/templates/security/login_user.html b/web/pgadmin/templates/security/login_user.html index 1eeee61..8c14a5b 100644 --- a/web/pgadmin/templates/security/login_user.html +++ b/web/pgadmin/templates/security/login_user.html @@ -20,5 +20,5 @@ </div> </fieldset> </form> -<span class="help-block">{{ _('Forgotten your <a href="%(url)s">password</a>?', url=url_for('security.forgot_password')) }}</span> +<span class="help-block">{{ _('Forgotten your <a href="%(url)s">password</a>?', url=url_for('browser.forgot_password')) }}</span> {% endblock %} diff --git a/web/pgadmin/templates/security/reset_password.html b/web/pgadmin/templates/security/reset_password.html index 476d3a3..67dfff6 100644 --- a/web/pgadmin/templates/security/reset_password.html +++ b/web/pgadmin/templates/security/reset_password.html @@ -1,7 +1,7 @@ {% extends "security/panel.html" %} {% block panel_title %}{{ _('%(appname)s Password Reset', appname=config.APP_NAME) }}{% endblock %} {% block panel_body %} -<form action="{{ url_for_security('reset_password', token=reset_password_token) }}" method="POST" +<form action="{{ url_for('browser.reset_password', token=reset_password_token) }}" method="POST" name="reset_password_form"> {{ reset_password_form.hidden_tag() }} <fieldset>