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
|
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
|
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
|
*Connect to server* to authenticate with the server, and start using pgAdmin to
|
||||||
manage objects that reside on the server.
|
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.
|
field.
|
||||||
* Provide your password in the *Password* field.
|
* Provide your password in the *Password* field.
|
||||||
* Click the *Login* button to securely log into pgAdmin.
|
* 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
|
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 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 #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 #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 #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 #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 #4208 <https://redmine.postgresql.org/issues/4208>`_ - Update the UI logo.
|
||||||
| `Bug #4217 <https://redmine.postgresql.org/issues/4217>`_ - Fixed CSRF security vulnerability issue.
|
| `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.
|
# Set to False to disable password saving.
|
||||||
ALLOW_SAVE_TUNNEL_PASSWORD = False
|
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
|
# 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)
|
Security(app, user_datastore, register_blueprint=False)
|
||||||
else:
|
else:
|
||||||
app.config['SECURITY_PASSWORD_SALT'] = current_salt
|
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
|
# This will upgrade the plaintext password of all the user as per the
|
||||||
# SECURITY_PASSWORD_HASH.
|
# SECURITY_PASSWORD_HASH.
|
||||||
for user in users:
|
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_mail import Mail
|
||||||
from flask_paranoid import Paranoid
|
from flask_paranoid import Paranoid
|
||||||
from flask_security import Security, SQLAlchemyUserDatastore, current_user
|
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.datastructures import ImmutableDict
|
||||||
from werkzeug.local import LocalProxy
|
from werkzeug.local import LocalProxy
|
||||||
from werkzeug.utils import find_modules
|
from werkzeug.utils import find_modules
|
||||||
|
|
||||||
from pgadmin.model import db, Role, Server, ServerGroup, \
|
from pgadmin.model import db, Role, Server, ServerGroup, \
|
||||||
User, Keys, Version, SCHEMA_VERSION as CURRENT_SCHEMA_VERSION
|
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.preferences import Preferences
|
||||||
from pgadmin.utils.session import create_session_interface, pga_unauthorised
|
from pgadmin.utils.session import create_session_interface, pga_unauthorised
|
||||||
from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader
|
from pgadmin.utils.versioned_template_loader import VersionedTemplateLoader
|
||||||
@ -575,12 +574,23 @@ def create_app(app_name=None):
|
|||||||
def force_session_write(app, user):
|
def force_session_write(app, user):
|
||||||
session.force_write = True
|
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)
|
@user_logged_out.connect_via(app)
|
||||||
def current_user_cleanup(app, user):
|
def current_user_cleanup(app, user):
|
||||||
from config import PG_DEFAULT_DRIVER
|
from config import PG_DEFAULT_DRIVER
|
||||||
from pgadmin.utils.driver import get_driver
|
from pgadmin.utils.driver import get_driver
|
||||||
from flask import current_app
|
from flask import current_app
|
||||||
|
|
||||||
|
# remove key
|
||||||
|
current_app.keyManager.reset()
|
||||||
|
|
||||||
for mdl in current_app.logout_hooks:
|
for mdl in current_app.logout_hooks:
|
||||||
try:
|
try:
|
||||||
mdl.on_logout(user)
|
mdl.on_logout(user)
|
||||||
@ -631,6 +641,12 @@ def create_app(app_name=None):
|
|||||||
abort(401)
|
abort(401)
|
||||||
login_user(user)
|
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
|
@app.after_request
|
||||||
def after_request(response):
|
def after_request(response):
|
||||||
if 'key' in request.args:
|
if 'key' in request.args:
|
||||||
@ -711,6 +727,9 @@ def create_app(app_name=None):
|
|||||||
current_app.logger.error(e, exc_info=True)
|
current_app.logger.error(e, exc_info=True)
|
||||||
return e
|
return e
|
||||||
|
|
||||||
|
# Intialize the key manager
|
||||||
|
app.keyManager = KeyManager()
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
# Protection against CSRF attacks
|
# Protection against CSRF attacks
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
@ -18,7 +18,7 @@ from socket import error as SOCKETErrorException
|
|||||||
|
|
||||||
import six
|
import six
|
||||||
from flask import current_app, render_template, url_for, make_response, \
|
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_babelex import gettext
|
||||||
from flask_gravatar import Gravatar
|
from flask_gravatar import Gravatar
|
||||||
from flask_login import current_user, login_required
|
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.utils.preferences import Preferences
|
||||||
from pgadmin.browser.register_browser_preferences import \
|
from pgadmin.browser.register_browser_preferences import \
|
||||||
register_browser_preferences
|
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:
|
try:
|
||||||
import urllib.request as urlreq
|
import urllib.request as urlreq
|
||||||
@ -220,7 +223,10 @@ class BrowserModule(PgAdminModule):
|
|||||||
Returns:
|
Returns:
|
||||||
list: a list of url endpoints exposed to the client.
|
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__)
|
blueprint = BrowserModule(MODULE_NAME, __name__)
|
||||||
@ -682,6 +688,133 @@ def get_nodes():
|
|||||||
return make_json_response(data=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
|
# Only register route if SECURITY_CHANGEABLE is set to True
|
||||||
# We can't access app context here so cannot
|
# We can't access app context here so cannot
|
||||||
# use app.config['SECURITY_CHANGEABLE']
|
# 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:
|
if request.json is None and not has_error:
|
||||||
after_this_request(_commit)
|
after_this_request(_commit)
|
||||||
do_flash(*get_message('PASSWORD_CHANGE'))
|
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
|
return redirect(get_url(_security.post_change_view) or
|
||||||
get_url(_security.post_login_view))
|
get_url(_security.post_login_view))
|
||||||
|
|
||||||
|
@ -26,6 +26,8 @@ import config
|
|||||||
from config import PG_DEFAULT_DRIVER
|
from config import PG_DEFAULT_DRIVER
|
||||||
from pgadmin.model import db, Server, ServerGroup, User
|
from pgadmin.model import db, Server, ServerGroup, User
|
||||||
from pgadmin.utils.driver import get_driver
|
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):
|
def has_any(data, keys):
|
||||||
@ -117,9 +119,16 @@ class ServerModule(sg.ServerGroupPluginModule):
|
|||||||
driver = get_driver(PG_DEFAULT_DRIVER)
|
driver = get_driver(PG_DEFAULT_DRIVER)
|
||||||
|
|
||||||
for server in servers:
|
for server in servers:
|
||||||
|
connected = False
|
||||||
|
manager = None
|
||||||
|
try:
|
||||||
manager = driver.connection_manager(server.id)
|
manager = driver.connection_manager(server.id)
|
||||||
conn = manager.connection()
|
conn = manager.connection()
|
||||||
connected = conn.connected()
|
connected = conn.connected()
|
||||||
|
except CryptKeyMissing:
|
||||||
|
# show the nodes at least even if not able to connect.
|
||||||
|
pass
|
||||||
|
|
||||||
in_recovery = None
|
in_recovery = None
|
||||||
wal_paused = None
|
wal_paused = None
|
||||||
|
|
||||||
@ -723,6 +732,11 @@ class ServerNode(PGChildNodeView):
|
|||||||
request.data, encoding='utf-8'
|
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
|
# Some fields can be provided with service file so they are optional
|
||||||
if 'service' in data and not data['service']:
|
if 'service' in data and not data['service']:
|
||||||
required_args.extend([
|
required_args.extend([
|
||||||
@ -807,7 +821,7 @@ class ServerNode(PGChildNodeView):
|
|||||||
# login with password
|
# login with password
|
||||||
have_password = True
|
have_password = True
|
||||||
password = data['password']
|
password = data['password']
|
||||||
password = encrypt(password, current_user.password)
|
password = encrypt(password, crypt_key)
|
||||||
elif 'passfile' in data and data["passfile"] != '':
|
elif 'passfile' in data and data["passfile"] != '':
|
||||||
passfile = data['passfile']
|
passfile = data['passfile']
|
||||||
setattr(server, 'passfile', passfile)
|
setattr(server, 'passfile', passfile)
|
||||||
@ -817,7 +831,7 @@ class ServerNode(PGChildNodeView):
|
|||||||
have_tunnel_password = True
|
have_tunnel_password = True
|
||||||
tunnel_password = data['tunnel_password']
|
tunnel_password = data['tunnel_password']
|
||||||
tunnel_password = \
|
tunnel_password = \
|
||||||
encrypt(tunnel_password, current_user.password)
|
encrypt(tunnel_password, crypt_key)
|
||||||
|
|
||||||
status, errmsg = conn.connect(
|
status, errmsg = conn.connect(
|
||||||
password=password,
|
password=password,
|
||||||
@ -998,6 +1012,11 @@ class ServerNode(PGChildNodeView):
|
|||||||
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
|
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
|
||||||
conn = manager.connection()
|
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 using SSH Tunnel
|
||||||
if server.use_ssh_tunnel:
|
if server.use_ssh_tunnel:
|
||||||
if 'tunnel_password' not in data:
|
if 'tunnel_password' not in data:
|
||||||
@ -1014,12 +1033,12 @@ class ServerNode(PGChildNodeView):
|
|||||||
# Encrypt the password before saving with user's login
|
# Encrypt the password before saving with user's login
|
||||||
# password key.
|
# password key.
|
||||||
try:
|
try:
|
||||||
tunnel_password = encrypt(tunnel_password, user.password) \
|
tunnel_password = encrypt(tunnel_password, crypt_key) \
|
||||||
if tunnel_password is not None else \
|
if tunnel_password is not None else \
|
||||||
server.tunnel_password
|
server.tunnel_password
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.exception(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:
|
if 'password' not in data:
|
||||||
conn_passwd = getattr(conn, 'password', None)
|
conn_passwd = getattr(conn, 'password', None)
|
||||||
@ -1038,11 +1057,11 @@ class ServerNode(PGChildNodeView):
|
|||||||
# Encrypt the password before saving with user's login
|
# Encrypt the password before saving with user's login
|
||||||
# password key.
|
# password key.
|
||||||
try:
|
try:
|
||||||
password = encrypt(password, user.password) \
|
password = encrypt(password, crypt_key) \
|
||||||
if password is not None else server.password
|
if password is not None else server.password
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.exception(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
|
# Check do we need to prompt for the database server or ssh tunnel
|
||||||
# password or both. Return the password template in case password is
|
# password or both. Return the password template in case password is
|
||||||
@ -1235,6 +1254,7 @@ class ServerNode(PGChildNodeView):
|
|||||||
"""
|
"""
|
||||||
try:
|
try:
|
||||||
data = json.loads(request.form['data'], encoding='utf-8')
|
data = json.loads(request.form['data'], encoding='utf-8')
|
||||||
|
crypt_key = get_crypt_key()[1]
|
||||||
|
|
||||||
# Fetch Server Details
|
# Fetch Server Details
|
||||||
server = Server.query.filter_by(id=sid).first()
|
server = Server.query.filter_by(id=sid).first()
|
||||||
@ -1292,7 +1312,7 @@ class ServerNode(PGChildNodeView):
|
|||||||
|
|
||||||
# Check against old password only if no pgpass file
|
# Check against old password only if no pgpass file
|
||||||
if not is_passfile:
|
if not is_passfile:
|
||||||
decrypted_password = decrypt(manager.password, user.password)
|
decrypted_password = decrypt(manager.password, crypt_key)
|
||||||
|
|
||||||
if isinstance(decrypted_password, bytes):
|
if isinstance(decrypted_password, bytes):
|
||||||
decrypted_password = decrypted_password.decode()
|
decrypted_password = decrypted_password.decode()
|
||||||
@ -1328,7 +1348,7 @@ class ServerNode(PGChildNodeView):
|
|||||||
|
|
||||||
# Store password in sqlite only if no pgpass file
|
# Store password in sqlite only if no pgpass file
|
||||||
if not is_passfile:
|
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.
|
# Check if old password was stored in pgadmin4 sqlite database.
|
||||||
# If yes then update that password.
|
# If yes then update that password.
|
||||||
if server.password is not None and config.ALLOW_SAVE_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,
|
def get_response_for_password(self, server, status, prompt_password=False,
|
||||||
prompt_tunnel_password=False, errmsg=None):
|
prompt_tunnel_password=False, errmsg=None):
|
||||||
|
|
||||||
if server.use_ssh_tunnel:
|
if server.use_ssh_tunnel:
|
||||||
return make_json_response(
|
return make_json_response(
|
||||||
success=0,
|
success=0,
|
||||||
@ -1498,7 +1519,7 @@ class ServerNode(PGChildNodeView):
|
|||||||
server_label=server.name,
|
server_label=server.name,
|
||||||
username=server.username,
|
username=server.username,
|
||||||
errmsg=errmsg,
|
errmsg=errmsg,
|
||||||
_=gettext
|
_=gettext,
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
|
|
||||||
|
@ -166,7 +166,6 @@ class DatabaseView(PGChildNodeView):
|
|||||||
def wrap(f):
|
def wrap(f):
|
||||||
@wraps(f)
|
@wraps(f)
|
||||||
def wrapped(self, *args, **kwargs):
|
def wrapped(self, *args, **kwargs):
|
||||||
|
|
||||||
self.manager = get_driver(
|
self.manager = get_driver(
|
||||||
PG_DEFAULT_DRIVER
|
PG_DEFAULT_DRIVER
|
||||||
).connection_manager(
|
).connection_manager(
|
||||||
|
@ -489,11 +489,15 @@ define('pgadmin.node.database', [
|
|||||||
|
|
||||||
Alertify.pgNotifier('error', xhr, error, function(msg) {
|
Alertify.pgNotifier('error', xhr, error, function(msg) {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
|
if (msg == 'CRYPTKEY_SET') {
|
||||||
|
connect_to_database(_model, _data, _tree, _item, _wasConnected);
|
||||||
|
} else {
|
||||||
Alertify.dlgServerPass(
|
Alertify.dlgServerPass(
|
||||||
gettext('Connect to database'),
|
gettext('Connect to database'),
|
||||||
msg, _model, _data, _tree, _item, _status,
|
msg, _model, _data, _tree, _item, _status,
|
||||||
onSuccess, onFailure, onCancel
|
onSuccess, onFailure, onCancel
|
||||||
).resizeTo();
|
).resizeTo();
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -1126,10 +1126,14 @@ define('pgadmin.node.server', [
|
|||||||
|
|
||||||
Alertify.pgNotifier('error', xhr, error, function(msg) {
|
Alertify.pgNotifier('error', xhr, error, function(msg) {
|
||||||
setTimeout(function() {
|
setTimeout(function() {
|
||||||
|
if (msg == 'CRYPTKEY_SET') {
|
||||||
|
connect_to_server(_node, _data, _tree, _item, _wasConnected);
|
||||||
|
} else {
|
||||||
Alertify.dlgServerPass(
|
Alertify.dlgServerPass(
|
||||||
gettext('Connect to Server'),
|
gettext('Connect to Server'),
|
||||||
msg, _node, _data, _tree, _item, _wasConnected
|
msg, _node, _data, _tree, _item, _wasConnected
|
||||||
).resizeTo();
|
).resizeTo();
|
||||||
|
}
|
||||||
}, 100);
|
}, 100);
|
||||||
});
|
});
|
||||||
},
|
},
|
||||||
|
@ -1,23 +1,33 @@
|
|||||||
<form name="frmPassword" id="frmPassword" style="height: 100%; width: 100%" onsubmit="return false;">
|
<form name="frmPassword" id="frmPassword" style="height: 100%; width: 100%" onsubmit="return false;">
|
||||||
<div>{% if errmsg %}
|
<div>
|
||||||
<div class="highlight has-error">
|
|
||||||
<div class='control-label'>{{ errmsg }}</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
<div><b>{{ _('Please enter the password for the user \'{0}\' to connect the server - "{1}"').format(username,
|
<div><b>{{ _('Please enter the password for the user \'{0}\' to connect the server - "{1}"').format(username,
|
||||||
server_label) }}</b></div>
|
server_label) }}</b></div>
|
||||||
<div style="padding: 5px; height: 1px;"></div>
|
<div class="input-group row py-2">
|
||||||
<div style="width: 100%">
|
<label for="password" class="col-sm-2 col-form-label">Password</label>
|
||||||
<span style="width: 25%;display: inline-table;">Password</span>
|
<div class="col-sm-10">
|
||||||
<span style="width: 73%;display: inline-block;">
|
<input id="password" class="form-control" name="password" type="password">
|
||||||
<input style="width:100%" id="password" class="form-control" name="password" type="password">
|
</div>
|
||||||
</span>
|
</div>
|
||||||
<span style="margin-left: 25%; padding-top: 15px;width: 45%;display: inline-block;">
|
<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"
|
<input id="save_password" name="save_password" type="checkbox"
|
||||||
{% if not config.ALLOW_SAVE_PASSWORD %}disabled{% endif %}
|
{% 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>
|
</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>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -1,42 +1,53 @@
|
|||||||
<form name="frmPassword" id="frmPassword" style="height: 100%; width: 100%" onsubmit="return false;">
|
<form name="frmPassword" id="frmPassword" onsubmit="return false;">
|
||||||
<div>{% if errmsg %}
|
<div class="m-1">
|
||||||
<div class="highlight has-error">
|
|
||||||
<div class='control-label'>{{ errmsg }}</div>
|
|
||||||
</div>
|
|
||||||
{% endif %}
|
|
||||||
{% if prompt_tunnel_password %}
|
{% if prompt_tunnel_password %}
|
||||||
{% if tunnel_identity_file %}
|
{% 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>
|
<div><b>{{ _('SSH Tunnel password for the identity file \'{0}\' to connect the server "{1}"').format(tunnel_identity_file, tunnel_host) }}</b></div>
|
||||||
{% else %}
|
{% else %}
|
||||||
<div><b>{{ _('SSH Tunnel password for the user \'{0}\' to connect the server "{1}"').format(tunnel_username, tunnel_host) }}</b></div>
|
<div><b>{{ _('SSH Tunnel password for the user \'{0}\' to connect the server "{1}"').format(tunnel_username, tunnel_host) }}</b></div>
|
||||||
{% endif %}
|
{% endif %}
|
||||||
<div style="padding: 5px; height: 1px;"></div>
|
<div class="input-group py-2">
|
||||||
<div style="width: 100%">
|
<div class="w-100">
|
||||||
<span style="width: 97%;display: inline-block;">
|
<input id="tunnel_password" class="form-control" name="tunnel_password" type="password">
|
||||||
<input style="width:100%" id="tunnel_password" class="form-control" name="tunnel_password" type="password">
|
</div>
|
||||||
</span>
|
</div>
|
||||||
<span style="padding-top: 5px;display: inline-block;">
|
<div class="save-password-div input-group py-2">
|
||||||
<input id="save_tunnel_password" name="save_tunnel_password" type="checkbox"
|
<div class="w-100">
|
||||||
{% if not config.ALLOW_SAVE_TUNNEL_PASSWORD %}disabled{% endif %}
|
<input id="save_tunnel_password" name="save_tunnel_password" type="checkbox"
|
||||||
> Save Password
|
{% if not config.ALLOW_SAVE_PASSWORD %}disabled{% endif %}
|
||||||
</span>
|
>
|
||||||
|
<label for="save_tunnel_password" class="ml-1">Save Password</label>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div style="padding: 5px; height: 1px;"></div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
{% if prompt_password %}
|
{% if prompt_password %}
|
||||||
<div><b>{{ _('Database server password for the user \'{0}\' to connect the server "{1}"').format(username, server_label) }}</b></div>
|
<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 class="input-group py-2">
|
||||||
<div style="width: 100%">
|
<div class="w-100">
|
||||||
<span style="width: 97%;display: inline-block;">
|
<input id="password" class="form-control" name="password" type="password">
|
||||||
<input style="width:100%" id="password" class="form-control" name="password" type="password">
|
</div>
|
||||||
</span>
|
</div>
|
||||||
<span style="padding-top: 5px;display: inline-block;">
|
<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"
|
<input id="save_password" name="save_password" type="checkbox"
|
||||||
{% if not config.ALLOW_SAVE_PASSWORD %}disabled{% endif %}
|
{% 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>
|
||||||
<div style="padding: 5px; height: 1px;"></div>
|
|
||||||
{% endif %}
|
{% endif %}
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
@ -8,6 +8,9 @@
|
|||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
"""Server helper utilities"""
|
"""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):
|
def parse_priv_from_db(db_privileges):
|
||||||
@ -177,3 +180,70 @@ def validate_options(options, option_name, option_value):
|
|||||||
is_valid_options = True
|
is_valid_options = True
|
||||||
|
|
||||||
return is_valid_options, valid_options
|
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: {
|
menu_categories: {
|
||||||
/* name, label (pair) */
|
/* name, label (pair) */
|
||||||
@ -282,6 +281,7 @@ define('pgadmin.browser', [
|
|||||||
scripts[n] = _.isArray(scripts[n]) ? scripts[n] : [];
|
scripts[n] = _.isArray(scripts[n]) ? scripts[n] : [];
|
||||||
scripts[n].push({'name': m, 'path': p, loaded: false});
|
scripts[n].push({'name': m, 'path': p, loaded: false});
|
||||||
},
|
},
|
||||||
|
masterpass_callback_queue: [],
|
||||||
// Build the default layout
|
// Build the default layout
|
||||||
buildDefaultLayout: function(docker) {
|
buildDefaultLayout: function(docker) {
|
||||||
var browserPanel = docker.addPanel('browser', wcDocker.DOCK.LEFT);
|
var browserPanel = docker.addPanel('browser', wcDocker.DOCK.LEFT);
|
||||||
@ -542,13 +542,172 @@ define('pgadmin.browser', [
|
|||||||
.fail(function() {});
|
.fail(function() {});
|
||||||
}, 300000);
|
}, 300000);
|
||||||
|
|
||||||
|
obj.set_master_password('');
|
||||||
|
|
||||||
obj.Events.on('pgadmin:browser:tree:add', obj.onAddTreeNode, obj);
|
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:update', obj.onUpdateTreeNode, obj);
|
||||||
obj.Events.on('pgadmin:browser:tree:refresh', obj.onRefreshTreeNode, obj);
|
obj.Events.on('pgadmin:browser:tree:refresh', obj.onRefreshTreeNode, obj);
|
||||||
|
obj.Events.on('pgadmin-browser:tree:loadfail', obj.onLoadFailNode, obj);
|
||||||
|
|
||||||
obj.bind_beforeunload();
|
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() {
|
bind_beforeunload: function() {
|
||||||
$(window).on('beforeunload', function(e) {
|
$(window).on('beforeunload', function(e) {
|
||||||
/* Can open you in new tab */
|
/* Can open you in new tab */
|
||||||
@ -1619,10 +1778,13 @@ define('pgadmin.browser', [
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
Alertify.pgNotifier(
|
Alertify.pgNotifier(error, xhr, gettext('Error retrieving details for the node.'), function (msg) {
|
||||||
error, xhr, gettext('Error retrieving details for the node.'),
|
if (msg == 'CRYPTKEY_SET') {
|
||||||
function() { console.warn(arguments); }
|
fetchNodeInfo(_i, _d, _n);
|
||||||
);
|
} else {
|
||||||
|
console.warn(arguments);
|
||||||
|
}
|
||||||
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}.bind(this);
|
}.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) {
|
removeChildTreeNodesById: function(_parentNode, _collType, _childIds) {
|
||||||
var tree = pgBrowser.tree;
|
var tree = pgBrowser.tree;
|
||||||
if(_parentNode && _collType) {
|
if(_parentNode && _collType) {
|
||||||
|
@ -263,7 +263,7 @@ define([
|
|||||||
content.find('.pg-prop-coll-container').append(that.grid.render().$el);
|
content.find('.pg-prop-coll-container').append(that.grid.render().$el);
|
||||||
|
|
||||||
var timer;
|
var timer;
|
||||||
|
var getAjaxHook = function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: urlBase,
|
url: urlBase,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
@ -280,8 +280,7 @@ define([
|
|||||||
}
|
}
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
})
|
}).done(function(res) {
|
||||||
.done(function(res) {
|
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
|
|
||||||
if (_.isUndefined(that.grid) || _.isNull(that.grid)) return;
|
if (_.isUndefined(that.grid) || _.isNull(that.grid)) return;
|
||||||
@ -307,8 +306,7 @@ define([
|
|||||||
$msgContainer.text(gettext('No properties are available for the selected object.'));
|
$msgContainer.text(gettext('No properties are available for the selected object.'));
|
||||||
|
|
||||||
}
|
}
|
||||||
})
|
}).fail(function(xhr, error) {
|
||||||
.fail(function(xhr, error) {
|
|
||||||
pgBrowser.Events.trigger(
|
pgBrowser.Events.trigger(
|
||||||
'pgadmin:node:retrieval:error', 'properties', xhr, error.message, item, that
|
'pgadmin:node:retrieval:error', 'properties', xhr, error.message, item, that
|
||||||
);
|
);
|
||||||
@ -317,17 +315,24 @@ define([
|
|||||||
info: info,
|
info: info,
|
||||||
})) {
|
})) {
|
||||||
Alertify.pgNotifier(
|
Alertify.pgNotifier(
|
||||||
error, xhr,
|
error, xhr, S(gettext('Error retrieving properties - %s')).sprintf(
|
||||||
S(gettext('Error retrieving properties - %s')).sprintf(
|
error.message || that.label).value(),
|
||||||
error.message || that.label).value(), function() {
|
function(msg) {
|
||||||
|
if(msg === 'CRYPTKEY_SET') {
|
||||||
|
getAjaxHook();
|
||||||
|
} else {
|
||||||
console.warn(arguments);
|
console.warn(arguments);
|
||||||
});
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
}
|
}
|
||||||
// show failed message.
|
// show failed message.
|
||||||
$msgContainer.text(gettext('Failed to retrieve data from the server.'));
|
$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(),
|
let sel_row_models = this.grid.getSelectedModels(),
|
||||||
sel_rows = [],
|
sel_rows = [],
|
||||||
item = pgBrowser.tree.selected(),
|
item = pgBrowser.tree.selected(),
|
||||||
@ -356,16 +361,13 @@ define([
|
|||||||
title = gettext('DROP multiple objects?');
|
title = gettext('DROP multiple objects?');
|
||||||
}
|
}
|
||||||
|
|
||||||
|
let dropAjaxHook = function() {
|
||||||
Alertify.confirm(title, msg,
|
|
||||||
function() {
|
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: 'DELETE',
|
type: 'DELETE',
|
||||||
data: JSON.stringify({'ids': sel_rows}),
|
data: JSON.stringify({'ids': sel_rows}),
|
||||||
contentType: 'application/json; charset=utf-8',
|
contentType: 'application/json; charset=utf-8',
|
||||||
})
|
}).done(function(res) {
|
||||||
.done(function(res) {
|
|
||||||
if (res.success == 0) {
|
if (res.success == 0) {
|
||||||
pgBrowser.report_error(res.errormsg, res.info);
|
pgBrowser.report_error(res.errormsg, res.info);
|
||||||
} else {
|
} else {
|
||||||
@ -378,23 +380,14 @@ define([
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
return true;
|
return true;
|
||||||
})
|
}).fail(function(xhr, error) {
|
||||||
.fail(function(jqx) {
|
Alertify.pgNotifier(
|
||||||
var msg = jqx.responseText;
|
error, xhr,
|
||||||
/* 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'))
|
S(gettext('Error dropping %s'))
|
||||||
.sprintf(d._label.toLowerCase())
|
.sprintf(d._label.toLowerCase()).value(), function(msg) {
|
||||||
.value(), msg);
|
if (msg == 'CRYPTKEY_SET') {
|
||||||
|
onDrop(type, false);
|
||||||
|
} else {
|
||||||
$(pgBrowser.panels['properties'].panel).removeData('node-prop');
|
$(pgBrowser.panels['properties'].panel).removeData('node-prop');
|
||||||
pgBrowser.Events.trigger(
|
pgBrowser.Events.trigger(
|
||||||
'pgadmin:browser:tree:refresh', item || pgBrowser.tree.selected(), {
|
'pgadmin:browser:tree:refresh', item || pgBrowser.tree.selected(), {
|
||||||
@ -403,9 +396,17 @@ define([
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
});
|
});
|
||||||
},
|
};
|
||||||
null).show();
|
|
||||||
|
if(confirm) {
|
||||||
|
Alertify.confirm(title, msg, dropAjaxHook, null).show();
|
||||||
|
} else {
|
||||||
|
dropAjaxHook();
|
||||||
|
}
|
||||||
return;
|
return;
|
||||||
}.bind(that);
|
}.bind(that);
|
||||||
},
|
},
|
||||||
|
@ -404,6 +404,8 @@ define('pgadmin.browser.node', [
|
|||||||
}
|
}
|
||||||
}, 1000, ctx);
|
}, 1000, ctx);
|
||||||
|
|
||||||
|
|
||||||
|
var fetchAjaxHook = function() {
|
||||||
newModel.fetch({
|
newModel.fetch({
|
||||||
success: function() {
|
success: function() {
|
||||||
// Clear timeout and remove message
|
// Clear timeout and remove message
|
||||||
@ -416,27 +418,32 @@ define('pgadmin.browser.node', [
|
|||||||
setFocusOnEl();
|
setFocusOnEl();
|
||||||
newModel.startNewSession();
|
newModel.startNewSession();
|
||||||
},
|
},
|
||||||
error: function(xhr, error, message) {
|
error: function(model, xhr, options) {
|
||||||
var _label = that && item ?
|
var _label = that && item ?
|
||||||
that.getTreeNodeHierarchy(
|
that.getTreeNodeHierarchy(
|
||||||
item
|
item
|
||||||
)[that.type].label : '';
|
)[that.type].label : '';
|
||||||
pgBrowser.Events.trigger(
|
pgBrowser.Events.trigger(
|
||||||
'pgadmin:node:retrieval:error', 'properties',
|
'pgadmin:node:retrieval:error', 'properties',
|
||||||
xhr, error, message, item
|
xhr, options.textStatus, options.errorThrown, item
|
||||||
);
|
);
|
||||||
if (!Alertify.pgHandleItemError(
|
if (!Alertify.pgHandleItemError(
|
||||||
xhr, error, message, {
|
xhr, options.textStatus, options.errorThrown, {
|
||||||
item: item,
|
item: item,
|
||||||
info: info,
|
info: info,
|
||||||
}
|
}
|
||||||
)) {
|
)) {
|
||||||
Alertify.pgNotifier(
|
Alertify.pgNotifier(
|
||||||
error, xhr,
|
options.textStatus, xhr,
|
||||||
S(
|
S(
|
||||||
gettext('Error retrieving properties - %s')
|
gettext('Error retrieving properties - %s')
|
||||||
).sprintf(message || _label).value(),
|
).sprintf(options.errorThrown || _label).value(), function(msg) {
|
||||||
function() {}
|
if(msg === 'CRYPTKEY_SET') {
|
||||||
|
fetchAjaxHook();
|
||||||
|
} else {
|
||||||
|
console.warn(arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// Close the panel (if could not fetch properties)
|
// Close the panel (if could not fetch properties)
|
||||||
@ -445,9 +452,11 @@ define('pgadmin.browser.node', [
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
fetchAjaxHook();
|
||||||
} else {
|
} else {
|
||||||
// Yay - render the view now!
|
// Yay - render the view now!
|
||||||
// $(el).focus();
|
|
||||||
view.render();
|
view.render();
|
||||||
setFocusOnEl();
|
setFocusOnEl();
|
||||||
newModel.startNewSession();
|
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 config import PG_DEFAULT_DRIVER
|
||||||
from pgadmin.utils.ajax import make_json_response, precondition_required
|
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):
|
def is_version_in_range(sversion, min_ver, max_ver):
|
||||||
@ -321,9 +323,19 @@ class PGChildNodeView(NodeView):
|
|||||||
if 'did' in kwargs:
|
if 'did' in kwargs:
|
||||||
did = kwargs['did']
|
did = kwargs['did']
|
||||||
|
|
||||||
|
try:
|
||||||
conn = manager.connection(did=did)
|
conn = manager.connection(did=did)
|
||||||
|
|
||||||
if not conn.connected():
|
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(
|
return precondition_required(
|
||||||
gettext(
|
gettext(
|
||||||
"Connection to the server has been lost."
|
"Connection to the server has been lost."
|
||||||
|
@ -223,6 +223,7 @@ define('pgadmin.dashboard', [
|
|||||||
var div = dashboardPanel.layout().scene().find('.pg-panel-content');
|
var div = dashboardPanel.layout().scene().find('.pg-panel-content');
|
||||||
|
|
||||||
if (div) {
|
if (div) {
|
||||||
|
var ajaxHook = function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
@ -231,11 +232,26 @@ define('pgadmin.dashboard', [
|
|||||||
.done(function(data) {
|
.done(function(data) {
|
||||||
$(div).html(data);
|
$(div).html(data);
|
||||||
})
|
})
|
||||||
.fail(function() {
|
.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).html(
|
||||||
'<div class="alert alert-danger pg-panel-message" role="alert">' + gettext('An error occurred whilst loading the dashboard.') + '</div>'
|
'<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
|
// Cache the current IDs for next time
|
||||||
$(dashboardPanel).data('sid', -1);
|
$(dashboardPanel).data('sid', -1);
|
||||||
@ -337,6 +353,7 @@ define('pgadmin.dashboard', [
|
|||||||
/* Clear all the charts previous dashboards */
|
/* Clear all the charts previous dashboards */
|
||||||
self.clearChartFromStore();
|
self.clearChartFromStore();
|
||||||
|
|
||||||
|
let ajaxHook = function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
@ -346,11 +363,26 @@ define('pgadmin.dashboard', [
|
|||||||
$(div).html(data);
|
$(div).html(data);
|
||||||
self.init_dashboard();
|
self.init_dashboard();
|
||||||
})
|
})
|
||||||
.fail(function() {
|
.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).html(
|
||||||
'<div class="alert alert-danger pg-panel-message" role="alert">' + gettext('An error occurred whilst loading the dashboard.') + '</div>'
|
'<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);
|
$(dashboardPanel).data('server_status', true);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
|
@ -215,8 +215,13 @@ define('misc.dependencies', [
|
|||||||
Alertify.pgNotifier(
|
Alertify.pgNotifier(
|
||||||
error, xhr,
|
error, xhr,
|
||||||
S(gettext('Error retrieving data from the server: %s')).sprintf(
|
S(gettext('Error retrieving data from the server: %s')).sprintf(
|
||||||
message || _label).value(), function() {
|
message || _label).value(),
|
||||||
|
function(msg) {
|
||||||
|
if(msg === 'CRYPTKEY_SET') {
|
||||||
|
self.showDependencies(item, data, node);
|
||||||
|
} else {
|
||||||
console.warn(arguments);
|
console.warn(arguments);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// show failed message.
|
// show failed message.
|
||||||
|
@ -221,8 +221,13 @@ define('misc.dependents', [
|
|||||||
Alertify.pgNotifier(
|
Alertify.pgNotifier(
|
||||||
error, xhr,
|
error, xhr,
|
||||||
S(gettext('Error retrieving data from the server: %s')).sprintf(
|
S(gettext('Error retrieving data from the server: %s')).sprintf(
|
||||||
message || _label).value(), function() {
|
message || _label).value(),
|
||||||
|
function(msg) {
|
||||||
|
if(msg === 'CRYPTKEY_SET') {
|
||||||
|
self.showDependents(item, data, node);
|
||||||
|
} else {
|
||||||
console.warn(arguments);
|
console.warn(arguments);
|
||||||
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
// show failed message.
|
// show failed message.
|
||||||
|
@ -119,7 +119,7 @@ define('misc.sql', [
|
|||||||
|
|
||||||
sql = '';
|
sql = '';
|
||||||
var timer;
|
var timer;
|
||||||
|
var ajaxHook = function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
@ -130,20 +130,17 @@ define('misc.sql', [
|
|||||||
// Generate a timer for the request
|
// Generate a timer for the request
|
||||||
timer = setTimeout(function() {
|
timer = setTimeout(function() {
|
||||||
// Notify user if request is taking longer than 1 second
|
// Notify user if request is taking longer than 1 second
|
||||||
|
|
||||||
pgAdmin.Browser.editor.setValue(
|
pgAdmin.Browser.editor.setValue(
|
||||||
gettext('Retrieving data from the server...')
|
gettext('Retrieving data from the server...')
|
||||||
);
|
);
|
||||||
}, 1000);
|
}, 1000);
|
||||||
},
|
},
|
||||||
})
|
}).done(function(res) {
|
||||||
.done(function(res) {
|
|
||||||
if (pgAdmin.Browser.editor.getValue() != res) {
|
if (pgAdmin.Browser.editor.getValue() != res) {
|
||||||
pgAdmin.Browser.editor.setValue(res);
|
pgAdmin.Browser.editor.setValue(res);
|
||||||
}
|
}
|
||||||
clearTimeout(timer);
|
clearTimeout(timer);
|
||||||
})
|
}).fail(function(xhr, error, message) {
|
||||||
.fail(function(xhr, error, message) {
|
|
||||||
var _label = treeHierarchy[n_type].label;
|
var _label = treeHierarchy[n_type].label;
|
||||||
pgBrowser.Events.trigger(
|
pgBrowser.Events.trigger(
|
||||||
'pgadmin:node:retrieval:error', 'sql', xhr, error, message, item
|
'pgadmin:node:retrieval:error', 'sql', xhr, error, message, item
|
||||||
@ -156,11 +153,18 @@ define('misc.sql', [
|
|||||||
error, xhr,
|
error, xhr,
|
||||||
S(gettext('Error retrieving the information - %s')).sprintf(
|
S(gettext('Error retrieving the information - %s')).sprintf(
|
||||||
message || _label
|
message || _label
|
||||||
).value(),
|
).value(), function(msg) {
|
||||||
function() {}
|
if(msg === 'CRYPTKEY_SET') {
|
||||||
|
ajaxHook();
|
||||||
|
} else {
|
||||||
|
console.warn(arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
ajaxHook();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -232,6 +232,7 @@ define('misc.statistics', [
|
|||||||
msg = '';
|
msg = '';
|
||||||
var timer;
|
var timer;
|
||||||
// Set the url, fetch the data and update the collection
|
// Set the url, fetch the data and update the collection
|
||||||
|
var ajaxHook = function() {
|
||||||
$.ajax({
|
$.ajax({
|
||||||
url: url,
|
url: url,
|
||||||
type: 'GET',
|
type: 'GET',
|
||||||
@ -316,13 +317,21 @@ define('misc.statistics', [
|
|||||||
error, xhr,
|
error, xhr,
|
||||||
S(gettext('Error retrieving the information - %s')).sprintf(
|
S(gettext('Error retrieving the information - %s')).sprintf(
|
||||||
message || _label
|
message || _label
|
||||||
).value(),
|
).value(), function(msg) {
|
||||||
function() {}
|
if(msg === 'CRYPTKEY_SET') {
|
||||||
|
ajaxHook();
|
||||||
|
} else {
|
||||||
|
console.warn(arguments);
|
||||||
|
}
|
||||||
|
}
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
// show failed message.
|
// show failed message.
|
||||||
$msgContainer.text(gettext('Failed to retrieve data from the server.'));
|
$msgContainer.text(gettext('Failed to retrieve data from the server.'));
|
||||||
});
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
ajaxHook();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if (msg != '') {
|
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))
|
password = db.Column(db.String(256))
|
||||||
active = db.Column(db.Boolean(), nullable=False)
|
active = db.Column(db.Boolean(), nullable=False)
|
||||||
confirmed_at = db.Column(db.DateTime())
|
confirmed_at = db.Column(db.DateTime())
|
||||||
|
masterpass_check = db.Column(db.String(256))
|
||||||
roles = db.relationship('Role', secondary=roles_users,
|
roles = db.relationship('Role', secondary=roles_users,
|
||||||
backref=db.backref('users', lazy='dynamic'))
|
backref=db.backref('users', lazy='dynamic'))
|
||||||
|
|
||||||
|
@ -107,7 +107,15 @@ define([
|
|||||||
if (contentType.indexOf('application/json') == 0) {
|
if (contentType.indexOf('application/json') == 0) {
|
||||||
var resp = JSON.parse(msg);
|
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') {
|
onJSONResult && typeof(onJSONResult) == 'function') {
|
||||||
return onJSONResult(resp.result);
|
return onJSONResult(resp.result);
|
||||||
}
|
}
|
||||||
@ -375,6 +383,11 @@ define([
|
|||||||
reconnectServer();
|
reconnectServer();
|
||||||
});
|
});
|
||||||
return true;
|
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;
|
return false;
|
||||||
};
|
};
|
||||||
|
@ -230,6 +230,12 @@ class ExecuteQuery {
|
|||||||
this.sqlServerObject.handle_connection_lost(false, httpMessage);
|
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;
|
let msg = httpMessage.response.data.errormsg;
|
||||||
this.sqlServerObject.update_msg_history(false, msg);
|
this.sqlServerObject.update_msg_history(false, msg);
|
||||||
}
|
}
|
||||||
@ -240,6 +246,12 @@ class ExecuteQuery {
|
|||||||
httpMessage.response.data.info === 'CONNECTION_LOST';
|
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() {
|
removeGridViewMarker() {
|
||||||
if (this.sqlServerObject.gridView.marker) {
|
if (this.sqlServerObject.gridView.marker) {
|
||||||
this.sqlServerObject.gridView.marker.clear();
|
this.sqlServerObject.gridView.marker.clear();
|
||||||
|
@ -607,6 +607,9 @@ define([
|
|||||||
*/
|
*/
|
||||||
$('.wizard-progress-bar p').show();
|
$('.wizard-progress-bar p').show();
|
||||||
|
|
||||||
|
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({
|
coll.fetch({
|
||||||
success: function(c, xhr) {
|
success: function(c, xhr) {
|
||||||
$('.wizard-progress-bar p').html('');
|
$('.wizard-progress-bar p').html('');
|
||||||
@ -618,21 +621,28 @@ define([
|
|||||||
$('.pg-prop-status-bar').css('visibility', 'visible');
|
$('.pg-prop-status-bar').css('visibility', 'visible');
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
error: function(m, xhr) {
|
error: function(model, xhr, options) {
|
||||||
// If the main request fails as whole then
|
// If the main request fails as whole then
|
||||||
let msg;
|
$('.wizard-progress-bar p').removeClass('alert-info').addClass('alert-danger');
|
||||||
if (xhr && xhr.responseJSON && xhr.responseJSON.errormsg) {
|
$('.wizard-progress-bar p').text(gettext('Unable to fetch the database objects'));
|
||||||
msg = xhr.responseJSON.errormsg;
|
|
||||||
}
|
|
||||||
|
|
||||||
if(!msg) {
|
Alertify.pgNotifier(
|
||||||
msg = gettext('Unable to fetch the database objects due to an error');
|
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').removeClass('alert-info').addClass('alert-danger');
|
||||||
$('.wizard-progress-bar p').text(msg);
|
$('.wizard-progress-bar p').text(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
);
|
||||||
},
|
},
|
||||||
reset: true,
|
reset: true,
|
||||||
}, this);
|
}, this);
|
||||||
|
};
|
||||||
|
fetchAjaxHook();
|
||||||
|
|
||||||
//////////////////////////////////////////////////////////////////////
|
//////////////////////////////////////////////////////////////////////
|
||||||
// //
|
// //
|
||||||
|
@ -8,10 +8,8 @@
|
|||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
"""A blueprint module implementing the sqleditor frame."""
|
"""A blueprint module implementing the sqleditor frame."""
|
||||||
import codecs
|
|
||||||
import os
|
import os
|
||||||
import pickle
|
import pickle
|
||||||
import random
|
|
||||||
import sys
|
import sys
|
||||||
|
|
||||||
import simplejson as json
|
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 PgAdminModule
|
||||||
from pgadmin.utils import get_storage_directory
|
from pgadmin.utils import get_storage_directory
|
||||||
from pgadmin.utils.ajax import make_json_response, bad_request, \
|
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.driver import get_driver
|
||||||
from pgadmin.utils.menu import MenuItem
|
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.utils.sqlautocomplete.autocomplete import SQLAutoComplete
|
||||||
from pgadmin.tools.sqleditor.utils.query_tool_preferences import \
|
from pgadmin.tools.sqleditor.utils.query_tool_preferences import \
|
||||||
RegisterQueryToolPreferences
|
RegisterQueryToolPreferences
|
||||||
@ -176,7 +175,7 @@ def check_transaction_status(trans_id):
|
|||||||
use_binary_placeholder=True,
|
use_binary_placeholder=True,
|
||||||
array_to_string=True
|
array_to_string=True
|
||||||
)
|
)
|
||||||
except (ConnectionLost, SSHTunnelConnectionLost) as e:
|
except (ConnectionLost, SSHTunnelConnectionLost, CryptKeyMissing):
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.error(e)
|
current_app.logger.error(e)
|
||||||
|
@ -2008,6 +2008,11 @@ define('tools.querytool', [
|
|||||||
this.warn_before_continue();
|
this.warn_before_continue();
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
handle_cryptkey_missing: function() {
|
||||||
|
pgBrowser.set_master_password('', ()=>{
|
||||||
|
this.warn_before_continue();
|
||||||
|
});
|
||||||
|
},
|
||||||
warn_before_continue: function() {
|
warn_before_continue: function() {
|
||||||
var self = this;
|
var self = this;
|
||||||
|
|
||||||
|
@ -25,7 +25,8 @@ from pgadmin.tools.sqleditor.utils.update_session_grid_transaction import \
|
|||||||
update_session_grid_transaction
|
update_session_grid_transaction
|
||||||
from pgadmin.utils.ajax import make_json_response, internal_server_error
|
from pgadmin.utils.ajax import make_json_response, internal_server_error
|
||||||
from pgadmin.utils.driver import get_driver
|
from pgadmin.utils.driver import get_driver
|
||||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost
|
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
|
||||||
|
CryptKeyMissing
|
||||||
|
|
||||||
|
|
||||||
class StartRunningQuery:
|
class StartRunningQuery:
|
||||||
@ -63,7 +64,7 @@ class StartRunningQuery:
|
|||||||
auto_reconnect=False,
|
auto_reconnect=False,
|
||||||
use_binary_placeholder=True,
|
use_binary_placeholder=True,
|
||||||
array_to_string=True)
|
array_to_string=True)
|
||||||
except (ConnectionLost, SSHTunnelConnectionLost):
|
except (ConnectionLost, SSHTunnelConnectionLost, CryptKeyMissing):
|
||||||
raise
|
raise
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
self.logger.error(e)
|
self.logger.error(e)
|
||||||
@ -134,7 +135,7 @@ class StartRunningQuery:
|
|||||||
# and formatted_error is True.
|
# and formatted_error is True.
|
||||||
try:
|
try:
|
||||||
status, result = conn.execute_async(sql)
|
status, result = conn.execute_async(sql)
|
||||||
except (ConnectionLost, SSHTunnelConnectionLost):
|
except (ConnectionLost, SSHTunnelConnectionLost, CryptKeyMissing):
|
||||||
raise
|
raise
|
||||||
|
|
||||||
# If the transaction aborted for some reason and
|
# If the transaction aborted for some reason and
|
||||||
|
@ -14,6 +14,8 @@ from operator import attrgetter
|
|||||||
|
|
||||||
from flask import Blueprint, current_app
|
from flask import Blueprint, current_app
|
||||||
from flask_babelex import gettext
|
from flask_babelex import gettext
|
||||||
|
from flask_security import current_user, login_required
|
||||||
|
from threading import Lock
|
||||||
|
|
||||||
from .paths import get_storage_directory
|
from .paths import get_storage_directory
|
||||||
from .preferences import Preferences
|
from .preferences import Preferences
|
||||||
@ -330,3 +332,46 @@ SHORTCUT_FIELDS = [
|
|||||||
'label': gettext('Alt/Option')
|
'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
|
import datetime
|
||||||
from flask import session
|
from flask import session, request
|
||||||
|
from flask_login import current_user
|
||||||
from flask_babelex import gettext
|
from flask_babelex import gettext
|
||||||
import psycopg2
|
import psycopg2
|
||||||
from psycopg2.extensions import adapt
|
from psycopg2.extensions import adapt
|
||||||
@ -74,23 +75,25 @@ class Driver(BaseDriver):
|
|||||||
assert (sid is not None and isinstance(sid, int))
|
assert (sid is not None and isinstance(sid, int))
|
||||||
managers = None
|
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:
|
if session.sid not in self.managers:
|
||||||
self.managers[session.sid] = managers = dict()
|
self.managers[session.sid] = managers = dict()
|
||||||
if '__pgsql_server_managers' in session:
|
if '__pgsql_server_managers' in session:
|
||||||
session_managers = session['__pgsql_server_managers'].copy()
|
session_managers = session['__pgsql_server_managers'].copy()
|
||||||
session['__pgsql_server_managers'] = dict()
|
|
||||||
|
|
||||||
for server_id in session_managers:
|
manager = managers[str(sid)] = ServerManager(server_data)
|
||||||
s = Server.query.filter_by(id=server_id).first()
|
if sid in session_managers:
|
||||||
|
manager._restore(session_managers[sid])
|
||||||
if not s:
|
|
||||||
continue
|
|
||||||
|
|
||||||
manager = managers[str(server_id)] = ServerManager(s)
|
|
||||||
manager._restore(session_managers[server_id])
|
|
||||||
manager.update_session()
|
manager.update_session()
|
||||||
else:
|
else:
|
||||||
managers = self.managers[session.sid]
|
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()
|
managers['pinged'] = datetime.datetime.now()
|
||||||
if str(sid) not in managers:
|
if str(sid) not in managers:
|
||||||
|
@ -28,16 +28,17 @@ from pgadmin.utils.crypto import decrypt
|
|||||||
from psycopg2.extensions import adapt, encodings
|
from psycopg2.extensions import adapt, encodings
|
||||||
|
|
||||||
import config
|
import config
|
||||||
from pgadmin.model import Server, User
|
from pgadmin.model import User
|
||||||
from pgadmin.utils.exception import ConnectionLost
|
from pgadmin.utils.exception import ConnectionLost, CryptKeyMissing
|
||||||
from pgadmin.utils import get_complete_file_path
|
from pgadmin.utils import get_complete_file_path
|
||||||
from ..abstract import BaseDriver, BaseConnection
|
from ..abstract import BaseConnection
|
||||||
from .cursor import DictCursor
|
from .cursor import DictCursor
|
||||||
from .typecast import register_global_typecasters, \
|
from .typecast import register_global_typecasters, \
|
||||||
register_string_typecasters, register_binary_typecasters, \
|
register_string_typecasters, register_binary_typecasters, \
|
||||||
register_array_to_string_typecasters, ALL_JSON_TYPES
|
register_array_to_string_typecasters, ALL_JSON_TYPES
|
||||||
from .encoding import getEncoding, configureDriverEncodings
|
from .encoding import getEncoding, configureDriverEncodings
|
||||||
from pgadmin.utils import csv
|
from pgadmin.utils import csv
|
||||||
|
from pgadmin.utils.master_password import get_crypt_key
|
||||||
|
|
||||||
if sys.version_info < (3,):
|
if sys.version_info < (3,):
|
||||||
from StringIO import StringIO
|
from StringIO import StringIO
|
||||||
@ -242,10 +243,16 @@ class Connection(BaseConnection):
|
|||||||
if encpass is None:
|
if encpass is None:
|
||||||
encpass = self.password or getattr(manager, 'password', None)
|
encpass = self.password or getattr(manager, 'password', None)
|
||||||
|
|
||||||
|
self.password = encpass
|
||||||
|
|
||||||
# Reset the existing connection password
|
# Reset the existing connection password
|
||||||
if self.reconnecting is not False:
|
if self.reconnecting is not False:
|
||||||
self.password = None
|
self.password = None
|
||||||
|
|
||||||
|
crypt_key_present, crypt_key = get_crypt_key()
|
||||||
|
if not crypt_key_present:
|
||||||
|
raise CryptKeyMissing()
|
||||||
|
|
||||||
if encpass:
|
if encpass:
|
||||||
# Fetch Logged in User Details.
|
# Fetch Logged in User Details.
|
||||||
user = User.query.filter_by(id=current_user.id).first()
|
user = User.query.filter_by(id=current_user.id).first()
|
||||||
@ -254,14 +261,13 @@ class Connection(BaseConnection):
|
|||||||
return False, gettext("Unauthorized request.")
|
return False, gettext("Unauthorized request.")
|
||||||
|
|
||||||
try:
|
try:
|
||||||
password = decrypt(encpass, user.password)
|
password = decrypt(encpass, crypt_key)
|
||||||
# Handling of non ascii password (Python2)
|
# Handling of non ascii password (Python2)
|
||||||
if hasattr(str, 'decode'):
|
if hasattr(str, 'decode'):
|
||||||
password = password.decode('utf-8').encode('utf-8')
|
password = password.decode('utf-8').encode('utf-8')
|
||||||
# password is in bytes, for python3 we need it in string
|
# password is in bytes, for python3 we need it in string
|
||||||
elif isinstance(password, bytes):
|
elif isinstance(password, bytes):
|
||||||
password = password.decode()
|
password = password.decode()
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
manager.stop_ssh_tunnel()
|
manager.stop_ssh_tunnel()
|
||||||
current_app.logger.exception(e)
|
current_app.logger.exception(e)
|
||||||
@ -521,6 +527,9 @@ WHERE
|
|||||||
|
|
||||||
def __cursor(self, server_cursor=False):
|
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
|
# Check SSH Tunnel is alive or not. If used by the database
|
||||||
# server for the connection.
|
# server for the connection.
|
||||||
if self.manager.use_ssh_tunnel == 1:
|
if self.manager.use_ssh_tunnel == 1:
|
||||||
@ -1081,7 +1090,7 @@ WHERE
|
|||||||
current_app.logger.exception(e)
|
current_app.logger.exception(e)
|
||||||
self.reconnecting = False
|
self.reconnecting = False
|
||||||
|
|
||||||
current_app.warning(
|
current_app.logger.warning(
|
||||||
"Failed to reconnect the database server "
|
"Failed to reconnect the database server "
|
||||||
"(#{server_id})".format(
|
"(#{server_id})".format(
|
||||||
server_id=self.manager.sid,
|
server_id=self.manager.sid,
|
||||||
@ -1283,7 +1292,11 @@ WHERE
|
|||||||
if user is None:
|
if user is None:
|
||||||
return False, gettext("Unauthorized request.")
|
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:
|
try:
|
||||||
pg_conn = psycopg2.connect(
|
pg_conn = psycopg2.connect(
|
||||||
@ -1567,7 +1580,12 @@ Failed to reset the connection to the server due to following error:
|
|||||||
if user is None:
|
if user is None:
|
||||||
return False, gettext("Unauthorized request.")
|
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:
|
try:
|
||||||
pg_conn = psycopg2.connect(
|
pg_conn = psycopg2.connect(
|
||||||
|
@ -19,13 +19,19 @@ from flask_babelex import gettext
|
|||||||
|
|
||||||
from pgadmin.utils import get_complete_file_path
|
from pgadmin.utils import get_complete_file_path
|
||||||
from pgadmin.utils.crypto import decrypt
|
from pgadmin.utils.crypto import decrypt
|
||||||
|
from pgadmin.utils.master_password import process_masterpass_disabled
|
||||||
from .connection import Connection
|
from .connection import Connection
|
||||||
from pgadmin.model import Server, User
|
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:
|
if config.SUPPORT_SSH_TUNNEL:
|
||||||
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
|
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
|
||||||
|
|
||||||
|
connection_restore_lock = Lock()
|
||||||
|
|
||||||
|
|
||||||
class ServerManager(object):
|
class ServerManager(object):
|
||||||
"""
|
"""
|
||||||
@ -185,6 +191,10 @@ class ServerManager(object):
|
|||||||
maintenance_db_id = u'DB:{0}'.format(self.db)
|
maintenance_db_id = u'DB:{0}'.format(self.db)
|
||||||
if maintenance_db_id in self.connections:
|
if maintenance_db_id in self.connections:
|
||||||
conn = self.connections[maintenance_db_id]
|
conn = self.connections[maintenance_db_id]
|
||||||
|
# try to connect maintenance db if not connected
|
||||||
|
if not conn.connected():
|
||||||
|
conn.connect()
|
||||||
|
|
||||||
if conn.connected():
|
if conn.connected():
|
||||||
status, res = conn.execute_dict(u"""
|
status, res = conn.execute_dict(u"""
|
||||||
SELECT
|
SELECT
|
||||||
@ -205,6 +215,10 @@ WHERE db.oid = {0}""".format(did))
|
|||||||
"Could not find the specified database."
|
"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:
|
if database is None:
|
||||||
# Check SSH Tunnel is alive or not.
|
# Check SSH Tunnel is alive or not.
|
||||||
if self.use_ssh_tunnel == 1:
|
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
|
# restore server version from flask session if flask server was
|
||||||
# restarted. As we need server version to resolve sql template paths.
|
# 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
|
from pgadmin.browser.server_groups.servers.types import ServerType
|
||||||
|
|
||||||
self.ver = data.get('ver', None)
|
self.ver = data.get('ver', None)
|
||||||
@ -251,16 +274,12 @@ WHERE db.oid = {0}""".format(did))
|
|||||||
self.server_cls = st
|
self.server_cls = st
|
||||||
break
|
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
|
# We need to know about the existing server variant supports during
|
||||||
# first connection for identifications.
|
# first connection for identifications.
|
||||||
self.pinged = datetime.datetime.now()
|
self.pinged = datetime.datetime.now()
|
||||||
try:
|
try:
|
||||||
if 'password' in data and data['password']:
|
if 'password' in data and data['password']:
|
||||||
|
if hasattr(data['password'], 'encode'):
|
||||||
data['password'] = data['password'].encode('utf-8')
|
data['password'] = data['password'].encode('utf-8')
|
||||||
if 'tunnel_password' in data and data['tunnel_password']:
|
if 'tunnel_password' in data and data['tunnel_password']:
|
||||||
data['tunnel_password'] = \
|
data['tunnel_password'] = \
|
||||||
@ -269,21 +288,28 @@ WHERE db.oid = {0}""".format(did))
|
|||||||
current_app.logger.exception(e)
|
current_app.logger.exception(e)
|
||||||
|
|
||||||
connections = data['connections']
|
connections = data['connections']
|
||||||
|
|
||||||
|
with connection_restore_lock:
|
||||||
for conn_id in connections:
|
for conn_id in connections:
|
||||||
conn_info = connections[conn_id]
|
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(
|
conn = self.connections[conn_info['conn_id']] = Connection(
|
||||||
self, conn_info['conn_id'], conn_info['database'],
|
self, conn_info['conn_id'], conn_info['database'],
|
||||||
conn_info['auto_reconnect'], conn_info['async_'],
|
conn_info['auto_reconnect'], conn_info['async_'],
|
||||||
use_binary_placeholder=conn_info['use_binary_placeholder'],
|
use_binary_placeholder=conn_info[
|
||||||
|
'use_binary_placeholder'],
|
||||||
array_to_string=conn_info['array_to_string']
|
array_to_string=conn_info['array_to_string']
|
||||||
)
|
)
|
||||||
|
|
||||||
# only try to reconnect if connection was connected previously and
|
# only try to reconnect if connection was connected previously
|
||||||
# auto_reconnect is true.
|
# and auto_reconnect is true.
|
||||||
if conn_info['wasConnected'] and conn_info['auto_reconnect']:
|
if conn_info['wasConnected'] and conn_info['auto_reconnect']:
|
||||||
try:
|
try:
|
||||||
# Check SSH Tunnel needs to be created
|
# Check SSH Tunnel needs to be created
|
||||||
if self.use_ssh_tunnel == 1 and not self.tunnel_created:
|
if self.use_ssh_tunnel == 1 and \
|
||||||
|
not self.tunnel_created:
|
||||||
status, error = self.create_ssh_tunnel(
|
status, error = self.create_ssh_tunnel(
|
||||||
data['tunnel_password'])
|
data['tunnel_password'])
|
||||||
|
|
||||||
@ -294,11 +320,47 @@ WHERE db.oid = {0}""".format(did))
|
|||||||
password=data['password'],
|
password=data['password'],
|
||||||
server_types=ServerType.types()
|
server_types=ServerType.types()
|
||||||
)
|
)
|
||||||
# This will also update wasConnected flag in connection so
|
# This will also update wasConnected flag in
|
||||||
# no need to update the flag manually.
|
# 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:
|
except Exception as e:
|
||||||
current_app.logger.exception(e)
|
current_app.logger.exception(e)
|
||||||
self.connections.pop(conn_info['conn_id'])
|
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):
|
def release(self, database=None, conn_id=None, did=None):
|
||||||
# Stop the SSH tunnel if release() function calls without
|
# Stop the SSH tunnel if release() function calls without
|
||||||
|
@ -80,3 +80,28 @@ class SSHTunnelConnectionLost(HTTPException):
|
|||||||
def __repr__(self):
|
def __repr__(self):
|
||||||
return "Connection to the SSH Tunnel for host '{0}' has been lost. " \
|
return "Connection to the SSH Tunnel for host '{0}' has been lost. " \
|
||||||
"Reconnect to the database server".format(self.tunnel_host)
|
"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_CHANGEABLE = True
|
||||||
config.SECURITY_POST_CHANGE_VIEW = 'browser.change_password'
|
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 import test_setup
|
||||||
from regression.feature_utils.app_starter import AppStarter
|
from regression.feature_utils.app_starter import AppStarter
|
||||||
@ -187,7 +189,14 @@ def get_test_modules(arguments):
|
|||||||
global driver, app_starter, handle_cleanup
|
global driver, app_starter, handle_cleanup
|
||||||
|
|
||||||
if not config.SERVER_MODE:
|
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:
|
if arguments['exclude'] is not None:
|
||||||
exclude_pkgs += arguments['exclude'].split(',')
|
exclude_pkgs += arguments['exclude'].split(',')
|
||||||
|
|
||||||
|
Loading…
Reference in New Issue
Block a user