mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Added support to use standard OS secret store to save server/ssh tunnel passwords instead of master password in pgAdmin desktop mode. #5123
This commit is contained in:
parent
e2b27da2ef
commit
736879567f
@ -56,3 +56,5 @@ passwords. This is applicable only for desktop mode users.
|
|||||||
|
|
||||||
.. warning:: Resetting the master password will also remove all saved passwords
|
.. warning:: Resetting the master password will also remove all saved passwords
|
||||||
and close all existing established connections.
|
and close all existing established connections.
|
||||||
|
|
||||||
|
**Note:** pgAdmin 4 will use the OS password manager from version 7.2 onwards and fallback to master password if OS password manager is not available.
|
||||||
|
@ -53,4 +53,5 @@ azure-mgmt-subscription==3.1.1
|
|||||||
azure-identity==1.12.0
|
azure-identity==1.12.0
|
||||||
google-api-python-client==2.*
|
google-api-python-client==2.*
|
||||||
google-auth-oauthlib==1.0.0
|
google-auth-oauthlib==1.0.0
|
||||||
Werkzeug==2.2.3
|
Werkzeug==2.2.3
|
||||||
|
keyring==23.*
|
@ -12,6 +12,8 @@ a webserver, this will provide the WSGI interface, otherwise, we're going
|
|||||||
to start a web server."""
|
to start a web server."""
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
|
if sys.version_info <= (3, 9):
|
||||||
|
import select
|
||||||
|
|
||||||
if sys.version_info < (3, 4):
|
if sys.version_info < (3, 4):
|
||||||
raise RuntimeError('This application must be run under Python 3.4 '
|
raise RuntimeError('This application must be run under Python 3.4 '
|
||||||
|
@ -17,8 +17,12 @@ from smtplib import SMTPConnectError, SMTPResponseException, \
|
|||||||
SMTPAuthenticationError, SMTPSenderRefused, SMTPRecipientsRefused
|
SMTPAuthenticationError, SMTPSenderRefused, SMTPRecipientsRefused
|
||||||
from socket import error as SOCKETErrorException
|
from socket import error as SOCKETErrorException
|
||||||
from urllib.request import urlopen
|
from urllib.request import urlopen
|
||||||
|
from pgadmin.utils.constants import KEY_RING_SERVICE_NAME, \
|
||||||
|
KEY_RING_USERNAME_FORMAT, KEY_RING_DESKTOP_USER
|
||||||
|
|
||||||
import time
|
import time
|
||||||
|
|
||||||
|
import keyring
|
||||||
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, after_this_request, redirect, session
|
||||||
from flask_babel import gettext
|
from flask_babel import gettext
|
||||||
@ -741,11 +745,13 @@ def get_nodes():
|
|||||||
return make_json_response(data=nodes)
|
return make_json_response(data=nodes)
|
||||||
|
|
||||||
|
|
||||||
def form_master_password_response(existing=True, present=False, errmsg=None):
|
def form_master_password_response(existing=True, present=False, errmsg=None,
|
||||||
|
is_keyring=False):
|
||||||
return make_json_response(data={
|
return make_json_response(data={
|
||||||
'present': present,
|
'present': present,
|
||||||
'reset': existing,
|
'reset': existing,
|
||||||
'errmsg': errmsg,
|
'errmsg': errmsg,
|
||||||
|
'is_keyring': is_keyring,
|
||||||
'is_error': True if errmsg else False
|
'is_error': True if errmsg else False
|
||||||
})
|
})
|
||||||
|
|
||||||
@ -804,6 +810,51 @@ def set_master_password():
|
|||||||
if data != '':
|
if data != '':
|
||||||
data = json.loads(data)
|
data = json.loads(data)
|
||||||
|
|
||||||
|
if not config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
|
from pgadmin.model import Server
|
||||||
|
from pgadmin.utils.crypto import decrypt
|
||||||
|
desktop_user = current_user
|
||||||
|
try:
|
||||||
|
# pgAdmin will use the OS password manager to store the server
|
||||||
|
# password, here migrating the existing saved server password to
|
||||||
|
# OS password manager
|
||||||
|
if keyring.get_password(
|
||||||
|
KEY_RING_SERVICE_NAME, KEY_RING_DESKTOP_USER.format(
|
||||||
|
desktop_user.username)) or data['password']:
|
||||||
|
all_server = Server.query.all()
|
||||||
|
for server in all_server:
|
||||||
|
if server.password and data['password'] \
|
||||||
|
and server.save_password:
|
||||||
|
name = KEY_RING_USERNAME_FORMAT.format(server.name,
|
||||||
|
server.id)
|
||||||
|
password = decrypt(server.password,
|
||||||
|
data['password']).decode()
|
||||||
|
# Store the password using OS password manager
|
||||||
|
keyring.set_password(KEY_RING_SERVICE_NAME, name,
|
||||||
|
password)
|
||||||
|
setattr(server, 'password', password)
|
||||||
|
|
||||||
|
# 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,
|
||||||
|
is_keyring=True
|
||||||
|
)
|
||||||
|
else:
|
||||||
|
return form_master_password_response(
|
||||||
|
present=False,
|
||||||
|
is_keyring=True
|
||||||
|
)
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.warning(
|
||||||
|
'Fail set password using OS password manager'
|
||||||
|
', fallback to master password. Error: {0}'.format(e)
|
||||||
|
)
|
||||||
|
config.DISABLED_LOCAL_PASSWORD_STORAGE = True
|
||||||
|
|
||||||
# Master password is not applicable for server mode
|
# Master password is not applicable for server mode
|
||||||
# Enable master password if oauth is used
|
# Enable master password if oauth is used
|
||||||
if not config.SERVER_MODE or OAUTH2 in config.AUTHENTICATION_SOURCES \
|
if not config.SERVER_MODE or OAUTH2 in config.AUTHENTICATION_SOURCES \
|
||||||
|
@ -33,8 +33,12 @@ 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
|
||||||
from .... import socketio as sio
|
from .... import socketio as sio
|
||||||
|
|
||||||
|
import keyring
|
||||||
|
|
||||||
|
|
||||||
def has_any(data, keys):
|
def has_any(data, keys):
|
||||||
"""
|
"""
|
||||||
@ -714,6 +718,20 @@ 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:
|
||||||
|
server_name = KEY_RING_USERNAME_FORMAT.format(
|
||||||
|
s.name,
|
||||||
|
s.id)
|
||||||
|
# Get password form OS password manager
|
||||||
|
is_present = keyring.get_password(
|
||||||
|
KEY_RING_SERVICE_NAME, server_name)
|
||||||
|
# Delete saved password from OS password manager
|
||||||
|
if is_present:
|
||||||
|
keyring.delete_password(KEY_RING_SERVICE_NAME,
|
||||||
|
server_name)
|
||||||
|
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)
|
||||||
@ -1052,10 +1070,11 @@ class ServerNode(PGChildNodeView):
|
|||||||
if data[item] == '':
|
if data[item] == '':
|
||||||
data[item] = None
|
data[item] = None
|
||||||
|
|
||||||
# Get enc key
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
crypt_key_present, crypt_key = get_crypt_key()
|
# Get enc key
|
||||||
if not crypt_key_present:
|
crypt_key_present, crypt_key = get_crypt_key()
|
||||||
raise CryptKeyMissing
|
if not crypt_key_present:
|
||||||
|
raise CryptKeyMissing
|
||||||
|
|
||||||
# Some fields can be provided with service file so they are optional
|
# Some fields can be provided with service file so they are optional
|
||||||
if 'service' in data and not data['service']:
|
if 'service' in data and not data['service']:
|
||||||
@ -1147,7 +1166,9 @@ class ServerNode(PGChildNodeView):
|
|||||||
# login with password
|
# login with password
|
||||||
have_password = True
|
have_password = True
|
||||||
password = data['password']
|
password = data['password']
|
||||||
password = encrypt(password, crypt_key)
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
|
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']
|
||||||
@ -1155,8 +1176,10 @@ 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,
|
||||||
@ -1177,15 +1200,31 @@ 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:
|
||||||
setattr(server, 'password', password)
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
db.session.commit()
|
setattr(server, 'password', password)
|
||||||
|
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:
|
||||||
setattr(server, 'tunnel_password', tunnel_password)
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
db.session.commit()
|
setattr(server, 'tunnel_password', tunnel_password)
|
||||||
|
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)
|
||||||
|
|
||||||
user = manager.user_info
|
user = manager.user_info
|
||||||
connected = True
|
connected = True
|
||||||
@ -1379,18 +1418,29 @@ class ServerNode(PGChildNodeView):
|
|||||||
manager.update(server)
|
manager.update(server)
|
||||||
conn = manager.connection()
|
conn = manager.connection()
|
||||||
|
|
||||||
# Get enc key
|
crypt_key = None
|
||||||
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
|
||||||
|
|
||||||
# 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 server.tunnel_password is None:
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE \
|
||||||
|
and server.tunnel_password is None:
|
||||||
prompt_tunnel_password = True
|
prompt_tunnel_password = True
|
||||||
else:
|
else:
|
||||||
tunnel_password = server.tunnel_password
|
if not config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
|
# Get password form OS password manager
|
||||||
|
tunnel_password = keyring.get_password(
|
||||||
|
KEY_RING_SERVICE_NAME,
|
||||||
|
KEY_RING_TUNNEL_FORMAT.format(server.name,
|
||||||
|
server.id))
|
||||||
|
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 ''
|
||||||
@ -1400,9 +1450,16 @@ class ServerNode(PGChildNodeView):
|
|||||||
# Encrypt the password before saving with user's login
|
# Encrypt the password before saving with user's login
|
||||||
# password key.
|
# password key.
|
||||||
try:
|
try:
|
||||||
tunnel_password = encrypt(tunnel_password, crypt_key) \
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
if tunnel_password is not None else \
|
tunnel_password = encrypt(tunnel_password, crypt_key) \
|
||||||
server.tunnel_password
|
if tunnel_password is not None else \
|
||||||
|
server.tunnel_password
|
||||||
|
else:
|
||||||
|
# Get password form OS password manager
|
||||||
|
tunnel_password = keyring.get_password(
|
||||||
|
KEY_RING_SERVICE_NAME,
|
||||||
|
KEY_RING_TUNNEL_FORMAT.format(server.name,
|
||||||
|
server.id))
|
||||||
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))
|
||||||
@ -1423,20 +1480,38 @@ class ServerNode(PGChildNodeView):
|
|||||||
elif passfile_param and passfile_param != '':
|
elif passfile_param and passfile_param != '':
|
||||||
passfile = passfile_param
|
passfile = passfile_param
|
||||||
else:
|
else:
|
||||||
password = conn_passwd or server.password
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
|
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))
|
||||||
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
|
||||||
|
|
||||||
# Encrypt the password before saving with user's login
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
# password key.
|
try:
|
||||||
try:
|
# Encrypt the password before saving with user's login
|
||||||
password = encrypt(password, crypt_key) \
|
# password key.
|
||||||
if password is not None else server.password
|
password = encrypt(password, crypt_key) \
|
||||||
except Exception as e:
|
if password is not None else server.password
|
||||||
current_app.logger.exception(e)
|
except Exception as e:
|
||||||
return internal_server_error(errormsg=str(e))
|
current_app.logger.exception(e)
|
||||||
|
return internal_server_error(errormsg=str(e))
|
||||||
|
elif 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
|
||||||
@ -1486,7 +1561,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:
|
if password and config.SERVER_MODE:
|
||||||
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:
|
||||||
@ -1975,9 +2050,24 @@ class ServerNode(PGChildNodeView):
|
|||||||
server = ServerModule. \
|
server = ServerModule. \
|
||||||
get_shared_server_properties(server, shared_server)
|
get_shared_server_properties(server, shared_server)
|
||||||
|
|
||||||
if server.shared and server.user_id != current_user.id:
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
setattr(shared_server, 'save_password', None)
|
if server.shared and server.user_id != current_user.id:
|
||||||
|
setattr(shared_server, 'save_password', None)
|
||||||
|
else:
|
||||||
|
setattr(server, 'save_password', None)
|
||||||
else:
|
else:
|
||||||
|
try:
|
||||||
|
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)
|
setattr(server, 'save_password', None)
|
||||||
|
|
||||||
# If password was saved then clear the flag also
|
# If password was saved then clear the flag also
|
||||||
@ -2017,9 +2107,19 @@ 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."
|
||||||
|
@ -80,6 +80,9 @@ class MasterPasswordTestCase(BaseTestGenerator):
|
|||||||
)
|
)
|
||||||
self.assertEqual(response.status_code, 200)
|
self.assertEqual(response.status_code, 200)
|
||||||
|
|
||||||
|
if not config.SERVER_MODE:
|
||||||
|
self.skipTest(
|
||||||
|
"This test is skipped on Desktop mode.")
|
||||||
if hasattr(self, 'check_if_set'):
|
if hasattr(self, 'check_if_set'):
|
||||||
response = self.tester.get(
|
response = self.tester.get(
|
||||||
'/browser/master_password'
|
'/browser/master_password'
|
||||||
|
@ -9,6 +9,7 @@
|
|||||||
|
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
|
import keyring
|
||||||
|
|
||||||
# User configs loaded from config_local, config_distro etc.
|
# User configs loaded from config_local, config_distro etc.
|
||||||
custom_config_settings = {}
|
custom_config_settings = {}
|
||||||
@ -114,4 +115,13 @@ def evaluate_and_patch_config(config: dict) -> dict:
|
|||||||
# Enable PSQL in Desktop Mode.
|
# Enable PSQL in Desktop Mode.
|
||||||
config['ENABLE_PSQL'] = True
|
config['ENABLE_PSQL'] = True
|
||||||
|
|
||||||
|
if config.get('SERVER_MODE'):
|
||||||
|
config.setdefault('DISABLED_LOCAL_PASSWORD_STORAGE', True)
|
||||||
|
else:
|
||||||
|
k_name = keyring.get_keyring().name
|
||||||
|
if k_name == 'fail Keyring':
|
||||||
|
config.setdefault('DISABLED_LOCAL_PASSWORD_STORAGE', True)
|
||||||
|
else:
|
||||||
|
config.setdefault('DISABLED_LOCAL_PASSWORD_STORAGE', False)
|
||||||
|
|
||||||
return config
|
return config
|
||||||
|
@ -22,7 +22,7 @@ import { DefaultButton, PrimaryButton, PgIconButton } from '../components/Button
|
|||||||
import { useModalStyles } from '../helpers/ModalProvider';
|
import { useModalStyles } from '../helpers/ModalProvider';
|
||||||
import { FormFooterMessage, InputText, MESSAGE_TYPE } from '../components/FormComponents';
|
import { FormFooterMessage, InputText, MESSAGE_TYPE } from '../components/FormComponents';
|
||||||
|
|
||||||
export default function MasterPasswordContent({ closeModal, onResetPassowrd, onOK, onCancel, setHeight, isPWDPresent, data}) {
|
export default function MasterPasswordContent({ closeModal, onResetPassowrd, onOK, onCancel, setHeight, isPWDPresent, data, isKeyring}) {
|
||||||
const classes = useModalStyles();
|
const classes = useModalStyles();
|
||||||
const containerRef = useRef();
|
const containerRef = useRef();
|
||||||
const firstEleRef = useRef();
|
const firstEleRef = useRef();
|
||||||
@ -59,24 +59,44 @@ export default function MasterPasswordContent({ closeModal, onResetPassowrd, onO
|
|||||||
|
|
||||||
return (
|
return (
|
||||||
<Box display="flex" flexDirection="column" className={classes.container} ref={containerRef}>
|
<Box display="flex" flexDirection="column" className={classes.container} ref={containerRef}>
|
||||||
<Box flexGrow="1" p={2}>
|
{isKeyring ?
|
||||||
<Box>
|
<Box flexGrow="1" p={2}>
|
||||||
<span style={{ fontWeight: 'bold' }}>
|
<Box>
|
||||||
{isPWDPresent ? gettext('Please enter your master password.') : gettext('Please set a master password for pgAdmin.')}
|
<span style={{ fontWeight: 'bold' }}>
|
||||||
</span>
|
{gettext('Please enter your master password.')}
|
||||||
<br />
|
</span>
|
||||||
<span style={{ fontWeight: 'bold' }}>
|
<br />
|
||||||
{isPWDPresent ? gettext('This is required to unlock saved passwords and reconnect to the database server(s).') : gettext('This will be used to secure and later unlock saved passwords and other credentials.')}
|
<span style={{ fontWeight: 'bold' }}>
|
||||||
</span>
|
{gettext('This is required to migrate the existing saved Server password and SSH tunnel password to OS password manager, as pgAdmin 4 will now use the OS password manager in Desktop mode from version 7.2')}
|
||||||
|
</span>
|
||||||
|
</Box>
|
||||||
|
<Box marginTop='12px'>
|
||||||
|
<InputText inputRef={firstEleRef} type="password" value={formData['password']} maxLength={null}
|
||||||
|
onChange={(e) => onTextChange(e, 'password')} onKeyDown={(e) => onKeyDown(e)}/>
|
||||||
|
</Box>
|
||||||
|
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={data.errmsg} closable={false} style={{
|
||||||
|
position: 'unset', padding: '12px 0px 0px'
|
||||||
|
}} />
|
||||||
|
</Box> :
|
||||||
|
<Box flexGrow="1" p={2}>
|
||||||
|
<Box>
|
||||||
|
<span style={{ fontWeight: 'bold' }}>
|
||||||
|
{isPWDPresent ? gettext('Please enter your master password.') : gettext('Please set a master password for pgAdmin.')}
|
||||||
|
</span>
|
||||||
|
<br />
|
||||||
|
<span style={{ fontWeight: 'bold' }}>
|
||||||
|
{isPWDPresent ? gettext('This is required to unlock saved passwords and reconnect to the database server(s).') : gettext('This will be used to secure and later unlock saved passwords and other credentials.')}
|
||||||
|
</span>
|
||||||
|
</Box>
|
||||||
|
<Box marginTop='12px'>
|
||||||
|
<InputText inputRef={firstEleRef} type="password" value={formData['password']} maxLength={null}
|
||||||
|
onChange={(e) => onTextChange(e, 'password')} onKeyDown={(e) => onKeyDown(e)}/>
|
||||||
|
</Box>
|
||||||
|
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={data.errmsg} closable={false} style={{
|
||||||
|
position: 'unset', padding: '12px 0px 0px'
|
||||||
|
}} />
|
||||||
</Box>
|
</Box>
|
||||||
<Box marginTop='12px'>
|
}
|
||||||
<InputText inputRef={firstEleRef} type="password" value={formData['password']} maxLength={null}
|
|
||||||
onChange={(e) => onTextChange(e, 'password')} onKeyDown={(e) => onKeyDown(e)}/>
|
|
||||||
</Box>
|
|
||||||
<FormFooterMessage type={MESSAGE_TYPE.ERROR} message={data.errmsg} closable={false} style={{
|
|
||||||
position: 'unset', padding: '12px 0px 0px'
|
|
||||||
}} />
|
|
||||||
</Box>
|
|
||||||
<Box className={classes.footer}>
|
<Box className={classes.footer}>
|
||||||
<Box style={{ marginRight: 'auto' }}>
|
<Box style={{ marginRight: 'auto' }}>
|
||||||
<PgIconButton data-test="help-masterpassword" title={gettext('Help')} style={{ padding: '0.3rem', paddingLeft: '0.7rem' }} startIcon={<HelpIcon />} onClick={() => {
|
<PgIconButton data-test="help-masterpassword" title={gettext('Help')} style={{ padding: '0.3rem', paddingLeft: '0.7rem' }} startIcon={<HelpIcon />} onClick={() => {
|
||||||
@ -86,17 +106,19 @@ export default function MasterPasswordContent({ closeModal, onResetPassowrd, onO
|
|||||||
window.open(_url, 'pgadmin_help');
|
window.open(_url, 'pgadmin_help');
|
||||||
}} >
|
}} >
|
||||||
</PgIconButton>
|
</PgIconButton>
|
||||||
{isPWDPresent &&
|
{isPWDPresent && !isKeyring &&
|
||||||
<DefaultButton data-test="reset-masterpassword" style={{ marginLeft: '0.5rem' }} startIcon={<DeleteForeverIcon />}
|
<DefaultButton data-test="reset-masterpassword" style={{ marginLeft: '0.5rem' }} startIcon={<DeleteForeverIcon />}
|
||||||
onClick={() => {onResetPassowrd?.();}} >
|
onClick={() => {onResetPassowrd?.();}} >
|
||||||
{gettext('Reset Master Password')}
|
{gettext('Reset Master Password')}
|
||||||
</DefaultButton>
|
</DefaultButton>
|
||||||
}
|
}
|
||||||
</Box>
|
</Box>
|
||||||
<DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={() => {
|
{
|
||||||
onCancel?.();
|
!isKeyring && <DefaultButton data-test="close" startIcon={<CloseIcon />} onClick={() => {
|
||||||
closeModal();
|
onCancel?.();
|
||||||
}} >{gettext('Cancel')}</DefaultButton>
|
closeModal();
|
||||||
|
}} >{gettext('Cancel')}</DefaultButton>
|
||||||
|
}
|
||||||
<PrimaryButton ref={okBtnRef} data-test="save" className={classes.margin} startIcon={<CheckRoundedIcon />}
|
<PrimaryButton ref={okBtnRef} data-test="save" className={classes.margin} startIcon={<CheckRoundedIcon />}
|
||||||
disabled={formData.password.length == 0}
|
disabled={formData.password.length == 0}
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
@ -121,4 +143,5 @@ MasterPasswordContent.propTypes = {
|
|||||||
setHeight: PropTypes.func,
|
setHeight: PropTypes.func,
|
||||||
isPWDPresent: PropTypes.bool,
|
isPWDPresent: PropTypes.bool,
|
||||||
data: PropTypes.object,
|
data: PropTypes.object,
|
||||||
|
isKeyring: PropTypes.bool,
|
||||||
};
|
};
|
||||||
|
@ -160,7 +160,7 @@ 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)=> {
|
||||||
if(!res.data.data.present) {
|
if(!res.data.data.present) {
|
||||||
showMasterPassword(res.data.data.reset, res.data.data.errmsg, masterpass_callback_queue, cancel_callback);
|
showMasterPassword(res.data.data.reset, res.data.data.errmsg, masterpass_callback_queue, cancel_callback, res.data.data.is_keyring);
|
||||||
} else {
|
} else {
|
||||||
masterPassCallbacks(masterpass_callback_queue);
|
masterPassCallbacks(masterpass_callback_queue);
|
||||||
}
|
}
|
||||||
@ -170,7 +170,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) {
|
export function showMasterPassword(isPWDPresent, errmsg, masterpass_callback_queue, cancel_callback, is_keyring) {
|
||||||
const api = getApiInstance();
|
const api = getApiInstance();
|
||||||
let title = isPWDPresent ? gettext('Unlock Saved Passwords') : gettext('Set Master Password');
|
let title = isPWDPresent ? gettext('Unlock Saved Passwords') : gettext('Set Master Password');
|
||||||
|
|
||||||
@ -179,6 +179,7 @@ export function showMasterPassword(isPWDPresent, errmsg, masterpass_callback_que
|
|||||||
<MasterPasswordContent
|
<MasterPasswordContent
|
||||||
isPWDPresent= {isPWDPresent}
|
isPWDPresent= {isPWDPresent}
|
||||||
data={{'errmsg': errmsg}}
|
data={{'errmsg': errmsg}}
|
||||||
|
isKeyring={is_keyring}
|
||||||
setHeight={(containerHeight) => {
|
setHeight={(containerHeight) => {
|
||||||
setNewSize(pgAdmin.Browser.stdW.md, containerHeight);
|
setNewSize(pgAdmin.Browser.stdW.md, containerHeight);
|
||||||
}}
|
}}
|
||||||
|
@ -126,3 +126,9 @@ MY_STORAGE = 'my_storage'
|
|||||||
ACCESS_DENIED_MESSAGE = gettext(
|
ACCESS_DENIED_MESSAGE = gettext(
|
||||||
"Access denied: You’re having limited access. You’re not allowed to "
|
"Access denied: You’re having limited access. You’re not allowed to "
|
||||||
"Rename, Delete or Create any files/folders")
|
"Rename, Delete or Create any files/folders")
|
||||||
|
|
||||||
|
|
||||||
|
KEY_RING_SERVICE_NAME = 'pgAdmin4-Masterpass-Service'
|
||||||
|
KEY_RING_USERNAME_FORMAT = 'pgAdmin4-{0}-{1}'
|
||||||
|
KEY_RING_TUNNEL_FORMAT = 'pgAdmin4-tunnel-{0}-{1}'
|
||||||
|
KEY_RING_DESKTOP_USER = 'desktop-user-{0}'
|
||||||
|
@ -263,10 +263,19 @@ class Connection(BaseConnection):
|
|||||||
return True, None
|
return True, None
|
||||||
|
|
||||||
manager = self.manager
|
manager = self.manager
|
||||||
crypt_key_present, crypt_key = get_crypt_key()
|
|
||||||
|
|
||||||
password, encpass, is_update_password = self._check_user_password(
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
kwargs)
|
crypt_key_present, crypt_key = get_crypt_key()
|
||||||
|
|
||||||
|
if not crypt_key_present:
|
||||||
|
raise CryptKeyMissing()
|
||||||
|
|
||||||
|
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 \
|
||||||
@ -292,13 +301,15 @@ class Connection(BaseConnection):
|
|||||||
if self.reconnecting is not False:
|
if self.reconnecting is not False:
|
||||||
self.password = None
|
self.password = None
|
||||||
|
|
||||||
if not crypt_key_present:
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
raise CryptKeyMissing()
|
is_error, errmsg, password = self._decode_password(encpass,
|
||||||
|
manager,
|
||||||
is_error, errmsg, password = self._decode_password(encpass, manager,
|
password,
|
||||||
password, crypt_key)
|
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.
|
||||||
@ -657,7 +668,7 @@ WHERE db.datname = current_database()""")
|
|||||||
|
|
||||||
def __cursor(self, server_cursor=False, scrollable=False):
|
def __cursor(self, server_cursor=False, scrollable=False):
|
||||||
|
|
||||||
if not get_crypt_key()[0]:
|
if not get_crypt_key()[0] and config.SERVER_MODE:
|
||||||
raise CryptKeyMissing()
|
raise CryptKeyMissing()
|
||||||
|
|
||||||
# Check SSH Tunnel is alive or not. If used by the database
|
# Check SSH Tunnel is alive or not. If used by the database
|
||||||
@ -1547,13 +1558,13 @@ 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
|
||||||
|
|
||||||
crypt_key_present, crypt_key = get_crypt_key()
|
password = decrypt(password, crypt_key)\
|
||||||
if not crypt_key_present:
|
.decode()
|
||||||
return False, crypt_key
|
|
||||||
|
|
||||||
password = decrypt(password, crypt_key)\
|
|
||||||
.decode()
|
|
||||||
|
|
||||||
try:
|
try:
|
||||||
with ConnectionLocker(self.manager.kerberos_conn):
|
with ConnectionLocker(self.manager.kerberos_conn):
|
||||||
|
@ -30,6 +30,9 @@ from pgadmin.utils.master_password import get_crypt_key
|
|||||||
from pgadmin.utils.exception import ObjectGone
|
from pgadmin.utils.exception import ObjectGone
|
||||||
from pgadmin.utils.passexec import PasswordExec
|
from pgadmin.utils.passexec import PasswordExec
|
||||||
from psycopg.conninfo import make_conninfo
|
from psycopg.conninfo import make_conninfo
|
||||||
|
import keyring
|
||||||
|
from pgadmin.utils.constants import KEY_RING_SERVICE_NAME, \
|
||||||
|
KEY_RING_USERNAME_FORMAT, KEY_RING_TUNNEL_FORMAT
|
||||||
|
|
||||||
if config.SUPPORT_SSH_TUNNEL:
|
if config.SUPPORT_SSH_TUNNEL:
|
||||||
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
|
from sshtunnel import SSHTunnelForwarder, BaseSSHTunnelForwarderError
|
||||||
@ -78,6 +81,7 @@ class ServerManager(object):
|
|||||||
self.db_info = dict()
|
self.db_info = dict()
|
||||||
self.server_types = None
|
self.server_types = None
|
||||||
self.db_res = server.db_res
|
self.db_res = server.db_res
|
||||||
|
self.name = server.name
|
||||||
self.passexec = \
|
self.passexec = \
|
||||||
PasswordExec(server.passexec_cmd, server.passexec_expiration) \
|
PasswordExec(server.passexec_cmd, server.passexec_expiration) \
|
||||||
if server.passexec_cmd else None
|
if server.passexec_cmd else None
|
||||||
@ -232,7 +236,8 @@ WHERE db.oid = {0}""".format(did))
|
|||||||
"Could not find the specified database."
|
"Could not find the specified database."
|
||||||
))
|
))
|
||||||
|
|
||||||
if not get_crypt_key()[0]:
|
if not get_crypt_key()[0] and (
|
||||||
|
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()
|
||||||
|
|
||||||
@ -529,11 +534,14 @@ WHERE db.oid = {0}""".format(did))
|
|||||||
|
|
||||||
def export_password_env(self, env):
|
def export_password_env(self, env):
|
||||||
if self.password:
|
if self.password:
|
||||||
crypt_key_present, crypt_key = get_crypt_key()
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
if not crypt_key_present:
|
crypt_key_present, crypt_key = get_crypt_key()
|
||||||
return False, crypt_key
|
if not crypt_key_present:
|
||||||
|
return False, crypt_key
|
||||||
|
password = decrypt(self.password, crypt_key).decode()
|
||||||
|
else:
|
||||||
|
password = self.password
|
||||||
|
|
||||||
password = decrypt(self.password, crypt_key).decode()
|
|
||||||
os.environ[str(env)] = password
|
os.environ[str(env)] = password
|
||||||
|
|
||||||
def create_ssh_tunnel(self, tunnel_password):
|
def create_ssh_tunnel(self, tunnel_password):
|
||||||
@ -549,15 +557,23 @@ 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 != '':
|
||||||
crypt_key_present, crypt_key = get_crypt_key()
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
if not crypt_key_present:
|
crypt_key_present, crypt_key = get_crypt_key()
|
||||||
raise CryptKeyMissing()
|
if not crypt_key_present:
|
||||||
|
raise CryptKeyMissing()
|
||||||
|
|
||||||
try:
|
try:
|
||||||
tunnel_password = decrypt(tunnel_password, crypt_key)
|
if config.DISABLED_LOCAL_PASSWORD_STORAGE:
|
||||||
# password is in bytes, for python3 we need it in string
|
tunnel_password = decrypt(tunnel_password, crypt_key)
|
||||||
if isinstance(tunnel_password, bytes):
|
# password is in bytes, for python3 we need it in string
|
||||||
tunnel_password = tunnel_password.decode()
|
if isinstance(tunnel_password, bytes):
|
||||||
|
tunnel_password = tunnel_password.decode()
|
||||||
|
else:
|
||||||
|
# Get password form OS password manager
|
||||||
|
tunnel_password = keyring.get_password(
|
||||||
|
KEY_RING_SERVICE_NAME,
|
||||||
|
KEY_RING_TUNNEL_FORMAT.format(self.manager.name,
|
||||||
|
self.manager.sid))
|
||||||
|
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
current_app.logger.exception(e)
|
current_app.logger.exception(e)
|
||||||
|
Loading…
Reference in New Issue
Block a user