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:
Aditya Toshniwal
2019-05-28 12:00:18 +05:30
committed by Akshay Joshi
parent 6f0eafb223
commit dfa892d2a2
44 changed files with 1509 additions and 416 deletions

View File

@@ -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,
)
)

View File

@@ -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(

View File

@@ -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);
});
},

View File

@@ -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);
});
},

View File

@@ -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 %}
>&nbsp;&nbsp;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>

View File

@@ -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 %}
>&nbsp;&nbsp;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 %}
>&nbsp;&nbsp;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>

View File

@@ -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