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:
Aditya Toshniwal
2019-05-28 12:00:18 +05:30
committed by Akshay Joshi
parent 6f0eafb223
commit dfa892d2a2
44 changed files with 1509 additions and 416 deletions

View File

@@ -14,7 +14,8 @@ object.
"""
import datetime
from flask import session
from flask import session, request
from flask_login import current_user
from flask_babelex import gettext
import psycopg2
from psycopg2.extensions import adapt
@@ -74,23 +75,25 @@ class Driver(BaseDriver):
assert (sid is not None and isinstance(sid, int))
managers = None
server_data = Server.query.filter_by(id=sid).first()
if server_data is None:
return None
if session.sid not in self.managers:
self.managers[session.sid] = managers = dict()
if '__pgsql_server_managers' in session:
session_managers = session['__pgsql_server_managers'].copy()
session['__pgsql_server_managers'] = dict()
for server_id in session_managers:
s = Server.query.filter_by(id=server_id).first()
if not s:
continue
manager = managers[str(server_id)] = ServerManager(s)
manager._restore(session_managers[server_id])
manager = managers[str(sid)] = ServerManager(server_data)
if sid in session_managers:
manager._restore(session_managers[sid])
manager.update_session()
else:
managers = self.managers[session.sid]
if str(sid) in managers:
manager = managers[str(sid)]
manager._restore_connections()
manager.update_session()
managers['pinged'] = datetime.datetime.now()
if str(sid) not in managers:

View File

@@ -28,16 +28,17 @@ from pgadmin.utils.crypto import decrypt
from psycopg2.extensions import adapt, encodings
import config
from pgadmin.model import Server, User
from pgadmin.utils.exception import ConnectionLost
from pgadmin.model import User
from pgadmin.utils.exception import ConnectionLost, CryptKeyMissing
from pgadmin.utils import get_complete_file_path
from ..abstract import BaseDriver, BaseConnection
from ..abstract import BaseConnection
from .cursor import DictCursor
from .typecast import register_global_typecasters, \
register_string_typecasters, register_binary_typecasters, \
register_array_to_string_typecasters, ALL_JSON_TYPES
from .encoding import getEncoding, configureDriverEncodings
from pgadmin.utils import csv
from pgadmin.utils.master_password import get_crypt_key
if sys.version_info < (3,):
from StringIO import StringIO
@@ -242,10 +243,16 @@ class Connection(BaseConnection):
if encpass is None:
encpass = self.password or getattr(manager, 'password', None)
self.password = encpass
# Reset the existing connection password
if self.reconnecting is not False:
self.password = None
crypt_key_present, crypt_key = get_crypt_key()
if not crypt_key_present:
raise CryptKeyMissing()
if encpass:
# Fetch Logged in User Details.
user = User.query.filter_by(id=current_user.id).first()
@@ -254,14 +261,13 @@ class Connection(BaseConnection):
return False, gettext("Unauthorized request.")
try:
password = decrypt(encpass, user.password)
password = decrypt(encpass, crypt_key)
# Handling of non ascii password (Python2)
if hasattr(str, 'decode'):
password = password.decode('utf-8').encode('utf-8')
# password is in bytes, for python3 we need it in string
elif isinstance(password, bytes):
password = password.decode()
except Exception as e:
manager.stop_ssh_tunnel()
current_app.logger.exception(e)
@@ -521,6 +527,9 @@ WHERE
def __cursor(self, server_cursor=False):
if not get_crypt_key()[0]:
raise CryptKeyMissing()
# Check SSH Tunnel is alive or not. If used by the database
# server for the connection.
if self.manager.use_ssh_tunnel == 1:
@@ -1081,7 +1090,7 @@ WHERE
current_app.logger.exception(e)
self.reconnecting = False
current_app.warning(
current_app.logger.warning(
"Failed to reconnect the database server "
"(#{server_id})".format(
server_id=self.manager.sid,
@@ -1283,7 +1292,11 @@ WHERE
if user is None:
return False, gettext("Unauthorized request.")
password = decrypt(password, user.password).decode()
crypt_key_present, crypt_key = get_crypt_key()
if not crypt_key_present:
return False, crypt_key
password = decrypt(password, crypt_key).decode()
try:
pg_conn = psycopg2.connect(
@@ -1567,7 +1580,12 @@ Failed to reset the connection to the server due to following error:
if user is None:
return False, gettext("Unauthorized request.")
password = decrypt(password, user.password).decode()
crypt_key_present, crypt_key = get_crypt_key()
if not crypt_key_present:
return False, crypt_key
password = decrypt(password, crypt_key)\
.decode()
try:
pg_conn = psycopg2.connect(

View File

@@ -19,13 +19,19 @@ from flask_babelex import gettext
from pgadmin.utils import get_complete_file_path
from pgadmin.utils.crypto import decrypt
from pgadmin.utils.master_password import process_masterpass_disabled
from .connection import Connection
from pgadmin.model import Server, User
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
CryptKeyMissing
from pgadmin.utils.master_password import get_crypt_key
from threading import Lock
if config.SUPPORT_SSH_TUNNEL:
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
connection_restore_lock = Lock()
class ServerManager(object):
"""
@@ -185,6 +191,10 @@ class ServerManager(object):
maintenance_db_id = u'DB:{0}'.format(self.db)
if maintenance_db_id in self.connections:
conn = self.connections[maintenance_db_id]
# try to connect maintenance db if not connected
if not conn.connected():
conn.connect()
if conn.connected():
status, res = conn.execute_dict(u"""
SELECT
@@ -205,6 +215,10 @@ WHERE db.oid = {0}""".format(did))
"Could not find the specified database."
))
if not get_crypt_key()[0]:
# the reason its not connected might be missing key
raise CryptKeyMissing()
if database is None:
# Check SSH Tunnel is alive or not.
if self.use_ssh_tunnel == 1:
@@ -239,6 +253,15 @@ WHERE db.oid = {0}""".format(did))
"""
# restore server version from flask session if flask server was
# restarted. As we need server version to resolve sql template paths.
masterpass_processed = process_masterpass_disabled()
# The data variable is a copy so is not automatically synced
# update here
if masterpass_processed and 'password' in data:
data['password'] = None
if masterpass_processed and 'tunnel_password' in data:
data['tunnel_password'] = None
from pgadmin.browser.server_groups.servers.types import ServerType
self.ver = data.get('ver', None)
@@ -251,17 +274,13 @@ WHERE db.oid = {0}""".format(did))
self.server_cls = st
break
# Hmm.. we will not honour this request, when I already have
# connections
if len(self.connections) != 0:
return
# We need to know about the existing server variant supports during
# first connection for identifications.
self.pinged = datetime.datetime.now()
try:
if 'password' in data and data['password']:
data['password'] = data['password'].encode('utf-8')
if hasattr(data['password'], 'encode'):
data['password'] = data['password'].encode('utf-8')
if 'tunnel_password' in data and data['tunnel_password']:
data['tunnel_password'] = \
data['tunnel_password'].encode('utf-8')
@@ -269,36 +288,79 @@ WHERE db.oid = {0}""".format(did))
current_app.logger.exception(e)
connections = data['connections']
for conn_id in connections:
conn_info = connections[conn_id]
conn = self.connections[conn_info['conn_id']] = Connection(
self, conn_info['conn_id'], conn_info['database'],
conn_info['auto_reconnect'], conn_info['async_'],
use_binary_placeholder=conn_info['use_binary_placeholder'],
array_to_string=conn_info['array_to_string']
)
# only try to reconnect if connection was connected previously and
# auto_reconnect is true.
if conn_info['wasConnected'] and conn_info['auto_reconnect']:
try:
# Check SSH Tunnel needs to be created
if self.use_ssh_tunnel == 1 and not self.tunnel_created:
status, error = self.create_ssh_tunnel(
data['tunnel_password'])
# Check SSH Tunnel is alive or not.
self.check_ssh_tunnel_alive()
conn.connect(
password=data['password'],
server_types=ServerType.types()
with connection_restore_lock:
for conn_id in connections:
conn_info = connections[conn_id]
if conn_info['conn_id'] in self.connections:
conn = self.connections[conn_info['conn_id']]
else:
conn = self.connections[conn_info['conn_id']] = Connection(
self, conn_info['conn_id'], conn_info['database'],
conn_info['auto_reconnect'], conn_info['async_'],
use_binary_placeholder=conn_info[
'use_binary_placeholder'],
array_to_string=conn_info['array_to_string']
)
# This will also update wasConnected flag in connection so
# no need to update the flag manually.
except Exception as e:
current_app.logger.exception(e)
self.connections.pop(conn_info['conn_id'])
# only try to reconnect if connection was connected previously
# and auto_reconnect is true.
if conn_info['wasConnected'] and conn_info['auto_reconnect']:
try:
# Check SSH Tunnel needs to be created
if self.use_ssh_tunnel == 1 and \
not self.tunnel_created:
status, error = self.create_ssh_tunnel(
data['tunnel_password'])
# Check SSH Tunnel is alive or not.
self.check_ssh_tunnel_alive()
conn.connect(
password=data['password'],
server_types=ServerType.types()
)
# This will also update wasConnected flag in
# connection so no need to update the flag manually.
except CryptKeyMissing:
# maintain the status as this will help to restore once
# the key is available
conn.wasConnected = conn_info['wasConnected']
conn.auto_reconnect = conn_info['auto_reconnect']
except Exception as e:
current_app.logger.exception(e)
self.connections.pop(conn_info['conn_id'])
raise
def _restore_connections(self):
with connection_restore_lock:
for conn_id in self.connections:
conn = self.connections[conn_id]
# only try to reconnect if connection was connected previously
# and auto_reconnect is true.
wasConnected = conn.wasConnected
auto_reconnect = conn.auto_reconnect
if conn.wasConnected and conn.auto_reconnect:
try:
# Check SSH Tunnel needs to be created
if self.use_ssh_tunnel == 1 and \
not self.tunnel_created:
status, error = self.create_ssh_tunnel()
# Check SSH Tunnel is alive or not.
self.check_ssh_tunnel_alive()
conn.connect()
# This will also update wasConnected flag in
# connection so no need to update the flag manually.
except CryptKeyMissing:
# maintain the status as this will help to restore once
# the key is available
conn.wasConnected = wasConnected
conn.auto_reconnect = auto_reconnect
except Exception as e:
current_app.logger.exception(e)
raise
def release(self, database=None, conn_id=None, did=None):
# Stop the SSH tunnel if release() function calls without