Add support to save and clear SSH Tunnel password. Fixes #3511

This commit is contained in:
Akshay Joshi
2018-08-06 15:56:46 +05:30
parent 52fc0846cd
commit c8c5f83dfe
20 changed files with 331 additions and 73 deletions

View File

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

View File

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

View File

@@ -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 %}
>&nbsp;&nbsp;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 %}
>&nbsp;&nbsp;Save Password
</span>
</div>
<div style="padding: 5px; height: 1px;"></div>
{% endif %}
</div>
</form>

View File

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

View File

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

View File

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

View File

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

View File

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