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:
committed by
Akshay Joshi
parent
6f0eafb223
commit
dfa892d2a2
@@ -14,7 +14,8 @@ object.
|
||||
|
||||
"""
|
||||
import datetime
|
||||
from flask import session
|
||||
from flask import session, request
|
||||
from flask_login import current_user
|
||||
from flask_babelex import gettext
|
||||
import psycopg2
|
||||
from psycopg2.extensions import adapt
|
||||
@@ -74,23 +75,25 @@ class Driver(BaseDriver):
|
||||
assert (sid is not None and isinstance(sid, int))
|
||||
managers = None
|
||||
|
||||
server_data = Server.query.filter_by(id=sid).first()
|
||||
if server_data is None:
|
||||
return None
|
||||
|
||||
if session.sid not in self.managers:
|
||||
self.managers[session.sid] = managers = dict()
|
||||
if '__pgsql_server_managers' in session:
|
||||
session_managers = session['__pgsql_server_managers'].copy()
|
||||
session['__pgsql_server_managers'] = dict()
|
||||
|
||||
for server_id in session_managers:
|
||||
s = Server.query.filter_by(id=server_id).first()
|
||||
|
||||
if not s:
|
||||
continue
|
||||
|
||||
manager = managers[str(server_id)] = ServerManager(s)
|
||||
manager._restore(session_managers[server_id])
|
||||
manager = managers[str(sid)] = ServerManager(server_data)
|
||||
if sid in session_managers:
|
||||
manager._restore(session_managers[sid])
|
||||
manager.update_session()
|
||||
else:
|
||||
managers = self.managers[session.sid]
|
||||
if str(sid) in managers:
|
||||
manager = managers[str(sid)]
|
||||
manager._restore_connections()
|
||||
manager.update_session()
|
||||
|
||||
managers['pinged'] = datetime.datetime.now()
|
||||
if str(sid) not in managers:
|
||||
|
@@ -28,16 +28,17 @@ from pgadmin.utils.crypto import decrypt
|
||||
from psycopg2.extensions import adapt, encodings
|
||||
|
||||
import config
|
||||
from pgadmin.model import Server, User
|
||||
from pgadmin.utils.exception import ConnectionLost
|
||||
from pgadmin.model import User
|
||||
from pgadmin.utils.exception import ConnectionLost, CryptKeyMissing
|
||||
from pgadmin.utils import get_complete_file_path
|
||||
from ..abstract import BaseDriver, BaseConnection
|
||||
from ..abstract import BaseConnection
|
||||
from .cursor import DictCursor
|
||||
from .typecast import register_global_typecasters, \
|
||||
register_string_typecasters, register_binary_typecasters, \
|
||||
register_array_to_string_typecasters, ALL_JSON_TYPES
|
||||
from .encoding import getEncoding, configureDriverEncodings
|
||||
from pgadmin.utils import csv
|
||||
from pgadmin.utils.master_password import get_crypt_key
|
||||
|
||||
if sys.version_info < (3,):
|
||||
from StringIO import StringIO
|
||||
@@ -242,10 +243,16 @@ class Connection(BaseConnection):
|
||||
if encpass is None:
|
||||
encpass = self.password or getattr(manager, 'password', None)
|
||||
|
||||
self.password = encpass
|
||||
|
||||
# Reset the existing connection password
|
||||
if self.reconnecting is not False:
|
||||
self.password = None
|
||||
|
||||
crypt_key_present, crypt_key = get_crypt_key()
|
||||
if not crypt_key_present:
|
||||
raise CryptKeyMissing()
|
||||
|
||||
if encpass:
|
||||
# Fetch Logged in User Details.
|
||||
user = User.query.filter_by(id=current_user.id).first()
|
||||
@@ -254,14 +261,13 @@ class Connection(BaseConnection):
|
||||
return False, gettext("Unauthorized request.")
|
||||
|
||||
try:
|
||||
password = decrypt(encpass, user.password)
|
||||
password = decrypt(encpass, crypt_key)
|
||||
# Handling of non ascii password (Python2)
|
||||
if hasattr(str, 'decode'):
|
||||
password = password.decode('utf-8').encode('utf-8')
|
||||
# password is in bytes, for python3 we need it in string
|
||||
elif isinstance(password, bytes):
|
||||
password = password.decode()
|
||||
|
||||
except Exception as e:
|
||||
manager.stop_ssh_tunnel()
|
||||
current_app.logger.exception(e)
|
||||
@@ -521,6 +527,9 @@ WHERE
|
||||
|
||||
def __cursor(self, server_cursor=False):
|
||||
|
||||
if not get_crypt_key()[0]:
|
||||
raise CryptKeyMissing()
|
||||
|
||||
# Check SSH Tunnel is alive or not. If used by the database
|
||||
# server for the connection.
|
||||
if self.manager.use_ssh_tunnel == 1:
|
||||
@@ -1081,7 +1090,7 @@ WHERE
|
||||
current_app.logger.exception(e)
|
||||
self.reconnecting = False
|
||||
|
||||
current_app.warning(
|
||||
current_app.logger.warning(
|
||||
"Failed to reconnect the database server "
|
||||
"(#{server_id})".format(
|
||||
server_id=self.manager.sid,
|
||||
@@ -1283,7 +1292,11 @@ WHERE
|
||||
if user is None:
|
||||
return False, gettext("Unauthorized request.")
|
||||
|
||||
password = decrypt(password, user.password).decode()
|
||||
crypt_key_present, crypt_key = get_crypt_key()
|
||||
if not crypt_key_present:
|
||||
return False, crypt_key
|
||||
|
||||
password = decrypt(password, crypt_key).decode()
|
||||
|
||||
try:
|
||||
pg_conn = psycopg2.connect(
|
||||
@@ -1567,7 +1580,12 @@ Failed to reset the connection to the server due to following error:
|
||||
if user is None:
|
||||
return False, gettext("Unauthorized request.")
|
||||
|
||||
password = decrypt(password, user.password).decode()
|
||||
crypt_key_present, crypt_key = get_crypt_key()
|
||||
if not crypt_key_present:
|
||||
return False, crypt_key
|
||||
|
||||
password = decrypt(password, crypt_key)\
|
||||
.decode()
|
||||
|
||||
try:
|
||||
pg_conn = psycopg2.connect(
|
||||
|
@@ -19,13 +19,19 @@ from flask_babelex import gettext
|
||||
|
||||
from pgadmin.utils import get_complete_file_path
|
||||
from pgadmin.utils.crypto import decrypt
|
||||
from pgadmin.utils.master_password import process_masterpass_disabled
|
||||
from .connection import Connection
|
||||
from pgadmin.model import Server, User
|
||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost
|
||||
from pgadmin.utils.exception import ConnectionLost, SSHTunnelConnectionLost,\
|
||||
CryptKeyMissing
|
||||
from pgadmin.utils.master_password import get_crypt_key
|
||||
from threading import Lock
|
||||
|
||||
if config.SUPPORT_SSH_TUNNEL:
|
||||
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
|
||||
|
||||
connection_restore_lock = Lock()
|
||||
|
||||
|
||||
class ServerManager(object):
|
||||
"""
|
||||
@@ -185,6 +191,10 @@ class ServerManager(object):
|
||||
maintenance_db_id = u'DB:{0}'.format(self.db)
|
||||
if maintenance_db_id in self.connections:
|
||||
conn = self.connections[maintenance_db_id]
|
||||
# try to connect maintenance db if not connected
|
||||
if not conn.connected():
|
||||
conn.connect()
|
||||
|
||||
if conn.connected():
|
||||
status, res = conn.execute_dict(u"""
|
||||
SELECT
|
||||
@@ -205,6 +215,10 @@ WHERE db.oid = {0}""".format(did))
|
||||
"Could not find the specified database."
|
||||
))
|
||||
|
||||
if not get_crypt_key()[0]:
|
||||
# the reason its not connected might be missing key
|
||||
raise CryptKeyMissing()
|
||||
|
||||
if database is None:
|
||||
# Check SSH Tunnel is alive or not.
|
||||
if self.use_ssh_tunnel == 1:
|
||||
@@ -239,6 +253,15 @@ WHERE db.oid = {0}""".format(did))
|
||||
"""
|
||||
# restore server version from flask session if flask server was
|
||||
# restarted. As we need server version to resolve sql template paths.
|
||||
masterpass_processed = process_masterpass_disabled()
|
||||
|
||||
# The data variable is a copy so is not automatically synced
|
||||
# update here
|
||||
if masterpass_processed and 'password' in data:
|
||||
data['password'] = None
|
||||
if masterpass_processed and 'tunnel_password' in data:
|
||||
data['tunnel_password'] = None
|
||||
|
||||
from pgadmin.browser.server_groups.servers.types import ServerType
|
||||
|
||||
self.ver = data.get('ver', None)
|
||||
@@ -251,17 +274,13 @@ WHERE db.oid = {0}""".format(did))
|
||||
self.server_cls = st
|
||||
break
|
||||
|
||||
# Hmm.. we will not honour this request, when I already have
|
||||
# connections
|
||||
if len(self.connections) != 0:
|
||||
return
|
||||
|
||||
# We need to know about the existing server variant supports during
|
||||
# first connection for identifications.
|
||||
self.pinged = datetime.datetime.now()
|
||||
try:
|
||||
if 'password' in data and data['password']:
|
||||
data['password'] = data['password'].encode('utf-8')
|
||||
if hasattr(data['password'], 'encode'):
|
||||
data['password'] = data['password'].encode('utf-8')
|
||||
if 'tunnel_password' in data and data['tunnel_password']:
|
||||
data['tunnel_password'] = \
|
||||
data['tunnel_password'].encode('utf-8')
|
||||
@@ -269,36 +288,79 @@ WHERE db.oid = {0}""".format(did))
|
||||
current_app.logger.exception(e)
|
||||
|
||||
connections = data['connections']
|
||||
for conn_id in connections:
|
||||
conn_info = connections[conn_id]
|
||||
conn = self.connections[conn_info['conn_id']] = Connection(
|
||||
self, conn_info['conn_id'], conn_info['database'],
|
||||
conn_info['auto_reconnect'], conn_info['async_'],
|
||||
use_binary_placeholder=conn_info['use_binary_placeholder'],
|
||||
array_to_string=conn_info['array_to_string']
|
||||
)
|
||||
|
||||
# only try to reconnect if connection was connected previously and
|
||||
# auto_reconnect is true.
|
||||
if conn_info['wasConnected'] and conn_info['auto_reconnect']:
|
||||
try:
|
||||
# Check SSH Tunnel needs to be created
|
||||
if self.use_ssh_tunnel == 1 and not self.tunnel_created:
|
||||
status, error = self.create_ssh_tunnel(
|
||||
data['tunnel_password'])
|
||||
|
||||
# Check SSH Tunnel is alive or not.
|
||||
self.check_ssh_tunnel_alive()
|
||||
|
||||
conn.connect(
|
||||
password=data['password'],
|
||||
server_types=ServerType.types()
|
||||
with connection_restore_lock:
|
||||
for conn_id in connections:
|
||||
conn_info = connections[conn_id]
|
||||
if conn_info['conn_id'] in self.connections:
|
||||
conn = self.connections[conn_info['conn_id']]
|
||||
else:
|
||||
conn = self.connections[conn_info['conn_id']] = Connection(
|
||||
self, conn_info['conn_id'], conn_info['database'],
|
||||
conn_info['auto_reconnect'], conn_info['async_'],
|
||||
use_binary_placeholder=conn_info[
|
||||
'use_binary_placeholder'],
|
||||
array_to_string=conn_info['array_to_string']
|
||||
)
|
||||
# This will also update wasConnected flag in connection so
|
||||
# no need to update the flag manually.
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
self.connections.pop(conn_info['conn_id'])
|
||||
|
||||
# only try to reconnect if connection was connected previously
|
||||
# and auto_reconnect is true.
|
||||
if conn_info['wasConnected'] and conn_info['auto_reconnect']:
|
||||
try:
|
||||
# Check SSH Tunnel needs to be created
|
||||
if self.use_ssh_tunnel == 1 and \
|
||||
not self.tunnel_created:
|
||||
status, error = self.create_ssh_tunnel(
|
||||
data['tunnel_password'])
|
||||
|
||||
# Check SSH Tunnel is alive or not.
|
||||
self.check_ssh_tunnel_alive()
|
||||
|
||||
conn.connect(
|
||||
password=data['password'],
|
||||
server_types=ServerType.types()
|
||||
)
|
||||
# This will also update wasConnected flag in
|
||||
# connection so no need to update the flag manually.
|
||||
except CryptKeyMissing:
|
||||
# maintain the status as this will help to restore once
|
||||
# the key is available
|
||||
conn.wasConnected = conn_info['wasConnected']
|
||||
conn.auto_reconnect = conn_info['auto_reconnect']
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
self.connections.pop(conn_info['conn_id'])
|
||||
raise
|
||||
|
||||
def _restore_connections(self):
|
||||
with connection_restore_lock:
|
||||
for conn_id in self.connections:
|
||||
conn = self.connections[conn_id]
|
||||
# only try to reconnect if connection was connected previously
|
||||
# and auto_reconnect is true.
|
||||
wasConnected = conn.wasConnected
|
||||
auto_reconnect = conn.auto_reconnect
|
||||
if conn.wasConnected and conn.auto_reconnect:
|
||||
try:
|
||||
# Check SSH Tunnel needs to be created
|
||||
if self.use_ssh_tunnel == 1 and \
|
||||
not self.tunnel_created:
|
||||
status, error = self.create_ssh_tunnel()
|
||||
|
||||
# Check SSH Tunnel is alive or not.
|
||||
self.check_ssh_tunnel_alive()
|
||||
|
||||
conn.connect()
|
||||
# This will also update wasConnected flag in
|
||||
# connection so no need to update the flag manually.
|
||||
except CryptKeyMissing:
|
||||
# maintain the status as this will help to restore once
|
||||
# the key is available
|
||||
conn.wasConnected = wasConnected
|
||||
conn.auto_reconnect = auto_reconnect
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
raise
|
||||
|
||||
def release(self, database=None, conn_id=None, did=None):
|
||||
# Stop the SSH tunnel if release() function calls without
|
||||
|
Reference in New Issue
Block a user