mirror of
https://github.com/pgadmin-org/pgadmin4.git
synced 2025-02-25 18:55:31 -06:00
Add support to save and clear SSH Tunnel password. Fixes #3511
This commit is contained in:
@@ -139,7 +139,9 @@ class ServerModule(sg.ServerGroupPluginModule):
|
||||
in_recovery=in_recovery,
|
||||
wal_pause=wal_paused,
|
||||
is_password_saved=True if server.password is not None
|
||||
else False
|
||||
else False,
|
||||
is_tunnel_password_saved=True
|
||||
if server.tunnel_password is not None else False
|
||||
)
|
||||
|
||||
@property
|
||||
@@ -251,7 +253,8 @@ class ServerNode(PGChildNodeView):
|
||||
'delete': 'pause_wal_replay', 'put': 'resume_wal_replay'
|
||||
}],
|
||||
'check_pgpass': [{'get': 'check_pgpass'}],
|
||||
'clear_saved_password': [{'put': 'clear_saved_password'}]
|
||||
'clear_saved_password': [{'put': 'clear_saved_password'}],
|
||||
'clear_sshtunnel_password': [{'put': 'clear_sshtunnel_password'}]
|
||||
})
|
||||
EXP_IP4 = "^\s*((25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\." \
|
||||
"(25[0-5]|2[0-4][0-9]|[01]?[0-9][0-9]?)\." \
|
||||
@@ -362,7 +365,9 @@ class ServerNode(PGChildNodeView):
|
||||
in_recovery=in_recovery,
|
||||
wal_pause=wal_paused,
|
||||
is_password_saved=True if server.password is not None
|
||||
else False
|
||||
else False,
|
||||
is_tunnel_password_saved=True
|
||||
if server.tunnel_password is not None else False
|
||||
)
|
||||
)
|
||||
|
||||
@@ -417,7 +422,9 @@ class ServerNode(PGChildNodeView):
|
||||
in_recovery=in_recovery,
|
||||
wal_pause=wal_paused,
|
||||
is_password_saved=True if server.password is not None
|
||||
else False
|
||||
else False,
|
||||
is_tunnel_password_saved=True
|
||||
if server.tunnel_password is not None else False
|
||||
)
|
||||
)
|
||||
|
||||
@@ -787,6 +794,7 @@ class ServerNode(PGChildNodeView):
|
||||
conn = manager.connection()
|
||||
|
||||
have_password = False
|
||||
have_tunnel_password = False
|
||||
password = None
|
||||
passfile = None
|
||||
tunnel_password = ''
|
||||
@@ -801,6 +809,7 @@ class ServerNode(PGChildNodeView):
|
||||
db.session.commit()
|
||||
|
||||
if 'tunnel_password' in data and data["tunnel_password"] != '':
|
||||
have_tunnel_password = True
|
||||
tunnel_password = data['tunnel_password']
|
||||
tunnel_password = \
|
||||
encrypt(tunnel_password, current_user.password)
|
||||
@@ -828,6 +837,13 @@ class ServerNode(PGChildNodeView):
|
||||
setattr(server, 'password', password)
|
||||
db.session.commit()
|
||||
|
||||
if 'save_tunnel_password' in data and \
|
||||
data['save_tunnel_password'] and \
|
||||
have_tunnel_password and \
|
||||
config.ALLOW_SAVE_TUNNEL_PASSWORD:
|
||||
setattr(server, 'tunnel_password', tunnel_password)
|
||||
db.session.commit()
|
||||
|
||||
user = manager.user_info
|
||||
connected = True
|
||||
|
||||
@@ -969,6 +985,9 @@ class ServerNode(PGChildNodeView):
|
||||
passfile = None
|
||||
tunnel_password = None
|
||||
save_password = False
|
||||
save_tunnel_password = False
|
||||
prompt_password = False
|
||||
prompt_tunnel_password = False
|
||||
|
||||
# Connect the Server
|
||||
manager = get_driver(PG_DEFAULT_DRIVER).connection_manager(sid)
|
||||
@@ -977,10 +996,16 @@ class ServerNode(PGChildNodeView):
|
||||
# If server using SSH Tunnel
|
||||
if server.use_ssh_tunnel:
|
||||
if 'tunnel_password' not in data:
|
||||
return self.get_response_for_password(server, 428)
|
||||
if server.tunnel_password is None:
|
||||
prompt_tunnel_password = True
|
||||
else:
|
||||
tunnel_password = server.tunnel_password
|
||||
else:
|
||||
tunnel_password = data['tunnel_password'] if 'tunnel_password'\
|
||||
in data else ''
|
||||
tunnel_password = data['tunnel_password'] \
|
||||
if 'tunnel_password'in data else ''
|
||||
save_tunnel_password = data['save_tunnel_password'] \
|
||||
if tunnel_password and 'save_tunnel_password' in data \
|
||||
else False
|
||||
# Encrypt the password before saving with user's login
|
||||
# password key.
|
||||
try:
|
||||
@@ -995,9 +1020,7 @@ class ServerNode(PGChildNodeView):
|
||||
conn_passwd = getattr(conn, 'password', None)
|
||||
if conn_passwd is None and server.password is None and \
|
||||
server.passfile is None and server.service is None:
|
||||
# Return the password template in case password is not
|
||||
# provided, or password has not been saved earlier.
|
||||
return self.get_response_for_password(server, 428)
|
||||
prompt_password = True
|
||||
elif server.passfile and server.passfile != '':
|
||||
passfile = server.passfile
|
||||
else:
|
||||
@@ -1016,6 +1039,13 @@ class ServerNode(PGChildNodeView):
|
||||
current_app.logger.exception(e)
|
||||
return internal_server_error(errormsg=e.message)
|
||||
|
||||
# Check do we need to prompt for the database server or ssh tunnel
|
||||
# password or both. Return the password template in case password is
|
||||
# not provided, or password has not been saved earlier.
|
||||
if prompt_password or prompt_tunnel_password:
|
||||
return self.get_response_for_password(server, 428, prompt_password,
|
||||
prompt_tunnel_password)
|
||||
|
||||
status = True
|
||||
try:
|
||||
status, errmsg = conn.connect(
|
||||
@@ -1027,7 +1057,7 @@ class ServerNode(PGChildNodeView):
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
return self.get_response_for_password(
|
||||
server, 401, getattr(e, 'message', str(e)))
|
||||
server, 401, True, True, getattr(e, 'message', str(e)))
|
||||
|
||||
if not status:
|
||||
if hasattr(str, 'decode'):
|
||||
@@ -1037,8 +1067,8 @@ class ServerNode(PGChildNodeView):
|
||||
"Could not connected to server(#{0}) - '{1}'.\nError: {2}"
|
||||
.format(server.id, server.name, errmsg)
|
||||
)
|
||||
|
||||
return self.get_response_for_password(server, 401, errmsg)
|
||||
return self.get_response_for_password(server, 401, True,
|
||||
True, errmsg)
|
||||
else:
|
||||
if save_password and config.ALLOW_SAVE_PASSWORD:
|
||||
try:
|
||||
@@ -1054,6 +1084,19 @@ class ServerNode(PGChildNodeView):
|
||||
|
||||
return internal_server_error(errormsg=e.message)
|
||||
|
||||
if save_tunnel_password and config.ALLOW_SAVE_TUNNEL_PASSWORD:
|
||||
try:
|
||||
# Save the encrypted tunnel password.
|
||||
setattr(server, 'tunnel_password', tunnel_password)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
# Release Connection
|
||||
current_app.logger.exception(e)
|
||||
manager.release(database=server.maintenance_db)
|
||||
conn = None
|
||||
|
||||
return internal_server_error(errormsg=e.message)
|
||||
|
||||
current_app.logger.info('Connection Established for server: \
|
||||
%s - %s' % (server.id, server.name))
|
||||
# Update the recovery and wal pause option for the server
|
||||
@@ -1072,7 +1115,11 @@ class ServerNode(PGChildNodeView):
|
||||
'db': manager.db,
|
||||
'user': manager.user_info,
|
||||
'in_recovery': in_recovery,
|
||||
'wal_pause': wal_paused
|
||||
'wal_pause': wal_paused,
|
||||
'is_password_saved': True if server.password is not None
|
||||
else False,
|
||||
'is_tunnel_password_saved': True
|
||||
if server.tunnel_password is not None else False,
|
||||
}
|
||||
)
|
||||
|
||||
@@ -1418,7 +1465,8 @@ class ServerNode(PGChildNodeView):
|
||||
)
|
||||
return internal_server_error(errormsg=str(e))
|
||||
|
||||
def get_response_for_password(self, server, status, errmsg=None):
|
||||
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,
|
||||
@@ -1431,7 +1479,9 @@ class ServerNode(PGChildNodeView):
|
||||
tunnel_host=server.tunnel_host,
|
||||
tunnel_identity_file=server.tunnel_identity_file,
|
||||
errmsg=errmsg,
|
||||
_=gettext
|
||||
_=gettext,
|
||||
prompt_tunnel_password=prompt_tunnel_password,
|
||||
prompt_password=prompt_password
|
||||
)
|
||||
)
|
||||
else:
|
||||
@@ -1478,9 +1528,45 @@ class ServerNode(PGChildNodeView):
|
||||
|
||||
return make_json_response(
|
||||
success=1,
|
||||
info=gettext("Clear saved password successfully."),
|
||||
info=gettext("The saved password cleared successfully."),
|
||||
data={'is_password_saved': False}
|
||||
)
|
||||
|
||||
def clear_sshtunnel_password(self, gid, sid):
|
||||
"""
|
||||
This function is used to remove sshtunnel password stored into
|
||||
the pgAdmin's db file.
|
||||
|
||||
:param gid:
|
||||
:param sid:
|
||||
:return:
|
||||
"""
|
||||
try:
|
||||
server = Server.query.filter_by(
|
||||
user_id=current_user.id, id=sid
|
||||
).first()
|
||||
|
||||
if server is None:
|
||||
return make_json_response(
|
||||
success=0,
|
||||
info=gettext("Could not find the required server.")
|
||||
)
|
||||
|
||||
setattr(server, 'tunnel_password', None)
|
||||
db.session.commit()
|
||||
except Exception as e:
|
||||
current_app.logger.error(
|
||||
"Unable to clear ssh tunnel password."
|
||||
"\nError: {0}".format(str(e))
|
||||
)
|
||||
|
||||
return internal_server_error(errormsg=str(e))
|
||||
|
||||
return make_json_response(
|
||||
success=1,
|
||||
info=gettext("The saved password cleared successfully."),
|
||||
data={'is_tunnel_password_saved': False}
|
||||
)
|
||||
|
||||
|
||||
ServerNode.register_node_view(blueprint)
|
||||
|
||||
@@ -119,6 +119,18 @@ define('pgadmin.node.server', [
|
||||
}
|
||||
return false;
|
||||
},
|
||||
},{
|
||||
name: 'clear_sshtunnel_password', node: 'server', module: this,
|
||||
applies: ['object', 'context'], callback: 'clear_sshtunnel_password',
|
||||
label: gettext('Clear SSH Tunnel Password'), icon: 'fa fa-eraser',
|
||||
priority: 12,
|
||||
enable: function(node) {
|
||||
if (node && node._type === 'server' &&
|
||||
node.is_tunnel_password_saved) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
}]);
|
||||
|
||||
_.bindAll(this, 'connection_lost');
|
||||
@@ -648,6 +660,46 @@ define('pgadmin.node.server', [
|
||||
|
||||
return false;
|
||||
},
|
||||
|
||||
/* Reset stored ssh tunnel password */
|
||||
clear_sshtunnel_password: function(args){
|
||||
var input = args || {},
|
||||
obj = this,
|
||||
t = pgBrowser.tree,
|
||||
i = input.item || t.selected(),
|
||||
d = i && i.length == 1 ? t.itemData(i) : undefined;
|
||||
|
||||
if (!d)
|
||||
return false;
|
||||
|
||||
Alertify.confirm(
|
||||
gettext('Clear SSH Tunnel password'),
|
||||
S(
|
||||
gettext('Are you sure you want to clear the saved password of SSH Tunnel for server %s?')
|
||||
).sprintf(d.label).value(),
|
||||
function() {
|
||||
$.ajax({
|
||||
url: obj.generate_url(i, 'clear_sshtunnel_password', d, true),
|
||||
method:'PUT',
|
||||
})
|
||||
.done(function(res) {
|
||||
if (res.success == 1) {
|
||||
Alertify.success(res.info);
|
||||
t.itemData(i).is_tunnel_password_saved=res.data.is_tunnel_password_saved;
|
||||
}
|
||||
else {
|
||||
Alertify.error(res.info);
|
||||
}
|
||||
})
|
||||
.fail(function(xhr, status, error) {
|
||||
Alertify.pgRespErrorNotify(xhr, error);
|
||||
});
|
||||
},
|
||||
function() { return true; }
|
||||
);
|
||||
|
||||
return false;
|
||||
},
|
||||
},
|
||||
model: pgAdmin.Browser.Node.Model.extend({
|
||||
defaults: {
|
||||
@@ -679,6 +731,7 @@ define('pgadmin.node.server', [
|
||||
tunnel_identity_file: undefined,
|
||||
tunnel_password: undefined,
|
||||
tunnel_authentication: 0,
|
||||
save_tunnel_password: false,
|
||||
connect_timeout: 0,
|
||||
},
|
||||
// Default values!
|
||||
@@ -745,28 +798,13 @@ define('pgadmin.node.server', [
|
||||
},{
|
||||
id: 'save_password', controlLabel: gettext('Save password?'),
|
||||
type: 'checkbox', group: gettext('Connection'), mode: ['create'],
|
||||
deps: ['connect_now', 'use_ssh_tunnel'], visible: function(model) {
|
||||
deps: ['connect_now'], visible: function(model) {
|
||||
return model.get('connect_now') && model.isNew();
|
||||
},
|
||||
disabled: function(model) {
|
||||
disabled: function() {
|
||||
if (!current_user.allow_save_password)
|
||||
return true;
|
||||
|
||||
if (model.get('use_ssh_tunnel')) {
|
||||
if (model.get('save_password')) {
|
||||
Alertify.alert(
|
||||
gettext('Stored Password'),
|
||||
gettext('Database passwords cannot be stored when using SSH tunnelling. The \'Save password\' option has been turned off.')
|
||||
);
|
||||
}
|
||||
|
||||
setTimeout(function() {
|
||||
model.set('save_password', false);
|
||||
}, 10);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
return false;
|
||||
},
|
||||
},{
|
||||
@@ -918,6 +956,19 @@ define('pgadmin.node.server', [
|
||||
disabled: function(model) {
|
||||
return !model.get('use_ssh_tunnel');
|
||||
},
|
||||
}, {
|
||||
id: 'save_tunnel_password', controlLabel: gettext('Save password?'),
|
||||
type: 'checkbox', group: gettext('SSH Tunnel'), mode: ['create'],
|
||||
deps: ['connect_now', 'use_ssh_tunnel'], visible: function(model) {
|
||||
return model.get('connect_now') && model.isNew();
|
||||
},
|
||||
disabled: function(model) {
|
||||
if (!current_user.allow_save_tunnel_password ||
|
||||
!model.get('use_ssh_tunnel'))
|
||||
return true;
|
||||
|
||||
return false;
|
||||
},
|
||||
}, {
|
||||
id: 'hostaddr', label: gettext('Host address'), type: 'text', group: gettext('Advanced'),
|
||||
mode: ['properties', 'edit', 'create'], disabled: 'isConnected',
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
<div class='control-label'>{{ errmsg }}</div>
|
||||
</div>
|
||||
{% endif %}
|
||||
{% 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 %}
|
||||
@@ -14,15 +15,28 @@
|
||||
<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>
|
||||
<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;">
|
||||
<input id="save_password" name="save_password" type="checkbox"
|
||||
{% if not config.ALLOW_SAVE_PASSWORD %}disabled{% endif %}
|
||||
> Save Password
|
||||
</span>
|
||||
</div>
|
||||
<div style="padding: 5px; height: 1px;"></div>
|
||||
{% endif %}
|
||||
</div>
|
||||
</form>
|
||||
|
||||
@@ -20,13 +20,30 @@ class ServersWithSSHTunnelAddTestCase(BaseTestGenerator):
|
||||
(
|
||||
'Add server using SSH tunnel with password', dict(
|
||||
url='/browser/server/obj/',
|
||||
with_password=True
|
||||
with_password=True,
|
||||
save_password=False,
|
||||
)
|
||||
),
|
||||
(
|
||||
'Add server using SSH tunnel with identity file', dict(
|
||||
url='/browser/server/obj/',
|
||||
with_password=False
|
||||
with_password=False,
|
||||
save_password=False,
|
||||
)
|
||||
),
|
||||
(
|
||||
'Add server using SSH tunnel with password and saved it', dict(
|
||||
url='/browser/server/obj/',
|
||||
with_password=True,
|
||||
save_password=True,
|
||||
)
|
||||
),
|
||||
(
|
||||
'Add server using SSH tunnel with identity file and save the '
|
||||
'password', dict(
|
||||
url='/browser/server/obj/',
|
||||
with_password=False,
|
||||
save_password=True,
|
||||
)
|
||||
),
|
||||
]
|
||||
@@ -48,6 +65,9 @@ class ServersWithSSHTunnelAddTestCase(BaseTestGenerator):
|
||||
self.server['tunnel_authentication'] = 1
|
||||
self.server['tunnel_identity_file'] = 'pkey_rsa'
|
||||
|
||||
if self.save_password:
|
||||
self.server['tunnel_password'] = '123456'
|
||||
|
||||
response = self.tester.post(
|
||||
url,
|
||||
data=json.dumps(self.server),
|
||||
|
||||
@@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy
|
||||
#
|
||||
##########################################################################
|
||||
|
||||
SCHEMA_VERSION = 17
|
||||
SCHEMA_VERSION = 18
|
||||
|
||||
##########################################################################
|
||||
#
|
||||
@@ -164,6 +164,7 @@ class Server(db.Model):
|
||||
nullable=False
|
||||
)
|
||||
tunnel_identity_file = db.Column(db.String(64), nullable=True)
|
||||
tunnel_password = db.Column(db.String(64), nullable=True)
|
||||
|
||||
|
||||
class ModulePreference(db.Model):
|
||||
|
||||
@@ -150,7 +150,9 @@ def current_user_info():
|
||||
else 'postgres'
|
||||
),
|
||||
allow_save_password='true' if config.ALLOW_SAVE_PASSWORD
|
||||
else 'false'
|
||||
else 'false',
|
||||
allow_save_tunnel_password='true'
|
||||
if config.ALLOW_SAVE_TUNNEL_PASSWORD else 'false',
|
||||
),
|
||||
status=200,
|
||||
mimetype="application/javascript"
|
||||
|
||||
@@ -4,6 +4,7 @@ define('pgadmin.user_management.current_user', [], function() {
|
||||
'email': '{{ email }}',
|
||||
'is_admin': {{ is_admin }},
|
||||
'name': '{{ name }}',
|
||||
'allow_save_password': {{ allow_save_password }}
|
||||
'allow_save_password': {{ allow_save_password }},
|
||||
'allow_save_tunnel_password': {{ allow_save_tunnel_password }}
|
||||
}
|
||||
});
|
||||
|
||||
@@ -53,6 +53,7 @@ class ServerManager(object):
|
||||
self.server_type = None
|
||||
self.server_cls = None
|
||||
self.password = None
|
||||
self.tunnel_password = None
|
||||
|
||||
self.sid = server.id
|
||||
self.host = server.host
|
||||
@@ -84,6 +85,7 @@ class ServerManager(object):
|
||||
self.tunnel_username = server.tunnel_username
|
||||
self.tunnel_authentication = server.tunnel_authentication
|
||||
self.tunnel_identity_file = server.tunnel_identity_file
|
||||
self.tunnel_password = server.tunnel_password
|
||||
else:
|
||||
self.use_ssh_tunnel = 0
|
||||
self.tunnel_host = None
|
||||
@@ -91,6 +93,7 @@ class ServerManager(object):
|
||||
self.tunnel_username = None
|
||||
self.tunnel_authentication = None
|
||||
self.tunnel_identity_file = None
|
||||
self.tunnel_password = None
|
||||
|
||||
for con in self.connections:
|
||||
self.connections[con]._release()
|
||||
@@ -119,6 +122,17 @@ class ServerManager(object):
|
||||
else:
|
||||
res['password'] = self.password
|
||||
|
||||
if self.use_ssh_tunnel:
|
||||
if hasattr(self, 'tunnel_password') and self.tunnel_password:
|
||||
# If running under PY2
|
||||
if hasattr(self.tunnel_password, 'decode'):
|
||||
res['tunnel_password'] = \
|
||||
self.tunnel_password.decode('utf-8')
|
||||
else:
|
||||
res['tunnel_password'] = str(self.tunnel_password)
|
||||
else:
|
||||
res['tunnel_password'] = self.tunnel_password
|
||||
|
||||
connections = res['connections'] = dict()
|
||||
|
||||
for conn_id in self.connections:
|
||||
@@ -248,6 +262,9 @@ WHERE db.oid = {0}""".format(did))
|
||||
try:
|
||||
if 'password' in data and data['password']:
|
||||
data['password'] = data['password'].encode('utf-8')
|
||||
if 'tunnel_password' in data and data['tunnel_password']:
|
||||
data['tunnel_password'] = \
|
||||
data['tunnel_password'].encode('utf-8')
|
||||
except Exception as e:
|
||||
current_app.logger.exception(e)
|
||||
|
||||
@@ -265,6 +282,14 @@ WHERE db.oid = {0}""".format(did))
|
||||
# auto_reconnect is true.
|
||||
if conn_info['wasConnected'] and conn_info['auto_reconnect']:
|
||||
try:
|
||||
# Check SSH Tunnel needs to be created
|
||||
if self.use_ssh_tunnel == 1 and not self.tunnel_created:
|
||||
status, error = self.create_ssh_tunnel(
|
||||
data['tunnel_password'])
|
||||
|
||||
# Check SSH Tunnel is alive or not.
|
||||
self.check_ssh_tunnel_alive()
|
||||
|
||||
conn.connect(
|
||||
password=data['password'],
|
||||
server_types=ServerType.types()
|
||||
|
||||
Reference in New Issue
Block a user