mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
1. Added Master Password to increase the security of saved passwords. Fixes #4184
2. In server(web) mode, update all the saved server credentials when user password is changed. Fixes #3377
This commit is contained in:
parent
6f0eafb223
commit
dfa892d2a2
@ -24,6 +24,13 @@ dialog, right-click on the *Servers* node of the tree control, and select
|
||||
|
||||
server_dialog
|
||||
|
||||
A master password is required to secure and later unlock saved server passwords.
|
||||
It is set by the user and can be disabled using config.
|
||||
|
||||
.. toctree::
|
||||
|
||||
master_password
|
||||
|
||||
After defining a server connection, right-click on the server name, and select
|
||||
*Connect to server* to authenticate with the server, and start using pgAdmin to
|
||||
manage objects that reside on the server.
|
||||
|
BIN
docs/en_US/images/master_password_enter.png
Normal file
BIN
docs/en_US/images/master_password_enter.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 46 KiB |
BIN
docs/en_US/images/master_password_reset.png
Normal file
BIN
docs/en_US/images/master_password_reset.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 38 KiB |
BIN
docs/en_US/images/master_password_set.png
Normal file
BIN
docs/en_US/images/master_password_set.png
Normal file
Binary file not shown.
After Width: | Height: | Size: 40 KiB |
@ -16,6 +16,7 @@ Use the fields in the *Login* dialog to authenticate your connection:
|
||||
field.
|
||||
* Provide your password in the *Password* field.
|
||||
* Click the *Login* button to securely log into pgAdmin.
|
||||
* Please note that, if the pgAdmin server is restarted then you will be logged out. You need to re-login to continue.
|
||||
|
||||
Recovering a Lost Password
|
||||
**************************
|
||||
|
38
docs/en_US/master_password.rst
Normal file
38
docs/en_US/master_password.rst
Normal file
@ -0,0 +1,38 @@
|
||||
.. _master_password:
|
||||
|
||||
************************
|
||||
`Master Password`:index:
|
||||
************************
|
||||
|
||||
A master password is required to secure and later unlock the saved server passwords. This is applicable only for desktop mode users.
|
||||
|
||||
* You are prompted to enter the master password when you open the window for the first time after starting the application.
|
||||
* Once you set the master password, all the existing saved passwords will be re-encrypted using the master password.
|
||||
* The server passwords which are saved in the SQLite DB file are encrypted and decrypted using the master password.
|
||||
|
||||
.. image:: images/master_password_set.png
|
||||
:alt: Set master password
|
||||
:align: center
|
||||
|
||||
* You can disable the master password by setting the configuration parameter *MASTER_PASSWORD_REQUIRED=False*
|
||||
* Note that, if master password is disabled, then all the saved passwords will be removed.
|
||||
|
||||
.. warning:: If master password is disabled, then the saved passwords will be encrypted using a key
|
||||
which may not be as secure as master password. It is strongly recommended to use master password if you use "Save password" option.
|
||||
|
||||
* The master password is not stored anywhere on the physical storage. It is temporarily stored in the application memory and it does not get saved in case the application gets restarted.
|
||||
* You are prompted to enter the master password when pgAdmin server is restarted.
|
||||
|
||||
.. image:: images/master_password_enter.png
|
||||
:alt: Enter master password
|
||||
:align: center
|
||||
|
||||
|
||||
* If you forget the master password, you can use the "Reset Master Password" button to reset the password.
|
||||
|
||||
.. image:: images/master_password_reset.png
|
||||
:alt: Reset master password
|
||||
:align: center
|
||||
|
||||
.. warning:: Resetting the master password will also remove all the saved passwords and close all the existing established
|
||||
connections.
|
@ -10,9 +10,11 @@ This release contains a number of bug fixes since the release of pgAdmin4 4.6.
|
||||
Bug fixes
|
||||
*********
|
||||
|
||||
| `Bug #3377 <https://redmine.postgresql.org/issues/3377>`_ - In server(web) mode, update all the saved server credentials when user password is changed.
|
||||
| `Bug #3885 <https://redmine.postgresql.org/issues/3885>`_ - Fix the responsive layout of the main menu bar.
|
||||
| `Bug #4162 <https://redmine.postgresql.org/issues/4162>`_ - Fix syntax error when adding more than one column to the existing table.
|
||||
| `Bug #4164 <https://redmine.postgresql.org/issues/4164>`_ - Fix file browser path issue which occurs when client is on Windows and server is on Mac/Linux.
|
||||
| `Bug #4184 <https://redmine.postgresql.org/issues/4184>`_ - Added Master Password to increase the security of saved passwords.
|
||||
| `Bug #4194 <https://redmine.postgresql.org/issues/4194>`_ - Fix accessibility issue for menu navigation.
|
||||
| `Bug #4208 <https://redmine.postgresql.org/issues/4208>`_ - Update the UI logo.
|
||||
| `Bug #4217 <https://redmine.postgresql.org/issues/4217>`_ - Fixed CSRF security vulnerability issue.
|
||||
|
@ -419,6 +419,12 @@ SUPPORT_SSH_TUNNEL = True
|
||||
# Set to False to disable password saving.
|
||||
ALLOW_SAVE_TUNNEL_PASSWORD = False
|
||||
|
||||
##########################################################################
|
||||
# Master password is used to encrypt/decrypt saved server passwords
|
||||
# Applicable for desktop mode only
|
||||
##########################################################################
|
||||
MASTER_PASSWORD_REQUIRED = True
|
||||
|
||||
##########################################################################
|
||||
# Local config settings
|
||||
##########################################################################
|
||||
|
51
web/migrations/versions/35f29b1701bd_.py
Normal file
51
web/migrations/versions/35f29b1701bd_.py
Normal file
@ -0,0 +1,51 @@
|
||||
|
||||
"""empty message
|
||||
|
||||
Revision ID: 35f29b1701bd
|
||||
Revises: ec1cac3399c9
|
||||
Create Date: 2019-04-26 16:38:08.368471
|
||||
|
||||
"""
|
||||
import base64
|
||||
import os
|
||||
import sys
|
||||
|
||||
from pgadmin.model import db, Server
|
||||
|
||||
|
||||
# revision identifiers, used by Alembic.
|
||||
revision = '35f29b1701bd'
|
||||
down_revision = 'ec1cac3399c9'
|
||||
branch_labels = None
|
||||
depends_on = None
|
||||
|
||||
|
||||
def upgrade():
|
||||
db.engine.execute("ALTER TABLE user RENAME TO user_old")
|
||||
|
||||
db.engine.execute("""
|
||||
CREATE TABLE user (
|
||||
id INTEGER NOT NULL,
|
||||
email VARCHAR(256) NOT NULL,
|
||||
password VARCHAR(256),
|
||||
active BOOLEAN NOT NULL,
|
||||
confirmed_at DATETIME,
|
||||
masterpass_check VARCHAR(256),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE (email),
|
||||
CHECK (active IN (0, 1))
|
||||
);
|
||||
""")
|
||||
|
||||
db.engine.execute("""
|
||||
INSERT INTO user (
|
||||
id, email, password, active, confirmed_at
|
||||
) SELECT
|
||||
id, email, password, active, confirmed_at
|
||||
FROM user_old""")
|
||||
|
||||
db.engine.execute("DROP TABLE user_old")
|
||||
|
||||
|
||||
def downgrade():
|
||||
pass
|
@ -31,7 +31,10 @@ def upgrade():
|
||||
Security(app, user_datastore, register_blueprint=False)
|
||||
else:
|
||||
app.config['SECURITY_PASSWORD_SALT'] = current_salt
|
||||
users = User.query.all()
|
||||
|
||||
users = User.query.with_entities(
|
||||
User.id, User.email, User.password, User.active, User.confirmed_at)\
|
||||
.all()
|
||||
# This will upgrade the plaintext password of all the user as per the
|
||||
# SECURITY_PASSWORD_HASH.
|
||||
for user in users:
|
||||
|
@ -23,15 +23,14 @@ from flask_login import user_logged_in, user_logged_out
|
||||
from flask_mail import Mail
|
||||
from flask_paranoid import Paranoid
|
||||
from flask_security import Security, SQLAlchemyUserDatastore, current_user
|
||||
from flask_security.utils import login_user
|
||||
|
||||
from flask_security.utils import login_user, logout_user
|
||||
from werkzeug.datastructures import ImmutableDict
|
||||
from werkzeug.local import LocalProxy
|
||||
from werkzeug.utils import find_modules
|
||||
|
||||
from pgadmin.model import db, Role, Server, ServerGroup, \
|
||||
User, Keys, Version, SCHEMA_VERSION as CURRENT_SCHEMA_VERSION
|
||||
from pgadmin.utils import PgAdminModule, driver
|
||||
from pgadmin.utils import PgAdminModule, driver, KeyManager
|
||||
from pgadmin.utils.preferences import Preferences
|
||||
from pgadmin.utils.session import create_session_interface, pga_unauthorised
|
||||
from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader
|
||||
@ -575,12 +574,23 @@ def create_app(app_name=None):
|
||||
def force_session_write(app, user):
|
||||
session.force_write = True
|
||||
|
||||
@user_logged_in.connect_via(app)
|
||||
def store_crypt_key(app, user):
|
||||
# in desktop mode, master password is used to encrypt/decrypt
|
||||
# and is stored in the keyManager memory
|
||||
if config.SERVER_MODE:
|
||||
if 'password' in request.form:
|
||||
current_app.keyManager.set(request.form['password'])
|
||||
|
||||
@user_logged_out.connect_via(app)
|
||||
def current_user_cleanup(app, user):
|
||||
from config import PG_DEFAULT_DRIVER
|
||||
from pgadmin.utils.driver import get_driver
|
||||
from flask import current_app
|
||||
|
||||
# remove key
|
||||
current_app.keyManager.reset()
|
||||
|
||||
for mdl in current_app.logout_hooks:
|
||||
try:
|
||||
mdl.on_logout(user)
|
||||
@ -631,6 +641,12 @@ def create_app(app_name=None):
|
||||
abort(401)
|
||||
login_user(user)
|
||||
|
||||
# if the server is restarted the in memory key will be lost
|
||||
# but the user session may still be active. Logout the user
|
||||
# to get the key again when login
|
||||
if config.SERVER_MODE and current_app.keyManager.get() is None:
|
||||
logout_user()
|
||||
|
||||
@app.after_request
|
||||
def after_request(response):
|
||||
if 'key' in request.args:
|
||||
@ -711,6 +727,9 @@ def create_app(app_name=None):
|
||||
current_app.logger.error(e, exc_info=True)
|
||||
return e
|
||||
|
||||
# Intialize the key manager
|
||||
app.keyManager = KeyManager()
|
||||
|
||||
##########################################################################
|
||||
# Protection against CSRF attacks
|
||||
##########################################################################
|
||||
|
@ -18,7 +18,7 @@ from socket import error as SOCKETErrorException
|
||||
|
||||
import six
|
||||
from flask import current_app, render_template, url_for, make_response, \
|
||||
flash, Response, request, after_this_request, redirect
|
||||
flash, Response, request, after_this_request, redirect, session
|
||||
from flask_babelex import gettext
|
||||
from flask_gravatar import Gravatar
|
||||
from flask_login import current_user, login_required
|
||||
@ -41,6 +41,9 @@ from pgadmin.utils.csrf import pgCSRFProtect
|
||||
from pgadmin.utils.preferences import Preferences
|
||||
from pgadmin.browser.register_browser_preferences import \
|
||||
register_browser_preferences
|
||||
from pgadmin.utils.master_password import validate_master_password, \
|
||||
set_masterpass_check_text, cleanup_master_password, get_crypt_key, \
|
||||
set_crypt_key, process_masterpass_disabled
|
||||
|
||||
try:
|
||||
import urllib.request as urlreq
|
||||
@ -220,7 +223,10 @@ class BrowserModule(PgAdminModule):
|
||||
Returns:
|
||||
list: a list of url endpoints exposed to the client.
|
||||
"""
|
||||
return ['browser.index', 'browser.nodes']
|
||||
return ['browser.index', 'browser.nodes',
|
||||
'browser.check_master_password',
|
||||
'browser.set_master_password',
|
||||
'browser.reset_master_password']
|
||||
|
||||
|
||||
blueprint = BrowserModule(MODULE_NAME, __name__)
|
||||
@ -682,6 +688,133 @@ def get_nodes():
|
||||
return make_json_response(data=nodes)
|
||||
|
||||
|
||||
def form_master_password_response(existing=True, present=False, errmsg=None):
|
||||
content_new = (
|
||||
gettext("Set Master Password"),
|
||||
"<br/>".join([
|
||||
gettext("Please set a master password for pgAdmin."),
|
||||
gettext("This will be used to secure and later unlock saved "
|
||||
"passwords and other credentials.")])
|
||||
)
|
||||
content_existing = (
|
||||
gettext("Unlock Saved Passwords"),
|
||||
"<br/>".join([
|
||||
gettext("Please enter your master password."),
|
||||
gettext("This is required to unlock saved passwords and "
|
||||
"reconnect to the database server(s).")])
|
||||
)
|
||||
|
||||
return make_json_response(data={
|
||||
'present': present,
|
||||
'title': content_existing[0] if existing else content_new[0],
|
||||
'content': render_template(
|
||||
'browser/master_password.html',
|
||||
content_text=content_existing[1] if existing else content_new[1],
|
||||
errmsg=errmsg
|
||||
),
|
||||
'reset': existing
|
||||
})
|
||||
|
||||
|
||||
@blueprint.route("/master_password", endpoint="check_master_password",
|
||||
methods=["GET"])
|
||||
def check_master_password():
|
||||
"""
|
||||
Checks if the master password is available in the memory
|
||||
This password will be used to encrypt/decrypt saved server passwords
|
||||
"""
|
||||
return make_json_response(data=get_crypt_key()[0])
|
||||
|
||||
|
||||
@blueprint.route("/master_password", endpoint="reset_master_password",
|
||||
methods=["DELETE"])
|
||||
def reset_master_password():
|
||||
"""
|
||||
Removes the master password and remove all saved passwords
|
||||
This password will be used to encrypt/decrypt saved server passwords
|
||||
"""
|
||||
cleanup_master_password()
|
||||
return make_json_response(data=get_crypt_key()[0])
|
||||
|
||||
|
||||
@blueprint.route("/master_password", endpoint="set_master_password",
|
||||
methods=["POST"])
|
||||
def set_master_password():
|
||||
"""
|
||||
Set the master password and store in the memory
|
||||
This password will be used to encrypt/decrypt saved server passwords
|
||||
"""
|
||||
|
||||
data = None
|
||||
|
||||
if hasattr(request.data, 'decode'):
|
||||
data = request.data.decode('utf-8')
|
||||
|
||||
if data != '':
|
||||
data = json.loads(data)
|
||||
|
||||
# Master password is not applicable for server mode
|
||||
if not config.SERVER_MODE and config.MASTER_PASSWORD_REQUIRED:
|
||||
|
||||
if data != '' and data.get('password', '') != '':
|
||||
# if master pass is set previously
|
||||
if current_user.masterpass_check is not None:
|
||||
if not validate_master_password(data.get('password')):
|
||||
return form_master_password_response(
|
||||
existing=True,
|
||||
present=False,
|
||||
errmsg=gettext("Incorrect master password")
|
||||
)
|
||||
|
||||
# store the master pass in the memory
|
||||
set_crypt_key(data.get('password'))
|
||||
|
||||
if current_user.masterpass_check is None:
|
||||
# master check is not set, which means the server password
|
||||
# data is old and is encrypted with old key
|
||||
# Re-encrypt with new key
|
||||
|
||||
from pgadmin.browser.server_groups.servers.utils \
|
||||
import reencrpyt_server_passwords
|
||||
reencrpyt_server_passwords(
|
||||
current_user.id, current_user.password,
|
||||
data.get('password'))
|
||||
|
||||
# set the encrypted sample text with the new
|
||||
# master pass
|
||||
set_masterpass_check_text(data.get('password'))
|
||||
|
||||
elif not get_crypt_key()[0] and \
|
||||
current_user.masterpass_check is not None:
|
||||
return form_master_password_response(
|
||||
existing=True,
|
||||
present=False,
|
||||
)
|
||||
elif not get_crypt_key()[0]:
|
||||
return form_master_password_response(
|
||||
existing=False,
|
||||
present=False,
|
||||
)
|
||||
|
||||
# if master password is disabled now, but was used once then
|
||||
# remove all the saved passwords
|
||||
process_masterpass_disabled()
|
||||
|
||||
if config.SERVER_MODE and current_user.masterpass_check is None:
|
||||
|
||||
crypt_key = get_crypt_key()[1]
|
||||
from pgadmin.browser.server_groups.servers.utils \
|
||||
import reencrpyt_server_passwords
|
||||
reencrpyt_server_passwords(
|
||||
current_user.id, current_user.password, crypt_key)
|
||||
|
||||
set_masterpass_check_text(crypt_key)
|
||||
|
||||
return form_master_password_response(
|
||||
present=True,
|
||||
)
|
||||
|
||||
|
||||
# Only register route if SECURITY_CHANGEABLE is set to True
|
||||
# We can't access app context here so cannot
|
||||
# use app.config['SECURITY_CHANGEABLE']
|
||||
@ -740,6 +873,15 @@ if hasattr(config, 'SECURITY_CHANGEABLE') and config.SECURITY_CHANGEABLE:
|
||||
if request.json is None and not has_error:
|
||||
after_this_request(_commit)
|
||||
do_flash(*get_message('PASSWORD_CHANGE'))
|
||||
|
||||
old_key = get_crypt_key()[1]
|
||||
set_crypt_key(form.new_password.data, False)
|
||||
|
||||
from pgadmin.browser.server_groups.servers.utils \
|
||||
import reencrpyt_server_passwords
|
||||
reencrpyt_server_passwords(
|
||||
current_user.id, old_key, form.new_password.data)
|
||||
|
||||
return redirect(get_url(_security.post_change_view) or
|
||||
get_url(_security.post_login_view))
|
||||
|
||||
|
@ -26,6 +26,8 @@ import config
|
||||
from config import PG_DEFAULT_DRIVER
|
||||
from pgadmin.model import db, Server, ServerGroup, User
|
||||
from pgadmin.utils.driver import get_driver
|
||||
from pgadmin.utils.master_password import get_crypt_key
|
||||
from pgadmin.utils.exception import CryptKeyMissing
|
||||
|
||||
|
||||
def has_any(data, keys):
|
||||
@ -117,9 +119,16 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||
driver = get_driver(PG_DEFAULT_DRIVER)
|
||||
|
||||
for server in servers:
|
||||
manager = driver.connection_manager(server.id)
|
||||
conn = manager.connection()
|
||||
connected = conn.connected()
|
||||
connected = False
|
||||
manager = None
|
||||
try:
|
||||
manager = driver.connection_manager(server.id)
|
||||
conn = manager.connection()
|
||||
connected = conn.connected()
|
||||
except CryptKeyMissing:
|
||||
# show the nodes at least even if not able to connect.
|
||||
pass
|
||||
|
||||
in_recovery = None
|
||||
wal_paused = None
|
||||
|
||||
@ -723,6 +732,11 @@ class ServerNode(PGChildNodeView):
|
||||
request.data, encoding='utf-8'
|
||||
)
|
||||
|
||||
# Get enc key
|
||||
crypt_key_present, crypt_key = get_crypt_key()
|
||||
if not crypt_key_present:
|
||||
raise CryptKeyMissing
|
||||
|
||||
# Some fields can be provided with service file so they are optional
|
||||
if 'service' in data and not data['service']:
|
||||
required_args.extend([
|
||||
@ -807,7 +821,7 @@ class ServerNode(PGChildNodeView):
|
||||
# login with password
|
||||
have_password = True
|
||||
password = data['password']
|
||||
password = encrypt(password, current_user.password)
|
||||
password = encrypt(password, crypt_key)
|
||||
elif 'passfile' in data and data["passfile"] != '':
|
||||
passfile = data['passfile']
|
||||
setattr(server, 'passfile', passfile)
|
||||
@ -817,7 +831,7 @@ class ServerNode(PGChildNodeView):
|
||||
have_tunnel_password = True
|
||||
tunnel_password = data['tunnel_password']
|
||||
tunnel_password = \
|
||||
encrypt(tunnel_password, current_user.password)
|
||||
encrypt(tunnel_password, crypt_key)
|
||||
|
||||
status, errmsg = conn.connect(
|
||||
password=password,
|
||||
@ -998,6 +1012,11 @@ class ServerNode(PGChildNodeView):
|
||||
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
|
||||
conn = manager.connection()
|
||||
|
||||
# Get enc key
|
||||
crypt_key_present, crypt_key = get_crypt_key()
|
||||
if not crypt_key_present:
|
||||
raise CryptKeyMissing
|
||||
|
||||
# If server using SSH Tunnel
|
||||
if server.use_ssh_tunnel:
|
||||
if 'tunnel_password' not in data:
|
||||
@ -1014,12 +1033,12 @@ class ServerNode(PGChildNodeView):
|
||||
# Encrypt the password before saving with user's login
|
||||
# password key.
|
||||
try:
|
||||
tunnel_password = encrypt(tunnel_password, user.password) \
|
||||
tunnel_password = encrypt(tunnel_password, crypt_key) \
|
||||
if tunnel_password is not None else \
|
||||
server.tunnel_password
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
return internal_server_error(errormsg=e.message)
|
||||
return internal_server_error(errormsg=str(e))
|
||||
|
||||
if 'password' not in data:
|
||||
conn_passwd = getattr(conn, 'password', None)
|
||||
@ -1038,11 +1057,11 @@ class ServerNode(PGChildNodeView):
|
||||
# Encrypt the password before saving with user's login
|
||||
# password key.
|
||||
try:
|
||||
password = encrypt(password, user.password) \
|
||||
password = encrypt(password, crypt_key) \
|
||||
if password is not None else server.password
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
return internal_server_error(errormsg=e.message)
|
||||
return internal_server_error(errormsg=str(e))
|
||||
|
||||
# Check do we need to prompt for the database server or ssh tunnel
|
||||
# password or both. Return the password template in case password is
|
||||
@ -1235,6 +1254,7 @@ class ServerNode(PGChildNodeView):
|
||||
"""
|
||||
try:
|
||||
data = json.loads(request.form['data'], encoding='utf-8')
|
||||
crypt_key = get_crypt_key()[1]
|
||||
|
||||
# Fetch Server Details
|
||||
server = Server.query.filter_by(id=sid).first()
|
||||
@ -1292,7 +1312,7 @@ class ServerNode(PGChildNodeView):
|
||||
|
||||
# Check against old password only if no pgpass file
|
||||
if not is_passfile:
|
||||
decrypted_password = decrypt(manager.password, user.password)
|
||||
decrypted_password = decrypt(manager.password, crypt_key)
|
||||
|
||||
if isinstance(decrypted_password, bytes):
|
||||
decrypted_password = decrypted_password.decode()
|
||||
@ -1328,7 +1348,7 @@ class ServerNode(PGChildNodeView):
|
||||
|
||||
# Store password in sqlite only if no pgpass file
|
||||
if not is_passfile:
|
||||
password = encrypt(data['newPassword'], user.password)
|
||||
password = encrypt(data['newPassword'], crypt_key)
|
||||
# Check if old password was stored in pgadmin4 sqlite database.
|
||||
# If yes then update that password.
|
||||
if server.password is not None and config.ALLOW_SAVE_PASSWORD:
|
||||
@ -1472,6 +1492,7 @@ class ServerNode(PGChildNodeView):
|
||||
|
||||
def get_response_for_password(self, server, status, prompt_password=False,
|
||||
prompt_tunnel_password=False, errmsg=None):
|
||||
|
||||
if server.use_ssh_tunnel:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
@ -1498,7 +1519,7 @@ class ServerNode(PGChildNodeView):
|
||||
server_label=server.name,
|
||||
username=server.username,
|
||||
errmsg=errmsg,
|
||||
_=gettext
|
||||
_=gettext,
|
||||
)
|
||||
)
|
||||
|
||||
|
@ -166,7 +166,6 @@ class DatabaseView(PGChildNodeView):
|
||||
def wrap(f):
|
||||
@wraps(f)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
|
||||
self.manager = get_driver(
|
||||
PG_DEFAULT_DRIVER
|
||||
).connection_manager(
|
||||
|
@ -489,11 +489,15 @@ define('pgadmin.node.database', [
|
||||
|
||||
Alertify.pgNotifier('error', xhr, error, function(msg) {
|
||||
setTimeout(function() {
|
||||
Alertify.dlgServerPass(
|
||||
gettext('Connect to database'),
|
||||
msg, _model, _data, _tree, _item, _status,
|
||||
onSuccess, onFailure, onCancel
|
||||
).resizeTo();
|
||||
if (msg == 'CRYPTKEY_SET') {
|
||||
connect_to_database(_model, _data, _tree, _item, _wasConnected);
|
||||
} else {
|
||||
Alertify.dlgServerPass(
|
||||
gettext('Connect to database'),
|
||||
msg, _model, _data, _tree, _item, _status,
|
||||
onSuccess, onFailure, onCancel
|
||||
).resizeTo();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
|
@ -1126,10 +1126,14 @@ define('pgadmin.node.server', [
|
||||
|
||||
Alertify.pgNotifier('error', xhr, error, function(msg) {
|
||||
setTimeout(function() {
|
||||
Alertify.dlgServerPass(
|
||||
gettext('Connect to Server'),
|
||||
msg, _node, _data, _tree, _item, _wasConnected
|
||||
).resizeTo();
|
||||
if (msg == 'CRYPTKEY_SET') {
|
||||
connect_to_server(_node, _data, _tree, _item, _wasConnected);
|
||||
} else {
|
||||
Alertify.dlgServerPass(
|
||||
gettext('Connect to Server'),
|
||||
msg, _node, _data, _tree, _item, _wasConnected
|
||||
).resizeTo();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
|
@ -1,23 +1,33 @@
|
||||
<form name="frmPassword" id="frmPassword" style="height: 100%; width: 100%" onsubmit="return false;">
|
||||
<div>{% if errmsg %}
|
||||
<div class="highlight has-error">
|
||||
<div class='control-label'>{{ errmsg }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<div><b>{{ _('Please enter the password for the user \'{0}\' to connect the server - "{1}"').format(username,
|
||||
server_label) }}</b></div>
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
<div style="width: 100%">
|
||||
<span style="width: 25%;display: inline-table;">Password</span>
|
||||
<span style="width: 73%;display: inline-block;">
|
||||
<input style="width:100%" id="password" class="form-control" name="password" type="password">
|
||||
</span>
|
||||
<span style="margin-left: 25%; padding-top: 15px;width: 45%;display: inline-block;">
|
||||
<div class="input-group row py-2">
|
||||
<label for="password" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="password" class="form-control" name="password" type="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-password-div input-group row py-2">
|
||||
<label for="password" class="col-sm-2 col-form-label"></label>
|
||||
<div class="col-sm-10">
|
||||
<input id="save_password" name="save_password" type="checkbox"
|
||||
{% if not config.ALLOW_SAVE_PASSWORD %}disabled{% endif %}
|
||||
> Save Password
|
||||
</span>
|
||||
>
|
||||
<label for="save_password">Save Password</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
{% if errmsg %}
|
||||
<div class='pg-prop-status-bar p-0'>
|
||||
<div class="error-in-footer">
|
||||
<div class="d-flex px-2 py-1">
|
||||
<div class="pr-2">
|
||||
<i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="alert-text">{{ errmsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
@ -1,42 +1,53 @@
|
||||
<form name="frmPassword" id="frmPassword" style="height: 100%; width: 100%" onsubmit="return false;">
|
||||
<div>{% if errmsg %}
|
||||
<div class="highlight has-error">
|
||||
<div class='control-label'>{{ errmsg }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form name="frmPassword" id="frmPassword" onsubmit="return false;">
|
||||
<div class="m-1">
|
||||
{% if prompt_tunnel_password %}
|
||||
{% if tunnel_identity_file %}
|
||||
<div><b>{{ _('SSH Tunnel password for the identity file \'{0}\' to connect the server "{1}"').format(tunnel_identity_file, tunnel_host) }}</b></div>
|
||||
{% else %}
|
||||
<div><b>{{ _('SSH Tunnel password for the user \'{0}\' to connect the server "{1}"').format(tunnel_username, tunnel_host) }}</b></div>
|
||||
{% endif %}
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
<div style="width: 100%">
|
||||
<span style="width: 97%;display: inline-block;">
|
||||
<input style="width:100%" id="tunnel_password" class="form-control" name="tunnel_password" type="password">
|
||||
</span>
|
||||
<span style="padding-top: 5px;display: inline-block;">
|
||||
<input id="save_tunnel_password" name="save_tunnel_password" type="checkbox"
|
||||
{% if not config.ALLOW_SAVE_TUNNEL_PASSWORD %}disabled{% endif %}
|
||||
> Save Password
|
||||
</span>
|
||||
<div class="input-group py-2">
|
||||
<div class="w-100">
|
||||
<input id="tunnel_password" class="form-control" name="tunnel_password" type="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-password-div input-group py-2">
|
||||
<div class="w-100">
|
||||
<input id="save_tunnel_password" name="save_tunnel_password" type="checkbox"
|
||||
{% if not config.ALLOW_SAVE_PASSWORD %}disabled{% endif %}
|
||||
>
|
||||
<label for="save_tunnel_password" class="ml-1">Save Password</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
{% endif %}
|
||||
{% if prompt_password %}
|
||||
<div><b>{{ _('Database server password for the user \'{0}\' to connect the server "{1}"').format(username, server_label) }}</b></div>
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
<div style="width: 100%">
|
||||
<span style="width: 97%;display: inline-block;">
|
||||
<input style="width:100%" id="password" class="form-control" name="password" type="password">
|
||||
</span>
|
||||
<span style="padding-top: 5px;display: inline-block;">
|
||||
<div class="input-group py-2">
|
||||
<div class="w-100">
|
||||
<input id="password" class="form-control" name="password" type="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-password-div input-group py-2">
|
||||
<label for="password" class="col-sm-2 col-form-label"></label>
|
||||
<div class="w-100">
|
||||
<input id="save_password" name="save_password" type="checkbox"
|
||||
{% if not config.ALLOW_SAVE_PASSWORD %}disabled{% endif %}
|
||||
> Save Password
|
||||
</span>
|
||||
>
|
||||
<label for="save_password" class="ml-1">Save Password</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if errmsg %}
|
||||
<div class='pg-prop-status-bar p-0'>
|
||||
<div class="error-in-footer">
|
||||
<div class="d-flex px-2 py-1">
|
||||
<div class="pr-2">
|
||||
<i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="alert-text">{{ errmsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
@ -8,6 +8,9 @@
|
||||
##########################################################################
|
||||
|
||||
"""Server helper utilities"""
|
||||
from pgadmin.utils.crypto import encrypt, decrypt
|
||||
import config
|
||||
from pgadmin.model import db, Server
|
||||
|
||||
|
||||
def parse_priv_from_db(db_privileges):
|
||||
@ -177,3 +180,70 @@ def validate_options(options, option_name, option_value):
|
||||
is_valid_options = True
|
||||
|
||||
return is_valid_options, valid_options
|
||||
|
||||
|
||||
def reencrpyt_server_passwords(user_id, old_key, new_key):
|
||||
"""
|
||||
This function will decrypt the saved passwords in SQLite with old key
|
||||
and then encrypt with new key
|
||||
"""
|
||||
from pgadmin.utils.driver import get_driver
|
||||
driver = get_driver(config.PG_DEFAULT_DRIVER)
|
||||
|
||||
for server in Server.query.filter_by(user_id=user_id).all():
|
||||
manager = driver.connection_manager(server.id)
|
||||
|
||||
# Check if old password was stored in pgadmin4 sqlite database.
|
||||
# If yes then update that password.
|
||||
if server.password is not None:
|
||||
password = decrypt(server.password, old_key)
|
||||
|
||||
if isinstance(password, bytes):
|
||||
password = password.decode()
|
||||
|
||||
password = encrypt(password, new_key)
|
||||
setattr(server, 'password', password)
|
||||
manager.password = password
|
||||
elif manager.password is not None:
|
||||
password = decrypt(manager.password, old_key)
|
||||
|
||||
if isinstance(password, bytes):
|
||||
password = password.decode()
|
||||
|
||||
password = encrypt(password, new_key)
|
||||
manager.password = password
|
||||
|
||||
if server.tunnel_password is not None:
|
||||
tunnel_password = decrypt(server.tunnel_password, old_key)
|
||||
if isinstance(tunnel_password, bytes):
|
||||
tunnel_password = tunnel_password.decode()
|
||||
|
||||
tunnel_password = encrypt(tunnel_password, new_key)
|
||||
setattr(server, 'tunnel_password', tunnel_password)
|
||||
manager.tunnel_password = tunnel_password
|
||||
elif manager.tunnel_password is not None:
|
||||
tunnel_password = decrypt(manager.tunnel_password, old_key)
|
||||
|
||||
if isinstance(tunnel_password, bytes):
|
||||
tunnel_password = tunnel_password.decode()
|
||||
|
||||
tunnel_password = encrypt(tunnel_password, new_key)
|
||||
manager.tunnel_password = tunnel_password
|
||||
|
||||
db.session.commit()
|
||||
manager.update_session()
|
||||
|
||||
|
||||
def remove_saved_passwords(user_id):
|
||||
"""
|
||||
This function will remove all the saved passwords for the server
|
||||
"""
|
||||
|
||||
try:
|
||||
db.session.query(Server) \
|
||||
.filter(Server.user_id == user_id) \
|
||||
.update({Server.password: None, Server.tunnel_password: None})
|
||||
db.session.commit()
|
||||
except Exception as _:
|
||||
db.session.rollback()
|
||||
raise
|
||||
|
@ -262,7 +262,6 @@ define('pgadmin.browser', [
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
},
|
||||
menu_categories: {
|
||||
/* name, label (pair) */
|
||||
@ -282,6 +281,7 @@ define('pgadmin.browser', [
|
||||
scripts[n] = _.isArray(scripts[n]) ? scripts[n] : [];
|
||||
scripts[n].push({'name': m, 'path': p, loaded: false});
|
||||
},
|
||||
masterpass_callback_queue: [],
|
||||
// Build the default layout
|
||||
buildDefaultLayout: function(docker) {
|
||||
var browserPanel = docker.addPanel('browser', wcDocker.DOCK.LEFT);
|
||||
@ -492,8 +492,8 @@ define('pgadmin.browser', [
|
||||
node = obj.Nodes[d._type];
|
||||
|
||||
/* If the node specific callback returns false, we will also return
|
||||
* false for further processing.
|
||||
*/
|
||||
* false for further processing.
|
||||
*/
|
||||
if (_.isObject(node.callbacks) &&
|
||||
eventName in node.callbacks &&
|
||||
typeof node.callbacks[eventName] == 'function' &&
|
||||
@ -542,13 +542,172 @@ define('pgadmin.browser', [
|
||||
.fail(function() {});
|
||||
}, 300000);
|
||||
|
||||
obj.set_master_password('');
|
||||
|
||||
obj.Events.on('pgadmin:browser:tree:add', obj.onAddTreeNode, obj);
|
||||
obj.Events.on('pgadmin:browser:tree:update', obj.onUpdateTreeNode, obj);
|
||||
obj.Events.on('pgadmin:browser:tree:refresh', obj.onRefreshTreeNode, obj);
|
||||
obj.Events.on('pgadmin-browser:tree:loadfail', obj.onLoadFailNode, obj);
|
||||
|
||||
obj.bind_beforeunload();
|
||||
},
|
||||
|
||||
init_master_password: function() {
|
||||
let self = this;
|
||||
// Master password dialog
|
||||
if (!Alertify.dlgMasterPass) {
|
||||
Alertify.dialog('dlgMasterPass', function factory() {
|
||||
return {
|
||||
main: function(title, message, reset) {
|
||||
this.set('title', title);
|
||||
this.message = message;
|
||||
this.reset = reset;
|
||||
},
|
||||
setup:function() {
|
||||
return {
|
||||
buttons:[{
|
||||
text: gettext('Reset Master Password'), className: 'btn btn-secondary fa fa-trash-o pg-alertify-button pull-left',
|
||||
},{
|
||||
text: gettext('Cancel'), className: 'btn btn-secondary fa fa-times pg-alertify-button',
|
||||
key: 27,
|
||||
},{
|
||||
text: gettext('OK'), key: 13, className: 'btn btn-primary fa fa-check pg-alertify-button',
|
||||
}],
|
||||
focus: {element: '#password', select: true},
|
||||
options: {
|
||||
modal: true, resizable: false, maximizable: false, pinnable: false,
|
||||
},
|
||||
};
|
||||
},
|
||||
prepare:function() {
|
||||
let self = this;
|
||||
let $password = null;
|
||||
let $okBtn = $(self.__internal.buttons[2].element);
|
||||
|
||||
self.setContent(self.message);
|
||||
$password = $(self.elements.body).find('#password');
|
||||
|
||||
/* Reset button hide */
|
||||
if(!self.reset) {
|
||||
$(self.__internal.buttons[0].element).addClass('d-none');
|
||||
} else {
|
||||
$(self.__internal.buttons[0].element).removeClass('d-none');
|
||||
}
|
||||
|
||||
/* Enable ok only if password entered */
|
||||
$okBtn.prop('disabled', true);
|
||||
$password.on('input', ()=>{
|
||||
if($password.val() != '') {
|
||||
$okBtn.prop('disabled', false);
|
||||
} else {
|
||||
$okBtn.prop('disabled', true);
|
||||
}
|
||||
});
|
||||
},
|
||||
callback: function(event) {
|
||||
let parentDialog = this;
|
||||
|
||||
if (event.index == 2) {
|
||||
/* OK Button */
|
||||
self.set_master_password(
|
||||
$('#frmMasterPassword #password').val(),
|
||||
parentDialog.set_callback,
|
||||
);
|
||||
} else if(event.index == 1) {
|
||||
/* Cancel button */
|
||||
self.masterpass_callback_queue = [];
|
||||
} else if(event.index == 0) {
|
||||
/* Reset Button */
|
||||
event.cancel = true;
|
||||
|
||||
Alertify.confirm(gettext('Reset Master Password'),
|
||||
gettext('This will remove all the saved passwords. This will also remove established connections to '
|
||||
+ 'the server and you may need to reconnect again. Do you wish to continue ?'),
|
||||
function() {
|
||||
/* If user clicks Yes */
|
||||
self.reset_master_password();
|
||||
parentDialog.close();
|
||||
return true;
|
||||
},
|
||||
function() {/* If user clicks No */ return true;}
|
||||
).set('labels', {
|
||||
ok: gettext('Yes'),
|
||||
cancel: gettext('No'),
|
||||
});
|
||||
}
|
||||
},
|
||||
};
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
check_master_password: function(on_resp_callback) {
|
||||
$.ajax({
|
||||
url: url_for('browser.check_master_password'),
|
||||
type: 'GET',
|
||||
contentType: 'application/json',
|
||||
}).done((res)=> {
|
||||
if(on_resp_callback) {
|
||||
if(res.data) {
|
||||
on_resp_callback(true);
|
||||
} else {
|
||||
on_resp_callback(false);
|
||||
}
|
||||
}
|
||||
}).fail(function(xhr, status, error) {
|
||||
Alertify.pgRespErrorNotify(xhr, error);
|
||||
});
|
||||
},
|
||||
|
||||
reset_master_password: function() {
|
||||
let self = this;
|
||||
$.ajax({
|
||||
url: url_for('browser.set_master_password'),
|
||||
type: 'DELETE',
|
||||
contentType: 'application/json',
|
||||
}).done((res)=> {
|
||||
if(!res.data) {
|
||||
self.set_master_password('');
|
||||
}
|
||||
}).fail(function(xhr, status, error) {
|
||||
Alertify.pgRespErrorNotify(xhr, error);
|
||||
});
|
||||
},
|
||||
|
||||
set_master_password: function(password='', set_callback=()=>{}) {
|
||||
let data=null, self = this;
|
||||
|
||||
if(password != null || password!='') {
|
||||
data = JSON.stringify({
|
||||
'password': password,
|
||||
});
|
||||
}
|
||||
|
||||
self.masterpass_callback_queue.push(set_callback);
|
||||
|
||||
$.ajax({
|
||||
url: url_for('browser.set_master_password'),
|
||||
type: 'POST',
|
||||
data: data,
|
||||
dataType: 'json',
|
||||
contentType: 'application/json',
|
||||
}).done((res)=> {
|
||||
if(!res.data.present) {
|
||||
self.init_master_password();
|
||||
Alertify.dlgMasterPass(res.data.title, res.data.content, res.data.reset);
|
||||
} else {
|
||||
setTimeout(()=>{
|
||||
while(self.masterpass_callback_queue.length > 0) {
|
||||
let callback = self.masterpass_callback_queue.shift();
|
||||
callback();
|
||||
}
|
||||
}, 500);
|
||||
}
|
||||
}).fail(function(xhr, status, error) {
|
||||
Alertify.pgRespErrorNotify(xhr, error);
|
||||
});
|
||||
},
|
||||
|
||||
bind_beforeunload: function() {
|
||||
$(window).on('beforeunload', function(e) {
|
||||
/* Can open you in new tab */
|
||||
@ -1619,10 +1778,13 @@ define('pgadmin.browser', [
|
||||
});
|
||||
}
|
||||
|
||||
Alertify.pgNotifier(
|
||||
error, xhr, gettext('Error retrieving details for the node.'),
|
||||
function() { console.warn(arguments); }
|
||||
);
|
||||
Alertify.pgNotifier(error, xhr, gettext('Error retrieving details for the node.'), function (msg) {
|
||||
if (msg == 'CRYPTKEY_SET') {
|
||||
fetchNodeInfo(_i, _d, _n);
|
||||
} else {
|
||||
console.warn(arguments);
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
}.bind(this);
|
||||
@ -1665,6 +1827,21 @@ define('pgadmin.browser', [
|
||||
}
|
||||
},
|
||||
|
||||
onLoadFailNode: function(_nodeData) {
|
||||
let self = this,
|
||||
isSelected = self.tree.isSelected(_nodeData);
|
||||
|
||||
/** Check if master password set **/
|
||||
self.check_master_password((is_set)=>{
|
||||
if(!is_set) {
|
||||
self.set_master_password('', ()=>{
|
||||
if(isSelected) { self.tree.select(_nodeData); }
|
||||
self.tree.open(_nodeData);
|
||||
});
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
removeChildTreeNodesById: function(_parentNode, _collType, _childIds) {
|
||||
var tree = pgBrowser.tree;
|
||||
if(_parentNode && _collType) {
|
||||
|
@ -263,25 +263,24 @@ define([
|
||||
content.find('.pg-prop-coll-container').append(that.grid.render().$el);
|
||||
|
||||
var timer;
|
||||
var getAjaxHook = function() {
|
||||
$.ajax({
|
||||
url: urlBase,
|
||||
type: 'GET',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
|
||||
// Generate a timer for the request
|
||||
timer = setTimeout(function() {
|
||||
// notify user if request is taking longer than 1 second
|
||||
|
||||
$.ajax({
|
||||
url: urlBase,
|
||||
type: 'GET',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader(pgAdmin.csrf_token_header, pgAdmin.csrf_token);
|
||||
// Generate a timer for the request
|
||||
timer = setTimeout(function() {
|
||||
// notify user if request is taking longer than 1 second
|
||||
|
||||
$msgContainer.text(gettext('Retrieving data from the server...'));
|
||||
$msgContainer.removeClass('d-none');
|
||||
if (self.grid) {
|
||||
self.grid.remove();
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
})
|
||||
.done(function(res) {
|
||||
$msgContainer.text(gettext('Retrieving data from the server...'));
|
||||
$msgContainer.removeClass('d-none');
|
||||
if (self.grid) {
|
||||
self.grid.remove();
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
}).done(function(res) {
|
||||
clearTimeout(timer);
|
||||
|
||||
if (_.isUndefined(that.grid) || _.isNull(that.grid)) return;
|
||||
@ -307,8 +306,7 @@ define([
|
||||
$msgContainer.text(gettext('No properties are available for the selected object.'));
|
||||
|
||||
}
|
||||
})
|
||||
.fail(function(xhr, error) {
|
||||
}).fail(function(xhr, error) {
|
||||
pgBrowser.Events.trigger(
|
||||
'pgadmin:node:retrieval:error', 'properties', xhr, error.message, item, that
|
||||
);
|
||||
@ -317,17 +315,24 @@ define([
|
||||
info: info,
|
||||
})) {
|
||||
Alertify.pgNotifier(
|
||||
error, xhr,
|
||||
S(gettext('Error retrieving properties - %s')).sprintf(
|
||||
error.message || that.label).value(), function() {
|
||||
console.warn(arguments);
|
||||
});
|
||||
error, xhr, S(gettext('Error retrieving properties - %s')).sprintf(
|
||||
error.message || that.label).value(),
|
||||
function(msg) {
|
||||
if(msg === 'CRYPTKEY_SET') {
|
||||
getAjaxHook();
|
||||
} else {
|
||||
console.warn(arguments);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
// show failed message.
|
||||
$msgContainer.text(gettext('Failed to retrieve data from the server.'));
|
||||
});
|
||||
};
|
||||
getAjaxHook();
|
||||
|
||||
var onDrop = function(type) {
|
||||
var onDrop = function(type, confirm=true) {
|
||||
let sel_row_models = this.grid.getSelectedModels(),
|
||||
sel_rows = [],
|
||||
item = pgBrowser.tree.selected(),
|
||||
@ -356,18 +361,32 @@ define([
|
||||
title = gettext('DROP multiple objects?');
|
||||
}
|
||||
|
||||
|
||||
Alertify.confirm(title, msg,
|
||||
function() {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'DELETE',
|
||||
data: JSON.stringify({'ids': sel_rows}),
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
})
|
||||
.done(function(res) {
|
||||
if (res.success == 0) {
|
||||
pgBrowser.report_error(res.errormsg, res.info);
|
||||
let dropAjaxHook = function() {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'DELETE',
|
||||
data: JSON.stringify({'ids': sel_rows}),
|
||||
contentType: 'application/json; charset=utf-8',
|
||||
}).done(function(res) {
|
||||
if (res.success == 0) {
|
||||
pgBrowser.report_error(res.errormsg, res.info);
|
||||
} else {
|
||||
$(pgBrowser.panels['properties'].panel).removeData('node-prop');
|
||||
pgBrowser.Events.trigger(
|
||||
'pgadmin:browser:tree:refresh', item || pgBrowser.tree.selected(), {
|
||||
success: function() {
|
||||
node.callbacks.selected.apply(node, [item]);
|
||||
},
|
||||
});
|
||||
}
|
||||
return true;
|
||||
}).fail(function(xhr, error) {
|
||||
Alertify.pgNotifier(
|
||||
error, xhr,
|
||||
S(gettext('Error dropping %s'))
|
||||
.sprintf(d._label.toLowerCase()).value(), function(msg) {
|
||||
if (msg == 'CRYPTKEY_SET') {
|
||||
onDrop(type, false);
|
||||
} else {
|
||||
$(pgBrowser.panels['properties'].panel).removeData('node-prop');
|
||||
pgBrowser.Events.trigger(
|
||||
@ -375,37 +394,19 @@ define([
|
||||
success: function() {
|
||||
node.callbacks.selected.apply(node, [item]);
|
||||
},
|
||||
});
|
||||
}
|
||||
);
|
||||
}
|
||||
return true;
|
||||
})
|
||||
.fail(function(jqx) {
|
||||
var msg = jqx.responseText;
|
||||
/* Error from the server */
|
||||
if (jqx.status == 417 || jqx.status == 410 || jqx.status == 500) {
|
||||
try {
|
||||
var data = JSON.parse(jqx.responseText);
|
||||
msg = data.errormsg;
|
||||
} catch (e) {
|
||||
console.warn(e.stack || e);
|
||||
}
|
||||
}
|
||||
pgBrowser.report_error(
|
||||
S(gettext('Error dropping %s'))
|
||||
.sprintf(d._label.toLowerCase())
|
||||
.value(), msg);
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
|
||||
$(pgBrowser.panels['properties'].panel).removeData('node-prop');
|
||||
pgBrowser.Events.trigger(
|
||||
'pgadmin:browser:tree:refresh', item || pgBrowser.tree.selected(), {
|
||||
success: function() {
|
||||
node.callbacks.selected.apply(node, [item]);
|
||||
},
|
||||
}
|
||||
);
|
||||
});
|
||||
},
|
||||
null).show();
|
||||
if(confirm) {
|
||||
Alertify.confirm(title, msg, dropAjaxHook, null).show();
|
||||
} else {
|
||||
dropAjaxHook();
|
||||
}
|
||||
return;
|
||||
}.bind(that);
|
||||
},
|
||||
|
@ -404,50 +404,59 @@ define('pgadmin.browser.node', [
|
||||
}
|
||||
}, 1000, ctx);
|
||||
|
||||
newModel.fetch({
|
||||
success: function() {
|
||||
// Clear timeout and remove message
|
||||
clearTimeout(timer);
|
||||
$msgDiv.addClass('d-none');
|
||||
|
||||
// We got the latest attributes of the object. Render the view
|
||||
// now.
|
||||
view.render();
|
||||
setFocusOnEl();
|
||||
newModel.startNewSession();
|
||||
},
|
||||
error: function(xhr, error, message) {
|
||||
var _label = that && item ?
|
||||
that.getTreeNodeHierarchy(
|
||||
item
|
||||
)[that.type].label : '';
|
||||
pgBrowser.Events.trigger(
|
||||
'pgadmin:node:retrieval:error', 'properties',
|
||||
xhr, error, message, item
|
||||
);
|
||||
if (!Alertify.pgHandleItemError(
|
||||
xhr, error, message, {
|
||||
item: item,
|
||||
info: info,
|
||||
}
|
||||
)) {
|
||||
Alertify.pgNotifier(
|
||||
error, xhr,
|
||||
S(
|
||||
gettext('Error retrieving properties - %s')
|
||||
).sprintf(message || _label).value(),
|
||||
function() {}
|
||||
var fetchAjaxHook = function() {
|
||||
newModel.fetch({
|
||||
success: function() {
|
||||
// Clear timeout and remove message
|
||||
clearTimeout(timer);
|
||||
$msgDiv.addClass('d-none');
|
||||
|
||||
// We got the latest attributes of the object. Render the view
|
||||
// now.
|
||||
view.render();
|
||||
setFocusOnEl();
|
||||
newModel.startNewSession();
|
||||
},
|
||||
error: function(model, xhr, options) {
|
||||
var _label = that && item ?
|
||||
that.getTreeNodeHierarchy(
|
||||
item
|
||||
)[that.type].label : '';
|
||||
pgBrowser.Events.trigger(
|
||||
'pgadmin:node:retrieval:error', 'properties',
|
||||
xhr, options.textStatus, options.errorThrown, item
|
||||
);
|
||||
}
|
||||
// Close the panel (if could not fetch properties)
|
||||
if (cancelFunc) {
|
||||
cancelFunc();
|
||||
}
|
||||
},
|
||||
});
|
||||
if (!Alertify.pgHandleItemError(
|
||||
xhr, options.textStatus, options.errorThrown, {
|
||||
item: item,
|
||||
info: info,
|
||||
}
|
||||
)) {
|
||||
Alertify.pgNotifier(
|
||||
options.textStatus, xhr,
|
||||
S(
|
||||
gettext('Error retrieving properties - %s')
|
||||
).sprintf(options.errorThrown || _label).value(), function(msg) {
|
||||
if(msg === 'CRYPTKEY_SET') {
|
||||
fetchAjaxHook();
|
||||
} else {
|
||||
console.warn(arguments);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
// Close the panel (if could not fetch properties)
|
||||
if (cancelFunc) {
|
||||
cancelFunc();
|
||||
}
|
||||
},
|
||||
});
|
||||
};
|
||||
|
||||
fetchAjaxHook();
|
||||
} else {
|
||||
// Yay - render the view now!
|
||||
// $(el).focus();
|
||||
view.render();
|
||||
setFocusOnEl();
|
||||
newModel.startNewSession();
|
||||
|
23
web/pgadmin/browser/templates/browser/master_password.html
Normal file
23
web/pgadmin/browser/templates/browser/master_password.html
Normal file
@ -0,0 +1,23 @@
|
||||
<form name="frmMasterPassword" id="frmMasterPassword" style="height: 100%; width: 100%" onsubmit="return false;">
|
||||
<div>
|
||||
<div><b>{{ content_text|safe }}</b></div>
|
||||
<div class="input-group row py-2">
|
||||
<label for="password" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input type="password" class="form-control" id="password" name="password">
|
||||
</div>
|
||||
</div>
|
||||
{% if errmsg %}
|
||||
<div class='pg-prop-status-bar p-0'>
|
||||
<div class="error-in-footer">
|
||||
<div class="d-flex px-2 py-1">
|
||||
<div class="pr-2">
|
||||
<i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="alert-text">{{ errmsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
106
web/pgadmin/browser/tests/test_master_password.py
Normal file
106
web/pgadmin/browser/tests/test_master_password.py
Normal file
@ -0,0 +1,106 @@
|
||||
##########################################################################
|
||||
#
|
||||
# pgAdmin 4 - PostgreSQL Tools
|
||||
#
|
||||
# Copyright (C) 2013 - 2019, The pgAdmin Development Team
|
||||
# This software is released under the PostgreSQL Licence
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
import json
|
||||
|
||||
from pgadmin.utils.route import BaseTestGenerator
|
||||
import config
|
||||
|
||||
|
||||
class MasterPasswordTestCase(BaseTestGenerator):
|
||||
"""
|
||||
This class validates the change password functionality
|
||||
by defining change password scenarios; where dict of
|
||||
parameters describes the scenario appended by test name.
|
||||
"""
|
||||
|
||||
scenarios = [
|
||||
# This testcase validates invalid confirmation password
|
||||
('TestCase for Create master password dialog', dict(
|
||||
password="",
|
||||
content=(
|
||||
"Set Master Password",
|
||||
[
|
||||
"Please set a master password for pgAdmin.",
|
||||
"This will be used to secure and later unlock saved "
|
||||
"passwords and other credentials."
|
||||
]
|
||||
)
|
||||
)),
|
||||
('TestCase for Setting Master Password', dict(
|
||||
password="masterpasstest",
|
||||
check_if_set=True,
|
||||
)),
|
||||
('TestCase for Resetting Master Password', dict(
|
||||
reset=True,
|
||||
password="",
|
||||
content=(
|
||||
"Set Master Password",
|
||||
[
|
||||
"Please set a master password for pgAdmin.",
|
||||
"This will be used to secure and later unlock saved "
|
||||
"passwords and other credentials."
|
||||
]
|
||||
)
|
||||
)),
|
||||
]
|
||||
|
||||
def setUp(self):
|
||||
config.MASTER_PASSWORD_REQUIRED = True
|
||||
|
||||
def runTest(self):
|
||||
"""This function will check change password functionality."""
|
||||
req_data = dict()
|
||||
|
||||
if hasattr(self, 'password'):
|
||||
req_data['password'] = self.password
|
||||
|
||||
if hasattr(self, 'restart'):
|
||||
req_data['restart'] = self.restart
|
||||
|
||||
if hasattr(self, 'reset'):
|
||||
req_data['reset'] = self.reset
|
||||
|
||||
if config.SERVER_MODE:
|
||||
response = self.tester.post(
|
||||
'/browser/master_password',
|
||||
data=json.dumps(req_data),
|
||||
)
|
||||
|
||||
self.assertEquals(response.json['data']['present'], True)
|
||||
else:
|
||||
if 'reset' in req_data:
|
||||
response = self.tester.delete(
|
||||
'/browser/master_password'
|
||||
)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(response.json['data'], False)
|
||||
else:
|
||||
response = self.tester.post(
|
||||
'/browser/master_password',
|
||||
data=json.dumps(req_data),
|
||||
)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
|
||||
if hasattr(self, 'content'):
|
||||
self.assertEquals(response.json['data']['title'],
|
||||
self.content[0])
|
||||
|
||||
for text in self.content[1]:
|
||||
self.assertIn(text, response.json['data']['content'])
|
||||
|
||||
if hasattr(self, 'check_if_set'):
|
||||
response = self.tester.get(
|
||||
'/browser/master_password'
|
||||
)
|
||||
self.assertEquals(response.status_code, 200)
|
||||
self.assertEquals(response.json['data'], True)
|
||||
|
||||
def tearDown(self):
|
||||
config.MASTER_PASSWORD_REQUIRED = False
|
@ -18,6 +18,8 @@ from flask_babelex import gettext
|
||||
|
||||
from config import PG_DEFAULT_DRIVER
|
||||
from pgadmin.utils.ajax import make_json_response, precondition_required
|
||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
|
||||
CryptKeyMissing
|
||||
|
||||
|
||||
def is_version_in_range(sversion, min_ver, max_ver):
|
||||
@ -321,9 +323,19 @@ class PGChildNodeView(NodeView):
|
||||
if 'did' in kwargs:
|
||||
did = kwargs['did']
|
||||
|
||||
conn = manager.connection(did=did)
|
||||
|
||||
if not conn.connected():
|
||||
try:
|
||||
conn = manager.connection(did=did)
|
||||
if not conn.connected():
|
||||
status, msg = conn.connect()
|
||||
if not status:
|
||||
return precondition_required(
|
||||
gettext(
|
||||
"Connection to the server has been lost."
|
||||
)
|
||||
)
|
||||
except (ConnectionLost, SSHTunnelConnectionLost, CryptKeyMissing):
|
||||
raise
|
||||
except Exception as e:
|
||||
return precondition_required(
|
||||
gettext(
|
||||
"Connection to the server has been lost."
|
||||
|
@ -223,19 +223,35 @@ define('pgadmin.dashboard', [
|
||||
var div = dashboardPanel.layout().scene().find('.pg-panel-content');
|
||||
|
||||
if (div) {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
dataType: 'html',
|
||||
})
|
||||
.done(function(data) {
|
||||
$(div).html(data);
|
||||
var ajaxHook = function() {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
dataType: 'html',
|
||||
})
|
||||
.fail(function() {
|
||||
$(div).html(
|
||||
'<div class="alert alert-danger pg-panel-message" role="alert">' + gettext('An error occurred whilst loading the dashboard.') + '</div>'
|
||||
);
|
||||
});
|
||||
.done(function(data) {
|
||||
$(div).html(data);
|
||||
})
|
||||
.fail(function(xhr, error) {
|
||||
Alertify.pgNotifier(
|
||||
error, xhr,
|
||||
gettext('An error occurred whilst loading the dashboard.'),
|
||||
function(msg) {
|
||||
if(msg === 'CRYPTKEY_SET') {
|
||||
ajaxHook();
|
||||
} else {
|
||||
$(div).html(
|
||||
'<div class="alert alert-danger pg-panel-message" role="alert">' + gettext('An error occurred whilst loading the dashboard.') + '</div>'
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
$(div).html(
|
||||
'<div class="alert alert-info pg-panel-message" role="alert">' + gettext('Loading dashboard...') + '</div>'
|
||||
);
|
||||
ajaxHook();
|
||||
|
||||
// Cache the current IDs for next time
|
||||
$(dashboardPanel).data('sid', -1);
|
||||
@ -337,20 +353,36 @@ define('pgadmin.dashboard', [
|
||||
/* Clear all the charts previous dashboards */
|
||||
self.clearChartFromStore();
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
dataType: 'html',
|
||||
})
|
||||
.done(function(data) {
|
||||
$(div).html(data);
|
||||
self.init_dashboard();
|
||||
let ajaxHook = function() {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
dataType: 'html',
|
||||
})
|
||||
.fail(function() {
|
||||
$(div).html(
|
||||
'<div class="alert alert-danger pg-panel-message" role="alert">' + gettext('An error occurred whilst loading the dashboard.') + '</div>'
|
||||
);
|
||||
});
|
||||
.done(function(data) {
|
||||
$(div).html(data);
|
||||
self.init_dashboard();
|
||||
})
|
||||
.fail(function(xhr, error) {
|
||||
Alertify.pgNotifier(
|
||||
error, xhr,
|
||||
gettext('An error occurred whilst loading the dashboard.'),
|
||||
function(msg) {
|
||||
if(msg === 'CRYPTKEY_SET') {
|
||||
ajaxHook();
|
||||
} else {
|
||||
$(div).html(
|
||||
'<div class="alert alert-danger pg-panel-message" role="alert">' + gettext('An error occurred whilst loading the dashboard.') + '</div>'
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
});
|
||||
};
|
||||
$(div).html(
|
||||
'<div class="alert alert-info pg-panel-message" role="alert">' + gettext('Loading dashboard...') + '</div>'
|
||||
);
|
||||
ajaxHook();
|
||||
$(dashboardPanel).data('server_status', true);
|
||||
}
|
||||
} else {
|
||||
|
@ -215,8 +215,13 @@ define('misc.dependencies', [
|
||||
Alertify.pgNotifier(
|
||||
error, xhr,
|
||||
S(gettext('Error retrieving data from the server: %s')).sprintf(
|
||||
message || _label).value(), function() {
|
||||
console.warn(arguments);
|
||||
message || _label).value(),
|
||||
function(msg) {
|
||||
if(msg === 'CRYPTKEY_SET') {
|
||||
self.showDependencies(item, data, node);
|
||||
} else {
|
||||
console.warn(arguments);
|
||||
}
|
||||
});
|
||||
}
|
||||
// show failed message.
|
||||
|
@ -221,8 +221,13 @@ define('misc.dependents', [
|
||||
Alertify.pgNotifier(
|
||||
error, xhr,
|
||||
S(gettext('Error retrieving data from the server: %s')).sprintf(
|
||||
message || _label).value(), function() {
|
||||
console.warn(arguments);
|
||||
message || _label).value(),
|
||||
function(msg) {
|
||||
if(msg === 'CRYPTKEY_SET') {
|
||||
self.showDependents(item, data, node);
|
||||
} else {
|
||||
console.warn(arguments);
|
||||
}
|
||||
});
|
||||
}
|
||||
// show failed message.
|
||||
|
@ -119,31 +119,28 @@ define('misc.sql', [
|
||||
|
||||
sql = '';
|
||||
var timer;
|
||||
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader(
|
||||
pgAdmin.csrf_token_header, pgAdmin.csrf_token
|
||||
);
|
||||
// Generate a timer for the request
|
||||
timer = setTimeout(function() {
|
||||
// Notify user if request is taking longer than 1 second
|
||||
|
||||
pgAdmin.Browser.editor.setValue(
|
||||
gettext('Retrieving data from the server...')
|
||||
var ajaxHook = function() {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader(
|
||||
pgAdmin.csrf_token_header, pgAdmin.csrf_token
|
||||
);
|
||||
}, 1000);
|
||||
},
|
||||
})
|
||||
.done(function(res) {
|
||||
// Generate a timer for the request
|
||||
timer = setTimeout(function() {
|
||||
// Notify user if request is taking longer than 1 second
|
||||
pgAdmin.Browser.editor.setValue(
|
||||
gettext('Retrieving data from the server...')
|
||||
);
|
||||
}, 1000);
|
||||
},
|
||||
}).done(function(res) {
|
||||
if (pgAdmin.Browser.editor.getValue() != res) {
|
||||
pgAdmin.Browser.editor.setValue(res);
|
||||
}
|
||||
clearTimeout(timer);
|
||||
})
|
||||
.fail(function(xhr, error, message) {
|
||||
}).fail(function(xhr, error, message) {
|
||||
var _label = treeHierarchy[n_type].label;
|
||||
pgBrowser.Events.trigger(
|
||||
'pgadmin:node:retrieval:error', 'sql', xhr, error, message, item
|
||||
@ -156,11 +153,18 @@ define('misc.sql', [
|
||||
error, xhr,
|
||||
S(gettext('Error retrieving the information - %s')).sprintf(
|
||||
message || _label
|
||||
).value(),
|
||||
function() {}
|
||||
).value(), function(msg) {
|
||||
if(msg === 'CRYPTKEY_SET') {
|
||||
ajaxHook();
|
||||
} else {
|
||||
console.warn(arguments);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
};
|
||||
ajaxHook();
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -232,97 +232,106 @@ define('misc.statistics', [
|
||||
msg = '';
|
||||
var timer;
|
||||
// Set the url, fetch the data and update the collection
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader(
|
||||
pgAdmin.csrf_token_header, pgAdmin.csrf_token
|
||||
);
|
||||
// Generate a timer for the request
|
||||
timer = setTimeout(function() {
|
||||
// notify user if request is taking longer than 1 second
|
||||
|
||||
$msgContainer.text(gettext('Retrieving data from the server...'));
|
||||
$msgContainer.removeClass('d-none');
|
||||
if (self.grid) {
|
||||
self.grid.remove();
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
})
|
||||
.done(function(res) {
|
||||
// clear timer and reset message.
|
||||
clearTimeout(timer);
|
||||
$msgContainer.text('');
|
||||
if (res.data) {
|
||||
var data = self.data = res.data;
|
||||
if (node.hasCollectiveStatistics || data['rows'].length > 1) {
|
||||
// Listen scroll event to load more rows
|
||||
pgBrowser.Events.on(
|
||||
'pgadmin-browser:panel-statistics:' +
|
||||
wcDocker.EVENT.SCROLLED,
|
||||
self.__loadMoreRows
|
||||
);
|
||||
self.__createMultiLineStatistics.call(self, data, node.statsPrettifyFields);
|
||||
} else {
|
||||
// Do not listen the scroll event
|
||||
pgBrowser.Events.off(
|
||||
'pgadmin-browser:panel-statistics:' +
|
||||
wcDocker.EVENT.SCROLLED,
|
||||
self.__loadMoreRows
|
||||
);
|
||||
self.__createSingleLineStatistics.call(self, data, node.statsPrettifyFields);
|
||||
}
|
||||
|
||||
if (self.grid) {
|
||||
delete self.grid;
|
||||
self.grid = null;
|
||||
}
|
||||
|
||||
self.grid = new Backgrid.Grid({
|
||||
emptyText: 'No data found',
|
||||
columns: self.columns,
|
||||
collection: self.collection,
|
||||
className: GRID_CLASSES,
|
||||
});
|
||||
self.grid.render();
|
||||
$gridContainer.empty();
|
||||
$gridContainer.append(self.grid.$el);
|
||||
|
||||
if (!$msgContainer.hasClass('d-none')) {
|
||||
$msgContainer.addClass('d-none');
|
||||
}
|
||||
$gridContainer.removeClass('d-none');
|
||||
|
||||
} else if (res.info) {
|
||||
if (!$gridContainer.hasClass('d-none')) {
|
||||
$gridContainer.addClass('d-none');
|
||||
}
|
||||
$msgContainer.text(res.info);
|
||||
$msgContainer.removeClass('d-none');
|
||||
}
|
||||
})
|
||||
.fail(function(xhr, error, message) {
|
||||
var _label = treeHierarchy[n_type].label;
|
||||
pgBrowser.Events.trigger(
|
||||
'pgadmin:node:retrieval:error', 'statistics', xhr, error, message, item
|
||||
);
|
||||
if (!Alertify.pgHandleItemError(xhr, error, message, {
|
||||
item: item,
|
||||
info: treeHierarchy,
|
||||
})) {
|
||||
Alertify.pgNotifier(
|
||||
error, xhr,
|
||||
S(gettext('Error retrieving the information - %s')).sprintf(
|
||||
message || _label
|
||||
).value(),
|
||||
function() {}
|
||||
var ajaxHook = function() {
|
||||
$.ajax({
|
||||
url: url,
|
||||
type: 'GET',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader(
|
||||
pgAdmin.csrf_token_header, pgAdmin.csrf_token
|
||||
);
|
||||
}
|
||||
// show failed message.
|
||||
$msgContainer.text(gettext('Failed to retrieve data from the server.'));
|
||||
});
|
||||
// Generate a timer for the request
|
||||
timer = setTimeout(function() {
|
||||
// notify user if request is taking longer than 1 second
|
||||
|
||||
$msgContainer.text(gettext('Retrieving data from the server...'));
|
||||
$msgContainer.removeClass('d-none');
|
||||
if (self.grid) {
|
||||
self.grid.remove();
|
||||
}
|
||||
}, 1000);
|
||||
},
|
||||
})
|
||||
.done(function(res) {
|
||||
// clear timer and reset message.
|
||||
clearTimeout(timer);
|
||||
$msgContainer.text('');
|
||||
if (res.data) {
|
||||
var data = self.data = res.data;
|
||||
if (node.hasCollectiveStatistics || data['rows'].length > 1) {
|
||||
// Listen scroll event to load more rows
|
||||
pgBrowser.Events.on(
|
||||
'pgadmin-browser:panel-statistics:' +
|
||||
wcDocker.EVENT.SCROLLED,
|
||||
self.__loadMoreRows
|
||||
);
|
||||
self.__createMultiLineStatistics.call(self, data, node.statsPrettifyFields);
|
||||
} else {
|
||||
// Do not listen the scroll event
|
||||
pgBrowser.Events.off(
|
||||
'pgadmin-browser:panel-statistics:' +
|
||||
wcDocker.EVENT.SCROLLED,
|
||||
self.__loadMoreRows
|
||||
);
|
||||
self.__createSingleLineStatistics.call(self, data, node.statsPrettifyFields);
|
||||
}
|
||||
|
||||
if (self.grid) {
|
||||
delete self.grid;
|
||||
self.grid = null;
|
||||
}
|
||||
|
||||
self.grid = new Backgrid.Grid({
|
||||
emptyText: 'No data found',
|
||||
columns: self.columns,
|
||||
collection: self.collection,
|
||||
className: GRID_CLASSES,
|
||||
});
|
||||
self.grid.render();
|
||||
$gridContainer.empty();
|
||||
$gridContainer.append(self.grid.$el);
|
||||
|
||||
if (!$msgContainer.hasClass('d-none')) {
|
||||
$msgContainer.addClass('d-none');
|
||||
}
|
||||
$gridContainer.removeClass('d-none');
|
||||
|
||||
} else if (res.info) {
|
||||
if (!$gridContainer.hasClass('d-none')) {
|
||||
$gridContainer.addClass('d-none');
|
||||
}
|
||||
$msgContainer.text(res.info);
|
||||
$msgContainer.removeClass('d-none');
|
||||
}
|
||||
})
|
||||
.fail(function(xhr, error, message) {
|
||||
var _label = treeHierarchy[n_type].label;
|
||||
pgBrowser.Events.trigger(
|
||||
'pgadmin:node:retrieval:error', 'statistics', xhr, error, message, item
|
||||
);
|
||||
if (!Alertify.pgHandleItemError(xhr, error, message, {
|
||||
item: item,
|
||||
info: treeHierarchy,
|
||||
})) {
|
||||
Alertify.pgNotifier(
|
||||
error, xhr,
|
||||
S(gettext('Error retrieving the information - %s')).sprintf(
|
||||
message || _label
|
||||
).value(), function(msg) {
|
||||
if(msg === 'CRYPTKEY_SET') {
|
||||
ajaxHook();
|
||||
} else {
|
||||
console.warn(arguments);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
// show failed message.
|
||||
$msgContainer.text(gettext('Failed to retrieve data from the server.'));
|
||||
});
|
||||
};
|
||||
|
||||
ajaxHook();
|
||||
}
|
||||
}
|
||||
if (msg != '') {
|
||||
|
@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
SCHEMA_VERSION = 22
|
||||
SCHEMA_VERSION = 23
|
||||
|
||||
##########################################################################
|
||||
#
|
||||
@ -70,6 +70,7 @@ class User(db.Model, UserMixin):
|
||||
password = db.Column(db.String(256))
|
||||
active = db.Column(db.Boolean(), nullable=False)
|
||||
confirmed_at = db.Column(db.DateTime())
|
||||
masterpass_check = db.Column(db.String(256))
|
||||
roles = db.relationship('Role', secondary=roles_users,
|
||||
backref=db.backref('users', lazy='dynamic'))
|
||||
|
||||
|
@ -107,7 +107,15 @@ define([
|
||||
if (contentType.indexOf('application/json') == 0) {
|
||||
var resp = JSON.parse(msg);
|
||||
|
||||
if (resp.result != null && (!resp.errormsg || resp.errormsg == '') &&
|
||||
if(resp.info == 'CRYPTKEY_MISSING') {
|
||||
var pgBrowser = window.pgAdmin.Browser;
|
||||
pgBrowser.set_master_password('', ()=> {
|
||||
if(onJSONResult && typeof(onJSONResult) == 'function') {
|
||||
onJSONResult('CRYPTKEY_SET');
|
||||
}
|
||||
});
|
||||
return;
|
||||
} else if (resp.result != null && (!resp.errormsg || resp.errormsg == '') &&
|
||||
onJSONResult && typeof(onJSONResult) == 'function') {
|
||||
return onJSONResult(resp.result);
|
||||
}
|
||||
@ -375,6 +383,11 @@ define([
|
||||
reconnectServer();
|
||||
});
|
||||
return true;
|
||||
} else if (jsonResp && jsonResp.info == 'CRYPTKEY_MISSING' && xhr.status == 503) {
|
||||
/* Suppress the error here and handle in Alertify.pgNotifier wherever
|
||||
* required, as it has callback option
|
||||
*/
|
||||
return false;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
@ -230,6 +230,12 @@ class ExecuteQuery {
|
||||
this.sqlServerObject.handle_connection_lost(false, httpMessage);
|
||||
}
|
||||
|
||||
if(this.isCryptKeyMissing(httpMessage)) {
|
||||
this.sqlServerObject.saveState('execute', [this.explainPlan]);
|
||||
this.sqlServerObject.handle_cryptkey_missing();
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = httpMessage.response.data.errormsg;
|
||||
this.sqlServerObject.update_msg_history(false, msg);
|
||||
}
|
||||
@ -240,6 +246,12 @@ class ExecuteQuery {
|
||||
httpMessage.response.data.info === 'CONNECTION_LOST';
|
||||
}
|
||||
|
||||
isCryptKeyMissing(httpMessage) {
|
||||
return httpMessage.response.status === 503 &&
|
||||
httpMessage.response.data.info !== undefined &&
|
||||
httpMessage.response.data.info === 'CRYPTKEY_MISSING';
|
||||
}
|
||||
|
||||
removeGridViewMarker() {
|
||||
if (this.sqlServerObject.gridView.marker) {
|
||||
this.sqlServerObject.gridView.marker.clear();
|
||||
|
@ -607,32 +607,42 @@ define([
|
||||
*/
|
||||
$('.wizard-progress-bar p').show();
|
||||
|
||||
coll.fetch({
|
||||
success: function(c, xhr) {
|
||||
$('.wizard-progress-bar p').html('');
|
||||
$('.wizard-progress-bar').hide();
|
||||
c.set(xhr.result, {parse: true});
|
||||
// If some objects failed while fetching then we will notify the user
|
||||
if (xhr && xhr.info && xhr.info !== '') {
|
||||
$('.pg-prop-status-bar .alert-text').html(xhr.info);
|
||||
$('.pg-prop-status-bar').css('visibility', 'visible');
|
||||
}
|
||||
},
|
||||
error: function(m, xhr) {
|
||||
// If the main request fails as whole then
|
||||
let msg;
|
||||
if (xhr && xhr.responseJSON && xhr.responseJSON.errormsg) {
|
||||
msg = xhr.responseJSON.errormsg;
|
||||
}
|
||||
var fetchAjaxHook = function() {
|
||||
$('.wizard-progress-bar p').removeClass('alert-danger').addClass('alert-info');
|
||||
$('.wizard-progress-bar p').text(gettext('Please wait while fetching records...'));
|
||||
coll.fetch({
|
||||
success: function(c, xhr) {
|
||||
$('.wizard-progress-bar p').html('');
|
||||
$('.wizard-progress-bar').hide();
|
||||
c.set(xhr.result, {parse: true});
|
||||
// If some objects failed while fetching then we will notify the user
|
||||
if (xhr && xhr.info && xhr.info !== '') {
|
||||
$('.pg-prop-status-bar .alert-text').html(xhr.info);
|
||||
$('.pg-prop-status-bar').css('visibility', 'visible');
|
||||
}
|
||||
},
|
||||
error: function(model, xhr, options) {
|
||||
// If the main request fails as whole then
|
||||
$('.wizard-progress-bar p').removeClass('alert-info').addClass('alert-danger');
|
||||
$('.wizard-progress-bar p').text(gettext('Unable to fetch the database objects'));
|
||||
|
||||
if(!msg) {
|
||||
msg = gettext('Unable to fetch the database objects due to an error');
|
||||
}
|
||||
$('.wizard-progress-bar p').removeClass('alert-info').addClass('alert-danger');
|
||||
$('.wizard-progress-bar p').text(msg);
|
||||
},
|
||||
reset: true,
|
||||
}, this);
|
||||
Alertify.pgNotifier(
|
||||
options.textStatus, xhr,
|
||||
gettext('Unable to fetch the database objects'),
|
||||
function(msg) {
|
||||
if(msg === 'CRYPTKEY_SET') {
|
||||
fetchAjaxHook();
|
||||
} else {
|
||||
$('.wizard-progress-bar p').removeClass('alert-info').addClass('alert-danger');
|
||||
$('.wizard-progress-bar p').text(msg);
|
||||
}
|
||||
}
|
||||
);
|
||||
},
|
||||
reset: true,
|
||||
}, this);
|
||||
};
|
||||
fetchAjaxHook();
|
||||
|
||||
//////////////////////////////////////////////////////////////////////
|
||||
// //
|
||||
|
@ -8,10 +8,8 @@
|
||||
##########################################################################
|
||||
|
||||
"""A blueprint module implementing the sqleditor frame."""
|
||||
import codecs
|
||||
import os
|
||||
import pickle
|
||||
import random
|
||||
import sys
|
||||
|
||||
import simplejson as json
|
||||
@ -32,10 +30,11 @@ from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import \
|
||||
from pgadmin.utils import PgAdminModule
|
||||
from pgadmin.utils import get_storage_directory
|
||||
from pgadmin.utils.ajax import make_json_response, bad_request, \
|
||||
success_return, internal_server_error, unauthorized
|
||||
success_return, internal_server_error
|
||||
from pgadmin.utils.driver import get_driver
|
||||
from pgadmin.utils.menu import MenuItem
|
||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost
|
||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
|
||||
CryptKeyMissing
|
||||
from pgadmin.utils.sqlautocomplete.autocomplete import SQLAutoComplete
|
||||
from pgadmin.tools.sqleditor.utils.query_tool_preferences import \
|
||||
RegisterQueryToolPreferences
|
||||
@ -176,7 +175,7 @@ def check_transaction_status(trans_id):
|
||||
use_binary_placeholder=True,
|
||||
array_to_string=True
|
||||
)
|
||||
except (ConnectionLost, SSHTunnelConnectionLost) as e:
|
||||
except (ConnectionLost, SSHTunnelConnectionLost, CryptKeyMissing):
|
||||
raise
|
||||
except Exception as e:
|
||||
current_app.logger.error(e)
|
||||
|
@ -2008,6 +2008,11 @@ define('tools.querytool', [
|
||||
this.warn_before_continue();
|
||||
}
|
||||
},
|
||||
handle_cryptkey_missing: function() {
|
||||
pgBrowser.set_master_password('', ()=>{
|
||||
this.warn_before_continue();
|
||||
});
|
||||
},
|
||||
warn_before_continue: function() {
|
||||
var self = this;
|
||||
|
||||
|
@ -25,7 +25,8 @@ from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import \
|
||||
update_session_grid_transaction
|
||||
from pgadmin.utils.ajax import make_json_response, internal_server_error
|
||||
from pgadmin.utils.driver import get_driver
|
||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost
|
||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
|
||||
CryptKeyMissing
|
||||
|
||||
|
||||
class StartRunningQuery:
|
||||
@ -63,7 +64,7 @@ class StartRunningQuery:
|
||||
auto_reconnect=False,
|
||||
use_binary_placeholder=True,
|
||||
array_to_string=True)
|
||||
except (ConnectionLost, SSHTunnelConnectionLost):
|
||||
except (ConnectionLost, SSHTunnelConnectionLost, CryptKeyMissing):
|
||||
raise
|
||||
except Exception as e:
|
||||
self.logger.error(e)
|
||||
@ -134,7 +135,7 @@ class StartRunningQuery:
|
||||
# and formatted_error is True.
|
||||
try:
|
||||
status, result = conn.execute_async(sql)
|
||||
except (ConnectionLost, SSHTunnelConnectionLost):
|
||||
except (ConnectionLost, SSHTunnelConnectionLost, CryptKeyMissing):
|
||||
raise
|
||||
|
||||
# If the transaction aborted for some reason and
|
||||
|
@ -14,6 +14,8 @@ from operator import attrgetter
|
||||
|
||||
from flask import Blueprint, current_app
|
||||
from flask_babelex import gettext
|
||||
from flask_security import current_user, login_required
|
||||
from threading import Lock
|
||||
|
||||
from .paths import get_storage_directory
|
||||
from .preferences import Preferences
|
||||
@ -330,3 +332,46 @@ SHORTCUT_FIELDS = [
|
||||
'label': gettext('Alt/Option')
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
class KeyManager:
|
||||
def __init__(self):
|
||||
self.users = dict()
|
||||
self.lock = Lock()
|
||||
|
||||
@login_required
|
||||
def get(self):
|
||||
user = self.users.get(current_user.id, None)
|
||||
if user is not None:
|
||||
return user.get('key', None)
|
||||
|
||||
@login_required
|
||||
def set(self, _key, _new_login=True):
|
||||
with self.lock:
|
||||
user = self.users.get(current_user.id, None)
|
||||
if user is None:
|
||||
self.users[current_user.id] = dict(
|
||||
session_count=1, key=_key)
|
||||
else:
|
||||
if _new_login:
|
||||
user['session_count'] += 1
|
||||
user['key'] = _key
|
||||
|
||||
@login_required
|
||||
def reset(self):
|
||||
with self.lock:
|
||||
user = self.users.get(current_user.id, None)
|
||||
|
||||
if user is not None:
|
||||
# This will not decrement if session expired
|
||||
user['session_count'] -= 1
|
||||
if user['session_count'] == 0:
|
||||
del self.users[current_user.id]
|
||||
|
||||
@login_required
|
||||
def hard_reset(self):
|
||||
with self.lock:
|
||||
user = self.users.get(current_user.id, None)
|
||||
|
||||
if user is not None:
|
||||
del self.users[current_user.id]
|
||||
|
@ -14,7 +14,8 @@ object.
|
||||
|
||||
"""
|
||||
import datetime
|
||||
from flask import session
|
||||
from flask import session, request
|
||||
from flask_login import current_user
|
||||
from flask_babelex import gettext
|
||||
import psycopg2
|
||||
from psycopg2.extensions import adapt
|
||||
@ -74,23 +75,25 @@ class Driver(BaseDriver):
|
||||
assert (sid is not None and isinstance(sid, int))
|
||||
managers = None
|
||||
|
||||
server_data = Server.query.filter_by(id=sid).first()
|
||||
if server_data is None:
|
||||
return None
|
||||
|
||||
if session.sid not in self.managers:
|
||||
self.managers[session.sid] = managers = dict()
|
||||
if '__pgsql_server_managers' in session:
|
||||
session_managers = session['__pgsql_server_managers'].copy()
|
||||
session['__pgsql_server_managers'] = dict()
|
||||
|
||||
for server_id in session_managers:
|
||||
s = Server.query.filter_by(id=server_id).first()
|
||||
|
||||
if not s:
|
||||
continue
|
||||
|
||||
manager = managers[str(server_id)] = ServerManager(s)
|
||||
manager._restore(session_managers[server_id])
|
||||
manager = managers[str(sid)] = ServerManager(server_data)
|
||||
if sid in session_managers:
|
||||
manager._restore(session_managers[sid])
|
||||
manager.update_session()
|
||||
else:
|
||||
managers = self.managers[session.sid]
|
||||
if str(sid) in managers:
|
||||
manager = managers[str(sid)]
|
||||
manager._restore_connections()
|
||||
manager.update_session()
|
||||
|
||||
managers['pinged'] = datetime.datetime.now()
|
||||
if str(sid) not in managers:
|
||||
|
@ -28,16 +28,17 @@ from pgadmin.utils.crypto import decrypt
|
||||
from psycopg2.extensions import adapt, encodings
|
||||
|
||||
import config
|
||||
from pgadmin.model import Server, User
|
||||
from pgadmin.utils.exception import ConnectionLost
|
||||
from pgadmin.model import User
|
||||
from pgadmin.utils.exception import ConnectionLost, CryptKeyMissing
|
||||
from pgadmin.utils import get_complete_file_path
|
||||
from ..abstract import BaseDriver, BaseConnection
|
||||
from ..abstract import BaseConnection
|
||||
from .cursor import DictCursor
|
||||
from .typecast import register_global_typecasters, \
|
||||
register_string_typecasters, register_binary_typecasters, \
|
||||
register_array_to_string_typecasters, ALL_JSON_TYPES
|
||||
from .encoding import getEncoding, configureDriverEncodings
|
||||
from pgadmin.utils import csv
|
||||
from pgadmin.utils.master_password import get_crypt_key
|
||||
|
||||
if sys.version_info < (3,):
|
||||
from StringIO import StringIO
|
||||
@ -242,10 +243,16 @@ class Connection(BaseConnection):
|
||||
if encpass is None:
|
||||
encpass = self.password or getattr(manager, 'password', None)
|
||||
|
||||
self.password = encpass
|
||||
|
||||
# Reset the existing connection password
|
||||
if self.reconnecting is not False:
|
||||
self.password = None
|
||||
|
||||
crypt_key_present, crypt_key = get_crypt_key()
|
||||
if not crypt_key_present:
|
||||
raise CryptKeyMissing()
|
||||
|
||||
if encpass:
|
||||
# Fetch Logged in User Details.
|
||||
user = User.query.filter_by(id=current_user.id).first()
|
||||
@ -254,14 +261,13 @@ class Connection(BaseConnection):
|
||||
return False, gettext("Unauthorized request.")
|
||||
|
||||
try:
|
||||
password = decrypt(encpass, user.password)
|
||||
password = decrypt(encpass, crypt_key)
|
||||
# Handling of non ascii password (Python2)
|
||||
if hasattr(str, 'decode'):
|
||||
password = password.decode('utf-8').encode('utf-8')
|
||||
# password is in bytes, for python3 we need it in string
|
||||
elif isinstance(password, bytes):
|
||||
password = password.decode()
|
||||
|
||||
except Exception as e:
|
||||
manager.stop_ssh_tunnel()
|
||||
current_app.logger.exception(e)
|
||||
@ -521,6 +527,9 @@ WHERE
|
||||
|
||||
def __cursor(self, server_cursor=False):
|
||||
|
||||
if not get_crypt_key()[0]:
|
||||
raise CryptKeyMissing()
|
||||
|
||||
# Check SSH Tunnel is alive or not. If used by the database
|
||||
# server for the connection.
|
||||
if self.manager.use_ssh_tunnel == 1:
|
||||
@ -1081,7 +1090,7 @@ WHERE
|
||||
current_app.logger.exception(e)
|
||||
self.reconnecting = False
|
||||
|
||||
current_app.warning(
|
||||
current_app.logger.warning(
|
||||
"Failed to reconnect the database server "
|
||||
"(#{server_id})".format(
|
||||
server_id=self.manager.sid,
|
||||
@ -1283,7 +1292,11 @@ WHERE
|
||||
if user is None:
|
||||
return False, gettext("Unauthorized request.")
|
||||
|
||||
password = decrypt(password, user.password).decode()
|
||||
crypt_key_present, crypt_key = get_crypt_key()
|
||||
if not crypt_key_present:
|
||||
return False, crypt_key
|
||||
|
||||
password = decrypt(password, crypt_key).decode()
|
||||
|
||||
try:
|
||||
pg_conn = psycopg2.connect(
|
||||
@ -1567,7 +1580,12 @@ Failed to reset the connection to the server due to following error:
|
||||
if user is None:
|
||||
return False, gettext("Unauthorized request.")
|
||||
|
||||
password = decrypt(password, user.password).decode()
|
||||
crypt_key_present, crypt_key = get_crypt_key()
|
||||
if not crypt_key_present:
|
||||
return False, crypt_key
|
||||
|
||||
password = decrypt(password, crypt_key)\
|
||||
.decode()
|
||||
|
||||
try:
|
||||
pg_conn = psycopg2.connect(
|
||||
|
@ -19,13 +19,19 @@ from flask_babelex import gettext
|
||||
|
||||
from pgadmin.utils import get_complete_file_path
|
||||
from pgadmin.utils.crypto import decrypt
|
||||
from pgadmin.utils.master_password import process_masterpass_disabled
|
||||
from .connection import Connection
|
||||
from pgadmin.model import Server, User
|
||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost
|
||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
|
||||
CryptKeyMissing
|
||||
from pgadmin.utils.master_password import get_crypt_key
|
||||
from threading import Lock
|
||||
|
||||
if config.SUPPORT_SSH_TUNNEL:
|
||||
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
|
||||
|
||||
connection_restore_lock = Lock()
|
||||
|
||||
|
||||
class ServerManager(object):
|
||||
"""
|
||||
@ -185,6 +191,10 @@ class ServerManager(object):
|
||||
maintenance_db_id = u'DB:{0}'.format(self.db)
|
||||
if maintenance_db_id in self.connections:
|
||||
conn = self.connections[maintenance_db_id]
|
||||
# try to connect maintenance db if not connected
|
||||
if not conn.connected():
|
||||
conn.connect()
|
||||
|
||||
if conn.connected():
|
||||
status, res = conn.execute_dict(u"""
|
||||
SELECT
|
||||
@ -205,6 +215,10 @@ WHERE db.oid = {0}""".format(did))
|
||||
"Could not find the specified database."
|
||||
))
|
||||
|
||||
if not get_crypt_key()[0]:
|
||||
# the reason its not connected might be missing key
|
||||
raise CryptKeyMissing()
|
||||
|
||||
if database is None:
|
||||
# Check SSH Tunnel is alive or not.
|
||||
if self.use_ssh_tunnel == 1:
|
||||
@ -239,6 +253,15 @@ WHERE db.oid = {0}""".format(did))
|
||||
"""
|
||||
# restore server version from flask session if flask server was
|
||||
# restarted. As we need server version to resolve sql template paths.
|
||||
masterpass_processed = process_masterpass_disabled()
|
||||
|
||||
# The data variable is a copy so is not automatically synced
|
||||
# update here
|
||||
if masterpass_processed and 'password' in data:
|
||||
data['password'] = None
|
||||
if masterpass_processed and 'tunnel_password' in data:
|
||||
data['tunnel_password'] = None
|
||||
|
||||
from pgadmin.browser.server_groups.servers.types import ServerType
|
||||
|
||||
self.ver = data.get('ver', None)
|
||||
@ -251,17 +274,13 @@ WHERE db.oid = {0}""".format(did))
|
||||
self.server_cls = st
|
||||
break
|
||||
|
||||
# Hmm.. we will not honour this request, when I already have
|
||||
# connections
|
||||
if len(self.connections) != 0:
|
||||
return
|
||||
|
||||
# We need to know about the existing server variant supports during
|
||||
# first connection for identifications.
|
||||
self.pinged = datetime.datetime.now()
|
||||
try:
|
||||
if 'password' in data and data['password']:
|
||||
data['password'] = data['password'].encode('utf-8')
|
||||
if hasattr(data['password'], 'encode'):
|
||||
data['password'] = data['password'].encode('utf-8')
|
||||
if 'tunnel_password' in data and data['tunnel_password']:
|
||||
data['tunnel_password'] = \
|
||||
data['tunnel_password'].encode('utf-8')
|
||||
@ -269,36 +288,79 @@ WHERE db.oid = {0}""".format(did))
|
||||
current_app.logger.exception(e)
|
||||
|
||||
connections = data['connections']
|
||||
for conn_id in connections:
|
||||
conn_info = connections[conn_id]
|
||||
conn = self.connections[conn_info['conn_id']] = Connection(
|
||||
self, conn_info['conn_id'], conn_info['database'],
|
||||
conn_info['auto_reconnect'], conn_info['async_'],
|
||||
use_binary_placeholder=conn_info['use_binary_placeholder'],
|
||||
array_to_string=conn_info['array_to_string']
|
||||
)
|
||||
|
||||
# only try to reconnect if connection was connected previously and
|
||||
# auto_reconnect is true.
|
||||
if conn_info['wasConnected'] and conn_info['auto_reconnect']:
|
||||
try:
|
||||
# Check SSH Tunnel needs to be created
|
||||
if self.use_ssh_tunnel == 1 and not self.tunnel_created:
|
||||
status, error = self.create_ssh_tunnel(
|
||||
data['tunnel_password'])
|
||||
|
||||
# Check SSH Tunnel is alive or not.
|
||||
self.check_ssh_tunnel_alive()
|
||||
|
||||
conn.connect(
|
||||
password=data['password'],
|
||||
server_types=ServerType.types()
|
||||
with connection_restore_lock:
|
||||
for conn_id in connections:
|
||||
conn_info = connections[conn_id]
|
||||
if conn_info['conn_id'] in self.connections:
|
||||
conn = self.connections[conn_info['conn_id']]
|
||||
else:
|
||||
conn = self.connections[conn_info['conn_id']] = Connection(
|
||||
self, conn_info['conn_id'], conn_info['database'],
|
||||
conn_info['auto_reconnect'], conn_info['async_'],
|
||||
use_binary_placeholder=conn_info[
|
||||
'use_binary_placeholder'],
|
||||
array_to_string=conn_info['array_to_string']
|
||||
)
|
||||
# This will also update wasConnected flag in connection so
|
||||
# no need to update the flag manually.
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
self.connections.pop(conn_info['conn_id'])
|
||||
|
||||
# only try to reconnect if connection was connected previously
|
||||
# and auto_reconnect is true.
|
||||
if conn_info['wasConnected'] and conn_info['auto_reconnect']:
|
||||
try:
|
||||
# Check SSH Tunnel needs to be created
|
||||
if self.use_ssh_tunnel == 1 and \
|
||||
not self.tunnel_created:
|
||||
status, error = self.create_ssh_tunnel(
|
||||
data['tunnel_password'])
|
||||
|
||||
# Check SSH Tunnel is alive or not.
|
||||
self.check_ssh_tunnel_alive()
|
||||
|
||||
conn.connect(
|
||||
password=data['password'],
|
||||
server_types=ServerType.types()
|
||||
)
|
||||
# This will also update wasConnected flag in
|
||||
# connection so no need to update the flag manually.
|
||||
except CryptKeyMissing:
|
||||
# maintain the status as this will help to restore once
|
||||
# the key is available
|
||||
conn.wasConnected = conn_info['wasConnected']
|
||||
conn.auto_reconnect = conn_info['auto_reconnect']
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
self.connections.pop(conn_info['conn_id'])
|
||||
raise
|
||||
|
||||
def _restore_connections(self):
|
||||
with connection_restore_lock:
|
||||
for conn_id in self.connections:
|
||||
conn = self.connections[conn_id]
|
||||
# only try to reconnect if connection was connected previously
|
||||
# and auto_reconnect is true.
|
||||
wasConnected = conn.wasConnected
|
||||
auto_reconnect = conn.auto_reconnect
|
||||
if conn.wasConnected and conn.auto_reconnect:
|
||||
try:
|
||||
# Check SSH Tunnel needs to be created
|
||||
if self.use_ssh_tunnel == 1 and \
|
||||
not self.tunnel_created:
|
||||
status, error = self.create_ssh_tunnel()
|
||||
|
||||
# Check SSH Tunnel is alive or not.
|
||||
self.check_ssh_tunnel_alive()
|
||||
|
||||
conn.connect()
|
||||
# This will also update wasConnected flag in
|
||||
# connection so no need to update the flag manually.
|
||||
except CryptKeyMissing:
|
||||
# maintain the status as this will help to restore once
|
||||
# the key is available
|
||||
conn.wasConnected = wasConnected
|
||||
conn.auto_reconnect = auto_reconnect
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
raise
|
||||
|
||||
def release(self, database=None, conn_id=None, did=None):
|
||||
# Stop the SSH tunnel if release() function calls without
|
||||
|
@ -80,3 +80,28 @@ class SSHTunnelConnectionLost(HTTPException):
|
||||
def __repr__(self):
|
||||
return "Connection to the SSH Tunnel for host '{0}' has been lost. " \
|
||||
"Reconnect to the database server".format(self.tunnel_host)
|
||||
|
||||
|
||||
class CryptKeyMissing(HTTPException):
|
||||
"""
|
||||
Exception
|
||||
"""
|
||||
|
||||
def __init__(self):
|
||||
HTTPException.__init__(self)
|
||||
|
||||
@property
|
||||
def name(self):
|
||||
return HTTP_STATUS_CODES.get(503, 'Service Unavailable')
|
||||
|
||||
def get_response(self, environ=None):
|
||||
return service_unavailable(
|
||||
_("Crypt key is missing."),
|
||||
info="CRYPTKEY_MISSING",
|
||||
)
|
||||
|
||||
def __str__(self):
|
||||
return "Crypt key is missing."
|
||||
|
||||
def __repr__(self):
|
||||
return "Crypt key is missing."
|
||||
|
119
web/pgadmin/utils/master_password.py
Normal file
119
web/pgadmin/utils/master_password.py
Normal file
@ -0,0 +1,119 @@
|
||||
import config
|
||||
from flask import current_app
|
||||
from flask_login import current_user
|
||||
from pgadmin.model import db, User, Server
|
||||
from pgadmin.utils.crypto import encrypt, decrypt
|
||||
|
||||
|
||||
MASTERPASS_CHECK_TEXT = 'ideas are bulletproof'
|
||||
|
||||
|
||||
def set_crypt_key(_key, _new_login=True):
|
||||
"""
|
||||
Set the crypt key
|
||||
:param _key: The key
|
||||
:param _new_login: Is fresh login or password change
|
||||
"""
|
||||
current_app.keyManager.set(_key, _new_login)
|
||||
|
||||
|
||||
def get_crypt_key():
|
||||
"""
|
||||
Returns the crypt key
|
||||
:return: the key
|
||||
"""
|
||||
enc_key = current_app.keyManager.get()
|
||||
|
||||
# if desktop mode and master pass disabled then use the password hash
|
||||
if not config.MASTER_PASSWORD_REQUIRED \
|
||||
and not config.SERVER_MODE:
|
||||
return True, current_user.password
|
||||
# if desktop mode and master pass enabled
|
||||
elif config.MASTER_PASSWORD_REQUIRED \
|
||||
and not config.SERVER_MODE and enc_key is None:
|
||||
return False, None
|
||||
else:
|
||||
return True, enc_key
|
||||
|
||||
|
||||
def validate_master_password(password):
|
||||
"""
|
||||
Validate the password/key against the stored encrypted text
|
||||
:param password: password/key
|
||||
:return: Valid or not
|
||||
"""
|
||||
# master pass is incorrect if decryption fails
|
||||
try:
|
||||
decrypted_text = decrypt(current_user.masterpass_check, password)
|
||||
|
||||
if isinstance(decrypted_text, bytes):
|
||||
decrypted_text = decrypted_text.decode()
|
||||
|
||||
if MASTERPASS_CHECK_TEXT != decrypted_text:
|
||||
return False
|
||||
else:
|
||||
return True
|
||||
except Exception as _:
|
||||
False
|
||||
|
||||
|
||||
def set_masterpass_check_text(password, clear=False):
|
||||
"""
|
||||
Set the encrypted text which will be used later to validate entered key
|
||||
:param password: password/key
|
||||
:param clear: remove the encrypted text
|
||||
"""
|
||||
try:
|
||||
masterpass_check = None
|
||||
if not clear:
|
||||
masterpass_check = encrypt(MASTERPASS_CHECK_TEXT, password)
|
||||
|
||||
# set the encrypted sample text with the new
|
||||
# master pass
|
||||
db.session.query(User) \
|
||||
.filter(User.id == current_user.id) \
|
||||
.update({User.masterpass_check: masterpass_check})
|
||||
db.session.commit()
|
||||
|
||||
except Exception as _:
|
||||
db.session.rollback()
|
||||
raise
|
||||
|
||||
|
||||
def cleanup_master_password():
|
||||
"""
|
||||
Remove the master password and saved passwords from DB which are
|
||||
encrypted using master password. Also remove the encrypted text
|
||||
"""
|
||||
|
||||
# also remove the master password check string as it will help if master
|
||||
# password entered/enabled again
|
||||
set_masterpass_check_text('', clear=True)
|
||||
|
||||
from pgadmin.browser.server_groups.servers.utils \
|
||||
import remove_saved_passwords
|
||||
remove_saved_passwords(current_user.id)
|
||||
|
||||
current_app.keyManager.hard_reset()
|
||||
|
||||
from pgadmin.utils.driver import get_driver
|
||||
driver = get_driver(config.PG_DEFAULT_DRIVER)
|
||||
|
||||
for server in Server.query.filter_by(user_id=current_user.id).all():
|
||||
manager = driver.connection_manager(server.id)
|
||||
manager.update(server)
|
||||
|
||||
|
||||
def process_masterpass_disabled():
|
||||
"""
|
||||
On master password disable, remove the connection data from session as it
|
||||
may have saved password which will cause trouble
|
||||
:param session: Flask session
|
||||
:param conn_data: connection manager copy from session if any
|
||||
"""
|
||||
if not config.SERVER_MODE and not config.MASTER_PASSWORD_REQUIRED \
|
||||
and current_user.masterpass_check is not None:
|
||||
cleanup_master_password()
|
||||
return True
|
||||
|
||||
return False
|
@ -59,6 +59,8 @@ if config.SERVER_MODE is True:
|
||||
config.SECURITY_CHANGEABLE = True
|
||||
config.SECURITY_POST_CHANGE_VIEW = 'browser.change_password'
|
||||
|
||||
# disable master password for test cases
|
||||
config.MASTER_PASSWORD_REQUIRED = False
|
||||
|
||||
from regression import test_setup
|
||||
from regression.feature_utils.app_starter import AppStarter
|
||||
@ -187,7 +189,14 @@ def get_test_modules(arguments):
|
||||
global driver, app_starter, handle_cleanup
|
||||
|
||||
if not config.SERVER_MODE:
|
||||
exclude_pkgs.append("browser.tests")
|
||||
# following test cases applicable only for server mode
|
||||
exclude_pkgs.extend([
|
||||
"browser.tests.test_change_password",
|
||||
"browser.tests.test_gravatar_image_display",
|
||||
"browser.tests.test_login",
|
||||
"browser.tests.test_logout",
|
||||
"browser.tests.test_reset_password",
|
||||
])
|
||||
if arguments['exclude'] is not None:
|
||||
exclude_pkgs += arguments['exclude'].split(',')
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user