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>

Reply via email to