mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
1. Added Master Password to increase the security of saved passwords. Fixes #4184
2. In server(web) mode, update all the saved server credentials when user password is changed. Fixes #3377
This commit is contained in:
committed by
Akshay Joshi
parent
6f0eafb223
commit
dfa892d2a2
@@ -26,6 +26,8 @@ import config
|
||||
from config import PG_DEFAULT_DRIVER
|
||||
from pgadmin.model import db, Server, ServerGroup, User
|
||||
from pgadmin.utils.driver import get_driver
|
||||
from pgadmin.utils.master_password import get_crypt_key
|
||||
from pgadmin.utils.exception import CryptKeyMissing
|
||||
|
||||
|
||||
def has_any(data, keys):
|
||||
@@ -117,9 +119,16 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||
driver = get_driver(PG_DEFAULT_DRIVER)
|
||||
|
||||
for server in servers:
|
||||
manager = driver.connection_manager(server.id)
|
||||
conn = manager.connection()
|
||||
connected = conn.connected()
|
||||
connected = False
|
||||
manager = None
|
||||
try:
|
||||
manager = driver.connection_manager(server.id)
|
||||
conn = manager.connection()
|
||||
connected = conn.connected()
|
||||
except CryptKeyMissing:
|
||||
# show the nodes at least even if not able to connect.
|
||||
pass
|
||||
|
||||
in_recovery = None
|
||||
wal_paused = None
|
||||
|
||||
@@ -723,6 +732,11 @@ class ServerNode(PGChildNodeView):
|
||||
request.data, encoding='utf-8'
|
||||
)
|
||||
|
||||
# Get enc key
|
||||
crypt_key_present, crypt_key = get_crypt_key()
|
||||
if not crypt_key_present:
|
||||
raise CryptKeyMissing
|
||||
|
||||
# Some fields can be provided with service file so they are optional
|
||||
if 'service' in data and not data['service']:
|
||||
required_args.extend([
|
||||
@@ -807,7 +821,7 @@ class ServerNode(PGChildNodeView):
|
||||
# login with password
|
||||
have_password = True
|
||||
password = data['password']
|
||||
password = encrypt(password, current_user.password)
|
||||
password = encrypt(password, crypt_key)
|
||||
elif 'passfile' in data and data["passfile"] != '':
|
||||
passfile = data['passfile']
|
||||
setattr(server, 'passfile', passfile)
|
||||
@@ -817,7 +831,7 @@ class ServerNode(PGChildNodeView):
|
||||
have_tunnel_password = True
|
||||
tunnel_password = data['tunnel_password']
|
||||
tunnel_password = \
|
||||
encrypt(tunnel_password, current_user.password)
|
||||
encrypt(tunnel_password, crypt_key)
|
||||
|
||||
status, errmsg = conn.connect(
|
||||
password=password,
|
||||
@@ -998,6 +1012,11 @@ class ServerNode(PGChildNodeView):
|
||||
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
|
||||
conn = manager.connection()
|
||||
|
||||
# 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.use_ssh_tunnel:
|
||||
if 'tunnel_password' not in data:
|
||||
@@ -1014,12 +1033,12 @@ class ServerNode(PGChildNodeView):
|
||||
# Encrypt the password before saving with user's login
|
||||
# password key.
|
||||
try:
|
||||
tunnel_password = encrypt(tunnel_password, user.password) \
|
||||
tunnel_password = encrypt(tunnel_password, crypt_key) \
|
||||
if tunnel_password is not None else \
|
||||
server.tunnel_password
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
return internal_server_error(errormsg=e.message)
|
||||
return internal_server_error(errormsg=str(e))
|
||||
|
||||
if 'password' not in data:
|
||||
conn_passwd = getattr(conn, 'password', None)
|
||||
@@ -1038,11 +1057,11 @@ class ServerNode(PGChildNodeView):
|
||||
# Encrypt the password before saving with user's login
|
||||
# password key.
|
||||
try:
|
||||
password = encrypt(password, user.password) \
|
||||
password = encrypt(password, crypt_key) \
|
||||
if password is not None else server.password
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
return internal_server_error(errormsg=e.message)
|
||||
return internal_server_error(errormsg=str(e))
|
||||
|
||||
# Check do we need to prompt for the database server or ssh tunnel
|
||||
# password or both. Return the password template in case password is
|
||||
@@ -1235,6 +1254,7 @@ class ServerNode(PGChildNodeView):
|
||||
"""
|
||||
try:
|
||||
data = json.loads(request.form['data'], encoding='utf-8')
|
||||
crypt_key = get_crypt_key()[1]
|
||||
|
||||
# Fetch Server Details
|
||||
server = Server.query.filter_by(id=sid).first()
|
||||
@@ -1292,7 +1312,7 @@ class ServerNode(PGChildNodeView):
|
||||
|
||||
# Check against old password only if no pgpass file
|
||||
if not is_passfile:
|
||||
decrypted_password = decrypt(manager.password, user.password)
|
||||
decrypted_password = decrypt(manager.password, crypt_key)
|
||||
|
||||
if isinstance(decrypted_password, bytes):
|
||||
decrypted_password = decrypted_password.decode()
|
||||
@@ -1328,7 +1348,7 @@ class ServerNode(PGChildNodeView):
|
||||
|
||||
# Store password in sqlite only if no pgpass file
|
||||
if not is_passfile:
|
||||
password = encrypt(data['newPassword'], user.password)
|
||||
password = encrypt(data['newPassword'], crypt_key)
|
||||
# Check if old password was stored in pgadmin4 sqlite database.
|
||||
# If yes then update that password.
|
||||
if server.password is not None and config.ALLOW_SAVE_PASSWORD:
|
||||
@@ -1472,6 +1492,7 @@ class ServerNode(PGChildNodeView):
|
||||
|
||||
def get_response_for_password(self, server, status, prompt_password=False,
|
||||
prompt_tunnel_password=False, errmsg=None):
|
||||
|
||||
if server.use_ssh_tunnel:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
@@ -1498,7 +1519,7 @@ class ServerNode(PGChildNodeView):
|
||||
server_label=server.name,
|
||||
username=server.username,
|
||||
errmsg=errmsg,
|
||||
_=gettext
|
||||
_=gettext,
|
||||
)
|
||||
)
|
||||
|
||||
|
||||
@@ -166,7 +166,6 @@ class DatabaseView(PGChildNodeView):
|
||||
def wrap(f):
|
||||
@wraps(f)
|
||||
def wrapped(self, *args, **kwargs):
|
||||
|
||||
self.manager = get_driver(
|
||||
PG_DEFAULT_DRIVER
|
||||
).connection_manager(
|
||||
|
||||
@@ -489,11 +489,15 @@ define('pgadmin.node.database', [
|
||||
|
||||
Alertify.pgNotifier('error', xhr, error, function(msg) {
|
||||
setTimeout(function() {
|
||||
Alertify.dlgServerPass(
|
||||
gettext('Connect to database'),
|
||||
msg, _model, _data, _tree, _item, _status,
|
||||
onSuccess, onFailure, onCancel
|
||||
).resizeTo();
|
||||
if (msg == 'CRYPTKEY_SET') {
|
||||
connect_to_database(_model, _data, _tree, _item, _wasConnected);
|
||||
} else {
|
||||
Alertify.dlgServerPass(
|
||||
gettext('Connect to database'),
|
||||
msg, _model, _data, _tree, _item, _status,
|
||||
onSuccess, onFailure, onCancel
|
||||
).resizeTo();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1126,10 +1126,14 @@ define('pgadmin.node.server', [
|
||||
|
||||
Alertify.pgNotifier('error', xhr, error, function(msg) {
|
||||
setTimeout(function() {
|
||||
Alertify.dlgServerPass(
|
||||
gettext('Connect to Server'),
|
||||
msg, _node, _data, _tree, _item, _wasConnected
|
||||
).resizeTo();
|
||||
if (msg == 'CRYPTKEY_SET') {
|
||||
connect_to_server(_node, _data, _tree, _item, _wasConnected);
|
||||
} else {
|
||||
Alertify.dlgServerPass(
|
||||
gettext('Connect to Server'),
|
||||
msg, _node, _data, _tree, _item, _wasConnected
|
||||
).resizeTo();
|
||||
}
|
||||
}, 100);
|
||||
});
|
||||
},
|
||||
|
||||
@@ -1,23 +1,33 @@
|
||||
<form name="frmPassword" id="frmPassword" style="height: 100%; width: 100%" onsubmit="return false;">
|
||||
<div>{% if errmsg %}
|
||||
<div class="highlight has-error">
|
||||
<div class='control-label'>{{ errmsg }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<div>
|
||||
<div><b>{{ _('Please enter the password for the user \'{0}\' to connect the server - "{1}"').format(username,
|
||||
server_label) }}</b></div>
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
<div style="width: 100%">
|
||||
<span style="width: 25%;display: inline-table;">Password</span>
|
||||
<span style="width: 73%;display: inline-block;">
|
||||
<input style="width:100%" id="password" class="form-control" name="password" type="password">
|
||||
</span>
|
||||
<span style="margin-left: 25%; padding-top: 15px;width: 45%;display: inline-block;">
|
||||
<div class="input-group row py-2">
|
||||
<label for="password" class="col-sm-2 col-form-label">Password</label>
|
||||
<div class="col-sm-10">
|
||||
<input id="password" class="form-control" name="password" type="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-password-div input-group row py-2">
|
||||
<label for="password" class="col-sm-2 col-form-label"></label>
|
||||
<div class="col-sm-10">
|
||||
<input id="save_password" name="save_password" type="checkbox"
|
||||
{% if not config.ALLOW_SAVE_PASSWORD %}disabled{% endif %}
|
||||
> Save Password
|
||||
</span>
|
||||
>
|
||||
<label for="save_password">Save Password</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
{% if errmsg %}
|
||||
<div class='pg-prop-status-bar p-0'>
|
||||
<div class="error-in-footer">
|
||||
<div class="d-flex px-2 py-1">
|
||||
<div class="pr-2">
|
||||
<i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="alert-text">{{ errmsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -1,42 +1,53 @@
|
||||
<form name="frmPassword" id="frmPassword" style="height: 100%; width: 100%" onsubmit="return false;">
|
||||
<div>{% if errmsg %}
|
||||
<div class="highlight has-error">
|
||||
<div class='control-label'>{{ errmsg }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
<form name="frmPassword" id="frmPassword" onsubmit="return false;">
|
||||
<div class="m-1">
|
||||
{% if prompt_tunnel_password %}
|
||||
{% if tunnel_identity_file %}
|
||||
<div><b>{{ _('SSH Tunnel password for the identity file \'{0}\' to connect the server "{1}"').format(tunnel_identity_file, tunnel_host) }}</b></div>
|
||||
{% else %}
|
||||
<div><b>{{ _('SSH Tunnel password for the user \'{0}\' to connect the server "{1}"').format(tunnel_username, tunnel_host) }}</b></div>
|
||||
{% endif %}
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
<div style="width: 100%">
|
||||
<span style="width: 97%;display: inline-block;">
|
||||
<input style="width:100%" id="tunnel_password" class="form-control" name="tunnel_password" type="password">
|
||||
</span>
|
||||
<span style="padding-top: 5px;display: inline-block;">
|
||||
<input id="save_tunnel_password" name="save_tunnel_password" type="checkbox"
|
||||
{% if not config.ALLOW_SAVE_TUNNEL_PASSWORD %}disabled{% endif %}
|
||||
> Save Password
|
||||
</span>
|
||||
<div class="input-group py-2">
|
||||
<div class="w-100">
|
||||
<input id="tunnel_password" class="form-control" name="tunnel_password" type="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-password-div input-group py-2">
|
||||
<div class="w-100">
|
||||
<input id="save_tunnel_password" name="save_tunnel_password" type="checkbox"
|
||||
{% if not config.ALLOW_SAVE_PASSWORD %}disabled{% endif %}
|
||||
>
|
||||
<label for="save_tunnel_password" class="ml-1">Save Password</label>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
{% endif %}
|
||||
{% if prompt_password %}
|
||||
<div><b>{{ _('Database server password for the user \'{0}\' to connect the server "{1}"').format(username, server_label) }}</b></div>
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
<div style="width: 100%">
|
||||
<span style="width: 97%;display: inline-block;">
|
||||
<input style="width:100%" id="password" class="form-control" name="password" type="password">
|
||||
</span>
|
||||
<span style="padding-top: 5px;display: inline-block;">
|
||||
<div class="input-group py-2">
|
||||
<div class="w-100">
|
||||
<input id="password" class="form-control" name="password" type="password">
|
||||
</div>
|
||||
</div>
|
||||
<div class="save-password-div input-group py-2">
|
||||
<label for="password" class="col-sm-2 col-form-label"></label>
|
||||
<div class="w-100">
|
||||
<input id="save_password" name="save_password" type="checkbox"
|
||||
{% if not config.ALLOW_SAVE_PASSWORD %}disabled{% endif %}
|
||||
> Save Password
|
||||
</span>
|
||||
>
|
||||
<label for="save_password" class="ml-1">Save Password</label>
|
||||
</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% if errmsg %}
|
||||
<div class='pg-prop-status-bar p-0'>
|
||||
<div class="error-in-footer">
|
||||
<div class="d-flex px-2 py-1">
|
||||
<div class="pr-2">
|
||||
<i class="fa fa-exclamation-triangle text-danger" aria-hidden="true"></i>
|
||||
</div>
|
||||
<div class="alert-text">{{ errmsg }}</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -8,6 +8,9 @@
|
||||
##########################################################################
|
||||
|
||||
"""Server helper utilities"""
|
||||
from pgadmin.utils.crypto import encrypt, decrypt
|
||||
import config
|
||||
from pgadmin.model import db, Server
|
||||
|
||||
|
||||
def parse_priv_from_db(db_privileges):
|
||||
@@ -177,3 +180,70 @@ def validate_options(options, option_name, option_value):
|
||||
is_valid_options = True
|
||||
|
||||
return is_valid_options, valid_options
|
||||
|
||||
|
||||
def reencrpyt_server_passwords(user_id, old_key, new_key):
|
||||
"""
|
||||
This function will decrypt the saved passwords in SQLite with old key
|
||||
and then encrypt with new key
|
||||
"""
|
||||
from pgadmin.utils.driver import get_driver
|
||||
driver = get_driver(config.PG_DEFAULT_DRIVER)
|
||||
|
||||
for server in Server.query.filter_by(user_id=user_id).all():
|
||||
manager = driver.connection_manager(server.id)
|
||||
|
||||
# Check if old password was stored in pgadmin4 sqlite database.
|
||||
# If yes then update that password.
|
||||
if server.password is not None:
|
||||
password = decrypt(server.password, old_key)
|
||||
|
||||
if isinstance(password, bytes):
|
||||
password = password.decode()
|
||||
|
||||
password = encrypt(password, new_key)
|
||||
setattr(server, 'password', password)
|
||||
manager.password = password
|
||||
elif manager.password is not None:
|
||||
password = decrypt(manager.password, old_key)
|
||||
|
||||
if isinstance(password, bytes):
|
||||
password = password.decode()
|
||||
|
||||
password = encrypt(password, new_key)
|
||||
manager.password = password
|
||||
|
||||
if server.tunnel_password is not None:
|
||||
tunnel_password = decrypt(server.tunnel_password, old_key)
|
||||
if isinstance(tunnel_password, bytes):
|
||||
tunnel_password = tunnel_password.decode()
|
||||
|
||||
tunnel_password = encrypt(tunnel_password, new_key)
|
||||
setattr(server, 'tunnel_password', tunnel_password)
|
||||
manager.tunnel_password = tunnel_password
|
||||
elif manager.tunnel_password is not None:
|
||||
tunnel_password = decrypt(manager.tunnel_password, old_key)
|
||||
|
||||
if isinstance(tunnel_password, bytes):
|
||||
tunnel_password = tunnel_password.decode()
|
||||
|
||||
tunnel_password = encrypt(tunnel_password, new_key)
|
||||
manager.tunnel_password = tunnel_password
|
||||
|
||||
db.session.commit()
|
||||
manager.update_session()
|
||||
|
||||
|
||||
def remove_saved_passwords(user_id):
|
||||
"""
|
||||
This function will remove all the saved passwords for the server
|
||||
"""
|
||||
|
||||
try:
|
||||
db.session.query(Server) \
|
||||
.filter(Server.user_id == user_id) \
|
||||
.update({Server.password: None, Server.tunnel_password: None})
|
||||
db.session.commit()
|
||||
except Exception as _:
|
||||
db.session.rollback()
|
||||
raise
|
||||
|
||||
Reference in New Issue
Block a user