diff --git a/web/migrations/versions/a091c9611d20_.py b/web/migrations/versions/a091c9611d20_.py new file mode 100644 index 000000000..d9da2b10f --- /dev/null +++ b/web/migrations/versions/a091c9611d20_.py @@ -0,0 +1,72 @@ + +"""empty message + +Revision ID: a091c9611d20 +Revises: 84700139beb0 +Create Date: 2020-07-14 17:20:22.705737 + +""" +from pgadmin.model import db + + +# revision identifiers, used by Alembic. +revision = 'a091c9611d20' +down_revision = '84700139beb0' +branch_labels = None +depends_on = None + + +def upgrade(): + db.engine.execute( + 'ALTER TABLE server ADD COLUMN shared BOOLEAN' + ) + + db.engine.execute(""" + CREATE TABLE sharedserver ( + id INTEGER NOT NULL, + user_id INTEGER NOT NULL, + server_owner VARCHAR(64), + servergroup_id INTEGER NOT NULL, + name VARCHAR(128) NOT NULL, + host VARCHAR(128), + port INTEGER NOT NULL CHECK(port >= 1 AND port <= 65534), + maintenance_db VARCHAR(64), + username VARCHAR(64), + password VARCHAR(64), + role VARCHAR(64), + ssl_mode VARCHAR(16) NOT NULL CHECK(ssl_mode IN + ( 'allow' , 'prefer' , 'require' , 'disable' , + 'verify-ca' , 'verify-full' ) + ), + comment VARCHAR(1024), + discovery_id VARCHAR(128), + hostaddr TEXT(1024), + db_res TEXT, + passfile TEXT, + sslcert TEXT, + sslkey TEXT, + sslrootcert TEXT, + sslcrl TEXT, + sslcompression INTEGER DEFAULT 0, + bgcolor TEXT(10), + fgcolor TEXT(10), + service TEXT, + use_ssh_tunnel INTEGER DEFAULT 0, + tunnel_host TEXT, + tunnel_port TEXT, + tunnel_username TEXT, + tunnel_authentication INTEGER DEFAULT 0, + tunnel_identity_file TEXT, + shared BOOLEAN NOT NULL, + save_password BOOLEAN NOT NULL, + tunnel_password VARCHAR(64), + connect_timeout INTEGER , + PRIMARY KEY(id), + FOREIGN KEY(user_id) REFERENCES user(id), + FOREIGN KEY(servergroup_id) REFERENCES servergroup(id) + ); + """) + + +def downgrade(): + pass diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py index 516ba2c4d..1a712151a 100644 --- a/web/pgadmin/__init__.py +++ b/web/pgadmin/__init__.py @@ -28,7 +28,7 @@ from werkzeug.datastructures import ImmutableDict from werkzeug.local import LocalProxy from werkzeug.utils import find_modules -from pgadmin.model import db, Role, Server, ServerGroup, \ +from pgadmin.model import db, Role, Server, SharedServer, ServerGroup, \ User, Keys, Version, SCHEMA_VERSION as CURRENT_SCHEMA_VERSION from pgadmin.utils import PgAdminModule, driver, KeyManager from pgadmin.utils.preferences import Preferences diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py index 948be41b8..215421cbf 100644 --- a/web/pgadmin/browser/register_browser_preferences.py +++ b/web/pgadmin/browser/register_browser_preferences.py @@ -9,6 +9,7 @@ from flask_babelex import gettext from pgadmin.utils.constants import PREF_LABEL_DISPLAY,\ PREF_LABEL_KEYBOARD_SHORTCUTS +import config LOCK_LAYOUT_LEVEL = { 'PREVENT_DOCKING': 'docking', @@ -23,6 +24,15 @@ def register_browser_preferences(self): gettext("Show system objects?"), 'boolean', False, category_label=PREF_LABEL_DISPLAY ) + if config.SERVER_MODE: + self.hide_shared_server = self.preference.register( + 'display', 'hide_shared_server', + gettext("Hide shared server?"), 'boolean', False, + category_label=gettext('Display'), + help_str=gettext( + 'If set to true, then all shared server will be hidden' + ) + ) self.preference.register( 'display', 'enable_acitree_animation', diff --git a/web/pgadmin/browser/server_groups/__init__.py b/web/pgadmin/browser/server_groups/__init__.py index 717795bde..cbb3fde6a 100644 --- a/web/pgadmin/browser/server_groups/__init__.py +++ b/web/pgadmin/browser/server_groups/__init__.py @@ -13,7 +13,7 @@ import simplejson as json from abc import ABCMeta, abstractmethod import six -from flask import request, jsonify +from flask import request, jsonify, render_template from flask_babelex import gettext from flask_security import current_user, login_required from pgadmin.browser import BrowserPluginModule @@ -22,7 +22,27 @@ from pgadmin.utils.ajax import make_json_response, gone, \ make_response as ajax_response, bad_request from pgadmin.utils.menu import MenuItem from sqlalchemy import exc -from pgadmin.model import db, ServerGroup +from pgadmin.model import db, ServerGroup, Server +import config +from pgadmin.utils.preferences import Preferences + + +def get_icon_css_class(group_id, group_user_id, + default_val='icon-server_group'): + """ + Returns css value + :param group_id: + :param group_user_id: + :param default_val: + :return: default_val + """ + if (config.SERVER_MODE and + group_user_id != current_user.id and + ServerGroupModule.has_shared_server(group_id)): + default_val = 'icon-server_group_shared' + + return default_val + SG_NOT_FOUND_ERROR = 'The specified server group could not be found.' @@ -31,19 +51,50 @@ class ServerGroupModule(BrowserPluginModule): _NODE_TYPE = "server_group" node_icon = "icon-%s" % _NODE_TYPE + @property + def csssnippets(self): + """ + Returns a snippet of css to include in the page + """ + snippets = [render_template("css/server_group.css")] + + for submodule in self.submodules: + snippets.extend(submodule.csssnippets) + + return snippets + + @staticmethod + def has_shared_server(gid): + """ + To check whether given server group contains shared server or not + :param gid: + :return: True if servergroup contains shared server else false + """ + servers = Server.query.filter_by(servergroup_id=gid) + for s in servers: + if s.shared: + return True + return False + def get_nodes(self, *arg, **kwargs): """Return a JSON document listing the server groups for the user""" - groups = ServerGroup.query.filter_by( - user_id=current_user.id - ).order_by("id") + + if config.SERVER_MODE: + groups = ServerGroupView.get_all_server_groups() + else: + groups = ServerGroup.query.filter_by( + user_id=current_user.id + ).order_by("id") + for idx, group in enumerate(groups): yield self.generate_browser_node( "%d" % (group.id), None, group.name, - self.node_icon, + get_icon_css_class(group.id, group.user_id), True, self.node_type, - can_delete=True if idx > 0 else False + can_delete=True if idx > 0 else False, + user_id=group.user_id ) @property @@ -196,7 +247,7 @@ class ServerGroupView(NodeView): gid, None, servergroup.name, - self.node_icon, + get_icon_css_class(gid, servergroup.user_id), True, self.node_type, can_delete=True # This is user created hence can deleted @@ -207,10 +258,7 @@ class ServerGroupView(NodeView): def properties(self, gid): """Update the server-group properties""" - # There can be only one record at most - sg = ServerGroup.query.filter_by( - user_id=current_user.id, - id=gid).first() + sg = ServerGroup.query.filter(ServerGroup.id == gid).first() if sg is None: return make_json_response( @@ -220,7 +268,7 @@ class ServerGroupView(NodeView): ) else: return ajax_response( - response={'id': sg.id, 'name': sg.name}, + response={'id': sg.id, 'name': sg.name, 'user_id': sg.user_id}, status=200 ) @@ -246,7 +294,7 @@ class ServerGroupView(NodeView): "%d" % sg.id, None, sg.name, - self.node_icon, + get_icon_css_class(sg.id, sg.user_id), True, self.node_type, # This is user created hence can deleted @@ -292,13 +340,41 @@ class ServerGroupView(NodeView): def dependents(self, gid): return make_json_response(status=422) + @staticmethod + def get_all_server_groups(): + """ + Returns the list of server groups to show in server mode and + if there is any shared server in the group. + :return: server groups + """ + + # Don't display shared server if user has + # selected 'Hide shared server' + pref = Preferences.module('browser') + hide_shared_server = pref.preference('hide_shared_server').get() + + server_groups = ServerGroup.query.all() + groups = [] + for group in server_groups: + if hide_shared_server and \ + ServerGroupModule.has_shared_server(group.id) and \ + group.user_id != current_user.id: + continue + if group.user_id == current_user.id or \ + ServerGroupModule.has_shared_server(group.id): + groups.append(group) + return groups + @login_required def nodes(self, gid=None): """Return a JSON document listing the server groups for the user""" nodes = [] - if gid is None: - groups = ServerGroup.query.filter_by(user_id=current_user.id) + if config.SERVER_MODE: + + groups = self.get_all_server_groups() + else: + groups = ServerGroup.query.filter_by(user_id=current_user.id) for group in groups: nodes.append( @@ -306,14 +382,14 @@ class ServerGroupView(NodeView): "%d" % group.id, None, group.name, - self.node_icon, + get_icon_css_class(group.id, group.user_id), True, self.node_type ) ) else: - group = ServerGroup.query.filter_by(user_id=current_user.id, - id=gid).first() + group = ServerGroup.query.filter(ServerGroup.id == gid).first() + if not group: return gone( errormsg=gettext("Could not find the server group.") @@ -322,7 +398,7 @@ class ServerGroupView(NodeView): nodes = self.blueprint.generate_browser_node( "%d" % (group.id), None, group.name, - self.node_icon, + get_icon_css_class(group.id, group.user_id), True, self.node_type ) diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py index 2eb058a40..ed8fae0f0 100644 --- a/web/pgadmin/browser/server_groups/servers/__init__.py +++ b/web/pgadmin/browser/server_groups/servers/__init__.py @@ -23,7 +23,7 @@ from pgadmin.tools.sqleditor.utils.query_history import QueryHistory import config from config import PG_DEFAULT_DRIVER -from pgadmin.model import db, Server, ServerGroup, User +from pgadmin.model import db, Server, ServerGroup, User, SharedServer from pgadmin.utils.driver import get_driver from pgadmin.utils.master_password import get_crypt_key from pgadmin.utils.exception import CryptKeyMissing @@ -32,6 +32,8 @@ from psycopg2 import Error as psycopg2_Error, OperationalError from pgadmin.browser.server_groups.servers.utils import is_valid_ipaddress from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \ SERVER_CONNECTION_CLOSED +from sqlalchemy import or_ +from pgadmin.utils.preferences import Preferences def has_any(data, keys): @@ -65,6 +67,19 @@ def recovery_state(connection, postgres_version): return status, result, in_recovery, wal_paused +def get_preferences(): + """ + Get preferences setting + :return: whether to hide shared server or not. + """ + hide_shared_server = None + if config.SERVER_MODE: + pref = Preferences.module('browser') + hide_shared_server = pref.preference('hide_shared_server').get() + + return hide_shared_server + + def server_icon_and_background(is_connected, manager, server): """ @@ -92,6 +107,10 @@ def server_icon_and_background(is_connected, manager, server): return 'icon-{0}{1}'.format( manager.server_type, server_background_color ) + elif server.shared and config.SERVER_MODE: + return 'icon-shared-server-not-connected{0}'.format( + server_background_color + ) else: return 'icon-server-not-connected{0}'.format( server_background_color @@ -114,25 +133,93 @@ class ServerModule(sg.ServerGroupPluginModule): """ return sg.ServerGroupModule.node_type + @staticmethod + def get_shared_server_properties(server, sharedserver): + """ + Return shared server properties + :param server: + :param sharedserver: + :return: + """ + + server.bgcolor = sharedserver.bgcolor + server.fgcolor = sharedserver.fgcolor + server.name = sharedserver.name + server.role = sharedserver.role + server.tunnel_username = sharedserver.tunnel_username + server.tunnel_password = sharedserver.tunnel_password + server.save_password = sharedserver.save_password + server.passfile = sharedserver.passfile + server.servergroup_id = sharedserver.servergroup_id + server.sslcert = sharedserver.sslcert + server.username = sharedserver.username + server.server_owner = sharedserver.server_owner + + return server + + @staticmethod + def check_to_hide_shared_server(hide_shared_server, shared_server, + auto_detected_server): + + hide_server = False + if hide_shared_server or \ + shared_server.name == auto_detected_server: + hide_server = True + + return hide_server + @login_required def get_nodes(self, gid): """Return a JSON document listing the server groups for the user""" - servers = Server.query.filter_by(user_id=current_user.id, - servergroup_id=gid) + + hide_shared_server = get_preferences() + + servers = Server.query.filter( + or_(Server.user_id == current_user.id, Server.shared), + Server.servergroup_id == gid) driver = get_driver(PG_DEFAULT_DRIVER) for server in servers: + + if server.shared and server.user_id != current_user.id: + + shared_server, auto_detected_server = \ + self.get_shared_server(server, gid) + + if self.check_to_hide_shared_server(hide_shared_server, + shared_server, + auto_detected_server): + # Don't include shared server if hide shared server is + # set to true + continue + + # if hide_shared_server or \ + # shared_server.name == auto_detected_server: + # # Don't include shared server if hide shared server is + # # set to true + # continue + + # if shared_server.name == auto_detected_server: + # continue + server = self.get_shared_server_properties(server, + shared_server) connected = False manager = None errmsg = None was_connected = False in_recovery = None wal_paused = None + server_type = 'pg' + user_info = None try: manager = driver.connection_manager(server.id) conn = manager.connection() was_connected = conn.wasConnected + connected = conn.connected() + if connected: + server_type = manager.server_type + user_info = manager.user_info except CryptKeyMissing: # show the nodes at least even if not able to connect. pass @@ -148,17 +235,20 @@ class ServerModule(sg.ServerGroupPluginModule): True, self.node_type, connected=connected, - server_type=manager.server_type if connected else "pg", + server_type=server_type, version=manager.version, db=manager.db, - user=manager.user_info if connected else None, + user=user_info, in_recovery=in_recovery, wal_pause=wal_paused, is_password_saved=bool(server.save_password), is_tunnel_password_saved=True if server.tunnel_password is not None else False, was_connected=was_connected, - errmsg=errmsg + errmsg=errmsg, + user_id=server.user_id, + user_name=server.username, + shared=server.shared ) @property @@ -230,6 +320,82 @@ class ServerModule(sg.ServerGroupPluginModule): def get_exposed_url_endpoints(self): return ['NODE-server.connect_id'] + @staticmethod + def create_shared_server(data, gid): + """ + Create shared server + :param data: + :param gid: + :return: None + """ + + shared_server = None + try: + user = User.query.filter_by(id=data.user_id).first() + shared_server = SharedServer( + user_id=current_user.id, + server_owner=user.username, + servergroup_id=gid, + name=data.name, + host=data.host, + hostaddr=data.hostaddr, + port=data.port, + maintenance_db=None, + username=None, + save_password=0, + ssl_mode=data.ssl_mode, + comment=None, + role=data.role, + sslcert=None, + sslkey=None, + sslrootcert=None, + sslcrl=None, + bgcolor=data.bgcolor if data.bgcolor else None, + fgcolor=data.fgcolor if data.fgcolor else None, + service=data.service if data.service else None, + connect_timeout=0, + use_ssh_tunnel=0, + tunnel_host=None, + tunnel_port=22, + tunnel_username=None, + tunnel_authentication=0, + tunnel_identity_file=None, + shared=data.shared if data.shared else None + ) + db.session.add(shared_server) + db.session.commit() + except Exception as e: + if shared_server: + db.session.delete(shared_server) + db.session.commit() + + current_app.logger.exception(e) + return internal_server_error(errormsg=str(e)) + + @staticmethod + def get_shared_server(server, gid): + """ + return the shared server + :param server: + :param gid: + :return: shared_server + """ + auto_detected_server = None + shared_server = SharedServer.query.filter_by( + name=server.name, user_id=current_user.id, + servergroup_id=gid).first() + if server.discovery_id: + auto_detected_server = server.name + + if shared_server is None: + ServerModule.create_shared_server(server, gid) + + shared_server = SharedServer.query.filter_by( + name=server.name, user_id=current_user.id, + servergroup_id=gid).first() + + return shared_server, auto_detected_server + class ServerMenuItem(MenuItem): def __init__(self, **kwargs): @@ -327,19 +493,29 @@ class ServerNode(PGChildNodeView): Return a JSON document listing the servers under this server group for the user. """ - servers = Server.query.filter_by(user_id=current_user.id, - servergroup_id=gid) + servers = Server.query.filter( + or_(Server.user_id == current_user.id, + Server.shared), + Server.servergroup_id == gid) driver = get_driver(PG_DEFAULT_DRIVER) for server in servers: + if server.shared and server.user_id != current_user.id: + shared_server, auto_detected_server = \ + ServerModule.get_shared_server(server, gid) + server = \ + ServerModule.get_shared_server_properties(server, + shared_server) manager = driver.connection_manager(server.id) conn = manager.connection() connected = conn.connected() errmsg = None in_recovery = None wal_paused = None + server_type = 'pg' if connected: + server_type = manager.server_type status, result, in_recovery, wal_paused =\ recovery_state(conn, manager.version) if not status: @@ -356,7 +532,7 @@ class ServerNode(PGChildNodeView): True, self.node_type, connected=connected, - server_type=manager.server_type if connected else 'pg', + server_type=server_type, version=manager.version, db=manager.db, user=manager.user_info if connected else None, @@ -365,7 +541,9 @@ class ServerNode(PGChildNodeView): is_password_saved=bool(server.save_password), is_tunnel_password_saved=True if server.tunnel_password is not None else False, - errmsg=errmsg + errmsg=errmsg, + user_name=server.username, + shared=server.shared ) ) @@ -379,9 +557,13 @@ class ServerNode(PGChildNodeView): @login_required def node(self, gid, sid): """Return a JSON document listing the server groups for the user""" - server = Server.query.filter_by(user_id=current_user.id, - servergroup_id=gid, - id=sid).first() + server = Server.query.filter_by(id=sid).first() + + if server.shared and server.user_id != current_user.id: + shared_server, auto_detected_server = \ + ServerModule.get_shared_server(server, gid) + server = ServerModule.get_shared_server_properties(server, + shared_server) if server is None: return make_json_response( @@ -426,14 +608,37 @@ class ServerNode(PGChildNodeView): is_password_saved=bool(server.save_password), is_tunnel_password_saved=True if server.tunnel_password is not None else False, - errmsg=errmsg + errmsg=errmsg, + shared=server.shared, + user_name=server.username ), ) + def delete_shared_server(self, server_name, gid): + """ + Delete the shared server + :param server_name: + :return: + """ + try: + shared_server = SharedServer.query.filter_by(name=server_name, + servergroup_id=gid) + for s in shared_server: + get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id) + db.session.delete(s) + db.session.commit() + + except Exception as e: + current_app.logger.exception(e) + return make_json_response( + success=0, + errormsg=e.message) + @login_required def delete(self, gid, sid): """Delete a server node in the settings database.""" servers = Server.query.filter_by(user_id=current_user.id, id=sid) + server_name = None # TODO:: A server, which is connected, cannot be deleted if servers is None: @@ -449,10 +654,11 @@ class ServerNode(PGChildNodeView): else: try: for s in servers: + server_name = s.name get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id) db.session.delete(s) db.session.commit() - + self.delete_shared_server(server_name, gid) QueryHistory.clear_history(current_user.id, sid) except Exception as e: @@ -467,8 +673,8 @@ class ServerNode(PGChildNodeView): @login_required def update(self, gid, sid): """Update the server settings""" - server = Server.query.filter_by( - user_id=current_user.id, id=sid).first() + server = Server.query.filter_by(id=sid).first() + sharedserver = None if server is None: return make_json_response( @@ -477,6 +683,11 @@ class ServerNode(PGChildNodeView): errormsg=gettext("Could not find the required server.") ) + if config.SERVER_MODE and server.shared and \ + server.user_id != current_user.id: + sharedserver, auto_detected_server = \ + ServerModule.get_shared_server(server, gid) + # Not all parameters can be modified, while the server is connected config_param_map = { 'name': 'name', @@ -506,11 +717,12 @@ class ServerNode(PGChildNodeView): 'tunnel_username': 'tunnel_username', 'tunnel_authentication': 'tunnel_authentication', 'tunnel_identity_file': 'tunnel_identity_file', + 'shared': 'shared' } disp_lbl = { 'name': gettext('name'), - 'host': gettext('Host name/address'), + 'hostaddr': gettext('Host name/address'), 'port': gettext('Port'), 'db': gettext('Maintenance database'), 'username': gettext('Username'), @@ -541,7 +753,8 @@ class ServerNode(PGChildNodeView): self._server_modify_disallowed_when_connected( connected, data, disp_lbl) - idx = self._set_valid_attr_value(data, config_param_map, server) + idx = self._set_valid_attr_value(gid, data, config_param_map, server, + sharedserver) if idx == 0: return make_json_response( @@ -568,7 +781,11 @@ class ServerNode(PGChildNodeView): node=self.blueprint.generate_browser_node( "%d" % (server.id), server.servergroup_id, server.name, - server_icon_and_background(connected, manager, server), + server_icon_and_background( + connected, manager, sharedserver) + if server.shared and server.user_id != current_user.id + else server_icon_and_background( + connected, manager, server), True, self.node_type, connected=connected, @@ -577,7 +794,16 @@ class ServerNode(PGChildNodeView): ) ) - def _set_valid_attr_value(self, data, config_param_map, server): + @staticmethod + def _update_server_details(server, sharedserver, + config_param_map, arg, value): + if server.shared and server.user_id != current_user.id: + setattr(sharedserver, config_param_map[arg], value) + else: + setattr(server, config_param_map[arg], value) + + def _set_valid_attr_value(self, gid, data, config_param_map, server, + sharedserver): idx = 0 for arg in config_param_map: @@ -585,9 +811,14 @@ class ServerNode(PGChildNodeView): value = data[arg] # sqlite3 do not have boolean type so we need to convert # it manually to integer + if 'shared' in data and not data['shared']: + # Delete the shared server from DB if server + # owner uncheck shared property + self.delete_shared_server(server.name, gid) if arg == 'sslcompression': value = 1 if value else 0 - setattr(server, config_param_map[arg], value) + self._update_server_details(server, sharedserver, + config_param_map, arg, value) idx += 1 return idx @@ -613,11 +844,11 @@ class ServerNode(PGChildNodeView): """ Return list of attributes of all servers. """ - servers = Server.query.filter_by( - user_id=current_user.id, - servergroup_id=gid).order_by(Server.name) + servers = Server.query.filter( + or_(Server.user_id == current_user.id, + Server.shared), + Server.servergroup_id == gid).order_by(Server.name) sg = ServerGroup.query.filter_by( - user_id=current_user.id, id=gid ).first() res = [] @@ -625,6 +856,12 @@ class ServerNode(PGChildNodeView): driver = get_driver(PG_DEFAULT_DRIVER) for server in servers: + if server.shared and server.user_id != current_user.id: + shared_server, auto_detected_server = \ + ServerModule.get_shared_server(server, gid) + server = \ + ServerModule.get_shared_server_properties(server, + shared_server) manager = driver.connection_manager(server.id) conn = manager.connection() connected = conn.connected() @@ -653,8 +890,12 @@ class ServerNode(PGChildNodeView): @login_required def properties(self, gid, sid): """Return list of attributes of a server""" + + sslcert = None + sslkey = None + sslrootcert = None + sslcrl = None server = Server.query.filter_by( - user_id=current_user.id, id=sid).first() if server is None: @@ -663,9 +904,8 @@ class ServerNode(PGChildNodeView): success=0, errormsg=self.not_found_error_msg() ) - + server_owner = None sg = ServerGroup.query.filter_by( - user_id=current_user.id, id=server.servergroup_id ).first() @@ -675,52 +915,77 @@ class ServerNode(PGChildNodeView): conn = manager.connection() connected = conn.connected() + # if server.shared and not current_user.has_role("Administrator"): + if server.shared and server.user_id != current_user.id: + shared_server, auto_detected_server = \ + ServerModule.get_shared_server(server, gid) + server = ServerModule.get_shared_server_properties(server, + shared_server) + server_owner = server.server_owner + is_ssl = True if server.ssl_mode in self.SSL_MODES else False - return ajax_response( - response={ - 'id': server.id, - 'name': server.name, - 'host': server.host, - 'hostaddr': server.hostaddr, - 'port': server.port, - 'db': server.maintenance_db, - 'username': server.username, - 'gid': str(server.servergroup_id), - 'group-name': sg.name, - 'comment': server.comment, - 'role': server.role, - 'connected': connected, - 'version': manager.ver, - 'sslmode': server.ssl_mode, - 'server_type': manager.server_type if connected else 'pg', - 'bgcolor': server.bgcolor, - 'fgcolor': server.fgcolor, - 'db_res': server.db_res.split(',') if server.db_res else None, - 'passfile': server.passfile if server.passfile else None, - 'sslcert': server.sslcert if is_ssl else None, - 'sslkey': server.sslkey if is_ssl else None, - 'sslrootcert': server.sslrootcert if is_ssl else None, - 'sslcrl': server.sslcrl if is_ssl else None, - 'sslcompression': True if is_ssl and server.sslcompression - else False, - 'service': server.service if server.service else None, - 'connect_timeout': - server.connect_timeout if server.connect_timeout else 0, - 'use_ssh_tunnel': server.use_ssh_tunnel - if server.use_ssh_tunnel else 0, - 'tunnel_host': server.tunnel_host if server.tunnel_host - else None, - 'tunnel_port': server.tunnel_port if server.tunnel_port - else 22, - 'tunnel_username': server.tunnel_username - if server.tunnel_username else None, - 'tunnel_identity_file': server.tunnel_identity_file - if server.tunnel_identity_file else None, - 'tunnel_authentication': server.tunnel_authentication - if server.tunnel_authentication else 0 - } - ) + if is_ssl: + sslcert = server.sslcert + sslkey = server.sslkey + sslrootcert = server.sslrootcert + sslcrl = server.sslcrl + + use_ssh_tunnel = 0 + tunnel_host = None + tunnel_port = 22 + tunnel_username = None + tunnel_authentication = 0 + + if server.use_ssh_tunnel: + use_ssh_tunnel = server.use_ssh_tunnel + tunnel_host = server.tunnel_host + tunnel_port = server.tunnel_port + tunnel_username = server.tunnel_username + tunnel_authentication = server.tunnel_authentication + + response = { + 'id': server.id, + 'name': server.name, + 'server_owner': server_owner, + 'user_id': server.user_id, + 'host': server.host, + 'hostaddr': server.hostaddr, + 'port': server.port, + 'db': server.maintenance_db, + 'shared': server.shared if config.SERVER_MODE else None, + 'username': server.username, + 'gid': str(server.servergroup_id), + 'group-name': sg.name, + 'comment': server.comment, + 'role': server.role, + 'connected': connected, + 'version': manager.ver, + 'sslmode': server.ssl_mode, + 'server_type': manager.server_type if connected else 'pg', + 'bgcolor': server.bgcolor, + 'fgcolor': server.fgcolor, + 'db_res': server.db_res.split(',') if server.db_res else None, + 'passfile': server.passfile if server.passfile else None, + 'sslcert': sslcert, + 'sslkey': sslkey, + 'sslrootcert': sslrootcert, + 'sslcrl': sslcrl, + 'sslcompression': True if is_ssl and server.sslcompression + else False, + 'service': server.service if server.service else None, + 'connect_timeout': + server.connect_timeout if server.connect_timeout else 0, + 'use_ssh_tunnel': use_ssh_tunnel, + 'tunnel_host': tunnel_host, + 'tunnel_port': tunnel_port, + 'tunnel_username': tunnel_username, + 'tunnel_identity_file': server.tunnel_identity_file + if server.tunnel_identity_file else None, + 'tunnel_authentication': tunnel_authentication + } + + return ajax_response(response) @login_required def create(self, gid): @@ -802,11 +1067,11 @@ class ServerNode(PGChildNodeView): tunnel_port=data.get('tunnel_port', 22), tunnel_username=data.get('tunnel_username', None), tunnel_authentication=data.get('tunnel_authentication', 0), - tunnel_identity_file=data.get('tunnel_identity_file', None) + tunnel_identity_file=data.get('tunnel_identity_file', None), + shared=data.get('shared', None) ) db.session.add(server) db.session.commit() - connected = False user = None manager = None @@ -1006,9 +1271,23 @@ class ServerNode(PGChildNodeView): # Fetch Server Details server = Server.query.filter_by(id=sid).first() + shared_server = None + if server.shared and server.user_id != current_user.id: + shared_server, auto_detected_server = \ + ServerModule.get_shared_server(server, gid) + server = ServerModule.get_shared_server_properties(server, + shared_server) if server is None: return bad_request(self.not_found_error_msg()) + # Return if username is blank + if server.username is None: + return make_json_response( + status=200, + success=0, + errormsg=gettext( + u"Please enter the server details to connect") + ) if current_user and hasattr(current_user, 'id'): # Fetch User Details. user = User.query.filter_by(id=current_user.id).first() @@ -1063,7 +1342,6 @@ class ServerNode(PGChildNodeView): except Exception as e: current_app.logger.exception(e) return internal_server_error(errormsg=str(e)) - if 'password' not in data: conn_passwd = getattr(conn, 'password', None) if conn_passwd is None and not server.save_password and \ @@ -1125,10 +1403,18 @@ class ServerNode(PGChildNodeView): # every time user try to connect # 1 is True in SQLite as no boolean type setattr(server, 'save_password', 1) + if server.shared and server.user_id != current_user.id: + setattr(shared_server, 'save_password', 1) + else: + setattr(server, 'save_password', 1) + # Save the encrypted password using the user's login # password key, if there is any password to save if password: - setattr(server, 'password', password) + if server.shared and server.user_id != current_user.id: + setattr(shared_server, 'password', password) + else: + setattr(server, 'password', password) db.session.commit() except Exception as e: # Release Connection @@ -1560,21 +1846,39 @@ class ServerNode(PGChildNodeView): :return: """ try: - server = Server.query.filter_by( - user_id=current_user.id, id=sid - ).first() - + server = Server.query.filter_by(id=sid).first() + shared_server = None if server is None: return make_json_response( success=0, info=self.not_found_error_msg() ) - setattr(server, 'password', None) + if server.shared and server.user_id != current_user.id: + shared_server = SharedServer.query.filter_by( + name=server.name, user_id=current_user.id, + servergroup_id=gid).first() + + if shared_server is None: + return make_json_response( + success=0, + info=gettext("Could not find the required server.") + ) + server = ServerModule. \ + get_shared_server_properties(server, shared_server) + + if server.shared and server.user_id != current_user.id: + setattr(shared_server, 'save_password', None) + else: + setattr(server, 'save_password', None) + # If password was saved then clear the flag also # 0 is False in SQLite db if server.save_password: - setattr(server, 'save_password', 0) + if server.shared and server.user_id != current_user.id: + setattr(shared_server, 'save_password', 0) + else: + setattr(server, 'save_password', 0) db.session.commit() except Exception as e: current_app.logger.error( @@ -1599,10 +1903,7 @@ class ServerNode(PGChildNodeView): :return: """ try: - server = Server.query.filter_by( - user_id=current_user.id, id=sid - ).first() - + server = Server.query.filter_by(id=sid).first() if server is None: return make_json_response( success=0, diff --git a/web/pgadmin/browser/server_groups/servers/static/img/sharedserverbad.svg b/web/pgadmin/browser/server_groups/servers/static/img/sharedserverbad.svg new file mode 100644 index 000000000..4455089ac --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/static/img/sharedserverbad.svg @@ -0,0 +1,45 @@ + + + + + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js index 8c43a55f7..9170e0d44 100644 --- a/web/pgadmin/browser/server_groups/servers/static/js/server.js +++ b/web/pgadmin/browser/server_groups/servers/static/js/server.js @@ -56,7 +56,12 @@ define('pgadmin.node.server', [ type: 'server', dialogHelp: url_for('help.static', {'filename': 'server_dialog.html'}), label: gettext('Server'), - canDrop: true, + canDrop: function(node){ + var serverOwner = node.user_id; + if (serverOwner != current_user.id) + return false; + return true; + }, dropAsRemove: true, dropPriority: 5, hasStatistics: true, @@ -75,12 +80,12 @@ define('pgadmin.node.server', [ name: 'create_server_on_sg', node: 'server_group', module: this, applies: ['object', 'context'], callback: 'show_obj_properties', category: 'create', priority: 1, label: gettext('Server...'), - data: {action: 'create'}, icon: 'wcTabIcon icon-server', + data: {action: 'create'}, icon: 'wcTabIcon icon-server', enable: 'canCreate', },{ name: 'create_server', node: 'server', module: this, applies: ['object', 'context'], callback: 'show_obj_properties', category: 'create', priority: 3, label: gettext('Server...'), - data: {action: 'create'}, icon: 'wcTabIcon icon-server', + data: {action: 'create'}, icon: 'wcTabIcon icon-server', enable: 'canCreate', },{ name: 'connect_server', node: 'server', module: this, applies: ['object', 'context'], callback: 'connect_server', @@ -150,6 +155,13 @@ define('pgadmin.node.server', [ is_not_connected: function(node) { return (node && node.connected != true); }, + canCreate: function(node){ + var serverOwner = node.user_id; + if (serverOwner == current_user.id || _.isUndefined(serverOwner)) + return true; + return false; + + }, is_connected: function(node) { return (node && node.connected == true); }, @@ -226,28 +238,25 @@ define('pgadmin.node.server', [ d = t.itemData(i); t.removeIcon(i); d.connected = false; - d.icon = 'icon-server-not-connected'; + if (d.shared){ + d.icon = 'icon-shared-server-not-connected'; + }else{ + d.icon = 'icon-server-not-connected'; + } t.addIcon(i, {icon: d.icon}); obj.callbacks.refresh.apply(obj, [null, i]); if (pgBrowser.serverInfo && d._id in pgBrowser.serverInfo) { delete pgBrowser.serverInfo[d._id]; } - pgBrowser.enable_disable_menus(i); - // Trigger server disconnect event - pgBrowser.Events.trigger( - 'pgadmin:server:disconnect', - {item: i, data: d}, false - ); - } - else { - try { - Alertify.error(res.errormsg); - } catch (e) { - console.warn(e.stack || e); + else { + try { + Alertify.error(res.errormsg); + } catch (e) { + console.warn(e.stack || e); + } + t.unload(i); } - t.unload(i); - } - }) + }}) .fail(function(xhr, status, error) { Alertify.pgRespErrorNotify(xhr, error); t.unload(i); @@ -735,6 +744,7 @@ define('pgadmin.node.server', [ // Default values! initialize: function(attrs, args) { var isNew = (_.size(attrs) === 0); + console.warn('warn'); if (isNew) { this.set({'gid': args.node_info['server_group']._id}); @@ -745,12 +755,17 @@ define('pgadmin.node.server', [ id: 'id', label: gettext('ID'), type: 'int', mode: ['properties'], },{ id: 'name', label: gettext('Name'), type: 'text', - mode: ['properties', 'edit', 'create'], - },{ + mode: ['properties', 'edit', 'create'], disabled: 'isShared', + }, + { id: 'gid', label: gettext('Server group'), type: 'int', control: 'node-list-by-id', node: 'server_group', - mode: ['create', 'edit'], select2: {allowClear: false}, - },{ + mode: ['create', 'edit'], select2: {allowClear: false}, visible: 'isVisible', + }, + { + id: 'server_owner', label: gettext('Shared Server Owner'), type: 'text', mode: ['properties'], + }, + { id: 'server_type', label: gettext('Server type'), type: 'options', mode: ['properties'], visible: 'isConnected', 'options': supported_servers, @@ -773,11 +788,27 @@ define('pgadmin.node.server', [ id: 'connect_now', controlLabel: gettext('Connect now?'), type: 'checkbox', group: null, mode: ['create'], },{ + id: 'shared', label: gettext('Shared with all?'), type: 'switch', + mode: ['properties', 'create', 'edit'], 'options': {'size': 'mini'}, + readonly: function(model){ + var serverOwner = model.attributes.user_id; + if (!model.isNew() && serverOwner != current_user.id){ + return true; + } + return false; + },visible: function(){ + if (current_user.is_admin && pgAdmin.server_mode == 'True') + return true; + + return false; + }, + }, + { id: 'comment', label: gettext('Comments'), type: 'multiline', group: null, mode: ['properties', 'edit', 'create'], },{ id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'), - mode: ['properties', 'edit', 'create'], + mode: ['properties', 'edit', 'create'],disabled: 'isShared', control: Backform.InputControl.extend({ onChange: function() { Backform.InputControl.prototype.onChange.apply(this, arguments); @@ -798,7 +829,7 @@ define('pgadmin.node.server', [ }), },{ id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'), - mode: ['properties', 'edit', 'create'], min: 1, max: 65535, + mode: ['properties', 'edit', 'create'], min: 1, max: 65535, disabled: 'isShared', control: Backform.InputControl.extend({ onChange: function() { Backform.InputControl.prototype.onChange.apply(this, arguments); @@ -819,7 +850,7 @@ define('pgadmin.node.server', [ }), },{ id: 'db', label: gettext('Maintenance database'), type: 'text', group: gettext('Connection'), - mode: ['properties', 'edit', 'create'], readonly: 'isConnected', + mode: ['properties', 'edit', 'create'], readonly: 'isConnected',disabled: 'isShared', },{ id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'), mode: ['properties', 'edit', 'create'], @@ -1057,6 +1088,21 @@ define('pgadmin.node.server', [ mode: ['properties', 'edit', 'create'], readonly: 'isConnected', min: 0, }], + isVisible: function(model){ + var serverOwner = model.attributes.user_id; + if (!model.isNew() && serverOwner != current_user.id){ + return false; + } + return true; + + }, + isShared: function(model){ + var serverOwner = model.attributes.user_id; + if (!model.isNew() && serverOwner != current_user.id && model.attributes.shared){ + return true; + } + return false; + }, validate: function() { const validateModel = new modelValidation.ModelValidation(this); return validateModel.validate(); @@ -1153,7 +1199,36 @@ define('pgadmin.node.server', [ } }, }); + var connect_to_server = function(obj, data, tree, item, reconnect) { + // Open properties dialog in edit mode + const selectedTreeNode = tree.selected().length > 0 ? tree.selected() : tree.first(); + const selectedTreeNodeData = selectedTreeNode && selectedTreeNode.length === 1 ? tree.itemData(selectedTreeNode) : undefined; + var server_url = obj.generate_url(item, 'obj', data, true); + // Fetch the updated data + $.get(server_url) + .done(function(res) { + if (res.shared && _.isNull(res.username) && data.user_id != current_user.id){ + if (selectedTreeNodeData._type == 'server'){ + pgAdmin.Browser.Node.callbacks.show_obj_properties.call( + pgAdmin.Browser.Nodes[tree.itemData(item)._type], {action: 'edit'} + ); + data.is_connecting = false; + tree.unload(item); + tree.setInode(item); + tree.addIcon(item, {icon: 'icon-shared-server-not-connected'}); + }else{ + data.is_connecting = false; + tree.unload(item); + tree.setInode(item); + tree.addIcon(item, {icon: 'icon-shared-server-not-connected'}); + } + } + return; + }).always(function(){ + data.is_connecting = false; + }); + var wasConnected = reconnect || data.connected, onFailure = function( xhr, status, error, _node, _data, _tree, _item, _wasConnected @@ -1164,7 +1239,12 @@ define('pgadmin.node.server', [ // Let's not change the status of the tree node now. if (!_wasConnected) { tree.setInode(_item); - tree.addIcon(_item, {icon: 'icon-server-not-connected'}); + if (data.shared){ + tree.addIcon(_item, {icon: 'icon-shared-server-not-connected'}); + }else{ + tree.addIcon(_item, {icon: 'icon-server-not-connected'}); + } + } Alertify.pgNotifier('error', xhr, error, function(msg) { @@ -1321,7 +1401,11 @@ define('pgadmin.node.server', [ _tree.unload(_item); _tree.setInode(_item); _tree.removeIcon(_item); - _tree.addIcon(_item, {icon: 'icon-server-not-connected'}); + if (_data.shared){ + _tree.addIcon(_item, {icon: 'icon-shared-server-not-connected'}); + }else{ + _tree.addIcon(_item, {icon: 'icon-server-not-connected'}); + } obj.trigger('connect:cancelled', data._id, data.db, obj, _item, _data); pgBrowser.Events.trigger( 'pgadmin:server:connect:cancelled', data._id, _item, _data, obj @@ -1387,7 +1471,11 @@ define('pgadmin.node.server', [ }) .fail(function(xhr, status, error) { tree.setInode(item); - tree.addIcon(item, {icon: 'icon-server-not-connected'}); + if (data.shared){ + tree.addIcon(item, {icon: 'icon-shared-server-not-connected'}); + }else{ + tree.addIcon(item, {icon: 'icon-server-not-connected'}); + } Alertify.pgRespErrorNotify(xhr, error); }); }; diff --git a/web/pgadmin/browser/server_groups/servers/templates/css/servers.css b/web/pgadmin/browser/server_groups/servers/templates/css/servers.css index 93c4536d4..829c273aa 100644 --- a/web/pgadmin/browser/server_groups/servers/templates/css/servers.css +++ b/web/pgadmin/browser/server_groups/servers/templates/css/servers.css @@ -15,3 +15,21 @@ vertical-align: middle; height: 1.3em; } + +.icon-shared-server-not-connected { + background-image: url('{{ url_for('NODE-server.static', filename='img/sharedserverbad.svg') }}') !important; + background-repeat: no-repeat; + background-size: 20px !important; + align-content: center; + vertical-align: middle; + height: 1.3em; +} + +.icon-shared-server-not-connected { + background-image: url('{{ url_for('NODE-server.static', filename='img/sharedserverbad.svg') }}') !important; + background-repeat: no-repeat; + background-size: 20px !important; + align-content: center; + vertical-align: middle; + height: 1.3em; +} diff --git a/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json b/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json new file mode 100644 index 000000000..117a2982b --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json @@ -0,0 +1,862 @@ +{ + "add_server": [ + { + "name": "Add server with service id", + "url": "/browser/server/obj/", + "is_positive_test": true, + "owner_server": true, + "test_data": { + "service": "TestDB" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Test default server url", + "url": "/browser/server/obj/", + "is_positive_test": true, + "owner_server": true, + "test_data": { + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Add server with connect timeout", + "url": "/browser/server/obj/", + "is_positive_test": true, + "test_data": { + "connect_timeout": 5 + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Add server using SSH tunnel with password", + "url": "/browser/server/obj/", + "is_positive_test": true, + "ssh_tunnel": true, + "with_password": true, + "save_password": false, + "test_data": { + "use_ssh_tunnel": 1, + "tunnel_host": "127.0.0.1", + "tunnel_port": 22, + "tunnel_username": "user", + "tunnel_authentication": 1, + "tunnel_identity_file": "pkey_rsa" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Add server using SSH tunnel with identity file", + "url": "/browser/server/obj/", + "is_positive_test": true, + "ssh_tunnel": true, + "with_password": false, + "save_password": false, + "test_data": { + "use_ssh_tunnel": 1, + "tunnel_host": "127.0.0.1", + "tunnel_port": 22, + "tunnel_username": "user", + "tunnel_authentication": 1, + "tunnel_identity_file": "pkey_rsa" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Add server using SSH tunnel with password and saved it", + "url": "/browser/server/obj/", + "is_positive_test": true, + "ssh_tunnel": true, + "with_password": true, + "save_password": true, + "test_data": { + "use_ssh_tunnel": 1, + "tunnel_host": "127.0.0.1", + "tunnel_port": 22, + "tunnel_username": "user", + "tunnel_authentication": 0, + "tunnel_password": "123456" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Add server using SSH tunnel with identity file and save the password", + "url": "/browser/server/obj/", + "is_positive_test": true, + "ssh_tunnel": true, + "with_password": false, + "save_password": true, + "test_data": { + "use_ssh_tunnel": 1, + "tunnel_host": "127.0.0.1", + "tunnel_port": 22, + "tunnel_username": "user", + "tunnel_authentication": 1, + "tunnel_identity_file": "pkey_rsa", + "tunnel_password": "123456" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Add server with password and save password to true", + "url": "/browser/server/obj/", + "is_positive_test": true, + "with_pwd": true, + "with_save": true, + "owner_server": true, + "test_data": { + "service": "TestDB" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Add server with password and save password to false", + "url": "/browser/server/obj/", + "is_positive_test": true, + "with_pwd": true, + "with_save": false, + "owner_server": true, + "test_data": { + "service": "TestDB" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Add server without password and save password to true", + "url": "/browser/server/obj/", + "is_positive_test": true, + "with_pwd": false, + "with_save": true, + "owner_server": true, + "test_data": { + "service": "TestDB" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Add server with connect now", + "url": "/browser/server/obj/", + "is_positive_test": true, + "owner_server": true, + "test_data": { + "service": "TestDB" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + } + ], + "is_password_saved": [ + { + "name": "Connect server with 'save password", + "url": "/browser/server/connect/", + "is_positive_test": true, + "test_data": { + "is_password_saved": true + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200, + "message": "Server connected." + } + } + ], + "get_server": [ + { + "name": "Get a server URL", + "url": "/browser/server/obj/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Reload a server configuration", + "url": "/browser/server/reload/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get a server URL using wrong server id", + "url": "/browser/server/obj/", + "is_positive_test": true, + "incorrect_server_id": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 410 + } + }, + { + "name": "Get a server Node dependants", + "url": "/browser/server/dependent/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get a server Node dependency", + "url": "/browser/server/dependency/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get a server Node sql", + "url": "/browser/server/sql/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get a server Node msql", + "url": "/browser/server/msql/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get a server Node statistics", + "url": "/browser/server/stats/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get a server pgpass details", + "url": "/browser/server/check_pgpass/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 410 + } + } + ], + "get_shared_server": [ + { + "name": "Get a shared server", + "url": "/browser/server/obj/", + "is_positive_test": true, + "shared": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get a all shared server", + "url": "/browser/server/obj/", + "is_positive_test": true, + "shared": true, + "no_server_id": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get the all available shared server", + "url": "/browser/server/obj/", + "is_positive_test": true, + "no_server_id": true, + "shared": true, + "server_list": true, + "mocking_required": false, + "mock_data": { + }, + "expected_data": { + "status_code": 200 + } + } + ], + "get_all_server": [ + { + "name": "Get the all children of server", + "url": "/browser/server/children/", + "is_positive_test": true, + "children": true, + "mocking_required": false, + "mock_data": { + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "Get the all available servers", + "url": "/browser/server/nodes/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": { + }, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get the all available server of server group", + "url": "/browser/server/nodes/", + "is_positive_test": true, + "server_list": true, + "mocking_required": false, + "mock_data": { + }, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get the all available server of server group", + "url": "/browser/server/nodes/", + "is_positive_test": true, + "server_list": true, + "servers": true, + "mocking_required": false, + "mock_data": { + }, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Get the all connected servers", + "url": "/browser/server/nodes/", + "is_positive_test": true, + "server_list": true, + "servers": true, + "connected": true, + "mocking_required": false, + "mock_data": { + }, + "expected_data": { + "status_code": 200 + } + } + ], + "connect_server": [ + { + "name": "Get a server connection", + "url": "/browser/server/connect/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "connect to a server using password", + "url": "/browser/server/connect/", + "is_positive_test": true, + "connect": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Disconnect server test", + "url": "/browser/server/connect/", + "is_positive_test": true, + "disconnect": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Disconnect server when wrong server id passed", + "url": "/browser/server/connect/", + "is_positive_test": true, + "disconnect": true, + "wrong_server_id": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 400 + } + }, + { + "name": "Reload a server configuration", + "url": "/browser/server/reload/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while creating server restore point", + "url": "/browser/server/restore_point/", + "is_positive_test": true, + "restore_point": true, + "test_data": { + "Named restore point created": "PLACE_HOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 500 + } + } + ], + "delete_server": [ + { + "name": "Delete a server URL", + "url": "/browser/server/obj/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Disconnect server test", + "url": "/browser/server/connect/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Error while fetching a server to delete", + "url": "/browser/server/obj/", + "is_positive_test": false, + "mocking_required": true, + "mock_data": { + "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict", + "return_value": "(True, 'Mocked Internal Server Error')" + }, + "expected_data": { + "status_code": 500 + } + }, + { + "name": "server not found while deleting a server", + "url": "/browser/server/obj/", + "is_positive_test": true, + "invalid_server_id": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 410 + } + } + ], + "update_server": [ + { + "name": "update a server name", + "url": "/browser/server/obj/", + "is_positive_test": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "sslcompression": 1 + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "update a server details without data", + "url": "/browser/server/obj/", + "is_positive_test": true, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Update server with wrong server id", + "url": "/browser/server/obj/", + "is_positive_test": false, + "clear_save_password": true, + "wrong_server_id": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 410 + } + }, + { + "name": "Update server with incorrect hostaddr", + "url": "/browser/server/obj/", + "is_positive_test": true, + "test_data": { + "comment": "PLACE_HOLDER", + "hostaddr": "PLACE_HOLDER", + "db_res": "PLACE_HOLDER", + "id": "PLACE_HOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 400 + } + }, + { + "name": "update a server , make server shared", + "url": "/browser/server/obj/", + "is_positive_test": true, + "owner_server": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "shared": true + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Clear saved password", + "url": "/browser/server/clear_saved_password/", + "is_positive_test": true, + "clear_save_password": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "is_password_saved": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Clear saved password with wrong server id", + "url": "/browser/server/clear_saved_password/", + "is_positive_test": false, + "clear_save_password": true, + "wrong_server_id": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "is_password_saved": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "wal replay", + "url": "/browser/server/wal_replay/", + "is_positive_test": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "is_password_saved": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 410 + } + }, + { + "name": "Clear ssh tunnel password", + "url": "/browser/server/clear_sshtunnel_password/", + "is_positive_test": true, + "clear_save_password": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "is_password_saved": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Clear ssh tunnel password with wrong server id", + "url": "/browser/server/clear_sshtunnel_password/", + "is_positive_test": false, + "clear_save_password": true, + "wrong_server_id": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "is_password_saved": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "error while clearing a ssh password", + "url": "/browser/server/clear_sshtunnel_password/", + "is_positive_test": false, + "error_clearing_password": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "is_password_saved": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + } + ], + "update_shared_server": [ + { + "name": "update a server name", + "url": "/browser/server/obj/", + "is_positive_test": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "sslcompression": 1 + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "update a server details without data", + "url": "/browser/server/obj/", + "is_positive_test": true, + "test_data": {}, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Update server with wrong server id", + "url": "/browser/server/obj/", + "is_positive_test": false, + "clear_save_password": true, + "wrong_server_id": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 410 + } + }, + { + "name": "Update server with incorrect hostaddr", + "url": "/browser/server/obj/", + "is_positive_test": true, + "test_data": { + "comment": "PLACE_HOLDER", + "hostaddr": "PLACE_HOLDER", + "db_res": "PLACE_HOLDER", + "id": "PLACE_HOLDER" + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 400 + } + }, + { + "name": "update a server , make server shared", + "url": "/browser/server/obj/", + "is_positive_test": true, + "owner_server": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "shared": true + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Clear saved password when login user is not owner of server", + "url": "/browser/server/clear_saved_password/", + "is_positive_test": true, + "clear_save_password": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "is_password_saved": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Clear saved password with wrong server id", + "url": "/browser/server/clear_saved_password/", + "is_positive_test": false, + "clear_save_password": true, + "wrong_server_id": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "is_password_saved": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Clear ssh tunnel password", + "url": "/browser/server/clear_sshtunnel_password/", + "is_positive_test": true, + "clear_save_password": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "is_password_saved": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "Clear ssh tunnel password with wrong server id", + "url": "/browser/server/clear_sshtunnel_password/", + "is_positive_test": false, + "clear_save_password": true, + "wrong_server_id": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "is_password_saved": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + }, + { + "name": "error while clearing a ssh password", + "url": "/browser/server/clear_sshtunnel_password/", + "is_positive_test": false, + "error_clearing_password": true, + "test_data": { + "comment": "PLACE_HOLDER", + "id": "PLACE_HOLDER", + "is_password_saved": false + }, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + } + ], + "delete_multiple_server": [ + { + "name": "Delete multiple server", + "url": "/browser/server/obj/", + "is_positive_test": true, + "mocking_required": false, + "mock_data": {}, + "expected_data": { + "status_code": 200 + } + } + ] +} diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_add_server.py b/web/pgadmin/browser/server_groups/servers/tests/test_add_server.py new file mode 100644 index 000000000..1b23fcdb8 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/tests/test_add_server.py @@ -0,0 +1,84 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json + +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from . import utils as servers_utils + + +class AddServerTest(BaseTestGenerator): + """ This class will add the servers under default server group. """ + + scenarios = utils.generate_scenarios('add_server', + servers_utils.test_cases) + + def setUp(self): + pass + + def create_server(self, url): + return self.tester.post( + url, + data=json.dumps(self.server), + content_type='html/json' + ) + + def runTest(self): + """ This function will add the server under default server group.""" + url = "{0}{1}/".format(self.url, utils.SERVER_GROUP) + + # Add service name in the config + if 'connect_timeout' in self.test_data: + self.server['connect_timeout'] = self.test_data['connect_timeout'] + elif 'shared' in self.test_data: + self.server['shared'] = self.test_data['shared'] + elif 'service' in self.test_data: + self.server['service'] = self.test_data['service'] + + if hasattr(self, 'ssh_tunnel'): + self.server['use_ssh_tunnel'] = self.test_data['use_ssh_tunnel'] + self.server['tunnel_host'] = self.test_data['tunnel_host'] + self.server['tunnel_port'] = self.test_data['tunnel_port'] + self.server['tunnel_username'] = self.test_data['tunnel_username'] + + if self.with_password: + self.server['tunnel_authentication'] = self.test_data[ + 'tunnel_authentication'] + else: + self.server['tunnel_authentication'] = 1 + self.server['tunnel_identity_file'] = 'pkey_rsa' + + if self.save_password: + self.server['tunnel_password'] = self.test_data[ + 'tunnel_password'] + if 'connect_now' in self.test_data: + self.server['connect_now'] = self.test_data['connect_now'] + self.server['password'] = self.server['db_password'] + + if self.is_positive_test: + if hasattr(self, 'with_save'): + self.server['save_password'] = self.with_save + if hasattr(self, 'with_pwd') and not self.with_pwd: + # Remove the password from server object + db_password = self.server['db_password'] + del self.server['db_password'] + response = self.create_server(url) + self.assertEquals(response.status_code, + self.expected_data["status_code"]) + response_data = json.loads(response.data.decode('utf-8')) + self.server_id = response_data['node']['_id'] + + if hasattr(self, 'with_pwd') and not self.with_pwd: + # Remove the password from server object + self.server['db_password'] = db_password + + def tearDown(self): + """This function delete the server from SQLite """ + utils.delete_server_with_api(self.tester, self.server_id) diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_connect_timeout.py b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_connect_timeout.py deleted file mode 100644 index 94d555617..000000000 --- a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_connect_timeout.py +++ /dev/null @@ -1,47 +0,0 @@ -########################################################################## -# -# pgAdmin 4 - PostgreSQL Tools -# -# Copyright (C) 2013 - 2020, The pgAdmin Development Team -# This software is released under the PostgreSQL Licence -# -########################################################################## - -import json - -from pgadmin.utils.route import BaseTestGenerator -from regression.python_test_utils import test_utils as utils - - -class ServersWithConnectTimeoutAddTestCase(BaseTestGenerator): - """ This class will add the servers under default server group. """ - - scenarios = [ - # Fetch the default url for server object - ( - 'Default Server Node url', dict( - url='/browser/server/obj/' - ) - ) - ] - - def setUp(self): - pass - - def runTest(self): - """ This function will add the server under default server group.""" - url = "{0}{1}/".format(self.url, utils.SERVER_GROUP) - # Add service name in the config - self.server['connect_timeout'] = 5 - response = self.tester.post( - url, - data=json.dumps(self.server), - content_type='html/json' - ) - self.assertEqual(response.status_code, 200) - response_data = json.loads(response.data.decode('utf-8')) - self.server_id = response_data['node']['_id'] - - def tearDown(self): - """This function delete the server from SQLite """ - utils.delete_server_with_api(self.tester, self.server_id) diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py deleted file mode 100644 index 9b4d4d377..000000000 --- a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py +++ /dev/null @@ -1,47 +0,0 @@ -########################################################################## -# -# pgAdmin 4 - PostgreSQL Tools -# -# Copyright (C) 2013 - 2020, The pgAdmin Development Team -# This software is released under the PostgreSQL Licence -# -########################################################################## - -import json - -from pgadmin.utils.route import BaseTestGenerator -from regression.python_test_utils import test_utils as utils - - -class ServersWithServiceIDAddTestCase(BaseTestGenerator): - """ This class will add the servers under default server group. """ - - scenarios = [ - # Fetch the default url for server object - ( - 'Default Server Node url', dict( - url='/browser/server/obj/' - ) - ) - ] - - def setUp(self): - pass - - def runTest(self): - """ This function will add the server under default server group.""" - url = "{0}{1}/".format(self.url, utils.SERVER_GROUP) - # Add service name in the config - self.server['service'] = "TestDB" - response = self.tester.post( - url, - data=json.dumps(self.server), - content_type='html/json' - ) - self.assertEqual(response.status_code, 200) - response_data = json.loads(response.data.decode('utf-8')) - self.server_id = response_data['node']['_id'] - - def tearDown(self): - """This function delete the server from SQLite """ - utils.delete_server_with_api(self.tester, self.server_id) diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_ssh_tunnel.py b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_ssh_tunnel.py deleted file mode 100644 index 7c0b005d9..000000000 --- a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_ssh_tunnel.py +++ /dev/null @@ -1,82 +0,0 @@ -########################################################################## -# -# pgAdmin 4 - PostgreSQL Tools -# -# Copyright (C) 2013 - 2020, The pgAdmin Development Team -# This software is released under the PostgreSQL Licence -# -########################################################################## - -import json - -from pgadmin.utils.route import BaseTestGenerator -from regression.python_test_utils import test_utils as utils - - -class ServersWithSSHTunnelAddTestCase(BaseTestGenerator): - """ This class will add the servers under default server group. """ - - scenarios = [ - ( - 'Add server using SSH tunnel with password', dict( - url='/browser/server/obj/', - with_password=True, - save_password=False, - ) - ), - ( - 'Add server using SSH tunnel with identity file', dict( - url='/browser/server/obj/', - 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, - ) - ), - ] - - def setUp(self): - pass - - def runTest(self): - """ This function will add the server under default server group.""" - url = "{0}{1}/".format(self.url, utils.SERVER_GROUP) - # Add service name in the config - self.server['use_ssh_tunnel'] = 1 - self.server['tunnel_host'] = '127.0.0.1' - self.server['tunnel_port'] = 22 - self.server['tunnel_username'] = 'user' - if self.with_password: - self.server['tunnel_authentication'] = 0 - else: - 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), - content_type='html/json' - ) - self.assertEqual(response.status_code, 200) - response_data = json.loads(response.data.decode('utf-8')) - self.server_id = response_data['node']['_id'] - - def tearDown(self): - """This function delete the server from SQLite """ - utils.delete_server_with_api(self.tester, self.server_id) diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_all_server_get.py b/web/pgadmin/browser/server_groups/servers/tests/test_all_server_get.py new file mode 100644 index 000000000..cd35a7a05 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/tests/test_all_server_get.py @@ -0,0 +1,78 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## +import random + +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from . import utils as servers_utils + + +class AllServersGetTestCase(BaseTestGenerator): + """ + This class will fetch added servers under default server group + by response code. + """ + + scenarios = utils.generate_scenarios('get_all_server', + servers_utils.test_cases) + + def setUp(self): + """This function add the server to test the GET API""" + self.server['password'] = 'edb' + self.server_id = utils.create_server(self.server) + server_dict = {"server_id": self.server_id} + utils.write_node_info("sid", server_dict) + + def get_server(self): + return self.tester.get(self.url, follow_redirects=True) + + def connect_to_server(self, url): + return self.tester.post( + url, + data=self.server, + content_type='html/json' + ) + + def runTest(self): + """ This function will fetch the added servers to object browser. """ + server_id = parent_node_dict["server"][-1]["server_id"] + if not server_id: + raise Exception("Server not found to test GET API") + response = None + if self.is_positive_test: + if hasattr(self, 'invalid_server_group'): + self.url = self.url + '{0}/{1}?_={1}'.format( + utils.SERVER_GROUP, random.randint(1, 9999999)) + elif hasattr(self, 'children'): + + self.url = self.url + '{0}/{1}'.format( + utils.SERVER_GROUP, server_id) + elif hasattr(self, 'server_list'): + if hasattr(self, 'servers'): + server_id = '' + self.url = self.url + '{0}/{1}'.format( + utils.SERVER_GROUP, server_id) + else: + if hasattr(self, "connected"): + url = '/browser/server/connect/' + '{0}/{1}'.format( + utils.SERVER_GROUP, + self.server_id) + self.server['password'] = 'edb' + + self.connect_to_server(url) + self.url = self.url + '{0}/{1}?_={2}'.format( + utils.SERVER_GROUP, server_id, random.randint(1, 9999999)) + response = self.get_server() + self.assertEquals(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + """This function delete the server from SQLite """ + utils.delete_server_with_api(self.tester, self.server_id) diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_check_connect.py b/web/pgadmin/browser/server_groups/servers/tests/test_check_connect.py new file mode 100644 index 000000000..6d49cea7b --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/tests/test_check_connect.py @@ -0,0 +1,107 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from . import utils as servers_utils +import json + + +class ServersConnectTestCase(BaseTestGenerator): + """ + This class will fetch added servers under default server group + by response code. + """ + + scenarios = utils.generate_scenarios('connect_server', + servers_utils.test_cases) + + def get_ssh_tunnel(self): + print("in_get_ssh") + self.server['use_ssh_tunnel'] = 1 + self.server['tunnel_host'] = '127.0.0.1' + self.server['tunnel_port'] = 22 + self.server['tunnel_username'] = 'user' + if self.with_password: + self.server['tunnel_authentication'] = 0 + else: + self.server['tunnel_authentication'] = 1 + self.server['tunnel_identity_file'] = 'pkey_rsa' + + if self.save_password: + self.server['tunnel_password'] = '123456' + + def setUp(self): + """This function add the server to test the GET API""" + + self.server_id = utils.create_server(self.server) + server_dict = {"server_id": self.server_id} + utils.write_node_info("sid", server_dict) + + def get_server_connection(self, server_id): + return self.tester.get(self.url + str(utils.SERVER_GROUP) + '/' + + str(server_id), + follow_redirects=True) + + def server_disonnect(self, server_id): + return self.tester.delete(self.url + str(utils.SERVER_GROUP) + '/' + + str(server_id)) + + def connect_to_server(self, url): + return self.tester.post( + url, + data=json.dumps(self.server), + content_type='html/json' + ) + + def add_server_details(self, url): + return self.tester.post( + url, + data=str(self.test_data), + content_type='html/json' + ) + + def runTest(self): + """ This function will fetch the added servers to object browser. """ + server_id = parent_node_dict["server"][-1]["server_id"] + if not server_id: + raise Exception("Server not found to test GET API") + response = None + if self.is_positive_test: + if hasattr(self, 'disconnect'): + if hasattr(self, 'wrong_server_id'): + server_id = 99999 + response = self.server_disonnect(server_id) + elif hasattr(self, "connect"): + url = self.url + '{0}/{1}'.format( + utils.SERVER_GROUP, + self.server_id) + self.server['password'] = self.server['db_password'] + response = self.connect_to_server(url) + elif hasattr(self, 'restore_point') or hasattr(self, + 'change_password'): + connect_url = '/browser/server/connect/{0}/{1}'.format( + utils.SERVER_GROUP, + self.server_id) + url = self.url + '{0}/{1}'.format( + utils.SERVER_GROUP, + self.server_id) + + self.connect_to_server(connect_url) + response = self.add_server_details(url) + else: + response = self.get_server_connection(server_id) + + self.assertEquals(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + """This function delete the server from SQLite """ + utils.delete_server_with_api(self.tester, self.server_id) diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_is_password_saved.py b/web/pgadmin/browser/server_groups/servers/tests/test_is_password_saved.py new file mode 100644 index 000000000..7d144d3d7 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/tests/test_is_password_saved.py @@ -0,0 +1,52 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +import json + +from pgadmin.utils.route import BaseTestGenerator +from regression.python_test_utils import test_utils as utils +from . import utils as servers_utils + + +class IsPasswordSaved(BaseTestGenerator): + """ This class will test the save password functionality. """ + + scenarios = utils.generate_scenarios('is_password_saved', + servers_utils.test_cases) + + def setUp(self): + self.server_id = utils.create_server(self.server) + server_dict = {"server_id": self.server_id} + utils.write_node_info("sid", server_dict) + + def runTest(self): + """This function will execute the connect server APIs""" + response = self.tester.post( + self.url + str(utils.SERVER_GROUP) + '/' + str(self.server_id), + data=dict( + password=self.server['db_password'], + save_password='on'), + follow_redirects=True) + + expected_status_code = self.expected_data["status_code"] + actual_status_code = response.status_code + self.assertEquals(actual_status_code, expected_status_code) + response_data = json.loads(response.data.decode('utf-8')) + + expected_message = self.expected_data["message"] + actual_message = response_data["info"] + self.assertEquals(actual_message, expected_message) + + expected_is_password_saved = self.test_data["is_password_saved"] + actual_is_password_saved = response_data["data"]["is_password_saved"] + self.assertEquals(actual_is_password_saved, expected_is_password_saved) + + def tearDown(self): + """This function delete the server from SQLite """ + utils.delete_server_with_api(self.tester, self.server_id) diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_server_add.py b/web/pgadmin/browser/server_groups/servers/tests/test_server_add.py deleted file mode 100644 index 8444e1d72..000000000 --- a/web/pgadmin/browser/server_groups/servers/tests/test_server_add.py +++ /dev/null @@ -1,86 +0,0 @@ -########################################################################## -# -# pgAdmin 4 - PostgreSQL Tools -# -# Copyright (C) 2013 - 2020, The pgAdmin Development Team -# This software is released under the PostgreSQL Licence -# -########################################################################## - -import json -import copy -from pgadmin.utils.route import BaseTestGenerator -from regression.python_test_utils import test_utils as utils - - -class ServersAddTestCase(BaseTestGenerator): - """ This class will add the servers under default server group. """ - - scenarios = [ - # Fetch the default url for server object - ('Default Server Node url', dict(url='/browser/server/obj/')) - ] - - def setUp(self): - pass - - def runTest(self): - """ This function will add the server under default server group.""" - url = "{0}{1}/".format(self.url, utils.SERVER_GROUP) - response = self.tester.post(url, data=json.dumps(self.server), - content_type='html/json') - self.assertEqual(response.status_code, 200) - response_data = json.loads(response.data.decode('utf-8')) - self.server_id = response_data['node']['_id'] - server_dict = {"server_id": int(self.server_id)} - utils.write_node_info("sid", server_dict) - - def tearDown(self): - """This function delete the server from SQLite """ - utils.delete_server_with_api(self.tester, self.server_id) - - -class AddServersWithSavePasswordTestCase(BaseTestGenerator): - """ This class will add the servers under default server group. """ - - scenarios = [ - # Fetch the default url for server object - ('Add server with password and save password to true', - dict(url='/browser/server/obj/', with_pwd=True, with_save=True)), - ('Add server with password and save password to false', - dict(url='/browser/server/obj/', with_pwd=True, with_save=False)), - ('Add server without password and save password to true', - dict(url='/browser/server/obj/', with_pwd=False, with_save=True)), - ] - - def setUp(self): - pass - - def runTest(self): - """ This function will add the server under default server group.""" - url = "{0}{1}/".format(self.url, utils.SERVER_GROUP) - _server = copy.deepcopy(self.server) - # Update the flag as required - _server['save_password'] = self.with_save - if not self.with_pwd: - # Remove the password from server object - del _server['db_password'] - - response = self.tester.post(url, data=json.dumps(_server), - content_type='html/json') - self.assertEqual(response.status_code, 200) - response_data = json.loads(response.data.decode('utf-8')) - self.server_id = response_data['node']['_id'] - server_dict = {"server_id": int(self.server_id)} - # Fetch the node info to check if password was saved or not - response = self.tester.get(self.url.replace('obj', 'nodes') + - str(utils.SERVER_GROUP) + '/' + - str(self.server_id), - follow_redirects=True) - self.assertEqual(response.status_code, 200) - self.assertTrue('is_password_saved' in response.json['result']) - utils.write_node_info("sid", server_dict) - - def tearDown(self): - """This function delete the server from SQLite """ - utils.delete_server_with_api(self.tester, self.server_id) diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_server_delete.py b/web/pgadmin/browser/server_groups/servers/tests/test_server_delete.py index 30917bd3c..78911f5a8 100644 --- a/web/pgadmin/browser/server_groups/servers/tests/test_server_delete.py +++ b/web/pgadmin/browser/server_groups/servers/tests/test_server_delete.py @@ -9,15 +9,14 @@ from pgadmin.utils.route import BaseTestGenerator from regression.python_test_utils import test_utils as utils +from . import utils as servers_utils class ServerDeleteTestCase(BaseTestGenerator): """ This class will delete the last server present under tree node.""" - scenarios = [ - # Fetching the default url for server node - ('Default Server Node url', dict(url='/browser/server/obj/')) - ] + scenarios = utils.generate_scenarios('delete_server', + servers_utils.test_cases) def setUp(self): """This function add the server to test the DELETE API""" diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_server_get.py b/web/pgadmin/browser/server_groups/servers/tests/test_server_get.py index 0b4dc183b..5a83e2a08 100644 --- a/web/pgadmin/browser/server_groups/servers/tests/test_server_get.py +++ b/web/pgadmin/browser/server_groups/servers/tests/test_server_get.py @@ -10,6 +10,8 @@ from pgadmin.utils.route import BaseTestGenerator from regression import parent_node_dict from regression.python_test_utils import test_utils as utils +from . import utils as servers_utils +import json class ServersGetTestCase(BaseTestGenerator): @@ -18,26 +20,50 @@ class ServersGetTestCase(BaseTestGenerator): by response code. """ - scenarios = [ - # Fetch the default url for server node - ('Default Server Node url', dict(url='/browser/server/obj/')) - ] + scenarios = utils.generate_scenarios('get_server', + servers_utils.test_cases) def setUp(self): """This function add the server to test the GET API""" - self.server_id = utils.create_server(self.server) + if hasattr(self, 'shared'): + + self.server['shared'] = True + url = "{0}{1}/".format(self.url, utils.SERVER_GROUP) + response = self.tester.post( + url, + data=json.dumps(self.server), + content_type='html/json' + ) + response_data = json.loads(response.data.decode('utf-8')) + self.server_id = response_data['node']['_id'] + else: + self.server_id = utils.create_server(self.server) server_dict = {"server_id": self.server_id} utils.write_node_info("sid", server_dict) + def get_server(self, server_id): + return self.tester.get(self.url + str(utils.SERVER_GROUP) + '/' + + str(server_id), + follow_redirects=True) + def runTest(self): """ This function will fetch the added servers to object browser. """ server_id = parent_node_dict["server"][-1]["server_id"] if not server_id: raise Exception("Server not found to test GET API") - response = self.tester.get(self.url + str(utils.SERVER_GROUP) + '/' + - str(server_id), - follow_redirects=True) - self.assertEqual(response.status_code, 200) + response = None + if self.is_positive_test: + if hasattr(self, "incorrect_server_id"): + server_id = 9999 + if hasattr(self, "server_list"): + server_id = '' + if hasattr(self, "server_node"): + server_id = '' + if hasattr(self, 'shared'): + server_id = self.server_id + response = self.get_server(server_id) + self.assertEquals(response.status_code, + self.expected_data["status_code"]) def tearDown(self): """This function delete the server from SQLite """ diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_server_put.py b/web/pgadmin/browser/server_groups/servers/tests/test_server_put.py index 31c7a476e..74674c7cb 100644 --- a/web/pgadmin/browser/server_groups/servers/tests/test_server_put.py +++ b/web/pgadmin/browser/server_groups/servers/tests/test_server_put.py @@ -11,32 +11,61 @@ import json from pgadmin.utils.route import BaseTestGenerator from regression.python_test_utils import test_utils as utils +from . import utils as servers_utils class ServerUpdateTestCase(BaseTestGenerator): """ This class will update server's comment field. """ - scenarios = [ - # Fetching the default url for server node - ('Default Server Node url', dict(url='/browser/server/obj/')) - ] + scenarios = utils.generate_scenarios('update_server', + servers_utils.test_cases) def setUp(self): """This function add the server to test the PUT API""" - self.server_id = utils.create_server(self.server) + if hasattr(self, 'clear_save_password'): + self.server['save_password'] = 1 + create_server_url = "/browser/server/obj/{0}/".format( + utils.SERVER_GROUP) + + self.server_id = \ + servers_utils.create_server_with_api(self, create_server_url) server_dict = {"server_id": self.server_id} utils.write_node_info("sid", server_dict) + def update_server(self): + return self.tester.put( + self.url + str(utils.SERVER_GROUP) + '/' + + str(self.server_id), data=json.dumps(self.test_data), + content_type='html/json') + + def connect_to_server(self, url): + return self.tester.post( + url, + data=json.dumps(self.server), + content_type='html/json' + ) + def runTest(self): """This function update the server details""" if not self.server_id: raise Exception("No server to update.") - data = {"comment": self.server['comment'], "id": self.server_id} - put_response = self.tester.put( - self.url + str(utils.SERVER_GROUP) + '/' + - str(self.server_id), data=json.dumps(data), - content_type='html/json') - self.assertEqual(put_response.status_code, 200) + if 'comment' in self.test_data: + self.test_data["comment"] = self.server['comment'] + self.test_data["id"] = self.server_id + if self.is_positive_test: + if hasattr(self, 'server_connected'): + url = '/browser/server/connect/{0}/{1}'.format( + utils.SERVER_GROUP, + self.server_id) + self.server['password'] = self.server['db_password'] + self.connect_to_server(url) + put_response = self.update_server() + else: + if hasattr(self, 'wrong_server_id'): + self.server_id = 9999 + put_response = self.update_server() + self.assertEquals(put_response.status_code, + self.expected_data["status_code"]) def tearDown(self): """This function delete the server from SQLite""" diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_shared_server.py b/web/pgadmin/browser/server_groups/servers/tests/test_shared_server.py new file mode 100644 index 000000000..b3f8215a4 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/tests/test_shared_server.py @@ -0,0 +1,134 @@ +########################################################################## +# +# pgAdmin 4 - PostgreSQL Tools +# +# Copyright (C) 2013 - 2020, The pgAdmin Development Team +# This software is released under the PostgreSQL Licence +# +########################################################################## + +from pgadmin.utils.route import BaseTestGenerator +from regression import parent_node_dict +from regression.python_test_utils import test_utils as utils +from . import utils as servers_utils +import json +from regression.test_setup import config_data +from regression.python_test_utils.test_utils import \ + create_user_wise_test_client +import config + +test_user_details = config_data[ + 'pgAdmin4_test_non_admin_credentials'] + + +class SharedServersGetTestCase(BaseTestGenerator): + """ + This class will fetch added servers under default server group + by response code. + """ + + scenarios = utils.generate_scenarios('get_shared_server', + servers_utils.test_cases) + + def setUp(self): + """This function add the server to test the GET API""" + + if config.SERVER_MODE is False: + self.skipTest( + "Can not run shared servers test cases in the SERVER mode." + ) + self.server['shared'] = True + url = "{0}{1}/".format(self.url, utils.SERVER_GROUP) + response = self.tester.post( + url, + data=json.dumps(self.server), + content_type='html/json' + ) + response_data = json.loads(response.data.decode('utf-8')) + self.server_id = response_data['node']['_id'] + + server_dict = {"server_id": self.server_id} + utils.write_node_info("sid", server_dict) + + def get_server(self, server_id): + return self.tester.get(self.url + str(utils.SERVER_GROUP) + '/' + + str(server_id), + follow_redirects=True) + + @create_user_wise_test_client(test_user_details) + def runTest(self): + """ This function will fetch the added servers to object browser. """ + if not self.server_id: + raise Exception("Server not found to test GET API") + response = None + if self.is_positive_test: + if hasattr(self, 'no_server_id'): + if hasattr(self, 'server_list'): + self.url = '/browser/server/nodes/' + server_id = '' + response = self.get_server(server_id) + else: + response = self.get_server(self.server_id) + self.assertEquals(response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + """This function delete the server from SQLite """ + utils.delete_server_with_api(self.tester, self.server_id) + + +class SharedServerUpdateTestCase(BaseTestGenerator): + """ This class will update server's comment field. """ + + scenarios = utils.generate_scenarios('update_shared_server', + servers_utils.test_cases) + + def setUp(self): + """This function add the server to test the PUT API""" + if config.SERVER_MODE is False: + self.skipTest( + "Can not run shared servers test cases in the Desktop mode." + ) + self.server['shared'] = True + if hasattr(self, 'clear_save_password'): + self.server['save_password'] = 1 + create_server_url = "/browser/server/obj/{0}/".format( + utils.SERVER_GROUP) + + self.server_id = \ + servers_utils.create_server_with_api(self, create_server_url) + server_dict = {"server_id": self.server_id} + utils.write_node_info("sid", server_dict) + + def update_server(self): + return self.tester.put( + self.url + str(utils.SERVER_GROUP) + '/' + + str(self.server_id), data=json.dumps(self.test_data), + content_type='html/json') + + def connect_to_server(self, url): + return self.tester.post( + url, + data=json.dumps(self.server), + content_type='html/json' + ) + + def runTest(self): + """This function update the server details""" + if not self.server_id: + raise Exception("No server to update.") + if 'comment' in self.test_data: + self.test_data["comment"] = self.server['comment'] + self.test_data["id"] = self.server_id + if self.is_positive_test: + put_response = self.update_server() + else: + if hasattr(self, 'wrong_server_id'): + self.server_id = 9999 + put_response = self.update_server() + self.assertEquals(put_response.status_code, + self.expected_data["status_code"]) + + def tearDown(self): + """This function delete the server from SQLite""" + utils.delete_server_with_api(self.tester, self.server_id) diff --git a/web/pgadmin/browser/server_groups/servers/tests/utils.py b/web/pgadmin/browser/server_groups/servers/tests/utils.py new file mode 100644 index 000000000..8680b29fb --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/tests/utils.py @@ -0,0 +1,52 @@ +import os +import json +import sqlite3 +import config +from regression.python_test_utils import test_utils as utils + + +CURRENT_PATH = os.path.dirname(os.path.realpath(__file__)) +with open(CURRENT_PATH + "/servers_test_data.json") as data_file: + test_cases = json.load(data_file) + + +def create_server(server, SERVER_GROUP): + """This function is used to create server""" + try: + conn = sqlite3.connect(config.TEST_SQLITE_PATH) + # Create the server + cur = conn.cursor() + if 'shared' not in server: + server['shared'] = False + server_details = (1, SERVER_GROUP, server['name'], server['host'], + server['port'], server['db'], server['username'], + server['role'], server['sslmode'], server['comment'], + server['shared']) + cur.execute('INSERT INTO server (user_id, servergroup_id, name, host, ' + 'port, maintenance_db, username, role, ssl_mode,' + ' comment, shared) VALUES (?,?,?,?,?,?,?,?,?,?,?)', + server_details) + server_id = cur.lastrowid + conn.commit() + conn.close() + + type = utils.get_server_type(server) + server['type'] = type + + return server_id + except Exception as exception: + raise Exception("Error while creating server. %s" % exception) + + +def create_server_with_api(self, url): + try: + response = self.tester.post( + url, + data=json.dumps(self.server), + content_type='html/json' + ) + response_data = json.loads(response.data.decode('utf-8')) + server_id = response_data['node']['_id'] + return server_id + except Exception as exception: + raise Exception("Error while creating server. %s" % exception) diff --git a/web/pgadmin/browser/server_groups/static/img/server_group_shared.svg b/web/pgadmin/browser/server_groups/static/img/server_group_shared.svg new file mode 100644 index 000000000..6a4e22bf8 --- /dev/null +++ b/web/pgadmin/browser/server_groups/static/img/server_group_shared.svg @@ -0,0 +1,42 @@ + + + + + + + + + + + + + + + + + + diff --git a/web/pgadmin/browser/server_groups/static/js/server_group.js b/web/pgadmin/browser/server_groups/static/js/server_group.js index 9bd7df0f3..ac1a0c821 100644 --- a/web/pgadmin/browser/server_groups/static/js/server_group.js +++ b/web/pgadmin/browser/server_groups/static/js/server_group.js @@ -9,8 +9,8 @@ define('pgadmin.node.server_group', [ 'sources/gettext', 'sources/url_for', 'jquery', 'underscore', - 'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.node', -], function(gettext, url_for, $, _, pgAdmin) { + 'sources/pgadmin', 'pgadmin.user_management.current_user', 'pgadmin.browser', 'pgadmin.browser.node', +], function(gettext, url_for, $, _, pgAdmin, current_user) { if (!pgAdmin.Browser.Nodes['server_group']) { pgAdmin.Browser.Nodes['server_group'] = pgAdmin.Browser.Node.extend({ @@ -39,14 +39,25 @@ define('pgadmin.node.server_group', [ defaults: { id: undefined, name: null, + user_id: undefined, }, schema: [ { id: 'id', label: gettext('ID'), type: 'int', group: null, mode: ['properties'], + visible: function(model){ + if (model.attributes.user_id != current_user.id && !current_user.is_admin) + return false; + return true; + }, },{ id: 'name', label: gettext('Name'), type: 'text', group: null, mode: ['properties', 'edit', 'create'], + disabled: function(model){ + if (model.attributes.user_id != current_user.id && !_.isUndefined(model.attributes.user_id)) + return true; + return false; + }, }, ], validate: function() { @@ -69,7 +80,12 @@ define('pgadmin.node.server_group', [ return null; }, }), - canDrop: function(itemData) { return itemData.can_delete; }, + + canDrop: function(itemData) { + var serverOwner = itemData.user_id; + if (serverOwner != current_user.id) + return false; + return true; }, dropAsRemove: true, canDelete: function(i) { var s = pgAdmin.Browser.tree.siblings(i, true); diff --git a/web/pgadmin/browser/server_groups/templates/css/server_group.css b/web/pgadmin/browser/server_groups/templates/css/server_group.css new file mode 100644 index 000000000..3b5b8e58a --- /dev/null +++ b/web/pgadmin/browser/server_groups/templates/css/server_group.css @@ -0,0 +1,17 @@ +.icon-server_group { + background-image: url('{{ url_for('NODE-server_group.static', filename='img/server_group.svg') }}') !important; + background-repeat: no-repeat; + background-size: 20px !important; + align-content: center; + vertical-align: middle; + height: 1.3em; +} + +.icon-server_group_shared { + background-image: url('{{ url_for('NODE-server_group.static', filename='img/server_group_shared.svg') }}') !important; + background-repeat: no-repeat; + background-size: 20px !important; + align-content: center; + vertical-align: middle; + height: 1.3em; +} diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js index f5ab09399..0eb7840b0 100644 --- a/web/pgadmin/browser/static/js/browser.js +++ b/web/pgadmin/browser/static/js/browser.js @@ -19,7 +19,7 @@ define('pgadmin.browser', [ 'pgadmin.browser.error', 'pgadmin.browser.frame', 'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity', 'sources/codemirror/addon/fold/pgadmin-sqlfoldcode', - 'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state', + 'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state','jquery.acisortable', 'jquery.acifragment', ], function( tree, gettext, url_for, require, $, _, diff --git a/web/pgadmin/browser/templates/browser/js/utils.js b/web/pgadmin/browser/templates/browser/js/utils.js index 33dd6809e..516b62697 100644 --- a/web/pgadmin/browser/templates/browser/js/utils.js +++ b/web/pgadmin/browser/templates/browser/js/utils.js @@ -44,6 +44,7 @@ define('pgadmin.browser.utils', pgAdmin['csrf_token_header'] = '{{ current_app.config.get('WTF_CSRF_HEADERS')[0] }}'; pgAdmin['csrf_token'] = '{{ csrf_token() }}'; + pgAdmin['server_mode'] = '{{ current_app.config.get('SERVER_MODE') }}'; /* Get the inactivity related config */ pgAdmin['user_inactivity_timeout'] = {{ current_app.config.get('USER_INACTIVITY_TIMEOUT') }}; diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py index 03681e120..b33adc062 100644 --- a/web/pgadmin/model/__init__.py +++ b/web/pgadmin/model/__init__.py @@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy # ########################################################################## -SCHEMA_VERSION = 25 +SCHEMA_VERSION = 26 ########################################################################## # @@ -173,6 +173,7 @@ class Server(db.Model): ) tunnel_identity_file = db.Column(db.String(64), nullable=True) tunnel_password = db.Column(db.String(64), nullable=True) + shared = db.Column(db.Boolean(), nullable=False) class ModulePreference(db.Model): @@ -305,3 +306,88 @@ class Database(db.Model): nullable=False, primary_key=True ) + + +class SharedServer(db.Model): + """Define a shared Postgres server""" + + __tablename__ = 'sharedserver' + id = db.Column(db.Integer, primary_key=True) + user_id = db.Column( + db.Integer, + db.ForeignKey('user.id') + ) + server_owner = db.Column( + db.String(128), + db.ForeignKey('user.username') + ) + servergroup_id = db.Column( + db.Integer, + db.ForeignKey('servergroup.id'), + nullable=False + ) + name = db.Column(db.String(128), nullable=False) + host = db.Column(db.String(128), nullable=True) + hostaddr = db.Column(db.String(128), nullable=True) + port = db.Column( + db.Integer(), + db.CheckConstraint('port >= 1 AND port <= 65534'), + nullable=False) + maintenance_db = db.Column(db.String(64), nullable=True) + username = db.Column(db.String(64), nullable=False) + password = db.Column(db.String(64), nullable=True) + save_password = db.Column( + db.Integer(), + db.CheckConstraint('save_password >= 0 AND save_password <= 1'), + nullable=False + ) + role = db.Column(db.String(64), nullable=True) + ssl_mode = db.Column( + db.String(16), + db.CheckConstraint( + "ssl_mode IN ('allow', 'prefer', 'require', 'disable', " + "'verify-ca', 'verify-full')" + ), + nullable=False) + comment = db.Column(db.String(1024), nullable=True) + discovery_id = db.Column(db.String(128), nullable=True) + servers = db.relationship( + 'ServerGroup', + backref=db.backref('sharedserver', cascade="all, delete-orphan"), + lazy='joined' + ) + db_res = db.Column(db.Text(), nullable=True) + passfile = db.Column(db.Text(), nullable=True) + sslcert = db.Column(db.Text(), nullable=True) + sslkey = db.Column(db.Text(), nullable=True) + sslrootcert = db.Column(db.Text(), nullable=True) + sslcrl = db.Column(db.Text(), nullable=True) + sslcompression = db.Column( + db.Integer(), + db.CheckConstraint('sslcompression >= 0 AND sslcompression <= 1'), + nullable=False + ) + bgcolor = db.Column(db.Text(10), nullable=True) + fgcolor = db.Column(db.Text(10), nullable=True) + service = db.Column(db.Text(), nullable=True) + connect_timeout = db.Column(db.Integer(), nullable=False) + use_ssh_tunnel = db.Column( + db.Integer(), + db.CheckConstraint('use_ssh_tunnel >= 0 AND use_ssh_tunnel <= 1'), + nullable=False + ) + tunnel_host = db.Column(db.String(128), nullable=True) + tunnel_port = db.Column( + db.Integer(), + db.CheckConstraint('port <= 65534'), + nullable=True) + tunnel_username = db.Column(db.String(64), nullable=True) + tunnel_authentication = db.Column( + db.Integer(), + db.CheckConstraint('tunnel_authentication >= 0 AND ' + 'tunnel_authentication <= 1'), + nullable=False + ) + tunnel_identity_file = db.Column(db.String(64), nullable=True) + tunnel_password = db.Column(db.String(64), nullable=True) + shared = db.Column(db.Boolean(), nullable=False) diff --git a/web/pgadmin/preferences/__init__.py b/web/pgadmin/preferences/__init__.py index 6212f63f8..db11d1d09 100644 --- a/web/pgadmin/preferences/__init__.py +++ b/web/pgadmin/preferences/__init__.py @@ -23,6 +23,7 @@ from pgadmin.utils.ajax import success_return, \ from pgadmin.utils.menu import MenuItem from pgadmin.utils.preferences import Preferences from pgadmin.utils.constants import MIMETYPE_APP_JS +from pgadmin.browser.server_groups import ServerGroupModule as sgm MODULE_NAME = 'preferences' @@ -203,6 +204,7 @@ def save(pid): res, msg = Preferences.save( data['mid'], data['category_id'], data['id'], data['value']) + sgm.get_nodes(sgm) if not res: return internal_server_error(errormsg=msg) diff --git a/web/pgadmin/preferences/static/js/preferences.js b/web/pgadmin/preferences/static/js/preferences.js index 62eda7aa6..6c35cf0d6 100644 --- a/web/pgadmin/preferences/static/js/preferences.js +++ b/web/pgadmin/preferences/static/js/preferences.js @@ -463,6 +463,7 @@ define('pgadmin.preferences', [ } if (e.button.text == gettext('Save')) { + debugger; let requires_refresh = false; preferences.updateAll(); @@ -477,6 +478,29 @@ define('pgadmin.preferences', [ if(pref.name == 'theme') { requires_refresh = true; } + + if(pref.name == 'hide_shared_server') { + Alertify.confirm( + gettext('Browser tree refresh required'), + gettext('A browser tree refresh is required. Do you wish to refresh the tree?'), + function() { + pgAdmin.Browser.tree.destroy({ + success: function() { + pgAdmin.Browser.initializeBrowserTree(pgAdmin.Browser); + return true; + }, + }); + }, + function() { + preferences.reset(); + changed = {}; + return true; + } + ).set('labels', { + ok: gettext('Refresh'), + cancel: gettext('Later'), + }); + } }); if(requires_refresh) { diff --git a/web/pgadmin/tools/schema_diff/__init__.py b/web/pgadmin/tools/schema_diff/__init__.py index a325e9a01..babb05888 100644 --- a/web/pgadmin/tools/schema_diff/__init__.py +++ b/web/pgadmin/tools/schema_diff/__init__.py @@ -20,13 +20,14 @@ from flask_babelex import gettext from pgadmin.utils import PgAdminModule from pgadmin.utils.ajax import make_json_response, bad_request, \ make_response as ajax_response, internal_server_error -from pgadmin.model import Server +from pgadmin.model import Server, SharedServer from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry from pgadmin.tools.schema_diff.model import SchemaDiffModel from config import PG_DEFAULT_DRIVER from pgadmin.utils.driver import get_driver from pgadmin.utils.preferences import Preferences from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS +from sqlalchemy import or_ MODULE_NAME = 'schema_diff' @@ -270,6 +271,7 @@ def servers(): server id. """ res = {} + auto_detected_server = None try: """Return a JSON document listing the server groups for the user""" driver = get_driver(PG_DEFAULT_DRIVER) @@ -277,7 +279,19 @@ def servers(): from pgadmin.browser.server_groups.servers import\ server_icon_and_background - for server in Server.query.filter_by(user_id=current_user.id): + for server in Server.query.filter( + or_(Server.user_id == current_user.id, Server.shared)): + + shared_server = SharedServer.query.filter_by( + name=server.name, user_id=current_user.id, + servergroup_id=server.servergroup_id).first() + + if server.discovery_id: + auto_detected_server = server.name + + if shared_server and shared_server.name == auto_detected_server: + continue + manager = driver.connection_manager(server.id) conn = manager.connection() connected = conn.connected() diff --git a/web/pgadmin/tools/schema_diff/static/js/schema_diff.backform.js b/web/pgadmin/tools/schema_diff/static/js/schema_diff.backform.js index 55ef1e5f4..1b095db98 100644 --- a/web/pgadmin/tools/schema_diff/static/js/schema_diff.backform.js +++ b/web/pgadmin/tools/schema_diff/static/js/schema_diff.backform.js @@ -175,7 +175,7 @@ let SchemaDiffSelect2Control = let span = this.$el.find('.select2-selection .select2-selection__rendered span.wcTabIcon'), selSpan = this.$el.find('option:selected'); - if (span.hasClass('icon-server-not-connected')) { + if (span.hasClass('icon-server-not-connected') || span.hasClass('icon-shared-server-not-connected')) { let icon = (data.icon) ? data.icon : 'icon-pg'; span.removeClass('icon-server-not-connected'); span.addClass(icon); diff --git a/web/pgadmin/utils/driver/psycopg2/server_manager.py b/web/pgadmin/utils/driver/psycopg2/server_manager.py index c35a3726f..6d3ce25ff 100644 --- a/web/pgadmin/utils/driver/psycopg2/server_manager.py +++ b/web/pgadmin/utils/driver/psycopg2/server_manager.py @@ -66,6 +66,7 @@ class ServerManager(object): self.hostaddr = server.hostaddr self.port = server.port self.db = server.maintenance_db + self.shared = server.shared self.did = None self.user = server.username self.password = server.password diff --git a/web/regression/python_test_utils/test_utils.py b/web/regression/python_test_utils/test_utils.py index 6d66af15a..022856c0c 100644 --- a/web/regression/python_test_utils/test_utils.py +++ b/web/regression/python_test_utils/test_utils.py @@ -36,6 +36,8 @@ from regression import test_setup from pgadmin.utils.preferences import Preferences +from functools import wraps + CURRENT_PATH = os.path.abspath(os.path.join(os.path.dirname( os.path.realpath(__file__)), "../")) @@ -1598,3 +1600,107 @@ def get_selenoid_browsers_list(arguments): list_of_browsers = test_setup.config_data['selenoid_config'][ 'browsers_list'] return list_of_browsers + + +def login_using_user_account(tester): + """ + This function login the test client username and password + :param tester: test client + :type tester: flask test client object + :return: None + """ + username = tester.test_config_data['login_username'] + password = tester.test_config_data['login_password'] + response = tester.login(username, password) + + if response.status_code != 302: + print("Unable to login test client, email and password not found.", + file=sys.stderr) + sys.exit(1) + + +def logout_tester_account(tester): + """ + This function logout the test account + :param tester: test client + :type tester: flask test client object + :return: None + """ + tester.logout() + + +def create_user(user_details): + try: + conn = sqlite3.connect(config.TEST_SQLITE_PATH) + # Create the server + cur = conn.cursor() + user_details = ( + user_details['login_username'], user_details['login_username'], + user_details['login_password'], 1) + + cur.execute( + 'select * from user where username = "%s"' % user_details[0]) + user = cur.fetchone() + if user is None: + cur.execute('INSERT INTO user (username, email, password, active) ' + 'VALUES (?,?,?,?)', user_details) + user_id = cur.lastrowid + conn.commit() + else: + user_id = user[0] + conn.close() + + return user_id + except Exception as exception: + traceback.print_exc(file=sys.stderr) + raise ("Error while creating server. %s" % exception) + + +def get_test_user(self, user_details, + is_api=True, create_conn=True): + if user_details is None: + return None, None + + if is_api is True: + + # Create test_client for this user, and login through it. + test_client = self.app.test_client() + user = create_user(user_details) + if user is not None: + test_client.test_config_data = dict({ + "login_username": user_details['login_username'], + "login_password": user_details['login_password'] + }) + else: + return "User not created" + login_using_user_account(test_client) + user = test_client + + return user + + +def create_user_wise_test_client(user): + """ + This function creates new test client and pem database connection as per + provided user and execute the test cases. + :return: None + """ + + def multi_user_decorator(func): + @wraps(func) + def wrapper(self, *args, **kwargs): + main_tester = self.__class__.tester + try: + # Login with non-admin_user + test_user = get_test_user(self, user) + self.setTestClient(test_user) + + # Call 'runTest' with new test client + func(self, *args, **kwargs) + finally: + # Restore the original user and driver + self.__class__.tester = main_tester + + return wrapper + + return multi_user_decorator diff --git a/web/regression/test_config.json.in b/web/regression/test_config.json.in index 690f46c1c..393be6a84 100644 --- a/web/regression/test_config.json.in +++ b/web/regression/test_config.json.in @@ -11,6 +11,11 @@ "login_password": "PASSWORD", "login_username": "USER2@EXAMPLE.COM" }, + "pgAdmin4_test_non_admin_credentials": { + "new_password": "NEWPASSWORD", + "login_password": "PASSWORD", + "login_username": "USER@EXAMPLE.COM" + }, "pgAdmin4_ldap_credentials": { "login_password": "PASSWORD", "login_username": "USERNAME" diff --git a/web/setup.py b/web/setup.py index e9c42decd..cfdf2045a 100644 --- a/web/setup.py +++ b/web/setup.py @@ -110,6 +110,7 @@ def dump_servers(args): add_value(attr_dict, "Role", server.role) add_value(attr_dict, "SSLMode", server.ssl_mode) add_value(attr_dict, "Comment", server.comment) + add_value(attr_dict, "Shared", server.shared) add_value(attr_dict, "DBRestriction", server.db_res) add_value(attr_dict, "PassFile", server.passfile) add_value(attr_dict, "SSLCert", server.sslcert) @@ -258,6 +259,14 @@ def load_servers(args): for server in data["Servers"]: obj = data["Servers"][server] + # Check if server is shared.Won't import if user is non-admin + if 'Shared' in obj \ + and obj['Shared'] and \ + not user.has_role("Administrator"): + print("Can't import the server '%s' as it is shared " % + obj["Name"]) + continue + # Get the group. Create if necessary group_id = next( (g.id for g in groups if g.name == obj["Group"]), -1)