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/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 3c4ec6831..bf5be6c84 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,63 @@ 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") + + hide_shared_server = None + if config.SERVER_MODE: + pref = Preferences.module('browser') + hide_shared_server = pref.preference('hide_shared_server').get() + + if config.SERVER_MODE: + server_groups = ServerGroup.query.all() + groups = [] + for group in server_groups: + if hide_shared_server and self.has_shared_server( + group.id) and group.user_id != current_user.id: + continue + if group.user_id == current_user.id or \ + self.has_shared_server(group.id): + groups.append(group) + 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 +260,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 +271,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 +281,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 +307,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 @@ -306,14 +367,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 +383,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 98bb1974d..714b4c9e0 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 @@ -31,6 +31,8 @@ from pgadmin.tools.schema_diff.node_registry import SchemaDiffRegistry 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 +from sqlalchemy import or_ +from pgadmin.utils.preferences import Preferences def has_any(data, keys): @@ -91,6 +93,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 @@ -113,15 +119,55 @@ 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 + @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 = None + if config.SERVER_MODE: + pref = Preferences.module('browser') + hide_shared_server = pref.preference('hide_shared_server').get() + + 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: + # Don't include shared server if hide shared server is + # set to true + if hide_shared_server: + continue + shared_server = self.get_shared_server(server, gid) + server = self.get_shared_server_properties(server, + shared_server) + connected = False manager = None errmsg = None @@ -157,7 +203,10 @@ class ServerModule(sg.ServerGroupPluginModule): 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 @@ -229,6 +278,79 @@ 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 + """ + shared_server = SharedServer.query.filter_by( + name=server.name, user_id=current_user.id, + servergroup_id=gid).first() + + 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 + class ServerMenuItem(MenuItem): def __init__(self, **kwargs): @@ -326,12 +448,19 @@ 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 = 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() @@ -364,7 +493,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 ) ) @@ -378,9 +509,7 @@ 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 is None: return make_json_response( @@ -425,14 +554,36 @@ 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 ), ) + 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: @@ -448,10 +599,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: @@ -466,8 +618,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( @@ -476,6 +628,10 @@ 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 = ServerModule.get_shared_server(server, gid) + # Not all parameters can be modified, while the server is connected config_param_map = { 'name': 'name', @@ -505,11 +661,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'), @@ -540,7 +697,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(data, config_param_map, server, + sharedserver) if idx == 0: return make_json_response( @@ -567,7 +725,7 @@ 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, @@ -576,7 +734,7 @@ class ServerNode(PGChildNodeView): ) ) - def _set_valid_attr_value(self, data, config_param_map, server): + def _set_valid_attr_value(self, data, config_param_map, server, sharedserver): idx = 0 for arg in config_param_map: @@ -586,7 +744,11 @@ class ServerNode(PGChildNodeView): # it manually to integer if arg == 'sslcompression': value = 1 if value else 0 - setattr(server, config_param_map[arg], value) + # setattr(server, 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) idx += 1 return idx @@ -612,11 +774,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 = [] @@ -653,7 +815,6 @@ class ServerNode(PGChildNodeView): def properties(self, gid, sid): """Return list of attributes of a server""" server = Server.query.filter_by( - user_id=current_user.id, id=sid).first() if server is None: @@ -662,9 +823,7 @@ class ServerNode(PGChildNodeView): success=0, errormsg=self.not_found_error_msg() ) - sg = ServerGroup.query.filter_by( - user_id=current_user.id, id=server.servergroup_id ).first() @@ -676,50 +835,61 @@ class ServerNode(PGChildNodeView): 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 server.shared and not current_user.has_role("Administrator"): + if server.shared and server.user_id != current_user.id: + shared_server = ServerModule.get_shared_server(server, gid) + server = ServerModule.get_shared_server_properties(server, + shared_server) + + response = { + 'id': server.id, + 'name': server.name, + 'server_owner': server.server_owner if ( + server.shared and not current_user.has_role( + "Administrator")) else None, + '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': 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 + } + + return ajax_response(response) @login_required def create(self, gid): @@ -801,11 +971,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 @@ -1005,6 +1175,11 @@ 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 = 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()) @@ -1062,7 +1237,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 \ @@ -1124,10 +1298,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 @@ -1561,21 +1743,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( 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 7c1b58be8..a5ef532c6 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); @@ -745,12 +754,21 @@ 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: function(model){ + if (model.attributes.shared) + return true; + return false; + }, + }, + { 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,6 +791,22 @@ 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'], },{ @@ -1057,7 +1091,23 @@ 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; + + }, validate: function() { + var msg; + this.errorModel.clear(); + if (!this.isNew() && this.attributes.shared && this.attributes.user_id != current_user.id &&(_.isNull(this.get('username')))) { + msg = gettext('Username is not set,Please set the username.'); + this.errorModel.set('username', msg); + return msg; + } + const validateModel = new modelValidation.ModelValidation(this); return validateModel.validate(); }, @@ -1153,7 +1203,18 @@ 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; + if (data.shared && _.isNull(data.user_name) && data.user_id != current_user.id){ + if (selectedTreeNodeData._type == 'server') + pgAdmin.Browser.Node.callbacks.show_obj_properties.call( + pgAdmin.Browser.Nodes[tree.itemData(selectedTreeNode)._type], {action: 'edit'} + ); + return; + } var wasConnected = reconnect || data.connected, onFailure = function( xhr, status, error, _node, _data, _tree, _item, _wasConnected @@ -1164,7 +1225,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) { @@ -1312,7 +1378,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 @@ -1378,7 +1448,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..a178f4fe5 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,12 @@ 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..e9eb73ce0 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json @@ -0,0 +1,863 @@ +{ + "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, + "invalid_server_group": true, + "mocking_required": false, + "mock_data": { + }, + "expected_data": { + "status_code": 410 + } + }, + { + "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 cce03041a..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.assertEquals(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 9d8da94fd..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.assertEquals(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 e8b48c0fc..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.assertEquals(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..cca7ce57f --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/tests/test_check_connect.py @@ -0,0 +1,122 @@ +########################################################################## +# +# 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 + +test_user_details = config_data['pgAdmin4_test_non_admin_credentials'] + + +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' + # 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'] + # + # if self.save_password: + # self.server['tunnel_password'] = self.test_data[ + # 'tunnel_password'] + + 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 7fe3cf6a8..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.assertEquals(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.assertEquals(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.assertEquals(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 c2b184295..43220eb2f 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 9f92ffcce..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.assertEquals(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 8a69558e9..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.assertEquals(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..bf34ad123 --- /dev/null +++ b/web/pgadmin/browser/server_groups/servers/tests/test_shared_server.py @@ -0,0 +1,125 @@ +########################################################################## +# +# 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 + +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""" + 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""" + 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' + ) + + @create_user_wise_test_client(test_user_details) + 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..219b5954a 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,27 @@ 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 +82,11 @@ 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 itemData.can_delete; }, 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/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/utils/driver/psycopg2/server_manager.py b/web/pgadmin/utils/driver/psycopg2/server_manager.py index ea1d9efe1..77c4dec22 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 2f36b5e81..ae60e379f 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__)), "../")) @@ -1585,3 +1587,110 @@ 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: + raise Exception("Error while creating server. %s" % exception) + + +def get_test_user(self, user_details, + is_api=True, create_conn=True): + # assert id == 1 or id == 0 + test_client = self.app.test_client() + 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): + # self.user = user + main_tester = self.__class__.tester + try: + # Login with non-admin_user + test_user = get_test_user(self, user) + self.setTestClient(test_user) + # self.setTestClient(test_user['tester']) + + # 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/setup.py b/web/setup.py index 14bd5f9cc..e8a71f812 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) @@ -244,6 +245,14 @@ def load_servers(args): print_summary() sys.exit(1) + # Check if server is shared.Won't import if user is non-admin + if 'Shared' in obj: + if 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 = -1 for g in groups: