mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-01-09 07:33:19 -06:00
Refuse password changes (and tell the user) if the notification email cannot be sent. Fixes #2892
This commit is contained in:
parent
2995d6e9c5
commit
35a5cf22d5
@ -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)
|
||||
|
||||
|
@ -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(gettext(u'SMTP Socket error: {}\nYour password has not been changed.').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(gettext(u'SMTP error: {}\nYour password has not been changed.').format(e), 'danger')
|
||||
has_error = True
|
||||
except Exception as e:
|
||||
# Handle other exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(u'Error: {}\nYour password has not been changed.').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(gettext(u'SMTP Socket error: {}\nYour password has not been changed.').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(gettext(u'SMTP error: {}\nYour password has not been changed.').format(e), 'danger')
|
||||
has_error = True
|
||||
except Exception as e:
|
||||
# Handle other exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(u'Error: {}\nYour password has not been changed.').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(gettext(u'SMTP Socket error: {}\nYour password has not been changed.').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(gettext(u'SMTP error: {}\nYour password has not been changed.').format(e), 'danger')
|
||||
has_error = True
|
||||
except Exception as e:
|
||||
# Handle other exceptions.
|
||||
logging.exception(str(e), exc_info=True)
|
||||
flash(gettext(u'Error: {}\nYour password has not been changed.').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'))
|
||||
|
@ -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>
|
||||
|
@ -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") }}
|
||||
|
4
web/pgadmin/templates/security/email/change_notice.html
Normal file
4
web/pgadmin/templates/security/email/change_notice.html
Normal file
@ -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 %}
|
5
web/pgadmin/templates/security/email/change_notice.txt
Normal file
5
web/pgadmin/templates/security/email/change_notice.txt
Normal file
@ -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 %}
|
@ -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") }}
|
||||
|
@ -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 %}
|
||||
|
@ -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>
|
||||
|
Loading…
Reference in New Issue
Block a user