mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2024-11-29 12:03:52 -06:00
Allow pgAdmin to retrieve master password from external script/program. #4769
This commit is contained in:
parent
cba3dc9457
commit
702bc8c8ce
@ -21,6 +21,7 @@ New features
|
|||||||
************
|
************
|
||||||
|
|
||||||
| `Issue #3831 <https://github.com/pgadmin-org/pgadmin4/issues/3831>`_ - Add Option to only show active connections on Dashboard.
|
| `Issue #3831 <https://github.com/pgadmin-org/pgadmin4/issues/3831>`_ - Add Option to only show active connections on Dashboard.
|
||||||
|
| `Issue #4769 <https://github.com/pgadmin-org/pgadmin4/issues/4769>`_ - Allow pgAdmin to retrive master password from external script/program.
|
||||||
| `Issue #5048 <https://github.com/pgadmin-org/pgadmin4/issues/5048>`_ - Add an option to hide/show empty object collection nodes.
|
| `Issue #5048 <https://github.com/pgadmin-org/pgadmin4/issues/5048>`_ - Add an option to hide/show empty object collection nodes.
|
||||||
| `Issue #5123 <https://github.com/pgadmin-org/pgadmin4/issues/5123>`_ - Added support to use standard OS secret store to save server/ssh tunnel passwords instead of master password in pgAdmin desktop mode.
|
| `Issue #5123 <https://github.com/pgadmin-org/pgadmin4/issues/5123>`_ - Added support to use standard OS secret store to save server/ssh tunnel passwords instead of master password in pgAdmin desktop mode.
|
||||||
|
|
||||||
@ -50,4 +51,3 @@ Bug fixes
|
|||||||
| `Issue #6279 <https://github.com/pgadmin-org/pgadmin4/issues/6279>`_ - Fix incorrect number of foreign tables displayed. Show column comments in RE-SQL.
|
| `Issue #6279 <https://github.com/pgadmin-org/pgadmin4/issues/6279>`_ - Fix incorrect number of foreign tables displayed. Show column comments in RE-SQL.
|
||||||
| `Issue #6280 <https://github.com/pgadmin-org/pgadmin4/issues/6280>`_ - View SQL tab not quoting column comments.
|
| `Issue #6280 <https://github.com/pgadmin-org/pgadmin4/issues/6280>`_ - View SQL tab not quoting column comments.
|
||||||
| `Issue #6281 <https://github.com/pgadmin-org/pgadmin4/issues/6281>`_ - VarChar Field Sizes are missing from Query's Grid header.
|
| `Issue #6281 <https://github.com/pgadmin-org/pgadmin4/issues/6281>`_ - VarChar Field Sizes are missing from Query's Grid header.
|
||||||
| `Issue #6325 <https://github.com/pgadmin-org/pgadmin4/issues/6325>`_ - Fixed an issue where Operators collection node throwing an error.
|
|
||||||
|
@ -577,6 +577,24 @@ ALLOW_SAVE_TUNNEL_PASSWORD = False
|
|||||||
MASTER_PASSWORD_REQUIRED = True
|
MASTER_PASSWORD_REQUIRED = True
|
||||||
|
|
||||||
##########################################################################
|
##########################################################################
|
||||||
|
|
||||||
|
# pgAdmin encrypts the database connection and ssh tunnel password using a
|
||||||
|
# master password or pgAdmin login password (for other authentication sources)
|
||||||
|
# before storing it in the pgAdmin configuration database.
|
||||||
|
#
|
||||||
|
# Below setting is used to allow the user to specify the path to a script
|
||||||
|
# or program that will return an encryption key which will be used to
|
||||||
|
# encrypt the passwords. This setting is used only in server mode when
|
||||||
|
# auth sources are oauth, Kerberos, and webserver.
|
||||||
|
#
|
||||||
|
# You can pass the current username as an argument to the external script
|
||||||
|
# by specifying %u in config value.
|
||||||
|
# E.g. - MASTER_PASSWORD_HOOK = '<PATH>/passwdgen_script.sh %u'
|
||||||
|
##########################################################################
|
||||||
|
MASTER_PASSWORD_HOOK = None
|
||||||
|
|
||||||
|
##########################################################################
|
||||||
|
|
||||||
# Allows pgAdmin4 to create session cookies based on IP address, so even
|
# Allows pgAdmin4 to create session cookies based on IP address, so even
|
||||||
# if a cookie is stolen, the attacker will not be able to connect to the
|
# if a cookie is stolen, the attacker will not be able to connect to the
|
||||||
# server using that stolen cookie.
|
# server using that stolen cookie.
|
||||||
|
@ -11,7 +11,7 @@ import json
|
|||||||
import logging
|
import logging
|
||||||
import os
|
import os
|
||||||
import sys
|
import sys
|
||||||
from abc import ABCMeta, abstractmethod, abstractproperty
|
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
|
||||||
@ -35,7 +35,7 @@ from flask_security.recoverable import reset_password_token_status, \
|
|||||||
generate_reset_password_token, update_password
|
generate_reset_password_token, update_password
|
||||||
from flask_security.signals import reset_password_instructions_sent
|
from flask_security.signals import reset_password_instructions_sent
|
||||||
from flask_security.utils import config_value, do_flash, get_url, \
|
from flask_security.utils import config_value, do_flash, get_url, \
|
||||||
get_message, slash_url_suffix, login_user, send_mail, logout_user, \
|
get_message, slash_url_suffix, login_user, send_mail, \
|
||||||
get_post_logout_redirect
|
get_post_logout_redirect
|
||||||
from flask_security.views import _security, view_commit, _ctx
|
from flask_security.views import _security, view_commit, _ctx
|
||||||
from werkzeug.datastructures import MultiDict
|
from werkzeug.datastructures import MultiDict
|
||||||
@ -793,7 +793,12 @@ def reset_master_password():
|
|||||||
KEY_RING_DESKTOP_USER.format(
|
KEY_RING_DESKTOP_USER.format(
|
||||||
current_user.username), 'test')
|
current_user.username), 'test')
|
||||||
cleanup_master_password()
|
cleanup_master_password()
|
||||||
return make_json_response(data=get_crypt_key()[0])
|
status, crypt_key = get_crypt_key()
|
||||||
|
# Set masterpass_check if MASTER_PASSWORD_HOOK is set which provides
|
||||||
|
# encryption key
|
||||||
|
if config.MASTER_PASSWORD_REQUIRED and config.MASTER_PASSWORD_HOOK:
|
||||||
|
set_masterpass_check_text(crypt_key)
|
||||||
|
return make_json_response(data=status)
|
||||||
|
|
||||||
|
|
||||||
@blueprint.route("/master_password", endpoint="set_master_password",
|
@blueprint.route("/master_password", endpoint="set_master_password",
|
||||||
@ -872,9 +877,28 @@ def set_master_password():
|
|||||||
)
|
)
|
||||||
config.DISABLED_LOCAL_PASSWORD_STORAGE = True
|
config.DISABLED_LOCAL_PASSWORD_STORAGE = True
|
||||||
|
|
||||||
# Master password is not applicable for server mode
|
# If the master password is required and the master password hook
|
||||||
# Enable master password if oauth is used
|
# is specified then try to retrieve the encryption key and update data.
|
||||||
if not config.SERVER_MODE or OAUTH2 in config.AUTHENTICATION_SOURCES \
|
# If there is an error while retrieving it, return an error message.
|
||||||
|
if config.SERVER_MODE and config.MASTER_PASSWORD_REQUIRED and \
|
||||||
|
config.MASTER_PASSWORD_HOOK:
|
||||||
|
status, enc_key = get_crypt_key()
|
||||||
|
if status:
|
||||||
|
data = {'password': enc_key, 'submit_password': True}
|
||||||
|
else:
|
||||||
|
error = 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=error
|
||||||
|
)
|
||||||
|
|
||||||
|
# Master password is applicable for Desktop mode and in server mode
|
||||||
|
# only when auth sources are oauth, kerberos, webserver.
|
||||||
|
if (not config.SERVER_MODE) or OAUTH2 in config.AUTHENTICATION_SOURCES \
|
||||||
or KERBEROS in config.AUTHENTICATION_SOURCES \
|
or KERBEROS in config.AUTHENTICATION_SOURCES \
|
||||||
or WEBSERVER in config.AUTHENTICATION_SOURCES \
|
or WEBSERVER in config.AUTHENTICATION_SOURCES \
|
||||||
and config.MASTER_PASSWORD_REQUIRED:
|
and config.MASTER_PASSWORD_REQUIRED:
|
||||||
@ -882,12 +906,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 = gettext("Password mismatch error") if \
|
||||||
|
config.MASTER_PASSWORD_HOOK else \
|
||||||
|
gettext("Incorrect master password")
|
||||||
return form_master_password_response(
|
return form_master_password_response(
|
||||||
existing=True,
|
existing=True,
|
||||||
present=False,
|
present=False,
|
||||||
errmsg=gettext("Incorrect master password")
|
errmsg=errmsg
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# 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
|
||||||
@ -908,16 +936,23 @@ def set_master_password():
|
|||||||
# master pass
|
# master pass
|
||||||
set_masterpass_check_text(data.get('password'))
|
set_masterpass_check_text(data.get('password'))
|
||||||
|
|
||||||
|
# If password in request is empty then try to get it with
|
||||||
|
# get_crypt_key method. If get_crypt_key() returns false status and
|
||||||
|
# masterpass_check is already set, provide a pop to enter
|
||||||
|
# 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,
|
||||||
)
|
)
|
||||||
|
|
||||||
|
# If get_crypt_key return True,but crypt_key is none and
|
||||||
|
# user entered blank password, return error message.
|
||||||
elif not get_crypt_key()[1]:
|
elif not get_crypt_key()[1]:
|
||||||
error_message = None
|
error_message = None
|
||||||
if data.get('submit_password') and data.get('password') == '':
|
|
||||||
# If user attempted to enter a blank password, then throw error
|
# If user attempted to enter a blank password, then throw error
|
||||||
|
if data.get('submit_password') and data.get('password') == '':
|
||||||
error_message = gettext("Master password cannot be empty")
|
error_message = gettext("Master password cannot be empty")
|
||||||
return form_master_password_response(
|
return form_master_password_response(
|
||||||
existing=False,
|
existing=False,
|
||||||
|
@ -158,7 +158,33 @@ export function checkMasterPassword(data, masterpass_callback_queue, cancel_call
|
|||||||
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;
|
||||||
if(!res.data.data.present) {
|
if(!res.data.data.present) {
|
||||||
|
if(res.data.data.is_error){
|
||||||
|
//show notifier with error
|
||||||
|
if(!res.data.data.reset){
|
||||||
|
Notify.error(res.data.data.errmsg);
|
||||||
|
}else if(res.data.data.errmsg == 'Password mismatch error'){
|
||||||
|
Notify.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(() => {
|
||||||
|
Notify.info('The master password has been reset.');
|
||||||
|
})
|
||||||
|
.catch((err) => {
|
||||||
|
Notify.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);
|
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);
|
||||||
|
|
||||||
@ -223,6 +249,7 @@ export function showMasterPassword(isPWDPresent, errmsg, masterpass_callback_que
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export function showChangeServerPassword() {
|
export function showChangeServerPassword() {
|
||||||
let title = arguments[0],
|
let title = arguments[0],
|
||||||
nodeData = arguments[1],
|
nodeData = arguments[1],
|
||||||
|
@ -1,9 +1,8 @@
|
|||||||
import config
|
import config
|
||||||
from flask import current_app, session
|
from flask import current_app, session, 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.crypto import encrypt, decrypt
|
from pgadmin.utils.crypto import encrypt, decrypt
|
||||||
from pgadmin.utils.constants import KERBEROS
|
|
||||||
|
|
||||||
|
|
||||||
MASTERPASS_CHECK_TEXT = 'ideas are bulletproof'
|
MASTERPASS_CHECK_TEXT = 'ideas are bulletproof'
|
||||||
@ -30,12 +29,19 @@ def get_crypt_key():
|
|||||||
and not config.SERVER_MODE:
|
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 \
|
elif config.MASTER_PASSWORD_REQUIRED and \
|
||||||
|
config.MASTER_PASSWORD_HOOK is None\
|
||||||
and 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 \
|
elif not config.MASTER_PASSWORD_REQUIRED and config.SERVER_MODE and \
|
||||||
'pass_enc_key' in session:
|
'pass_enc_key' in session:
|
||||||
return True, session['pass_enc_key']
|
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
|
||||||
|
|
||||||
@ -121,3 +127,30 @@ def process_masterpass_disabled():
|
|||||||
return True
|
return True
|
||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
|
|
||||||
|
def get_master_password_from_master_hook(command):
|
||||||
|
"""
|
||||||
|
This method executes specified command & returns output.
|
||||||
|
:param command: Shell command with absolute path
|
||||||
|
:return: Output of command.
|
||||||
|
"""
|
||||||
|
import subprocess
|
||||||
|
try:
|
||||||
|
p = subprocess.Popen(command, stdout=subprocess.PIPE, shell=True)
|
||||||
|
out, err = p.communicate()
|
||||||
|
if p.returncode == 0:
|
||||||
|
output = out.decode() if hasattr(out, 'decode') else out
|
||||||
|
output = output.strip()
|
||||||
|
return True, output
|
||||||
|
else:
|
||||||
|
error = "Command '{0}' failed, exit-code={1} error = {2}".format(
|
||||||
|
command, p.returncode, str(err))
|
||||||
|
current_app.logger.error(error)
|
||||||
|
return False, None
|
||||||
|
except Exception as e:
|
||||||
|
current_app.logger.exception(
|
||||||
|
'Failed to retrieve master password from the master password hook'
|
||||||
|
' utility.Error: {0}'.format(e)
|
||||||
|
)
|
||||||
|
return False, None
|
||||||
|
Loading…
Reference in New Issue
Block a user