Revamp the current password saving implementation to keyring and reducing repeated OS user password prompts. #7076

The new implementation will store the master password in the keyring instead of storing each and every server password separately. The master password will be used to encrypt/decrypt server password when storing in the pgAdmin config DB.
This commit is contained in:
Yogesh Mahajan
2024-08-22 16:44:57 +05:30
committed by GitHub
parent cda7d7de90
commit 1257ec9969
16 changed files with 610 additions and 563 deletions

View File

@@ -583,7 +583,7 @@ ALLOW_SAVE_TUNNEL_PASSWORD = False
# Applicable for desktop mode only # Applicable for desktop mode only
########################################################################## ##########################################################################
MASTER_PASSWORD_REQUIRED = True MASTER_PASSWORD_REQUIRED = True
USE_OS_SECRET_STORAGE = True
########################################################################## ##########################################################################
# pgAdmin encrypts the database connection and ssh tunnel password using a # pgAdmin encrypts the database connection and ssh tunnel password using a

View File

@@ -30,7 +30,7 @@ from pgadmin.utils.ajax import make_json_response, internal_server_error
from pgadmin.authenticate.internal import BaseAuthentication from pgadmin.authenticate.internal import BaseAuthentication
from pgadmin.authenticate import get_auth_sources from pgadmin.authenticate import get_auth_sources
from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.utils.master_password import set_crypt_key
try: try:
import gssapi import gssapi
@@ -193,7 +193,8 @@ class KerberosAuthentication(BaseAuthentication):
if status: if status:
# Saving the first 15 characters of the kerberos key # Saving the first 15 characters of the kerberos key
# to encrypt/decrypt database password # to encrypt/decrypt database password
session['pass_enc_key'] = auth_header[1][0:15] pass_enc_key = auth_header[1][0:15]
set_crypt_key(pass_enc_key)
# Create user # Create user
retval = self.__auto_create_user( retval = self.__auto_create_user(
str(negotiate.initiator_name)) str(negotiate.initiator_name))

View File

@@ -26,6 +26,7 @@ from pgadmin.utils import PgAdminModule, get_safe_post_login_redirect, \
get_safe_post_logout_redirect get_safe_post_logout_redirect
from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.model import db from pgadmin.model import db
from pgadmin.utils.master_password import set_crypt_key
OAUTH2_LOGOUT = 'oauth2.logout' OAUTH2_LOGOUT = 'oauth2.logout'
OAUTH2_AUTHORIZE = 'oauth2.authorize' OAUTH2_AUTHORIZE = 'oauth2.authorize'
@@ -210,7 +211,8 @@ class OAuth2Authentication(BaseAuthentication):
session['oauth2_token'] = self.oauth2_clients[ session['oauth2_token'] = self.oauth2_clients[
self.oauth2_current_client].authorize_access_token() self.oauth2_current_client].authorize_access_token()
session['pass_enc_key'] = session['oauth2_token']['access_token'] pass_enc_key = session['oauth2_token']['access_token']
set_crypt_key(pass_enc_key)
if 'OAUTH2_LOGOUT_URL' in self.oauth2_config[ if 'OAUTH2_LOGOUT_URL' in self.oauth2_config[
self.oauth2_current_client]: self.oauth2_current_client]:

View File

@@ -12,7 +12,7 @@
import secrets import secrets
import string import string
import config import config
from flask import request, current_app, session, Response, render_template, \ from flask import request, current_app, Response, render_template, \
url_for url_for
from flask_babel import gettext from flask_babel import gettext
from flask_security import login_user from flask_security import login_user
@@ -23,6 +23,7 @@ from pgadmin.utils.constants import WEBSERVER
from pgadmin.utils import PgAdminModule from pgadmin.utils import PgAdminModule
from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.utils.csrf import pgCSRFProtect
from flask_security.utils import logout_user from flask_security.utils import logout_user
from pgadmin.utils.master_password import set_crypt_key
class WebserverModule(PgAdminModule): class WebserverModule(PgAdminModule):
@@ -89,8 +90,9 @@ class WebserverAuthentication(BaseAuthentication):
return False, gettext( return False, gettext(
"Webserver authenticate failed.") "Webserver authenticate failed.")
session['pass_enc_key'] = ''.join( pass_enc_key = ''.join(
(secrets.choice(string.ascii_lowercase) for _ in range(10))) (secrets.choice(string.ascii_lowercase) for _ in range(10)))
set_crypt_key(pass_enc_key)
useremail = request.environ.get('mail') useremail = request.environ.get('mail')
if not useremail: if not useremail:
useremail = '' useremail = ''

View File

@@ -10,22 +10,21 @@
import json import json
import logging import logging
import os import os
import secrets
import sys import sys
from abc import ABCMeta, abstractmethod from abc import ABCMeta, abstractmethod
from smtplib import SMTPConnectError, SMTPResponseException, \ from smtplib import SMTPConnectError, SMTPResponseException, \
SMTPServerDisconnected, SMTPDataError, SMTPHeloError, SMTPException, \ SMTPServerDisconnected, SMTPDataError, SMTPHeloError, SMTPException, \
SMTPAuthenticationError, SMTPSenderRefused, SMTPRecipientsRefused SMTPAuthenticationError, SMTPSenderRefused, SMTPRecipientsRefused
from socket import error as SOCKETErrorException from socket import error as SOCKETErrorException
from urllib.request import urlopen
from pgadmin.utils.constants import KEY_RING_SERVICE_NAME, \
KEY_RING_USERNAME_FORMAT, KEY_RING_DESKTOP_USER, KEY_RING_TUNNEL_FORMAT, \
MessageType
import time
import keyring import keyring
from keyring.errors import KeyringLocked
from pgadmin.utils.constants import KEY_RING_SERVICE_NAME, \
KEY_RING_USER_NAME,MessageType
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, session flash, Response, request, redirect, session
from flask_babel import gettext from flask_babel import gettext
from libgravatar import Gravatar from libgravatar import Gravatar
from flask_security import current_user from flask_security import current_user
@@ -44,22 +43,24 @@ from werkzeug.datastructures import MultiDict
import config import config
from pgadmin import current_blueprint from pgadmin import current_blueprint
from pgadmin.authenticate import get_logout_url from pgadmin.authenticate import get_logout_url
from pgadmin.authenticate.mfa.utils import mfa_required, is_mfa_enabled from pgadmin.authenticate.mfa.utils import is_mfa_enabled
from pgadmin.settings import get_setting, store_setting from pgadmin.settings import get_setting
from pgadmin.utils import PgAdminModule from pgadmin.utils import PgAdminModule
from pgadmin.utils.ajax import make_json_response, internal_server_error, \ from pgadmin.utils.ajax import make_json_response, internal_server_error, \
bad_request bad_request
from pgadmin.utils.csrf import pgCSRFProtect from pgadmin.utils.csrf import pgCSRFProtect
from pgadmin.utils.preferences import Preferences from pgadmin.utils.preferences import Preferences
from pgadmin.utils.menu import MenuItem
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, \ from pgadmin.utils.master_password import validate_master_password, \
set_masterpass_check_text, cleanup_master_password, get_crypt_key, \ set_masterpass_check_text, cleanup_master_password, get_crypt_key, \
set_crypt_key, process_masterpass_disabled set_crypt_key, process_masterpass_disabled, \
delete_local_storage_master_key, \
get_master_password_key_from_os_secret, \
get_master_password_from_master_hook
from pgadmin.model import User, db from pgadmin.model import User, db
from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE,\ from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE, \
INTERNAL, KERBEROS, LDAP, QT_DEFAULT_PLACEHOLDER, OAUTH2, WEBSERVER,\ INTERNAL, KERBEROS, LDAP, QT_DEFAULT_PLACEHOLDER, OAUTH2, WEBSERVER, \
VW_EDT_DEFAULT_PLACEHOLDER VW_EDT_DEFAULT_PLACEHOLDER
from pgadmin.authenticate import AuthSourceManager from pgadmin.authenticate import AuthSourceManager
from pgadmin.utils.exception import CryptKeyMissing from pgadmin.utils.exception import CryptKeyMissing
@@ -74,7 +75,7 @@ PGADMIN_BROWSER = 'pgAdmin.Browser'
PASS_ERROR_MSG = gettext('Your password has not been changed.') PASS_ERROR_MSG = gettext('Your password has not been changed.')
SMTP_SOCKET_ERROR = gettext( SMTP_SOCKET_ERROR = gettext(
'SMTP Socket error: {error}\n {pass_error}').format( 'SMTP Socket error: {error}\n {pass_error}').format(
error={}, pass_error=PASS_ERROR_MSG) error={}, pass_error=PASS_ERROR_MSG)
SMTP_ERROR = gettext('SMTP error: {error}\n {pass_error}').format( SMTP_ERROR = gettext('SMTP error: {error}\n {pass_error}').format(
error={}, pass_error=PASS_ERROR_MSG) error={}, pass_error=PASS_ERROR_MSG)
PASS_ERROR = gettext('Error: {error}\n {pass_error}').format( PASS_ERROR = gettext('Error: {error}\n {pass_error}').format(
@@ -631,14 +632,13 @@ def get_nodes():
def form_master_password_response(existing=True, present=False, errmsg=None, def form_master_password_response(existing=True, present=False, errmsg=None,
keyring_name='', keyring_name='', master_password_hook=''):
invalid_master_password_hook=False):
return make_json_response(data={ return make_json_response(data={
'present': present, 'present': present,
'reset': existing, 'reset': existing,
'errmsg': errmsg, 'errmsg': errmsg,
'keyring_name': keyring_name, 'keyring_name': keyring_name,
'invalid_master_password_hook': invalid_master_password_hook, 'master_password_hook': master_password_hook,
'is_error': True if errmsg else False 'is_error': True if errmsg else False
}) })
@@ -673,14 +673,11 @@ def reset_master_password():
Removes the master password and remove all saved passwords Removes the master password and remove all saved passwords
This password will be used to encrypt/decrypt saved server passwords This password will be used to encrypt/decrypt saved server passwords
""" """
if not config.DISABLED_LOCAL_PASSWORD_STORAGE:
# This is to set the Desktop user password so it will not ask for
# migrate exiting passwords as those are getting cleared
keyring.set_password(KEY_RING_SERVICE_NAME,
KEY_RING_DESKTOP_USER.format(
current_user.username), 'test')
cleanup_master_password() cleanup_master_password()
status, crypt_key = get_crypt_key() status, crypt_key = get_crypt_key()
if not status and config.MASTER_PASSWORD_HOOK:
crypt_key = get_master_password_from_master_hook()
# Set masterpass_check if MASTER_PASSWORD_HOOK is set which provides # Set masterpass_check if MASTER_PASSWORD_HOOK is set which provides
# encryption key # encryption key
if config.MASTER_PASSWORD_REQUIRED and config.MASTER_PASSWORD_HOOK: if config.MASTER_PASSWORD_REQUIRED and config.MASTER_PASSWORD_HOOK:
@@ -695,9 +692,7 @@ def set_master_password():
Set the master password and store in the memory Set the master password and store in the memory
This password will be used to encrypt/decrypt saved server passwords This password will be used to encrypt/decrypt saved server passwords
""" """
data = None data = None
if request.form: if request.form:
data = request.form data = request.form
elif request.data: elif request.data:
@@ -708,130 +703,105 @@ def set_master_password():
if data != '': if data != '':
data = json.loads(data) data = json.loads(data)
if not config.DISABLED_LOCAL_PASSWORD_STORAGE and \ keyring_name = ''
(config.ALLOW_SAVE_PASSWORD or config.ALLOW_SAVE_TUNNEL_PASSWORD): errmsg = ''
if data.get('password') and config.MASTER_PASSWORD_REQUIRED and\ if not config.SERVER_MODE:
not validate_master_password(data.get('password')): if config.USE_OS_SECRET_STORAGE:
return form_master_password_response( try:
present=False, # Try to get master key is from local os storage
keyring_name=config.KEYRING_NAME, master_key = get_master_password_key_from_os_secret()
errmsg=gettext("Incorrect master password") master_password = data.get('password', None)
) keyring_name = config.KEYRING_NAME
from pgadmin.model import Server if not master_key:
from pgadmin.utils.crypto import decrypt # Generate new one and migration required
desktop_user = current_user master_key = secrets.token_urlsafe(12)
enc_key = data['password'] # migrate existing server passwords
if not config.MASTER_PASSWORD_REQUIRED: from pgadmin.browser.server_groups.servers.utils \
status, enc_key = get_crypt_key() import migrate_saved_passwords
if not status: migrated_save_passwords, error = migrate_saved_passwords(
raise CryptKeyMissing master_key, master_password)
try: if migrated_save_passwords:
all_server = Server.query.all() # Update keyring
saved_password_servers = [server for server in all_server if
server.save_password]
# pgAdmin will use the OS password manager to store the server
# password, here migrating the existing saved server password to
# OS password manager
if len(saved_password_servers) > 0 and (keyring.get_password(
KEY_RING_SERVICE_NAME, KEY_RING_DESKTOP_USER.format(
desktop_user.username)) or enc_key):
is_migrated = False
for server in saved_password_servers:
if enc_key:
if server.password and config.ALLOW_SAVE_PASSWORD:
name = KEY_RING_USERNAME_FORMAT.format(server.name,
server.id)
password = decrypt(server.password,
enc_key).decode()
# Store the password using OS password manager
keyring.set_password(KEY_RING_SERVICE_NAME, name,
password)
is_migrated = True
setattr(server, 'password', None)
if server.tunnel_password and \
config.ALLOW_SAVE_TUNNEL_PASSWORD:
tname = KEY_RING_TUNNEL_FORMAT.format(server.name,
server.id)
tpassword = decrypt(server.tunnel_password,
enc_key).decode()
# Store the password using OS password manager
keyring.set_password(KEY_RING_SERVICE_NAME, tname,
tpassword)
is_migrated = True
setattr(server, 'tunnel_password', None)
db.session.commit()
# Store the password using OS password manager
keyring.set_password(KEY_RING_SERVICE_NAME,
KEY_RING_DESKTOP_USER.format(
desktop_user.username), 'test')
return form_master_password_response(
existing=True,
present=True,
keyring_name=config.KEYRING_NAME if is_migrated else ''
)
else:
if len(all_server) == 0:
# Store the password using OS password manager
keyring.set_password(KEY_RING_SERVICE_NAME,
KEY_RING_DESKTOP_USER.format(
desktop_user.username), 'test')
return form_master_password_response(
present=True,
)
else:
is_master_password_present = True
keyring_name = ''
for server in all_server:
is_password_present = \
server.save_password or server.tunnel_password
if server.password and is_password_present:
is_master_password_present = False
keyring_name = config.KEYRING_NAME
break
if is_master_password_present:
# Store the password using OS password manager
keyring.set_password(KEY_RING_SERVICE_NAME, keyring.set_password(KEY_RING_SERVICE_NAME,
KEY_RING_DESKTOP_USER.format( KEY_RING_USER_NAME,
desktop_user.username), master_key)
'test') # set crypt key
set_crypt_key(master_key)
return form_master_password_response(
existing=True,
present=True,
keyring_name=keyring_name)
else:
if not error:
set_crypt_key(master_key)
return form_master_password_response(
present=True)
# Migration failed
elif error != 'Master password required':
errmsg = error
return form_master_password_response(
existing=False,
present=True,
errmsg=errmsg,
keyring_name=keyring_name)
else:
current_app.logger.warning(
' Master key was already present in the keyring,'
' hence not doing any migration')
# Key is already generated and set, no migration required
# set crypt key
set_crypt_key(master_key)
return form_master_password_response( return form_master_password_response(
present=is_master_password_present, present=True)
keyring_name=keyring_name except KeyringLocked as e:
) current_app.logger.warning(
except Exception as e: 'Failed to set because Access Denied.'
current_app.logger.warning( ' Error: {0}'.format(e))
'Fail set password using OS password manager' config.USE_OS_SECRET_STORAGE = False
', fallback to master password. Error: {0}'.format(e) except Exception as e:
) current_app.logger.warning(
config.DISABLED_LOCAL_PASSWORD_STORAGE = True 'Failed to set encryption key using OS password manager'
', fallback to master password. Error: {0}'.format(e))
# If the master password is required and the master password hook # Also if masterpass_check is none it means previously
# is specified then try to retrieve the encryption key and update data. # passwords were migrated using keyring crypt key.
# If there is an error while retrieving it, return an error message. # Reset all passwords because we are going to master password
if config.SERVER_MODE and config.MASTER_PASSWORD_REQUIRED and \ # again and while setting master password, all server
config.MASTER_PASSWORD_HOOK: # passwords are decrypted using old key before re-encryption
status, enc_key = get_crypt_key() if current_user.masterpass_check is None:
if status: from pgadmin.browser.server_groups.servers.utils \
data = {'password': enc_key, 'submit_password': True} import remove_saved_passwords, update_session_manager
remove_saved_passwords(current_user.id)
update_session_manager(current_user.id)
# Disable local os storage if any exception while creation
config.USE_OS_SECRET_STORAGE = False
delete_local_storage_master_key()
else: else:
error = gettext('The master password could not be retrieved from ' # if os secret storage disabled now, but was used once then
'the MASTER_PASSWORD_HOOK utility specified {0}.' # remove all the saved passwords
'Please check that the hook utility is configured' delete_local_storage_master_key()
' correctly.'.format(config.MASTER_PASSWORD_HOOK)) else:
return form_master_password_response( # If the master password is required and the master password hook
existing=False, # is specified then try to retrieve the encryption key and update data.
present=False, # If there is an error while retrieving it, return an error message.
errmsg=error, if config.MASTER_PASSWORD_REQUIRED and config.MASTER_PASSWORD_HOOK:
invalid_master_password_hook=True master_password = get_master_password_from_master_hook()
) if master_password:
data = {'password': master_password, 'submit_password': True}
else:
errmsg = gettext(
'The master password could not be retrieved from the'
' MASTER_PASSWORD_HOOK utility specified {0}. Please check'
' that the hook utility is configured correctly.'.format(
config.MASTER_PASSWORD_HOOK))
return form_master_password_response(
existing=False,
present=False,
errmsg=errmsg,
master_password_hook=config.MASTER_PASSWORD_HOOK,
keyring_name=keyring_name
)
# Master password is applicable for Desktop mode and in server mode # Master password is applicable for Desktop mode and in server mode
# only when auth sources are oauth, kerberos, webserver. # only when auth sources are oauth, kerberos, webserver.
@@ -843,20 +813,16 @@ def set_master_password():
if current_user.masterpass_check is not None and \ if current_user.masterpass_check is not None and \
data.get('submit_password', False) and \ data.get('submit_password', False) and \
not validate_master_password(data.get('password')): not validate_master_password(data.get('password')):
errmsg = '' if config.MASTER_PASSWORD_HOOK \
else gettext("Incorrect master password")
invalid_master_password_hook = \
True if config.MASTER_PASSWORD_HOOK else False
return form_master_password_response( return form_master_password_response(
existing=True, existing=True,
present=False, present=False,
errmsg=errmsg, errmsg=errmsg,
invalid_master_password_hook=invalid_master_password_hook master_password_hook=config.MASTER_PASSWORD_HOOK,
keyring_name=keyring_name
) )
# if master password received in request # if master password received in request
if data != '' and data.get('password', '') != '': if data != '' and data.get('password', '') != '':
# store the master pass in the memory # store the master pass in the memory
set_crypt_key(data.get('password')) set_crypt_key(data.get('password'))
@@ -864,7 +830,6 @@ def set_master_password():
# master check is not set, which means the server password # master check is not set, which means the server password
# data is old and is encrypted with old key # data is old and is encrypted with old key
# Re-encrypt with new key # Re-encrypt with new key
from pgadmin.browser.server_groups.servers.utils \ from pgadmin.browser.server_groups.servers.utils \
import reencrpyt_server_passwords import reencrpyt_server_passwords
reencrpyt_server_passwords( reencrpyt_server_passwords(
@@ -877,13 +842,14 @@ def set_master_password():
# If password in request is empty then try to get it with # If password in request is empty then try to get it with
# get_crypt_key method. If get_crypt_key() returns false status and # get_crypt_key method. If get_crypt_key() returns false status and
# masterpass_check is already set, provide a pop to enter # masterpass_check is already set, provide a popup to enter
# master password(present) without the reset option.(existing). # master password(present) without the reset option.(existing).
elif not get_crypt_key()[0] and \ elif not get_crypt_key()[0] and \
current_user.masterpass_check is not None: current_user.masterpass_check is not None:
return form_master_password_response( return form_master_password_response(
existing=True, existing=True,
present=False, present=False,
keyring_name=keyring_name
) )
# If get_crypt_key return True,but crypt_key is none and # If get_crypt_key return True,but crypt_key is none and
@@ -896,7 +862,8 @@ def set_master_password():
return form_master_password_response( return form_master_password_response(
existing=False, existing=False,
present=False, present=False,
errmsg=error_message errmsg=error_message,
keyring_name=keyring_name
) )
# if master password is disabled now, but was used once then # if master password is disabled now, but was used once then
@@ -904,7 +871,6 @@ def set_master_password():
process_masterpass_disabled() process_masterpass_disabled()
if config.SERVER_MODE and current_user.masterpass_check is None: if config.SERVER_MODE and current_user.masterpass_check is None:
crypt_key = get_crypt_key()[1] crypt_key = get_crypt_key()[1]
from pgadmin.browser.server_groups.servers.utils \ from pgadmin.browser.server_groups.servers.utils \
import reencrpyt_server_passwords import reencrpyt_server_passwords
@@ -921,8 +887,6 @@ def set_master_password():
# 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']
if hasattr(config, 'SECURITY_CHANGEABLE') and config.SECURITY_CHANGEABLE: if hasattr(config, 'SECURITY_CHANGEABLE') and config.SECURITY_CHANGEABLE:
@blueprint.route("/change_password", endpoint="change_password", @blueprint.route("/change_password", endpoint="change_password",
methods=['GET', 'POST']) methods=['GET', 'POST'])

View File

@@ -38,11 +38,8 @@ from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
SERVER_CONNECTION_CLOSED SERVER_CONNECTION_CLOSED
from sqlalchemy import or_ from sqlalchemy import or_
from pgadmin.utils.preferences import Preferences from pgadmin.utils.preferences import Preferences
from pgadmin.utils.constants import KEY_RING_SERVICE_NAME, \
KEY_RING_USERNAME_FORMAT, KEY_RING_TUNNEL_FORMAT, KEY_RING_DESKTOP_USER
from .... import socketio as sio from .... import socketio as sio
from pgadmin.utils import get_complete_file_path from pgadmin.utils import get_complete_file_path
import keyring
def has_any(data, keys): def has_any(data, keys):
@@ -255,22 +252,6 @@ class ServerModule(sg.ServerGroupPluginModule):
except Exception as e: except Exception as e:
current_app.logger.exception(e) current_app.logger.exception(e)
errmsg = str(e) errmsg = str(e)
is_password_saved = bool(server.save_password)
is_tunnel_password_saved = bool(server.tunnel_password)
if not config.DISABLED_LOCAL_PASSWORD_STORAGE:
sname = KEY_RING_USERNAME_FORMAT.format(server.name, server.id)
spassword = keyring.get_password(
KEY_RING_SERVICE_NAME, sname)
is_password_saved = bool(spassword)
tunnelname = KEY_RING_TUNNEL_FORMAT.format(server.name,
server.id)
tunnel_password = keyring.get_password(KEY_RING_SERVICE_NAME,
tunnelname)
is_tunnel_password_saved = bool(tunnel_password)
yield self.generate_browser_node( yield self.generate_browser_node(
"%d" % (server.id), "%d" % (server.id),
gid, gid,
@@ -287,8 +268,8 @@ class ServerModule(sg.ServerGroupPluginModule):
wal_pause=wal_paused, wal_pause=wal_paused,
host=server.host, host=server.host,
port=server.port, port=server.port,
is_password_saved=is_password_saved, is_password_saved=bool(server.save_password),
is_tunnel_password_saved=is_tunnel_password_saved, is_tunnel_password_saved=bool(server.tunnel_password),
was_connected=was_connected, was_connected=was_connected,
errmsg=errmsg, errmsg=errmsg,
user_id=server.user_id, user_id=server.user_id,
@@ -605,22 +586,6 @@ class ServerNode(PGChildNodeView):
manager.release() manager.release()
errmsg = "{0} : {1}".format(server.name, result) errmsg = "{0} : {1}".format(server.name, result)
is_password_saved = bool(server.save_password)
is_tunnel_password_saved = bool(server.tunnel_password)
if not config.DISABLED_LOCAL_PASSWORD_STORAGE:
sname = KEY_RING_USERNAME_FORMAT.format(server.name, server.id)
spassword = keyring.get_password(
KEY_RING_SERVICE_NAME, sname)
is_password_saved = bool(spassword)
tunnelname = KEY_RING_TUNNEL_FORMAT.format(server.name,
server.id)
tunnel_password = keyring.get_password(KEY_RING_SERVICE_NAME,
tunnelname)
is_tunnel_password_saved = bool(tunnel_password)
res.append( res.append(
self.blueprint.generate_browser_node( self.blueprint.generate_browser_node(
"%d" % (server.id), "%d" % (server.id),
@@ -637,8 +602,8 @@ class ServerNode(PGChildNodeView):
user=manager.user_info if connected else None, user=manager.user_info if connected else None,
in_recovery=in_recovery, in_recovery=in_recovery,
wal_pause=wal_paused, wal_pause=wal_paused,
is_password_saved=is_password_saved, is_password_saved=bool(server.save_password),
is_tunnel_password_saved=is_tunnel_password_saved, is_tunnel_password_saved=bool(server.tunnel_password),
errmsg=errmsg, errmsg=errmsg,
username=server.username, username=server.username,
shared=server.shared, shared=server.shared,
@@ -761,20 +726,6 @@ class ServerNode(PGChildNodeView):
server_name = s.name server_name = s.name
get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id) get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id)
db.session.delete(s) db.session.delete(s)
if not config.DISABLED_LOCAL_PASSWORD_STORAGE:
try:
sname = KEY_RING_USERNAME_FORMAT.format(
s.name,
s.id)
# Get password form OS password manager
is_present = keyring.get_password(
KEY_RING_SERVICE_NAME, sname)
# Delete saved password from OS password manager
if is_present:
keyring.delete_password(KEY_RING_SERVICE_NAME,
sname)
except keyring.errors.KeyringError as e:
config.DISABLED_LOCAL_PASSWORD_STORAGE = True
db.session.commit() db.session.commit()
self.delete_shared_server(server_name, gid, sid) self.delete_shared_server(server_name, gid, sid)
QueryHistory.clear_history(current_user.id, sid) QueryHistory.clear_history(current_user.id, sid)
@@ -888,26 +839,6 @@ class ServerNode(PGChildNodeView):
) )
try: try:
if len(old_server_name) and old_server_name != server.name and \
not config.DISABLED_LOCAL_PASSWORD_STORAGE and \
server.save_password:
# If server name is changed then update keyring with
# new server name
password = keyring.get_password(
KEY_RING_SERVICE_NAME,
KEY_RING_USERNAME_FORMAT.format(old_server_name,
server.id))
keyring.set_password(
KEY_RING_SERVICE_NAME,
KEY_RING_USERNAME_FORMAT.format(server.name, server.id),
password)
server_name = KEY_RING_USERNAME_FORMAT.format(
old_server_name, server.id)
# Delete saved password from OS password manager
keyring.delete_password(KEY_RING_SERVICE_NAME,
server_name)
db.session.commit() db.session.commit()
except Exception as e: except Exception as e:
current_app.logger.exception(e) current_app.logger.exception(e)
@@ -1175,11 +1106,10 @@ class ServerNode(PGChildNodeView):
if data[item] == '': if data[item] == '':
data[item] = None data[item] = None
if config.DISABLED_LOCAL_PASSWORD_STORAGE: # Get enc key
# Get enc key crypt_key_present, crypt_key = get_crypt_key()
crypt_key_present, crypt_key = get_crypt_key() if not crypt_key_present:
if not crypt_key_present: raise CryptKeyMissing
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']:
@@ -1276,9 +1206,7 @@ class ServerNode(PGChildNodeView):
# login with password # login with password
have_password = True have_password = True
password = data['password'] password = data['password']
if config.DISABLED_LOCAL_PASSWORD_STORAGE: password = encrypt(password, crypt_key)
password = encrypt(password, crypt_key)
elif 'passfile' in data['connection_params'] and \ elif 'passfile' in data['connection_params'] and \
data['connection_params']['passfile'] != '': data['connection_params']['passfile'] != '':
passfile = data['connection_params']['passfile'] passfile = data['connection_params']['passfile']
@@ -1286,10 +1214,7 @@ class ServerNode(PGChildNodeView):
if 'tunnel_password' in data and data["tunnel_password"] != '': if 'tunnel_password' in data and data["tunnel_password"] != '':
have_tunnel_password = True have_tunnel_password = True
tunnel_password = data['tunnel_password'] tunnel_password = data['tunnel_password']
tunnel_password = encrypt(tunnel_password, crypt_key)
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
tunnel_password = \
encrypt(tunnel_password, crypt_key)
status, errmsg = conn.connect( status, errmsg = conn.connect(
password=password, password=password,
@@ -1310,32 +1235,15 @@ class ServerNode(PGChildNodeView):
else: else:
if 'save_password' in data and data['save_password'] and \ if 'save_password' in data and data['save_password'] and \
have_password and config.ALLOW_SAVE_PASSWORD: have_password and config.ALLOW_SAVE_PASSWORD:
if config.DISABLED_LOCAL_PASSWORD_STORAGE: setattr(server, 'password', password)
setattr(server, 'password', password) db.session.commit()
db.session.commit()
else:
# Store the password using OS password manager
keyring.set_password(
KEY_RING_SERVICE_NAME,
KEY_RING_USERNAME_FORMAT.format(server.name,
server.id),
password)
if 'save_tunnel_password' in data and \ if 'save_tunnel_password' in data and \
data['save_tunnel_password'] and \ data['save_tunnel_password'] and \
have_tunnel_password and \ have_tunnel_password and \
config.ALLOW_SAVE_TUNNEL_PASSWORD: config.ALLOW_SAVE_TUNNEL_PASSWORD:
if config.DISABLED_LOCAL_PASSWORD_STORAGE: setattr(server, 'tunnel_password', tunnel_password)
setattr(server, 'tunnel_password', tunnel_password) db.session.commit()
db.session.commit()
else:
# Store the password using OS password manager
keyring.set_password(
KEY_RING_SERVICE_NAME,
KEY_RING_TUNNEL_FORMAT.format(server.name,
server.id),
tunnel_password)
tunnel_password_saved = True
replication_type = get_replication_type(conn, replication_type = get_replication_type(conn,
manager.version) manager.version)
@@ -1540,39 +1448,18 @@ class ServerNode(PGChildNodeView):
conn = manager.connection() conn = manager.connection()
crypt_key = None crypt_key = None
if server.save_password: # Get enc key
if config.DISABLED_LOCAL_PASSWORD_STORAGE or \ crypt_key_present, crypt_key = get_crypt_key()
not keyring.get_password( if not crypt_key_present:
KEY_RING_SERVICE_NAME, raise CryptKeyMissing
KEY_RING_DESKTOP_USER.format(current_user.username)):
crypt_key_present, crypt_key = get_crypt_key()
if not crypt_key_present:
raise CryptKeyMissing
else:
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
# 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:
if config.DISABLED_LOCAL_PASSWORD_STORAGE \ if server.tunnel_password is None:
and server.tunnel_password is None:
prompt_tunnel_password = True prompt_tunnel_password = True
else: else:
if not config.DISABLED_LOCAL_PASSWORD_STORAGE: tunnel_password = server.tunnel_password
# Get password form OS password manager
tunnel_password = keyring.get_password(
KEY_RING_SERVICE_NAME,
KEY_RING_TUNNEL_FORMAT.format(server.name,
server.id))
prompt_tunnel_password = not bool(tunnel_password)
else:
tunnel_password = server.tunnel_password
else: else:
tunnel_password = data['tunnel_password'] \ tunnel_password = data['tunnel_password'] \
if 'tunnel_password' in data else '' if 'tunnel_password' in data else ''
@@ -1582,11 +1469,9 @@ 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:
if config.DISABLED_LOCAL_PASSWORD_STORAGE: tunnel_password = encrypt(tunnel_password, crypt_key) \
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=str(e)) return internal_server_error(errormsg=str(e))
@@ -1608,43 +1493,20 @@ class ServerNode(PGChildNodeView):
get_complete_file_path(passfile_param): get_complete_file_path(passfile_param):
passfile = passfile_param passfile = passfile_param
else: else:
if config.DISABLED_LOCAL_PASSWORD_STORAGE: password = conn_passwd or server.password
password = conn_passwd or server.password
else:
# Get password form OS password manager
password = keyring.get_password(
KEY_RING_SERVICE_NAME,
KEY_RING_USERNAME_FORMAT.format(server.name,
server.id))
prompt_password = (
True
if password is None and server.passexec_cmd is None
else False
)
else: else:
password = data['password'] if 'password' in data else None password = data['password'] if 'password' in data else None
save_password = data['save_password']\ save_password = data['save_password']\
if 'save_password' in data else False if 'save_password' in data else False
if config.DISABLED_LOCAL_PASSWORD_STORAGE: try:
try: # Encrypt the password before saving with user's login
# Encrypt the password before saving with user's login # password key.
# password key. password = encrypt(password, crypt_key) \
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=str(e))
return internal_server_error(errormsg=str(e))
elif save_password and config.ALLOW_SAVE_PASSWORD:
# Store the password using OS password manager
keyring.set_password(
KEY_RING_SERVICE_NAME,
KEY_RING_USERNAME_FORMAT.format(
server.name, server.id), password)
# Get password form OS password manager
password = keyring.get_password(
KEY_RING_SERVICE_NAME,
KEY_RING_USERNAME_FORMAT.format(server.name, server.id))
# 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
@@ -1691,7 +1553,7 @@ class ServerNode(PGChildNodeView):
# Save the encrypted password using the user's login # Save the encrypted password using the user's login
# password key, if there is any password to save # password key, if there is any password to save
if password and config.DISABLED_LOCAL_PASSWORD_STORAGE: if password:
if server.shared and server.user_id != current_user.id: if server.shared and server.user_id != current_user.id:
setattr(shared_server, 'password', password) setattr(shared_server, 'password', password)
else: else:
@@ -1707,18 +1569,8 @@ class ServerNode(PGChildNodeView):
if save_tunnel_password and config.ALLOW_SAVE_TUNNEL_PASSWORD: if save_tunnel_password and config.ALLOW_SAVE_TUNNEL_PASSWORD:
try: try:
if config.DISABLED_LOCAL_PASSWORD_STORAGE: # Save the encrypted tunnel password.
# Save the encrypted tunnel password. setattr(server, 'tunnel_password', tunnel_password)
setattr(server, 'tunnel_password', tunnel_password)
else:
# Store the password using OS password manager
keyring.set_password(
KEY_RING_SERVICE_NAME,
KEY_RING_TUNNEL_FORMAT.format(server.name,
server.id),
tunnel_password)
setattr(server, 'tunnel_password', None)
db.session.commit() db.session.commit()
except Exception as e: except Exception as e:
# Release Connection # Release Connection
@@ -1881,28 +1733,16 @@ class ServerNode(PGChildNodeView):
elif request.data: elif request.data:
data = json.loads(request.data) data = json.loads(request.data)
crypt_key = None # Get enc key
crypt_key_present, crypt_key = get_crypt_key()
if config.DISABLED_LOCAL_PASSWORD_STORAGE: if not crypt_key_present:
# Get enc key raise CryptKeyMissing
crypt_key_present, crypt_key = get_crypt_key()
if not crypt_key_present:
raise CryptKeyMissing
# Fetch Server Details # Fetch Server Details
server = Server.query.filter_by(id=sid).first() server = Server.query.filter_by(id=sid).first()
if server is None: if server is None:
return bad_request(self.not_found_error_msg()) return bad_request(self.not_found_error_msg())
spassword = None
if not config.DISABLED_LOCAL_PASSWORD_STORAGE and \
bool(server.save_password):
sname = KEY_RING_USERNAME_FORMAT.format(server.name,
server.id)
spassword = keyring.get_password(
KEY_RING_SERVICE_NAME, sname)
# Fetch User Details. # Fetch User Details.
user = User.query.filter_by(id=current_user.id).first() user = User.query.filter_by(id=current_user.id).first()
if user is None: if user is None:
@@ -1914,9 +1754,9 @@ class ServerNode(PGChildNodeView):
# If there is no password found for the server # If there is no password found for the server
# then check for pgpass file # then check for pgpass file
if (not server.password or spassword) and \ if not server.password and not manager.password and \
not manager.password and hasattr(server, 'connection_params') \ hasattr(server, 'connection_params') and \
and 'passfile' in server.connection_params and \ 'passfile' in server.connection_params and \
manager.get_connection_param_value('passfile') and \ manager.get_connection_param_value('passfile') and \
server.connection_params['passfile'] == \ server.connection_params['passfile'] == \
manager.get_connection_param_value('passfile'): manager.get_connection_param_value('passfile'):
@@ -1956,17 +1796,9 @@ 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:
if not config.DISABLED_LOCAL_PASSWORD_STORAGE: decrypted_password = decrypt(manager.password, crypt_key)
if spassword: if isinstance(decrypted_password, bytes):
decrypted_password = spassword decrypted_password = decrypted_password.decode()
else:
decrypted_password = manager.password
else:
decrypted_password = decrypt(manager.password, crypt_key)
if isinstance(decrypted_password, bytes):
decrypted_password = decrypted_password.decode()
password = data['password'] password = data['password']
# Validate old password before setting new. # Validate old password before setting new.
@@ -1998,17 +1830,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:
if config.DISABLED_LOCAL_PASSWORD_STORAGE: password = encrypt(data['newPassword'], crypt_key)
password = encrypt(data['newPassword'], crypt_key)
elif not config.DISABLED_LOCAL_PASSWORD_STORAGE:
if config.ALLOW_SAVE_PASSWORD and bool(
server.save_password):
keyring.set_password(
KEY_RING_SERVICE_NAME,
KEY_RING_USERNAME_FORMAT.format(server.name,
server.id),
data['newPassword'])
password = data['newPassword']
# 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:
@@ -2232,25 +2054,10 @@ class ServerNode(PGChildNodeView):
server = ServerModule. \ server = ServerModule. \
get_shared_server_properties(server, shared_server) get_shared_server_properties(server, shared_server)
if config.DISABLED_LOCAL_PASSWORD_STORAGE: if server.shared and server.user_id != current_user.id:
if server.shared and server.user_id != current_user.id: setattr(shared_server, 'password', None)
setattr(shared_server, 'password', None)
else:
setattr(server, 'password', None)
else: else:
try: setattr(server, 'password', None)
server_name = KEY_RING_USERNAME_FORMAT.format(server.name,
server.id)
# Get password form OS password manager
is_present = keyring.get_password(KEY_RING_SERVICE_NAME,
server_name)
if is_present:
# Delete saved password from OS password manager
keyring.delete_password(KEY_RING_SERVICE_NAME,
server_name)
except keyring.errors.KeyringError as e:
config.DISABLED_LOCAL_PASSWORD_STORAGE = True
setattr(server, 'save_password', None)
# If password was saved then clear the flag also # If password was saved then clear the flag also
# 0 is False in SQLite db # 0 is False in SQLite db
@@ -2289,19 +2096,8 @@ class ServerNode(PGChildNodeView):
success=0, success=0,
info=self.not_found_error_msg() info=self.not_found_error_msg()
) )
if config.DISABLED_LOCAL_PASSWORD_STORAGE: setattr(server, 'tunnel_password', None)
setattr(server, 'tunnel_password', None) db.session.commit()
db.session.commit()
else:
server_name = KEY_RING_TUNNEL_FORMAT.format(server.name,
server.id)
# Get password form OS password manager
is_present = keyring.get_password(KEY_RING_SERVICE_NAME,
server_name)
if is_present:
# Delete saved password from OS password manager
keyring.delete_password(KEY_RING_SERVICE_NAME, server_name)
setattr(server, 'tunnel_password', None)
except Exception as e: except Exception as e:
current_app.logger.error( current_app.logger.error(
"Unable to clear ssh tunnel password." "Unable to clear ssh tunnel password."

View File

@@ -9,12 +9,20 @@
"""Server helper utilities""" """Server helper utilities"""
from ipaddress import ip_address from ipaddress import ip_address
import keyring
from flask_login import current_user
from werkzeug.exceptions import InternalServerError from werkzeug.exceptions import InternalServerError
from flask import render_template from flask import render_template
from pgadmin.utils.constants import KEY_RING_USERNAME_FORMAT, \
KEY_RING_SERVICE_NAME, KEY_RING_USER_NAME, KEY_RING_TUNNEL_FORMAT, \
KEY_RING_DESKTOP_USER
from pgadmin.utils.crypto import encrypt, decrypt from pgadmin.utils.crypto import encrypt, decrypt
import config import config
from pgadmin.model import db, Server from pgadmin.model import db, Server
from flask import current_app
from pgadmin.utils.exception import CryptKeyMissing
from pgadmin.utils.master_password import validate_master_password, \
get_crypt_key, set_masterpass_check_text
def is_valid_ipaddress(address): def is_valid_ipaddress(address):
@@ -232,6 +240,157 @@ def _password_check(server, manager, old_key, new_key):
manager.password = password manager.password = password
def migrate_passwords_from_os_secret_storage(servers, enc_key):
"""
Migrate password stored in os secret storage
:param servers: server list
:param enc_key: new encryption key
:return: True if successful else False
"""
passwords_migrated = False
error = ''
try:
if len(servers) > 0:
for server in servers:
server_name = KEY_RING_USERNAME_FORMAT.format(server.name,
server.id)
server_password = keyring.get_password(
KEY_RING_SERVICE_NAME, server_name)
if server_password:
server_password = encrypt(server_password, enc_key)
setattr(server, 'password', server_password)
else:
setattr(server, 'save_password', 0)
tunnel_name = KEY_RING_TUNNEL_FORMAT.format(server.name,
server.id)
tunnel_password = keyring.get_password(
KEY_RING_SERVICE_NAME, tunnel_name)
if tunnel_password:
setattr(server, 'tunnel_password', tunnel_password)
keyring.delete_password(
KEY_RING_SERVICE_NAME, tunnel_name)
else:
setattr(server, 'tunnel_password', None)
passwords_migrated = True
except Exception as e:
error = 'Failed to migrate passwords stored using OS' \
' password manager.Error: {0}'.format(e)
current_app.logger.warning(error)
return passwords_migrated, error
def migrate_passwords_from_pgadmin_db(servers, old_key, enc_key):
"""
Migrates passwords stored in pgadmin db
:param servers: list of servers
:param old_key: old encryption key
:param enc_key: new encryption key
:return: True if successful else False
"""
error = ''
passwords_migrated = False
try:
for ser in servers:
if ser.password:
password = decrypt(ser.password, old_key).decode()
server_password = encrypt(password, enc_key)
setattr(ser, 'password', server_password)
if ser.tunnel_password:
password = decrypt(ser.tunnel_password, old_key).decode()
tunnel_password = encrypt(password, enc_key)
setattr(ser, 'tunnel_password', tunnel_password)
passwords_migrated = True
except Exception as e:
error = 'Failed to migrate passwords stored using master password or' \
' user password password manager. Error: {0}'.format(e)
current_app.logger.warning(error)
config.USE_OS_SECRET_STORAGE = False
return passwords_migrated, error
def migrate_saved_passwords(master_key, master_password):
"""
Function will migrate password stored in pgadmin db and os secret storage
with separate entry for each server(initial keyring implementation #5123).
Now all saved passwords will be stored in pgadmin db which are encrypted
using master_key which is stored in local os storage.
:param master_key: encryption key from local os storage
:param master_password: set by user if MASTER_PASSWORD_REQUIRED=True
:param old_crypt_key: enc_key with ith passwords were encrypted when
MASTER_PASSWORD_REQUIRED=False
:return: True if all passwords are migrated successfully.
"""
error = ''
old_key = None
passwords_migrated = False
if config.ALLOW_SAVE_PASSWORD or config.ALLOW_SAVE_TUNNEL_PASSWORD:
# Get servers with saved password
all_server = Server.query.all()
saved_password_servers = [ser for ser in all_server
if ser.save_password or ser.tunnel_password]
servers_with_pwd_in_os_secret = []
servers_with_pwd_in_pgadmin_db = []
for ser in saved_password_servers:
if ser.password is None:
servers_with_pwd_in_os_secret.append(ser)
else:
servers_with_pwd_in_pgadmin_db.append(ser)
# No server passwords are saved
if len(saved_password_servers) == 0:
current_app.logger.warning(
'There are no saved passwords')
return passwords_migrated, error
# If not master password received return and follow
# normal Master password path
if config.MASTER_PASSWORD_REQUIRED:
if current_user.masterpass_check is not None and \
not master_password:
error = 'Master password required'
return passwords_migrated, error
elif master_password:
old_key = master_password
else:
old_key = current_user.password
# servers passwords stored with os storage are present.
if len(servers_with_pwd_in_os_secret) > 0:
current_app.logger.warning(
'Re-encrypting passwords saved using os password manager')
passwords_migrated, error = \
migrate_passwords_from_os_secret_storage(
servers_with_pwd_in_os_secret, master_key)
if len(servers_with_pwd_in_pgadmin_db) > 0 and old_key:
# if master_password present and masterpass_check is present,
# server passwords are encrypted with master password
current_app.logger.warning(
'Re-encrypting passwords saved using master password')
passwords_migrated, error = migrate_passwords_from_pgadmin_db(
servers_with_pwd_in_pgadmin_db, old_key, master_key)
# clear master_pass check once passwords are migrated
if passwords_migrated:
set_masterpass_check_text('', clear=True)
if passwords_migrated:
# commit the changes once all are migrated
db.session.commit()
# Delete passwords from os password manager
if len(servers_with_pwd_in_os_secret) > 0:
delete_saved_passwords_from_os_secret_storage(
servers_with_pwd_in_os_secret)
# Update driver manager with new passwords
update_session_manager(saved_password_servers)
current_app.logger.warning('Password migration is successful')
return passwords_migrated, error
def reencrpyt_server_passwords(user_id, old_key, new_key): def reencrpyt_server_passwords(user_id, old_key, new_key):
""" """
This function will decrypt the saved passwords in SQLite with old key This function will decrypt the saved passwords in SQLite with old key
@@ -242,7 +401,6 @@ def reencrpyt_server_passwords(user_id, old_key, new_key):
for server in Server.query.filter_by(user_id=user_id).all(): for server in Server.query.filter_by(user_id=user_id).all():
manager = driver.connection_manager(server.id) manager = driver.connection_manager(server.id)
_password_check(server, manager, old_key, new_key) _password_check(server, manager, old_key, new_key)
if server.tunnel_password is not None: if server.tunnel_password is not None:
@@ -274,13 +432,88 @@ def remove_saved_passwords(user_id):
try: try:
db.session.query(Server) \ db.session.query(Server) \
.filter(Server.user_id == user_id) \ .filter(Server.user_id == user_id) \
.update({Server.password: None, Server.tunnel_password: None}) .update({Server.password: None, Server.tunnel_password: None,
Server.save_password: 0})
db.session.commit() db.session.commit()
except Exception: except Exception:
db.session.rollback() db.session.rollback()
raise raise
def delete_saved_passwords_from_os_secret_storage(servers):
"""
Delete passwords from os secret storage
:param servers: server list
:return: True if successful else False
"""
try:
# Clears entry created by initial keyring implementation
desktop_user_pass = \
KEY_RING_DESKTOP_USER.format(current_user.username)
if keyring.get_password(KEY_RING_SERVICE_NAME,desktop_user_pass):
keyring.delete_password(KEY_RING_SERVICE_NAME, desktop_user_pass)
if len(servers) > 0:
for server in servers:
server_name = KEY_RING_USERNAME_FORMAT.format(server.name,
server.id)
server_password = keyring.get_password(
KEY_RING_SERVICE_NAME, server_name)
if server_password:
keyring.delete_password(
KEY_RING_SERVICE_NAME, server_name)
else:
setattr(server, 'save_password', 0)
tunnel_name = KEY_RING_TUNNEL_FORMAT.format(server.name,
server.id)
tunnel_password = keyring.get_password(
KEY_RING_SERVICE_NAME, tunnel_name)
if tunnel_password:
keyring.delete_password(
KEY_RING_SERVICE_NAME, tunnel_name)
else:
setattr(server, 'tunnel_password', None)
return True
else:
# This means no server password to migrate
return False
except Exception as e:
current_app.logger.warning(
'Failed to delete passwords stored in OS password manager.'
'Error: {0}'.format(e))
return False
def update_session_manager(user_id=None, servers=None):
"""
Updates the passwords in the session
:param user_id:
:param servers:
:return:
"""
from pgadmin.model import Server
from pgadmin.utils.driver import get_driver
driver = get_driver(config.PG_DEFAULT_DRIVER)
try:
if user_id:
for server in Server.query.\
filter_by(user_id=current_user.id).all():
manager = driver.connection_manager(server.id)
manager.update(server)
elif servers:
for server in servers:
manager = driver.connection_manager(server.id)
manager.update(server)
else:
return False
db.session.commit()
return True
except Exception:
db.session.rollback()
raise
def get_replication_type(conn, sversion): def get_replication_type(conn, sversion):
status, res = conn.execute_dict(render_template( status, res = conn.execute_dict(render_template(
"/servers/sql/#{0}#/replication_type.sql".format(sversion) "/servers/sql/#{0}#/replication_type.sql".format(sversion)

View File

@@ -45,6 +45,7 @@ class MasterPasswordTestCase(BaseTestGenerator):
def setUp(self): def setUp(self):
config.MASTER_PASSWORD_REQUIRED = True config.MASTER_PASSWORD_REQUIRED = True
config.AUTHENTICATION_SOURCES = [INTERNAL] config.AUTHENTICATION_SOURCES = [INTERNAL]
config.USE_OS_SECRET_STORAGE = False
def runTest(self): def runTest(self):
"""This function will check change password functionality.""" """This function will check change password functionality."""

View File

@@ -116,15 +116,15 @@ def evaluate_and_patch_config(config: dict) -> dict:
config['ENABLE_PSQL'] = True config['ENABLE_PSQL'] = True
if config.get('SERVER_MODE'): if config.get('SERVER_MODE'):
config.setdefault('DISABLED_LOCAL_PASSWORD_STORAGE', True) config.setdefault('USE_OS_SECRET_STORAGE', False)
config.setdefault('KEYRING_NAME', '') config.setdefault('KEYRING_NAME', '')
else: else:
k_name = keyring.get_keyring().name k_name = keyring.get_keyring().name
# Setup USE_OS_SECRET_STORAGE false as no keyring backend available
if k_name == 'fail Keyring': if k_name == 'fail Keyring':
config.setdefault('DISABLED_LOCAL_PASSWORD_STORAGE', True) config['USE_OS_SECRET_STORAGE'] = False
config.setdefault('KEYRING_NAME', '') config['KEYRING_NAME'] = ''
else: else:
config.setdefault('DISABLED_LOCAL_PASSWORD_STORAGE', False)
config.setdefault('KEYRING_NAME', k_name) config.setdefault('KEYRING_NAME', k_name)
config.setdefault('SESSION_COOKIE_PATH', config.get('COOKIE_DEFAULT_PATH')) config.setdefault('SESSION_COOKIE_PATH', config.get('COOKIE_DEFAULT_PATH'))

View File

@@ -64,7 +64,7 @@ export default function MasterPasswordContent({ closeModal, onResetPassowrd, onO
</span> </span>
<br /> <br />
<span style={{ fontWeight: 'bold' }}> <span style={{ fontWeight: 'bold' }}>
<FormNote text={gettext(`pgAdmin now stores any saved passwords in ${keyringName}. Enter the master password for your existing pgAdmin saved passwords and they will be migrated to the operating system store when you click OK.`)}></FormNote> <FormNote text={gettext(`Saved passwords are encrypted using encryption key stored in ${keyringName}. Enter the master password for your existing pgAdmin saved passwords and they will be re-encrypted and saved when you click OK.`)}></FormNote>
</span> </span>
</Box> </Box>
<Box marginTop='12px'> <Box marginTop='12px'>

View File

@@ -81,39 +81,20 @@ export function checkMasterPassword(data, masterpass_callback_queue, cancel_call
const api = getApiInstance(); const api = getApiInstance();
api.post(url_for('browser.set_master_password'), data).then((res)=> { api.post(url_for('browser.set_master_password'), data).then((res)=> {
let isKeyring = res.data.data.keyring_name.length > 0; let isKeyring = res.data.data.keyring_name.length > 0;
let error = res.data.data.errmsg;
if(!res.data.data.present) { if(!res.data.data.present) {
if (res.data.data.invalid_master_password_hook){ showMasterPassword(res.data.data.reset, res.data.data.errmsg, masterpass_callback_queue, cancel_callback, res.data.data.keyring_name, res.data.data.master_password_hook);
if(res.data.data.is_error){
pgAdmin.Browser.notifier.error(res.data.data.errmsg);
}else{
pgAdmin.Browser.notifier.confirm(gettext('Reset Master Password'),
gettext('The master password retrieved from the master password hook utility is different from what was previously retrieved.') + '<br>'
+ gettext('Do you want to reset your master password to match?') + '<br><br>'
+ gettext('Note that this will close all open database connections and remove all saved passwords.'),
function() {
let _url = url_for('browser.reset_master_password');
api.delete(_url)
.then(() => {
pgAdmin.Browser.notifier.info('The master password has been reset.');
})
.catch((err) => {
pgAdmin.Browser.notifier.error(err.message);
});
return true;
},
function() {/* If user clicks No */ return true;}
);}
}else{
showMasterPassword(res.data.data.reset, res.data.data.errmsg, masterpass_callback_queue, cancel_callback, res.data.data.keyring_name);
}
} else { } else {
masterPassCallbacks(masterpass_callback_queue); masterPassCallbacks(masterpass_callback_queue);
if(isKeyring) { if(isKeyring) {
pgAdmin.Browser.notifier.alert(gettext('Migration successful'), if(error){
gettext(`Passwords previously saved by pgAdmin have been successfully migrated to ${res.data.data.keyring_name} and removed from the pgAdmin store.`)); pgAdmin.Browser.notifier.alert(gettext('Migration failed'),
gettext(`Passwords previously saved can not be re-encrypted using encryption key stored in the ${res.data.data.keyring_name}. due to ${error}`));
}else{
pgAdmin.Browser.notifier.alert(gettext('Migration successful'),
gettext(`Passwords previously saved are re-encrypted using encryption key stored in the ${res.data.data.keyring_name}.`));
}
} }
} }
}).catch(function(error) { }).catch(function(error) {
@@ -122,7 +103,7 @@ export function checkMasterPassword(data, masterpass_callback_queue, cancel_call
} }
// This functions is used to show the master password dialog. // This functions is used to show the master password dialog.
export function showMasterPassword(isPWDPresent, errmsg, masterpass_callback_queue, cancel_callback, keyring_name='') { export function showMasterPassword(isPWDPresent, errmsg, masterpass_callback_queue, cancel_callback, keyring_name='', master_password_hook='') {
const api = getApiInstance(); const api = getApiInstance();
let title = gettext('Set Master Password'); let title = gettext('Set Master Password');
if (keyring_name.length > 0) if (keyring_name.length > 0)
@@ -130,47 +111,73 @@ export function showMasterPassword(isPWDPresent, errmsg, masterpass_callback_que
else if (isPWDPresent) else if (isPWDPresent)
title = gettext('Unlock Saved Passwords'); title = gettext('Unlock Saved Passwords');
pgAdmin.Browser.notifier.showModal(title, (onClose)=> { if(master_password_hook){
return ( if(errmsg){
<MasterPasswordContent pgAdmin.Browser.notifier.error(errmsg);
isPWDPresent= {isPWDPresent} return true;
data={{'errmsg': errmsg}} }else{
keyringName={keyring_name} pgAdmin.Browser.notifier.confirm(gettext('Reset Master Password'),
closeModal={() => { gettext('The master password retrieved from the master password hook utility is different from what was previously retrieved.') + '<br>'
onClose(); + gettext('Do you want to reset your master password to match?') + '<br><br>'
}} + gettext('Note that this will close all open database connections and remove all saved passwords.'),
onResetPassowrd={(isKeyRing=false)=>{ function() {
pgAdmin.Browser.notifier.confirm(gettext('Reset Master Password'), let _url = url_for('browser.reset_master_password');
gettext('This will remove all the saved passwords. This will also remove established connections to ' const api = getApiInstance();
+ 'the server and you may need to reconnect again. Do you wish to continue?'), api.delete(_url)
function() { .then(() => {
let _url = url_for('browser.reset_master_password'); pgAdmin.Browser.notifier.info('The master password has been reset.');
})
.catch((err) => {
pgAdmin.Browser.notifier.error(err.message);
});
return true;
},
function() {/* If user clicks No */ return true;}
);}
}else{
api.delete(_url) pgAdmin.Browser.notifier.showModal(title, (onClose)=> {
.then(() => { return (
onClose(); <MasterPasswordContent
if(!isKeyRing) { isPWDPresent= {isPWDPresent}
showMasterPassword(false, null, masterpass_callback_queue, cancel_callback); data={{'errmsg': errmsg}}
} keyringName={keyring_name}
}) closeModal={() => {
.catch((err) => { onClose();
pgAdmin.Browser.notifier.error(err.message); }}
}); onResetPassowrd={(isKeyRing=false)=>{
return true; pgAdmin.Browser.notifier.confirm(gettext('Reset Master Password'),
}, gettext('This will remove all the saved passwords. This will also remove established connections to '
function() {/* If user clicks No */ return true;} + 'the server and you may need to reconnect again. Do you wish to continue?'),
); function() {
}} let _url = url_for('browser.reset_master_password');
onCancel={()=>{
cancel_callback?.(); api.delete(_url)
}} .then(() => {
onOK={(formData) => { onClose();
onClose(); if(!isKeyRing) {
checkMasterPassword(formData, masterpass_callback_queue, cancel_callback); showMasterPassword(false, null, masterpass_callback_queue, cancel_callback);
}} }
/> })
); .catch((err) => {
}); pgAdmin.Browser.notifier.error(err.message);
});
return true;
},
function() {/* If user clicks No */ return true;}
);
}}
onCancel={()=>{
cancel_callback?.();
}}
onOK={(formData) => {
onClose();
checkMasterPassword(formData, masterpass_callback_queue, cancel_callback);
}}
/>
);
});
}
} }
export function showChangeServerPassword() { export function showChangeServerPassword() {

View File

@@ -134,6 +134,7 @@ ACCESS_DENIED_MESSAGE = gettext(
KEY_RING_SERVICE_NAME = 'pgAdmin4' KEY_RING_SERVICE_NAME = 'pgAdmin4'
KEY_RING_USER_NAME = 'pgadmin4-master-password'
KEY_RING_USERNAME_FORMAT = KEY_RING_SERVICE_NAME + '-{0}-{1}' KEY_RING_USERNAME_FORMAT = KEY_RING_SERVICE_NAME + '-{0}-{1}'
KEY_RING_TUNNEL_FORMAT = KEY_RING_SERVICE_NAME + '-tunnel-{0}-{1}' KEY_RING_TUNNEL_FORMAT = KEY_RING_SERVICE_NAME + '-tunnel-{0}-{1}'
KEY_RING_DESKTOP_USER = KEY_RING_SERVICE_NAME + '-desktop-user-{0}' KEY_RING_DESKTOP_USER = KEY_RING_SERVICE_NAME + '-desktop-user-{0}'

View File

@@ -268,18 +268,11 @@ class Connection(BaseConnection):
manager = self.manager manager = self.manager
if config.DISABLED_LOCAL_PASSWORD_STORAGE: crypt_key_present, crypt_key = get_crypt_key()
crypt_key_present, crypt_key = get_crypt_key() if not crypt_key_present:
raise CryptKeyMissing()
if not crypt_key_present: password, encpass, is_update_password = \
raise CryptKeyMissing() self._check_user_password(kwargs)
password, encpass, is_update_password = self._check_user_password(
kwargs)
else:
password = None
encpass = kwargs['password'] if 'password' in kwargs else None
is_update_password = True
passfile = kwargs['passfile'] if 'passfile' in kwargs else None passfile = kwargs['passfile'] if 'passfile' in kwargs else None
tunnel_password = kwargs['tunnel_password'] if 'tunnel_password' in \ tunnel_password = kwargs['tunnel_password'] if 'tunnel_password' in \
@@ -305,15 +298,13 @@ class Connection(BaseConnection):
if self.reconnecting is not False: if self.reconnecting is not False:
self.password = None self.password = None
if config.DISABLED_LOCAL_PASSWORD_STORAGE: if not crypt_key_present:
is_error, errmsg, password = self._decode_password(encpass, raise CryptKeyMissing()
manager,
password, is_error, errmsg, password = self._decode_password(
crypt_key) encpass, manager, password, crypt_key)
if is_error: if is_error:
return False, errmsg return False, errmsg
else:
password = encpass
# If no password credential is found then connect request might # If no password credential is found then connect request might
# come from Query tool, ViewData grid, debugger etc tools. # come from Query tool, ViewData grid, debugger etc tools.
@@ -1580,13 +1571,11 @@ Failed to reset the connection to the server due to following error:
user = User.query.filter_by(id=current_user.id).first() user = User.query.filter_by(id=current_user.id).first()
if user is None: if user is None:
return False, self.UNAUTHORIZED_REQUEST return False, self.UNAUTHORIZED_REQUEST
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
crypt_key_present, crypt_key = get_crypt_key()
if not crypt_key_present:
return False, crypt_key
password = decrypt(password, crypt_key)\ crypt_key_present, crypt_key = get_crypt_key()
.decode() if not crypt_key_present:
return False, crypt_key
password = decrypt(password, crypt_key).decode()
try: try:
with ConnectionLocker(self.manager.kerberos_conn): with ConnectionLocker(self.manager.kerberos_conn):

View File

@@ -242,8 +242,7 @@ WHERE db.oid = {0}""".format(did))
"Could not find the specified database." "Could not find the specified database."
)) ))
if not get_crypt_key()[0] and ( if not get_crypt_key()[0]:
config.SERVER_MODE or config.DISABLED_LOCAL_PASSWORD_STORAGE):
# the reason its not connected might be missing key # the reason its not connected might be missing key
raise CryptKeyMissing() raise CryptKeyMissing()
@@ -537,16 +536,10 @@ WHERE db.oid = {0}""".format(did))
def export_password_env(self, env): def export_password_env(self, env):
if self.password: if self.password:
if config.DISABLED_LOCAL_PASSWORD_STORAGE: crypt_key_present, crypt_key = get_crypt_key()
crypt_key_present, crypt_key = get_crypt_key() if not crypt_key_present:
if not crypt_key_present: return False, crypt_key
return False, crypt_key password = decrypt(self.password, crypt_key).decode()
password = decrypt(self.password, crypt_key).decode()
elif hasattr(self.password, 'decode'):
password = self.password.decode('utf-8')
else:
password = self.password
os.environ[str(env)] = password os.environ[str(env)] = password
elif self.passexec: elif self.passexec:
password = self.passexec.get() password = self.passexec.get()
@@ -565,18 +558,15 @@ WHERE db.oid = {0}""".format(did))
return False, gettext("Unauthorized request.") return False, gettext("Unauthorized request.")
if tunnel_password is not None and tunnel_password != '': if tunnel_password is not None and tunnel_password != '':
if config.DISABLED_LOCAL_PASSWORD_STORAGE: crypt_key_present, crypt_key = get_crypt_key()
crypt_key_present, crypt_key = get_crypt_key() if not crypt_key_present:
if not crypt_key_present: raise CryptKeyMissing()
raise CryptKeyMissing()
try: try:
if config.DISABLED_LOCAL_PASSWORD_STORAGE: tunnel_password = decrypt(tunnel_password, crypt_key)
tunnel_password = decrypt(tunnel_password, crypt_key) # password is in bytes, for python3 we need it in string
# password is in bytes, for python3 we need it in string if isinstance(tunnel_password, bytes):
if isinstance(tunnel_password, bytes): tunnel_password = tunnel_password.decode()
tunnel_password = tunnel_password.decode()
except Exception as e: except Exception as e:
current_app.logger.exception(e) current_app.logger.exception(e)
return False, gettext("Failed to decrypt the SSH tunnel " return False, gettext("Failed to decrypt the SSH tunnel "

View File

@@ -1,10 +1,15 @@
import secrets
import keyring
from keyring.errors import KeyringError, KeyringLocked, NoKeyringError
import config import config
from flask import current_app, session, current_app from flask import current_app
from flask_login import current_user from flask_login import current_user
from pgadmin.model import db, User, Server from pgadmin.model import db, User, Server
from pgadmin.utils.constants import KEY_RING_SERVICE_NAME, KEY_RING_USER_NAME
from pgadmin.utils.crypto import encrypt, decrypt from pgadmin.utils.crypto import encrypt, decrypt
MASTERPASS_CHECK_TEXT = 'ideas are bulletproof' MASTERPASS_CHECK_TEXT = 'ideas are bulletproof'
@@ -23,29 +28,41 @@ def get_crypt_key():
:return: the key :return: the key
""" """
enc_key = current_app.keyManager.get() enc_key = current_app.keyManager.get()
# if desktop mode and master pass disabled then use the password hash # if desktop mode and master pass disabled then use the password hash
if not config.MASTER_PASSWORD_REQUIRED \ if not config.MASTER_PASSWORD_REQUIRED and\
and not config.SERVER_MODE: not config.USE_OS_SECRET_STORAGE and not config.SERVER_MODE:
return True, current_user.password return True, current_user.password
# if desktop mode and master pass enabled # if desktop mode and master pass enabled
elif config.MASTER_PASSWORD_REQUIRED and \ elif config.MASTER_PASSWORD_REQUIRED and \
config.MASTER_PASSWORD_HOOK is None\ enc_key is None:
and enc_key is None:
return False, None return False, None
elif not config.MASTER_PASSWORD_REQUIRED and config.SERVER_MODE and \
'pass_enc_key' in session:
return True, session['pass_enc_key']
elif config.MASTER_PASSWORD_REQUIRED and config.SERVER_MODE and \
config.MASTER_PASSWORD_HOOK and current_user.password is None:
cmd = config.MASTER_PASSWORD_HOOK
command = cmd.replace('%u', current_user.username) \
if '%u' in cmd else cmd
return get_master_password_from_master_hook(command)
else: else:
return True, enc_key return True, enc_key
def get_master_password_key_from_os_secret():
master_key = None
try:
# Try to get master key is from local os storage
master_key = keyring.get_password(
KEY_RING_SERVICE_NAME, KEY_RING_USER_NAME)
except KeyringLocked as e:
current_app.logger.warning(
'Failed to retrieve master key because Access Denied.'
' Error: {0}'.format(e))
config.USE_OS_SECRET_STORAGE = False
except Exception as e:
current_app.logger.warning(
'Failed to set encryption key using OS password manager'
', fallback to master password. Error: {0}'.format(e))
config.USE_OS_SECRET_STORAGE = False
return master_key
def generate_master_password_key_for_os_secret():
return secrets.token_urlsafe(12)
def validate_master_password(password): def validate_master_password(password):
""" """
Validate the password/key against the stored encrypted text Validate the password/key against the stored encrypted text
@@ -114,6 +131,47 @@ def cleanup_master_password():
manager.update(server) manager.update(server)
def delete_local_storage_master_key():
"""
Deletes the auto generated master key stored in keyring
"""
if not config.SERVER_MODE and not config.USE_OS_SECRET_STORAGE:
# Retrieve from os secret storage
try:
# try to get key
master_key = keyring.get_password(KEY_RING_SERVICE_NAME,
KEY_RING_USER_NAME)
if master_key:
keyring.delete_password(KEY_RING_SERVICE_NAME,
KEY_RING_USER_NAME)
from pgadmin.browser.server_groups.servers.utils \
import remove_saved_passwords
remove_saved_passwords(current_user.id)
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)
current_app.logger.warning(
'Deleted master key stored in OS password manager.')
except NoKeyringError as e:
current_app.logger.warning(
' Failed to delete master key stored in OS password manager'
' because Keyring backend not found. Error: {0}'.format(e))
config.USE_OS_SECRET_STORAGE = False
except KeyringLocked as e:
current_app.logger.warning(
' Failed to delete master key stored in OS password manager'
' because of Access Denied. Error: {0}'.format(e))
config.USE_OS_SECRET_STORAGE = False
except Exception as e:
current_app.logger.warning(
'Failed to delete master key stored in OS password manager.')
config.USE_OS_SECRET_STORAGE = False
def process_masterpass_disabled(): def process_masterpass_disabled():
""" """
On master password disable, remove the connection data from session as it On master password disable, remove the connection data from session as it
@@ -129,28 +187,30 @@ def process_masterpass_disabled():
return False return False
def get_master_password_from_master_hook(command): def get_master_password_from_master_hook():
""" """
This method executes specified command & returns output. This method executes specified command & returns output.
:param command: Shell command with absolute path :param command: Shell command with absolute path
:return: Output of command. :return: Output of command.
""" """
import subprocess import subprocess
cmd = config.MASTER_PASSWORD_HOOK
command = cmd.replace('%u', current_user.username) \
if '%u' in cmd else cmd
try: try:
p = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True) p = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
out, err = p.communicate() out, err = p.communicate()
if p.returncode == 0: if p.returncode == 0:
output = out.decode() if hasattr(out, 'decode') else out output = out.decode() if hasattr(out, 'decode') else out
output = output.strip() output = output.strip()
return True, output return output
else: else:
error = "Command '{0}' failed, exit-code={1} error = {2}".format( error = "Command '{0}' failed, exit-code={1} error = {2}".format(
command, p.returncode, str(err)) command, p.returncode, str(err))
current_app.logger.error(error) current_app.logger.error(error)
return False, None
except Exception as e: except Exception as e:
current_app.logger.exception( current_app.logger.exception(
'Failed to retrieve master password from the master password hook' 'Failed to retrieve master password from the master password hook'
' utility.Error: {0}'.format(e) ' utility.Error: {0}'.format(e)
) )
return False, None return None

View File

@@ -60,6 +60,7 @@ if config.SERVER_MODE is True:
# disable master password for test cases # disable master password for test cases
config.MASTER_PASSWORD_REQUIRED = False config.MASTER_PASSWORD_REQUIRED = False
config.USE_OS_SECRET_STORAGE = 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