public inbox for [email protected]  
help / color / mirror / Atom feed
From: Pradip Parkale <[email protected]>
To: Akshay Joshi <[email protected]>
Cc: pgadmin-hackers <[email protected]>
Subject: Re: [pgAdmin][RM4979]: Configuration files for data sources or similar.
Date: Tue, 1 Sep 2020 16:47:01 +0530
Message-ID: <CAJ9T6Su5LAOO=JW-ZpLs6wGUta7PMZ52wzcQWmSCPbSSR+U8Gw@mail.gmail.com> (raw)
In-Reply-To: <CANxoLDdVUbL7Eeen5jNX9ZrpvOe1zGqi_XCw8ECFzeJv8oJLCg@mail.gmail.com>
References: <CAJ9T6SvOzSPGyRgVg+8XksV_4=mOSSMoy8RaGKm0O9qFTT_ZQQ@mail.gmail.com>
	<CANxoLDcmxQsdmO-8Z9O6=zmuUs5KGkXzCmjqyoatCRCd=xBkdA@mail.gmail.com>
	<CAJ9T6SsV50gwjNP3D4YhONhtV6=RfgqE4hUGoDf91aBZMcahsA@mail.gmail.com>
	<CANxoLDdVUbL7Eeen5jNX9ZrpvOe1zGqi_XCw8ECFzeJv8oJLCg@mail.gmail.com>

Hi Akshay,

Please find the updated patch.

On Tue, Aug 25, 2020 at 5:53 PM Akshay Joshi <[email protected]>
wrote:

> Hi Pradip
>
> Following are the GUI review comments:
> *Testing Scenario*: I have three users U1 and U2 are Admin and U3 is a
> normal user. Login from U1 first time and pgAdmin4 discover all the local
> servers. Steps that I perform:
>
>    - Shared one server.
>    - Create a group 'Test' and move one server to that group.
>    - Share the server of the 'Test' group as well.
>
>              [image: Screenshot 2020-08-25 at 4.38.32 PM.png]
> *Issues:*
>
>    - When login using U2 or U3 PostgreSQL 9.2 and 10 shown twice which
>    should not. Refer below screenshot
>     [image: Screenshot 2020-08-25 at 4.30.22 PM.png]
>
> Fixed. If the server is auto-discovered and U1 shared the auto-discovered
server(PostgreSQL 9.2 in the above example) and then log in using U2, in
this case, shared server(PostgreSQL 9.2) will not visible to U2 as the same
server will be auto-discovered for U2.

>
>    -
>    - When expanding the server or clicking on the 'Connect Server' menu
>    of the shared server, it opens the properties dialog.
>
> This is expected. Properties dialogue will open the first time for a
shared server where the user has to enter server details. This will happen
for the user who is not the owner of the server.

>
>    - Try to update the IPAddress, Port it is not updating for the shared
>    server. Check other properties too.
>
> Fixed.

>
>    -
>    - When we set "Hide shared server?" setting to true/false. A complete
>    browser tree should be refreshed. With the current implementation, each
>    group needs to be refreshed individually and the server group still visible
>    while refreshing the complete page Browser Tree looks perfect.
>
> Fixed.

>
>    - When there is only one server in a group and we expanded the group
>    'Connect to server' dialog appears which should not. (*This may be an old
>    issue)
>
> This is expected and its an old behavior.

>
>    -
>    - Schema Diff shared servers are not visible inside the appropriate
>    group in the source and target server list.
>
> Fixed

>
>    - Documentation changes required (Create a new Redmine).
>
> Done.

>
>    -
>    - SonarQube shows 1 new Bug and 4 new code smell. (Fixed the Bug, code
>    smell can be fixed later)
>
> Fixed.

>
>    - 'pgAdmin4_test_non_admin_credentials' should be added in '
>    test_config.json.in' file.
>
> Fixed.

>
>    - For Desktop mode shared server test cases should be skipped. It is
>    failing at the moment.
>
> Fixed.

> Code review still remains :)
>
> On Fri, Aug 21, 2020 at 6:00 PM Pradip Parkale <
> [email protected]> wrote:
>
>> Hi Akshay,
>> Please find the updated patch.
>>
>> On Fri, Aug 21, 2020 at 1:55 PM Akshay Joshi <
>> [email protected]> wrote:
>>
>>> Hi Pradip
>>>
>>> The patch is not applied, can you please rebase and send it again.
>>>
>>> On Thu, Aug 20, 2020 at 3:58 PM Pradip Parkale <
>>> [email protected]> wrote:
>>>
>>>> Hi Hackers,
>>>>
>>>> Please find the attached patch for the shared server implementation.
>>>>
>>>> Few key points:
>>>>
>>>>    1. The admin who is the owner of the server user can share the
>>>>    server with other users.
>>>>    2. This option will be available only for admin users.
>>>>    3. If the user doesn't want to see the shared server then the
>>>>    option to hide the shared server is available in preferences.
>>>>    4. The user who is not the owner of the server, can't delete the
>>>>    shared server and server group.
>>>>    5. This option is only available in server mode.
>>>>
>>>>
>>>> --
>>>> Thanks & Regards,
>>>> Pradip Parkale
>>>> Software Engineer | EnterpriseDB Corporation
>>>>
>>>
>>>
>>> --
>>> *Thanks & Regards*
>>> *Akshay Joshi*
>>> *pgAdmin Hacker | Sr. Software Architect*
>>> *EDB Postgres <http://edbpostgres.com>*
>>>
>>> *Mobile: +91 976-788-8246*
>>>
>>
>>
>> --
>> Thanks & Regards,
>> Pradip Parkale
>> Software Engineer | EnterpriseDB Corporation
>>
>
>
> --
> *Thanks & Regards*
> *Akshay Joshi*
> *pgAdmin Hacker | Sr. Software Architect*
> *EDB Postgres <http://edbpostgres.com>*
>
> *Mobile: +91 976-788-8246*
>


-- 
Thanks & Regards,
Pradip Parkale
Software Engineer | EnterpriseDB Corporation


Attachments:

  [image/png] Screenshot 2020-08-25 at 4.30.22 PM.png (70.2K, 3-Screenshot%202020-08-25%20at%204.30.22%20PM.png)
  download | view image

  [image/png] Screenshot 2020-08-25 at 4.38.32 PM.png (52.9K, 4-Screenshot%202020-08-25%20at%204.38.32%20PM.png)
  download | view image

  [application/octet-stream] RM4979_v3.patch (140.2K, 5-RM4979_v3.patch)
  download | inline diff:
diff --git a/web/migrations/versions/a091c9611d20_.py b/web/migrations/versions/a091c9611d20_.py
new file mode 100644
index 000000000..d9da2b10f
--- /dev/null
+++ b/web/migrations/versions/a091c9611d20_.py
@@ -0,0 +1,72 @@
+
+"""empty message
+
+Revision ID: a091c9611d20
+Revises: 84700139beb0
+Create Date: 2020-07-14 17:20:22.705737
+
+"""
+from pgadmin.model import db
+
+
+# revision identifiers, used by Alembic.
+revision = 'a091c9611d20'
+down_revision = '84700139beb0'
+branch_labels = None
+depends_on = None
+
+
+def upgrade():
+    db.engine.execute(
+        'ALTER TABLE server ADD COLUMN shared BOOLEAN'
+    )
+
+    db.engine.execute("""
+            CREATE TABLE sharedserver (
+            id	INTEGER NOT NULL,
+            user_id	INTEGER NOT NULL,
+            server_owner VARCHAR(64),
+            servergroup_id	INTEGER NOT NULL,
+            name	VARCHAR(128) NOT NULL,
+            host	VARCHAR(128),
+            port	INTEGER NOT NULL CHECK(port >= 1 AND port <= 65534),
+            maintenance_db	VARCHAR(64),
+            username	VARCHAR(64),
+            password	VARCHAR(64),
+            role	VARCHAR(64),
+            ssl_mode	VARCHAR(16) NOT NULL CHECK(ssl_mode IN
+                ( 'allow' , 'prefer' , 'require' , 'disable' ,
+                  'verify-ca' , 'verify-full' )
+            ),
+            comment	VARCHAR(1024),
+            discovery_id	VARCHAR(128),
+            hostaddr	TEXT(1024),
+            db_res	TEXT,
+            passfile	TEXT,
+            sslcert	TEXT,
+            sslkey	TEXT,
+            sslrootcert	TEXT,
+            sslcrl	TEXT,
+            sslcompression	INTEGER DEFAULT 0,
+            bgcolor TEXT(10),
+            fgcolor TEXT(10),
+            service TEXT,
+            use_ssh_tunnel INTEGER DEFAULT 0,
+            tunnel_host TEXT,
+            tunnel_port TEXT,
+            tunnel_username TEXT,
+            tunnel_authentication INTEGER DEFAULT 0,
+            tunnel_identity_file TEXT,
+            shared BOOLEAN NOT NULL,
+            save_password BOOLEAN NOT NULL,
+            tunnel_password VARCHAR(64),
+            connect_timeout INTEGER ,
+            PRIMARY KEY(id),
+            FOREIGN KEY(user_id) REFERENCES user(id),
+            FOREIGN KEY(servergroup_id) REFERENCES servergroup(id)
+        );
+    """)
+
+
+def downgrade():
+    pass
diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py
index 516ba2c4d..1a712151a 100644
--- a/web/pgadmin/__init__.py
+++ b/web/pgadmin/__init__.py
@@ -28,7 +28,7 @@ from werkzeug.datastructures import ImmutableDict
 from werkzeug.local import LocalProxy
 from werkzeug.utils import find_modules
 
-from pgadmin.model import db, Role, Server, ServerGroup, \
+from pgadmin.model import db, Role, Server, SharedServer, ServerGroup, \
     User, Keys, Version, SCHEMA_VERSION as CURRENT_SCHEMA_VERSION
 from pgadmin.utils import PgAdminModule, driver, KeyManager
 from pgadmin.utils.preferences import Preferences
diff --git a/web/pgadmin/browser/register_browser_preferences.py b/web/pgadmin/browser/register_browser_preferences.py
index 948be41b8..215421cbf 100644
--- a/web/pgadmin/browser/register_browser_preferences.py
+++ b/web/pgadmin/browser/register_browser_preferences.py
@@ -9,6 +9,7 @@
 from flask_babelex import gettext
 from pgadmin.utils.constants import PREF_LABEL_DISPLAY,\
     PREF_LABEL_KEYBOARD_SHORTCUTS
+import config
 
 LOCK_LAYOUT_LEVEL = {
     'PREVENT_DOCKING': 'docking',
@@ -23,6 +24,15 @@ def register_browser_preferences(self):
         gettext("Show system objects?"), 'boolean', False,
         category_label=PREF_LABEL_DISPLAY
     )
+    if config.SERVER_MODE:
+        self.hide_shared_server = self.preference.register(
+            'display', 'hide_shared_server',
+            gettext("Hide shared server?"), 'boolean', False,
+            category_label=gettext('Display'),
+            help_str=gettext(
+                'If set to true, then all shared server will be hidden'
+            )
+        )
 
     self.preference.register(
         'display', 'enable_acitree_animation',
diff --git a/web/pgadmin/browser/server_groups/__init__.py b/web/pgadmin/browser/server_groups/__init__.py
index 717795bde..9f0b15ff3 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
@@ -296,9 +357,25 @@ class ServerGroupView(NodeView):
     def nodes(self, gid=None):
         """Return a JSON document listing the server groups for the user"""
         nodes = []
+        hide_shared_server = None
+        if config.SERVER_MODE:
+            pref = Preferences.module('browser')
+            hide_shared_server = pref.preference('hide_shared_server').get()
 
         if gid is None:
-            groups = ServerGroup.query.filter_by(user_id=current_user.id)
+            if config.SERVER_MODE:
+                server_groups = ServerGroup.query.all()
+                groups = []
+                for group in server_groups:
+                    if hide_shared_server and \
+                        ServerGroupModule.has_shared_server(group.id) and \
+                            group.user_id != current_user.id:
+                        continue
+                    if group.user_id == current_user.id or \
+                            ServerGroupModule.has_shared_server(group.id):
+                        groups.append(group)
+            else:
+                groups = ServerGroup.query.filter_by(user_id=current_user.id)
 
             for group in groups:
                 nodes.append(
@@ -306,14 +383,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 +399,7 @@ class ServerGroupView(NodeView):
             nodes = self.blueprint.generate_browser_node(
                 "%d" % (group.id), None,
                 group.name,
-                self.node_icon,
+                get_icon_css_class(group.id, group.user_id),
                 True,
                 self.node_type
             )
diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py
index 2eb058a40..8b1486cac 100644
--- a/web/pgadmin/browser/server_groups/servers/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/__init__.py
@@ -23,7 +23,7 @@ from pgadmin.tools.sqleditor.utils.query_history import QueryHistory
 
 import config
 from config import PG_DEFAULT_DRIVER
-from pgadmin.model import db, Server, ServerGroup, User
+from pgadmin.model import db, Server, ServerGroup, User, SharedServer
 from pgadmin.utils.driver import get_driver
 from pgadmin.utils.master_password import get_crypt_key
 from pgadmin.utils.exception import CryptKeyMissing
@@ -32,6 +32,8 @@ from psycopg2 import Error as psycopg2_Error, OperationalError
 from pgadmin.browser.server_groups.servers.utils import is_valid_ipaddress
 from pgadmin.utils.constants import UNAUTH_REQ, MIMETYPE_APP_JS, \
     SERVER_CONNECTION_CLOSED
+from sqlalchemy import or_
+from pgadmin.utils.preferences import Preferences
 
 
 def has_any(data, keys):
@@ -92,6 +94,10 @@ def server_icon_and_background(is_connected, manager, server):
         return 'icon-{0}{1}'.format(
             manager.server_type, server_background_color
         )
+    elif server.shared and config.SERVER_MODE:
+        return 'icon-shared-server-not-connected{0}'.format(
+            server_background_color
+        )
     else:
         return 'icon-server-not-connected{0}'.format(
             server_background_color
@@ -114,15 +120,61 @@ 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
+        auto_detected_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.discovery_id:
+                auto_detected_server = server.name
+            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)
+
+                if shared_server.name == auto_detected_server:
+                    continue
+                server = self.get_shared_server_properties(server,
+                                                           shared_server)
+
             connected = False
             manager = None
             errmsg = None
@@ -158,7 +210,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
@@ -230,6 +285,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):
@@ -327,12 +455,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()
@@ -365,7 +500,9 @@ class ServerNode(PGChildNodeView):
                     is_password_saved=bool(server.save_password),
                     is_tunnel_password_saved=True
                     if server.tunnel_password is not None else False,
-                    errmsg=errmsg
+                    errmsg=errmsg,
+                    user_name=server.username,
+                    shared=server.shared
                 )
             )
 
@@ -379,9 +516,12 @@ class ServerNode(PGChildNodeView):
     @login_required
     def node(self, gid, sid):
         """Return a JSON document listing the server groups for the user"""
-        server = Server.query.filter_by(user_id=current_user.id,
-                                        servergroup_id=gid,
-                                        id=sid).first()
+        server = Server.query.filter_by(id=sid).first()
+
+        if server.shared and server.user_id != current_user.id:
+            shared_server = ServerModule.get_shared_server(server, gid)
+            server = ServerModule.get_shared_server_properties(server,
+                                                               shared_server)
 
         if server is None:
             return make_json_response(
@@ -426,14 +566,37 @@ class ServerNode(PGChildNodeView):
                 is_password_saved=bool(server.save_password),
                 is_tunnel_password_saved=True
                 if server.tunnel_password is not None else False,
-                errmsg=errmsg
+                errmsg=errmsg,
+                shared=server.shared,
+                user_name=server.username
             ),
         )
 
+    def delete_shared_server(self, server_name, gid):
+        """
+        Delete the shared server
+        :param server_name:
+        :return:
+        """
+        try:
+            shared_server = SharedServer.query.filter_by(name=server_name,
+                                                         servergroup_id=gid)
+            for s in shared_server:
+                get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id)
+                db.session.delete(s)
+            db.session.commit()
+
+        except Exception as e:
+            current_app.logger.exception(e)
+            return make_json_response(
+                success=0,
+                errormsg=e.message)
+
     @login_required
     def delete(self, gid, sid):
         """Delete a server node in the settings database."""
         servers = Server.query.filter_by(user_id=current_user.id, id=sid)
+        server_name = None
 
         # TODO:: A server, which is connected, cannot be deleted
         if servers is None:
@@ -449,10 +612,11 @@ class ServerNode(PGChildNodeView):
         else:
             try:
                 for s in servers:
+                    server_name = s.name
                     get_driver(PG_DEFAULT_DRIVER).delete_manager(s.id)
                     db.session.delete(s)
                 db.session.commit()
-
+                self.delete_shared_server(server_name, gid)
                 QueryHistory.clear_history(current_user.id, sid)
 
             except Exception as e:
@@ -467,8 +631,8 @@ class ServerNode(PGChildNodeView):
     @login_required
     def update(self, gid, sid):
         """Update the server settings"""
-        server = Server.query.filter_by(
-            user_id=current_user.id, id=sid).first()
+        server = Server.query.filter_by(id=sid).first()
+        sharedserver = None
 
         if server is None:
             return make_json_response(
@@ -477,6 +641,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',
@@ -506,11 +674,12 @@ class ServerNode(PGChildNodeView):
             'tunnel_username': 'tunnel_username',
             'tunnel_authentication': 'tunnel_authentication',
             'tunnel_identity_file': 'tunnel_identity_file',
+            'shared': 'shared'
         }
 
         disp_lbl = {
             'name': gettext('name'),
-            'host': gettext('Host name/address'),
+            'hostaddr': gettext('Host name/address'),
             'port': gettext('Port'),
             'db': gettext('Maintenance database'),
             'username': gettext('Username'),
@@ -541,7 +710,8 @@ class ServerNode(PGChildNodeView):
         self._server_modify_disallowed_when_connected(
             connected, data, disp_lbl)
 
-        idx = self._set_valid_attr_value(data, config_param_map, server)
+        idx = self._set_valid_attr_value(gid, data, config_param_map, server,
+                                         sharedserver)
 
         if idx == 0:
             return make_json_response(
@@ -568,7 +738,11 @@ class ServerNode(PGChildNodeView):
             node=self.blueprint.generate_browser_node(
                 "%d" % (server.id), server.servergroup_id,
                 server.name,
-                server_icon_and_background(connected, manager, server),
+                server_icon_and_background(
+                    connected, manager, sharedserver)
+                if server.shared and server.user_id != current_user.id
+                else server_icon_and_background(
+                    connected, manager, server),
                 True,
                 self.node_type,
                 connected=connected,
@@ -577,7 +751,8 @@ class ServerNode(PGChildNodeView):
             )
         )
 
-    def _set_valid_attr_value(self, data, config_param_map, server):
+    def _set_valid_attr_value(self, gid, data, config_param_map, server,
+                              sharedserver):
 
         idx = 0
         for arg in config_param_map:
@@ -585,9 +760,17 @@ class ServerNode(PGChildNodeView):
                 value = data[arg]
                 # sqlite3 do not have boolean type so we need to convert
                 # it manually to integer
+                if 'shared' in data and not data['shared']:
+                    # Delete the shared server from DB if server
+                    # owner uncheck shared property
+                    self.delete_shared_server(server.name, gid)
                 if arg == 'sslcompression':
                     value = 1 if value else 0
-                setattr(server, config_param_map[arg], value)
+                # 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
@@ -613,11 +796,11 @@ class ServerNode(PGChildNodeView):
         """
         Return list of attributes of all servers.
         """
-        servers = Server.query.filter_by(
-            user_id=current_user.id,
-            servergroup_id=gid).order_by(Server.name)
+        servers = Server.query.filter(
+            or_(Server.user_id == current_user.id,
+                Server.shared),
+            Server.servergroup_id == gid).order_by(Server.name)
         sg = ServerGroup.query.filter_by(
-            user_id=current_user.id,
             id=gid
         ).first()
         res = []
@@ -625,6 +808,11 @@ class ServerNode(PGChildNodeView):
         driver = get_driver(PG_DEFAULT_DRIVER)
 
         for server in servers:
+            if server.shared and server.user_id != current_user.id:
+                shared_server = 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()
@@ -654,7 +842,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:
@@ -663,9 +850,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()
 
@@ -677,50 +862,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):
@@ -802,11 +998,11 @@ class ServerNode(PGChildNodeView):
                 tunnel_port=data.get('tunnel_port', 22),
                 tunnel_username=data.get('tunnel_username', None),
                 tunnel_authentication=data.get('tunnel_authentication', 0),
-                tunnel_identity_file=data.get('tunnel_identity_file', None)
+                tunnel_identity_file=data.get('tunnel_identity_file', None),
+                shared=data.get('shared', None)
             )
             db.session.add(server)
             db.session.commit()
-
             connected = False
             user = None
             manager = None
@@ -1006,9 +1202,22 @@ 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())
 
+        # Return if username is blank
+        if server.username is None:
+            return make_json_response(
+                status=200,
+                success=0,
+                errormsg=gettext(
+                    u"Please enter the server details to connect")
+            )
         if current_user and hasattr(current_user, 'id'):
             # Fetch User Details.
             user = User.query.filter_by(id=current_user.id).first()
@@ -1063,7 +1272,6 @@ class ServerNode(PGChildNodeView):
                 except Exception as e:
                     current_app.logger.exception(e)
                     return internal_server_error(errormsg=str(e))
-
         if 'password' not in data:
             conn_passwd = getattr(conn, 'password', None)
             if conn_passwd is None and not server.save_password and \
@@ -1125,10 +1333,18 @@ class ServerNode(PGChildNodeView):
                     # every time user try to connect
                     # 1 is True in SQLite as no boolean type
                     setattr(server, 'save_password', 1)
+                    if server.shared and server.user_id != current_user.id:
+                        setattr(shared_server, 'save_password', 1)
+                    else:
+                        setattr(server, 'save_password', 1)
+
                     # Save the encrypted password using the user's login
                     # password key, if there is any password to save
                     if password:
-                        setattr(server, 'password', password)
+                        if server.shared and server.user_id != current_user.id:
+                            setattr(shared_server, 'password', password)
+                        else:
+                            setattr(server, 'password', password)
                     db.session.commit()
                 except Exception as e:
                     # Release Connection
@@ -1560,21 +1776,39 @@ class ServerNode(PGChildNodeView):
         :return:
         """
         try:
-            server = Server.query.filter_by(
-                user_id=current_user.id, id=sid
-            ).first()
-
+            server = Server.query.filter_by(id=sid).first()
+            shared_server = None
             if server is None:
                 return make_json_response(
                     success=0,
                     info=self.not_found_error_msg()
                 )
 
-            setattr(server, 'password', None)
+            if server.shared and server.user_id != current_user.id:
+                shared_server = SharedServer.query.filter_by(
+                    name=server.name, user_id=current_user.id,
+                    servergroup_id=gid).first()
+
+                if shared_server is None:
+                    return make_json_response(
+                        success=0,
+                        info=gettext("Could not find the required server.")
+                    )
+                server = ServerModule. \
+                    get_shared_server_properties(server, shared_server)
+
+            if server.shared and server.user_id != current_user.id:
+                setattr(shared_server, 'save_password', None)
+            else:
+                setattr(server, 'save_password', None)
+
             # If password was saved then clear the flag also
             # 0 is False in SQLite db
             if server.save_password:
-                setattr(server, 'save_password', 0)
+                if server.shared and server.user_id != current_user.id:
+                    setattr(shared_server, 'save_password', 0)
+                else:
+                    setattr(server, 'save_password', 0)
             db.session.commit()
         except Exception as e:
             current_app.logger.error(
@@ -1599,10 +1833,7 @@ class ServerNode(PGChildNodeView):
         :return:
         """
         try:
-            server = Server.query.filter_by(
-                user_id=current_user.id, id=sid
-            ).first()
-
+            server = Server.query.filter_by(id=sid).first()
             if server is None:
                 return make_json_response(
                     success=0,
diff --git a/web/pgadmin/browser/server_groups/servers/static/img/sharedserverbad.svg b/web/pgadmin/browser/server_groups/servers/static/img/sharedserverbad.svg
new file mode 100644
index 000000000..4455089ac
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/static/img/sharedserverbad.svg
@@ -0,0 +1,45 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.2.2, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#F2F2F2;}
+	.st1{fill:#9BA5B0;}
+	.st2{fill:none;stroke:#9BA5B0;stroke-width:0.75;stroke-miterlimit:1;}
+	.st3{fill:#F7F7F7;}
+	.st4{fill:#354A5F;}
+	.st5{fill:none;stroke:#D0021B;stroke-width:1.5;stroke-linecap:round;stroke-linejoin:round;}
+</style>
+<g>
+	<path class="st0" d="M6.7,2.1h4.6c0.6,0,1.1,0.5,1.1,1.1v0.8c0,0.6-0.5,1.1-1.1,1.1H6.7c-0.6,0-1.1-0.5-1.1-1.1V3.2
+		C5.6,2.6,6.1,2.1,6.7,2.1z"/>
+	<path class="st1" d="M11.3,2.5c0.4,0,0.8,0.3,0.8,0.8v0.8c0,0.4-0.3,0.8-0.8,0.8H6.7C6.3,4.8,6,4.5,6,4.1V3.2
+		c0-0.4,0.3-0.7,0.8-0.7H11.3 M11.3,1.7H6.7c-0.8,0-1.5,0.7-1.5,1.5v0.8c0,0.8,0.7,1.5,1.5,1.5h4.6c0.8,0,1.5-0.7,1.5-1.5V3.2
+		C12.8,2.4,12.1,1.7,11.3,1.7L11.3,1.7z"/>
+	<line class="st2" x1="8.9" y1="3.6" x2="6.8" y2="3.6"/>
+	<path class="st0" d="M6.7,5.2h4.6c0.6,0,1.1,0.5,1.1,1.1v0.8c0,0.6-0.5,1.1-1.1,1.1H6.7c-0.6,0-1.1-0.5-1.1-1.1V6.3
+		C5.6,5.7,6.1,5.2,6.7,5.2z"/>
+	<path class="st1" d="M11.3,5.6c0.4,0,0.8,0.3,0.8,0.8v0.8c0,0.4-0.3,0.8-0.8,0.8H6.7C6.3,7.9,6,7.6,6,7.1V6.3
+		c0-0.4,0.3-0.7,0.8-0.7H11.3 M11.3,4.8H6.7c-0.8,0-1.5,0.7-1.5,1.5v0.8c0,0.8,0.7,1.5,1.5,1.5h4.6c0.8,0,1.5-0.7,1.5-1.5V6.3
+		C12.8,5.5,12.1,4.8,11.3,4.8L11.3,4.8z"/>
+	<line class="st2" x1="8.9" y1="6.7" x2="6.8" y2="6.7"/>
+	<path class="st0" d="M6.7,8.3h4.6c0.6,0,1.1,0.5,1.1,1.1v0.9c0,0.6-0.5,1.1-1.1,1.1c0,0,0,0,0,0H6.7c-0.6,0-1.1-0.5-1.1-1.1l0,0
+		V9.4C5.6,8.8,6.1,8.3,6.7,8.3C6.7,8.3,6.7,8.3,6.7,8.3z"/>
+	<path class="st1" d="M11.3,8.7c0.4,0,0.8,0.3,0.8,0.8v0.8c0,0.4-0.3,0.8-0.8,0.8H6.7C6.3,11,6,10.7,6,10.3V9.4C6,9,6.3,8.7,6.7,8.7
+		H11.3 M11.3,7.9H6.7c-0.8,0-1.5,0.7-1.5,1.5v0.8c0,0.8,0.7,1.5,1.5,1.5h4.6c0.8,0,1.5-0.7,1.5-1.5V9.4C12.8,8.6,12.1,7.9,11.3,7.9z
+		"/>
+	<line class="st2" x1="8.9" y1="9.8" x2="6.8" y2="9.8"/>
+</g>
+<path class="st3" d="M11.6,10.5c-0.6,0.3-0.8,0.1-1.1-0.2c-0.1-0.2-0.3-0.4-0.6-0.5L7.1,8.5C6.8,8.4,6.4,8.3,6.1,8.3
+	C5.1,8.1,4.2,8.4,3.4,9l-1.4,1.1v2.7h0.9v-0.2l3.7,1.8c0.4,0.2,0.7,0.3,1.1,0.3c0.3,0,0.7-0.1,1-0.2l6.1-2.6
+	c0.3-0.1,0.6-0.4,0.7-0.7c0.1-0.3,0.1-0.7,0-1c-0.3-0.6-1.1-0.9-1.7-0.6l-0.4,0.2L11.6,10.5"/>
+<path class="st4" d="M15.5,10.2c-0.3-0.6-1.1-0.9-1.7-0.6l-0.4,0.2l-1.8,0.8l0.3,0.8l1.8-0.8l0.4-0.2c0.2-0.1,0.5,0,0.6,0.2
+	c0.1,0.1,0.1,0.2,0,0.3c0,0.1-0.1,0.2-0.2,0.2l-6.1,2.6c-0.5,0.2-1,0.2-1.4,0l-4.1-2v-1.2l1-0.8C4.5,9.2,5.2,9,6,9.1
+	c0.3,0,0.6,0.1,0.8,0.2l2.8,1.2c0.1,0,0.1,0.1,0.2,0.2c0.1,0.1,0.1,0.3,0,0.4c0,0,0,0.1-0.1,0.1c-0.1,0.1-0.3,0.2-0.5,0.1l-2.2-1
+	l-0.4,0.8l2.2,1c0.2,0.1,0.4,0.1,0.5,0.1c0.4,0,0.7-0.2,1-0.4c0.1-0.1,0.2-0.2,0.2-0.3c0.2-0.4,0.1-0.9-0.1-1.2
+	c-0.1-0.2-0.3-0.4-0.6-0.5L7.1,8.5C6.8,8.4,6.4,8.3,6.1,8.3C5.1,8.1,4.2,8.4,3.4,9L2.9,9.4V9.3c0-0.6-0.4-1-1-1H1.4
+	c-0.6,0-1,0.4-1,1v4.1c0,0.6,0.4,1,1,1h0.5c0.6,0,1-0.4,1-1v-0.6h0v-0.2l3.7,1.8c0.4,0.2,0.7,0.3,1.1,0.3c0.3,0,0.7-0.1,1-0.2
+	l6.1-2.6c0.3-0.1,0.6-0.4,0.7-0.7C15.7,10.9,15.7,10.5,15.5,10.2z M1.9,13.4H1.4V9.3h0.5L1.9,13.4z"/>
+<line class="st5" x1="13.8" y1="1.3" x2="10.8" y2="4.3"/>
+<line class="st5" x1="13.8" y1="4.3" x2="10.8" y2="1.3"/>
+</svg>
diff --git a/web/pgadmin/browser/server_groups/servers/static/js/server.js b/web/pgadmin/browser/server_groups/servers/static/js/server.js
index 8c43a55f7..f6197ede2 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,17 @@ define('pgadmin.node.server', [
           id: 'id', label: gettext('ID'), type: 'int', mode: ['properties'],
         },{
           id: 'name', label: gettext('Name'), type: 'text',
-          mode: ['properties', 'edit', 'create'],
-        },{
+          mode: ['properties', 'edit', 'create'], disabled: 'isShared',
+        },
+        {
           id: 'gid', label: gettext('Server group'), type: 'int',
           control: 'node-list-by-id', node: 'server_group',
-          mode: ['create', 'edit'], select2: {allowClear: false},
-        },{
+          mode: ['create', 'edit'], select2: {allowClear: false}, visible: 'isVisible',
+        },
+        {
+          id: 'server_owner', label: gettext('Shared Server Owner'), type: 'text', mode: ['properties'],
+        },
+        {
           id: 'server_type', label: gettext('Server type'), type: 'options',
           mode: ['properties'], visible: 'isConnected',
           'options': supported_servers,
@@ -773,11 +787,27 @@ define('pgadmin.node.server', [
           id: 'connect_now', controlLabel: gettext('Connect now?'), type: 'checkbox',
           group: null, mode: ['create'],
         },{
+          id: 'shared', label: gettext('Shared with all?'), type: 'switch',
+          mode: ['properties', 'create', 'edit'], 'options': {'size': 'mini'},
+          readonly: function(model){
+            var serverOwner = model.attributes.user_id;
+            if (!model.isNew() && serverOwner != current_user.id){
+              return true;
+            }
+            return false;
+          },visible: function(){
+            if (current_user.is_admin && pgAdmin.server_mode == 'True')
+              return true;
+
+            return false;
+          },
+        },
+        {
           id: 'comment', label: gettext('Comments'), type: 'multiline', group: null,
           mode: ['properties', 'edit', 'create'],
         },{
           id: 'host', label: gettext('Host name/address'), type: 'text', group: gettext('Connection'),
-          mode: ['properties', 'edit', 'create'],
+          mode: ['properties', 'edit', 'create'],disabled: 'isShared',
           control: Backform.InputControl.extend({
             onChange: function() {
               Backform.InputControl.prototype.onChange.apply(this, arguments);
@@ -798,7 +828,7 @@ define('pgadmin.node.server', [
           }),
         },{
           id: 'port', label: gettext('Port'), type: 'int', group: gettext('Connection'),
-          mode: ['properties', 'edit', 'create'], min: 1, max: 65535,
+          mode: ['properties', 'edit', 'create'], min: 1, max: 65535, disabled: 'isShared',
           control: Backform.InputControl.extend({
             onChange: function() {
               Backform.InputControl.prototype.onChange.apply(this, arguments);
@@ -819,7 +849,7 @@ define('pgadmin.node.server', [
           }),
         },{
           id: 'db', label: gettext('Maintenance database'), type: 'text', group: gettext('Connection'),
-          mode: ['properties', 'edit', 'create'], readonly: 'isConnected',
+          mode: ['properties', 'edit', 'create'], readonly: 'isConnected',disabled: 'isShared',
         },{
           id: 'username', label: gettext('Username'), type: 'text', group: gettext('Connection'),
           mode: ['properties', 'edit', 'create'],
@@ -1057,6 +1087,21 @@ define('pgadmin.node.server', [
           mode: ['properties', 'edit', 'create'], readonly: 'isConnected',
           min: 0,
         }],
+        isVisible: function(model){
+          var serverOwner = model.attributes.user_id;
+          if (!model.isNew() && serverOwner != current_user.id){
+            return false;
+          }
+          return true;
+
+        },
+        isShared: function(model){
+            var serverOwner = model.attributes.user_id;
+            if (!model.isNew() && serverOwner != current_user.id && model.attributes.shared){
+              return true;
+            }
+          return false;
+        },
         validate: function() {
           const validateModel = new modelValidation.ModelValidation(this);
           return validateModel.validate();
@@ -1153,7 +1198,36 @@ define('pgadmin.node.server', [
         }
       },
     });
+
     var connect_to_server = function(obj, data, tree, item, reconnect) {
+    // Open properties dialog in edit mode
+      const selectedTreeNode = tree.selected().length > 0 ? tree.selected() : tree.first();
+      const selectedTreeNodeData = selectedTreeNode && selectedTreeNode.length === 1 ? tree.itemData(selectedTreeNode) : undefined;
+      var server_url = obj.generate_url(item, 'obj', data, true);
+      // Fetch the updated data
+      $.get(server_url)
+        .done(function(res) {
+          if (res.shared && _.isNull(res.username) && data.user_id != current_user.id){
+            if (selectedTreeNodeData._type == 'server'){
+                pgAdmin.Browser.Node.callbacks.show_obj_properties.call(
+                  pgAdmin.Browser.Nodes[tree.itemData(item)._type], {action: 'edit'}
+                );
+                data.is_connecting = false;
+                tree.unload(item);
+                tree.setInode(item);
+                tree.addIcon(item, {icon: 'icon-shared-server-not-connected'});
+          }else{
+            data.is_connecting = false;
+            tree.unload(item);
+            tree.setInode(item);
+            tree.addIcon(item, {icon: 'icon-shared-server-not-connected'});
+          }
+          }
+          return;
+        }).always(function(){
+          data.is_connecting = false;
+        });
+
       var wasConnected = reconnect || data.connected,
         onFailure = function(
           xhr, status, error, _node, _data, _tree, _item, _wasConnected
@@ -1164,7 +1238,12 @@ define('pgadmin.node.server', [
           // Let's not change the status of the tree node now.
           if (!_wasConnected) {
             tree.setInode(_item);
-            tree.addIcon(_item, {icon: 'icon-server-not-connected'});
+            if (data.shared){
+              tree.addIcon(_item, {icon: 'icon-shared-server-not-connected'});
+            }else{
+              tree.addIcon(_item, {icon: 'icon-server-not-connected'});
+            }
+
           }
 
           Alertify.pgNotifier('error', xhr, error, function(msg) {
@@ -1321,7 +1400,11 @@ define('pgadmin.node.server', [
         _tree.unload(_item);
         _tree.setInode(_item);
         _tree.removeIcon(_item);
-        _tree.addIcon(_item, {icon: 'icon-server-not-connected'});
+        if (_data.shared){
+          _tree.addIcon(_item, {icon: 'icon-shared-server-not-connected'});
+        }else{
+          _tree.addIcon(_item, {icon: 'icon-server-not-connected'});
+        }
         obj.trigger('connect:cancelled', data._id, data.db, obj, _item, _data);
         pgBrowser.Events.trigger(
           'pgadmin:server:connect:cancelled', data._id, _item, _data, obj
@@ -1387,7 +1470,11 @@ define('pgadmin.node.server', [
         })
         .fail(function(xhr, status, error) {
           tree.setInode(item);
-          tree.addIcon(item, {icon: 'icon-server-not-connected'});
+          if (data.shared){
+            tree.addIcon(item, {icon: 'icon-shared-server-not-connected'});
+          }else{
+            tree.addIcon(item, {icon: 'icon-server-not-connected'});
+          }
           Alertify.pgRespErrorNotify(xhr, error);
         });
     };
diff --git a/web/pgadmin/browser/server_groups/servers/templates/css/servers.css b/web/pgadmin/browser/server_groups/servers/templates/css/servers.css
index 93c4536d4..829c273aa 100644
--- a/web/pgadmin/browser/server_groups/servers/templates/css/servers.css
+++ b/web/pgadmin/browser/server_groups/servers/templates/css/servers.css
@@ -15,3 +15,21 @@
   vertical-align: middle;
   height: 1.3em;
 }
+
+.icon-shared-server-not-connected {
+  background-image: url('{{ url_for('NODE-server.static', filename='img/sharedserverbad.svg') }}') !important;
+  background-repeat: no-repeat;
+  background-size: 20px !important;
+  align-content: center;
+  vertical-align: middle;
+  height: 1.3em;
+}
+
+.icon-shared-server-not-connected {
+  background-image: url('{{ url_for('NODE-server.static', filename='img/sharedserverbad.svg') }}') !important;
+  background-repeat: no-repeat;
+  background-size: 20px !important;
+  align-content: center;
+  vertical-align: middle;
+  height: 1.3em;
+}
diff --git a/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json b/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json
new file mode 100644
index 000000000..117a2982b
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json
@@ -0,0 +1,862 @@
+{
+  "add_server": [
+    {
+      "name": "Add server with service id",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+        "service": "TestDB"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Test default server url",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Add server with connect timeout",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "test_data": {
+        "connect_timeout": 5
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Add server using SSH tunnel with password",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "ssh_tunnel": true,
+      "with_password": true,
+      "save_password": false,
+      "test_data": {
+        "use_ssh_tunnel": 1,
+        "tunnel_host": "127.0.0.1",
+        "tunnel_port": 22,
+        "tunnel_username": "user",
+        "tunnel_authentication": 1,
+        "tunnel_identity_file": "pkey_rsa"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Add server using SSH tunnel with identity file",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "ssh_tunnel": true,
+      "with_password": false,
+      "save_password": false,
+      "test_data": {
+        "use_ssh_tunnel": 1,
+        "tunnel_host": "127.0.0.1",
+        "tunnel_port": 22,
+        "tunnel_username": "user",
+        "tunnel_authentication": 1,
+        "tunnel_identity_file": "pkey_rsa"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Add server using SSH tunnel with password and saved it",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "ssh_tunnel": true,
+      "with_password": true,
+      "save_password": true,
+      "test_data": {
+        "use_ssh_tunnel": 1,
+        "tunnel_host": "127.0.0.1",
+        "tunnel_port": 22,
+        "tunnel_username": "user",
+        "tunnel_authentication": 0,
+        "tunnel_password": "123456"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Add server using SSH tunnel with identity file and save the password",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "ssh_tunnel": true,
+      "with_password": false,
+      "save_password": true,
+      "test_data": {
+        "use_ssh_tunnel": 1,
+        "tunnel_host": "127.0.0.1",
+        "tunnel_port": 22,
+        "tunnel_username": "user",
+        "tunnel_authentication": 1,
+        "tunnel_identity_file": "pkey_rsa",
+        "tunnel_password": "123456"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Add server with password and save password to true",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "with_pwd": true,
+      "with_save": true,
+      "owner_server": true,
+      "test_data": {
+        "service": "TestDB"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Add server with password and save password to false",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "with_pwd": true,
+      "with_save": false,
+      "owner_server": true,
+      "test_data": {
+        "service": "TestDB"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Add server without password and save password to true",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "with_pwd": false,
+      "with_save": true,
+      "owner_server": true,
+      "test_data": {
+        "service": "TestDB"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Add server with connect now",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+        "service": "TestDB"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    }
+  ],
+  "is_password_saved": [
+    {
+      "name": "Connect server with 'save password",
+      "url": "/browser/server/connect/",
+      "is_positive_test": true,
+      "test_data": {
+        "is_password_saved": true
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200,
+        "message": "Server connected."
+      }
+    }
+  ],
+  "get_server": [
+    {
+      "name": "Get a server URL",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Reload a server configuration",
+      "url": "/browser/server/reload/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Get a server URL using wrong server id",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "incorrect_server_id": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 410
+      }
+    },
+    {
+      "name": "Get a server Node dependants",
+      "url": "/browser/server/dependent/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Get a server Node dependency",
+      "url": "/browser/server/dependency/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Get a server Node sql",
+      "url": "/browser/server/sql/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Get a server Node msql",
+      "url": "/browser/server/msql/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Get a server Node statistics",
+      "url": "/browser/server/stats/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Get a server pgpass details",
+      "url": "/browser/server/check_pgpass/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 410
+      }
+    }
+  ],
+  "get_shared_server": [
+    {
+      "name": "Get a shared server",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "shared": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Get a all shared server",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "shared": true,
+      "no_server_id": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Get the all available shared server",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "no_server_id": true,
+      "shared": true,
+      "server_list": true,
+      "mocking_required": false,
+      "mock_data": {
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    }
+  ],
+  "get_all_server": [
+    {
+      "name": "Get the all children of server",
+      "url": "/browser/server/children/",
+      "is_positive_test": true,
+      "children": true,
+      "mocking_required": false,
+      "mock_data": {
+      },
+      "expected_data": {
+        "status_code": 500
+      }
+    },
+    {
+      "name": "Get the all available servers",
+      "url": "/browser/server/nodes/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Get the all available server of server group",
+      "url": "/browser/server/nodes/",
+      "is_positive_test": true,
+      "server_list": true,
+      "mocking_required": false,
+      "mock_data": {
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Get the all available server of server group",
+      "url": "/browser/server/nodes/",
+      "is_positive_test": true,
+      "server_list": true,
+      "servers": true,
+      "mocking_required": false,
+      "mock_data": {
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Get the all connected servers",
+      "url": "/browser/server/nodes/",
+      "is_positive_test": true,
+      "server_list": true,
+      "servers": true,
+      "connected": true,
+      "mocking_required": false,
+      "mock_data": {
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    }
+  ],
+  "connect_server": [
+    {
+      "name": "Get a server connection",
+      "url": "/browser/server/connect/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "connect to a server using password",
+      "url": "/browser/server/connect/",
+      "is_positive_test": true,
+      "connect": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Disconnect server test",
+      "url": "/browser/server/connect/",
+      "is_positive_test": true,
+      "disconnect": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Disconnect server when wrong server id passed",
+      "url": "/browser/server/connect/",
+      "is_positive_test": true,
+      "disconnect": true,
+      "wrong_server_id": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 400
+      }
+    },
+    {
+      "name": "Reload a server configuration",
+      "url": "/browser/server/reload/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Error while creating server restore point",
+      "url": "/browser/server/restore_point/",
+      "is_positive_test": true,
+      "restore_point": true,
+      "test_data": {
+        "Named restore point created": "PLACE_HOLDER"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 500
+      }
+    }
+  ],
+  "delete_server": [
+    {
+      "name": "Delete a server URL",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Disconnect server test",
+      "url": "/browser/server/connect/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Error while fetching a server to delete",
+      "url": "/browser/server/obj/",
+      "is_positive_test": false,
+      "mocking_required": true,
+      "mock_data": {
+        "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict",
+        "return_value": "(True, 'Mocked Internal Server Error')"
+      },
+      "expected_data": {
+        "status_code": 500
+      }
+    },
+    {
+      "name": "server not found while deleting a server",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "invalid_server_id": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 410
+      }
+    }
+  ],
+  "update_server": [
+    {
+      "name": "update a server name",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "sslcompression": 1
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "update a server details without data",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "test_data": {},
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Update server with wrong server id",
+      "url": "/browser/server/obj/",
+      "is_positive_test": false,
+      "clear_save_password": true,
+      "wrong_server_id": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 410
+      }
+    },
+    {
+      "name": "Update server with incorrect hostaddr",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "hostaddr": "PLACE_HOLDER",
+        "db_res": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 400
+      }
+    },
+    {
+      "name": "update a server , make server shared",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "shared": true
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Clear saved password",
+      "url": "/browser/server/clear_saved_password/",
+      "is_positive_test": true,
+      "clear_save_password": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Clear saved password with wrong server id",
+      "url": "/browser/server/clear_saved_password/",
+      "is_positive_test": false,
+      "clear_save_password": true,
+      "wrong_server_id": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "wal replay",
+      "url": "/browser/server/wal_replay/",
+      "is_positive_test": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 410
+      }
+    },
+    {
+      "name": "Clear ssh tunnel password",
+      "url": "/browser/server/clear_sshtunnel_password/",
+      "is_positive_test": true,
+      "clear_save_password": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Clear ssh tunnel password with wrong server id",
+      "url": "/browser/server/clear_sshtunnel_password/",
+      "is_positive_test": false,
+      "clear_save_password": true,
+      "wrong_server_id": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "error while clearing a ssh password",
+      "url": "/browser/server/clear_sshtunnel_password/",
+      "is_positive_test": false,
+      "error_clearing_password": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    }
+  ],
+  "update_shared_server": [
+    {
+      "name": "update a server name",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "sslcompression": 1
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "update a server details without data",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "test_data": {},
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Update server with wrong server id",
+      "url": "/browser/server/obj/",
+      "is_positive_test": false,
+      "clear_save_password": true,
+      "wrong_server_id": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 410
+      }
+    },
+    {
+      "name": "Update server with incorrect hostaddr",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "hostaddr": "PLACE_HOLDER",
+        "db_res": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 400
+      }
+    },
+    {
+      "name": "update a server , make server shared",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "shared": true
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Clear saved password when login user is not owner of server",
+      "url": "/browser/server/clear_saved_password/",
+      "is_positive_test": true,
+      "clear_save_password": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Clear saved password with wrong server id",
+      "url": "/browser/server/clear_saved_password/",
+      "is_positive_test": false,
+      "clear_save_password": true,
+      "wrong_server_id": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Clear ssh tunnel password",
+      "url": "/browser/server/clear_sshtunnel_password/",
+      "is_positive_test": true,
+      "clear_save_password": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Clear ssh tunnel password with wrong server id",
+      "url": "/browser/server/clear_sshtunnel_password/",
+      "is_positive_test": false,
+      "clear_save_password": true,
+      "wrong_server_id": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "error while clearing a ssh password",
+      "url": "/browser/server/clear_sshtunnel_password/",
+      "is_positive_test": false,
+      "error_clearing_password": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    }
+  ],
+  "delete_multiple_server": [
+    {
+      "name": "Delete multiple server",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    }
+  ]
+}
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_add_server.py b/web/pgadmin/browser/server_groups/servers/tests/test_add_server.py
new file mode 100644
index 000000000..1b23fcdb8
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_add_server.py
@@ -0,0 +1,84 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import json
+
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from . import utils as servers_utils
+
+
+class AddServerTest(BaseTestGenerator):
+    """ This class will add the servers under default server group. """
+
+    scenarios = utils.generate_scenarios('add_server',
+                                         servers_utils.test_cases)
+
+    def setUp(self):
+        pass
+
+    def create_server(self, url):
+        return self.tester.post(
+            url,
+            data=json.dumps(self.server),
+            content_type='html/json'
+        )
+
+    def runTest(self):
+        """ This function will add the server under default server group."""
+        url = "{0}{1}/".format(self.url, utils.SERVER_GROUP)
+
+        # Add service name in the config
+        if 'connect_timeout' in self.test_data:
+            self.server['connect_timeout'] = self.test_data['connect_timeout']
+        elif 'shared' in self.test_data:
+            self.server['shared'] = self.test_data['shared']
+        elif 'service' in self.test_data:
+            self.server['service'] = self.test_data['service']
+
+        if hasattr(self, 'ssh_tunnel'):
+            self.server['use_ssh_tunnel'] = self.test_data['use_ssh_tunnel']
+            self.server['tunnel_host'] = self.test_data['tunnel_host']
+            self.server['tunnel_port'] = self.test_data['tunnel_port']
+            self.server['tunnel_username'] = self.test_data['tunnel_username']
+
+            if self.with_password:
+                self.server['tunnel_authentication'] = self.test_data[
+                    'tunnel_authentication']
+            else:
+                self.server['tunnel_authentication'] = 1
+                self.server['tunnel_identity_file'] = 'pkey_rsa'
+
+            if self.save_password:
+                self.server['tunnel_password'] = self.test_data[
+                    'tunnel_password']
+        if 'connect_now' in self.test_data:
+            self.server['connect_now'] = self.test_data['connect_now']
+            self.server['password'] = self.server['db_password']
+
+        if self.is_positive_test:
+            if hasattr(self, 'with_save'):
+                self.server['save_password'] = self.with_save
+            if hasattr(self, 'with_pwd') and not self.with_pwd:
+                # Remove the password from server object
+                db_password = self.server['db_password']
+                del self.server['db_password']
+            response = self.create_server(url)
+        self.assertEquals(response.status_code,
+                          self.expected_data["status_code"])
+        response_data = json.loads(response.data.decode('utf-8'))
+        self.server_id = response_data['node']['_id']
+
+        if hasattr(self, 'with_pwd') and not self.with_pwd:
+            # Remove the password from server object
+            self.server['db_password'] = db_password
+
+    def tearDown(self):
+        """This function delete the server from SQLite """
+        utils.delete_server_with_api(self.tester, self.server_id)
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_connect_timeout.py b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_connect_timeout.py
deleted file mode 100644
index 94d555617..000000000
--- a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_connect_timeout.py
+++ /dev/null
@@ -1,47 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2020, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-import json
-
-from pgadmin.utils.route import BaseTestGenerator
-from regression.python_test_utils import test_utils as utils
-
-
-class ServersWithConnectTimeoutAddTestCase(BaseTestGenerator):
-    """ This class will add the servers under default server group. """
-
-    scenarios = [
-        # Fetch the default url for server object
-        (
-            'Default Server Node url', dict(
-                url='/browser/server/obj/'
-            )
-        )
-    ]
-
-    def setUp(self):
-        pass
-
-    def runTest(self):
-        """ This function will add the server under default server group."""
-        url = "{0}{1}/".format(self.url, utils.SERVER_GROUP)
-        # Add service name in the config
-        self.server['connect_timeout'] = 5
-        response = self.tester.post(
-            url,
-            data=json.dumps(self.server),
-            content_type='html/json'
-        )
-        self.assertEqual(response.status_code, 200)
-        response_data = json.loads(response.data.decode('utf-8'))
-        self.server_id = response_data['node']['_id']
-
-    def tearDown(self):
-        """This function delete the server from SQLite """
-        utils.delete_server_with_api(self.tester, self.server_id)
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py
deleted file mode 100644
index 9b4d4d377..000000000
--- a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_service_id.py
+++ /dev/null
@@ -1,47 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2020, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-import json
-
-from pgadmin.utils.route import BaseTestGenerator
-from regression.python_test_utils import test_utils as utils
-
-
-class ServersWithServiceIDAddTestCase(BaseTestGenerator):
-    """ This class will add the servers under default server group. """
-
-    scenarios = [
-        # Fetch the default url for server object
-        (
-            'Default Server Node url', dict(
-                url='/browser/server/obj/'
-            )
-        )
-    ]
-
-    def setUp(self):
-        pass
-
-    def runTest(self):
-        """ This function will add the server under default server group."""
-        url = "{0}{1}/".format(self.url, utils.SERVER_GROUP)
-        # Add service name in the config
-        self.server['service'] = "TestDB"
-        response = self.tester.post(
-            url,
-            data=json.dumps(self.server),
-            content_type='html/json'
-        )
-        self.assertEqual(response.status_code, 200)
-        response_data = json.loads(response.data.decode('utf-8'))
-        self.server_id = response_data['node']['_id']
-
-    def tearDown(self):
-        """This function delete the server from SQLite """
-        utils.delete_server_with_api(self.tester, self.server_id)
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_ssh_tunnel.py b/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_ssh_tunnel.py
deleted file mode 100644
index 7c0b005d9..000000000
--- a/web/pgadmin/browser/server_groups/servers/tests/test_add_server_with_ssh_tunnel.py
+++ /dev/null
@@ -1,82 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2020, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-import json
-
-from pgadmin.utils.route import BaseTestGenerator
-from regression.python_test_utils import test_utils as utils
-
-
-class ServersWithSSHTunnelAddTestCase(BaseTestGenerator):
-    """ This class will add the servers under default server group. """
-
-    scenarios = [
-        (
-            'Add server using SSH tunnel with password', dict(
-                url='/browser/server/obj/',
-                with_password=True,
-                save_password=False,
-            )
-        ),
-        (
-            'Add server using SSH tunnel with identity file', dict(
-                url='/browser/server/obj/',
-                with_password=False,
-                save_password=False,
-            )
-        ),
-        (
-            'Add server using SSH tunnel with password and saved it', dict(
-                url='/browser/server/obj/',
-                with_password=True,
-                save_password=True,
-            )
-        ),
-        (
-            'Add server using SSH tunnel with identity file and save the '
-            'password', dict(
-                url='/browser/server/obj/',
-                with_password=False,
-                save_password=True,
-            )
-        ),
-    ]
-
-    def setUp(self):
-        pass
-
-    def runTest(self):
-        """ This function will add the server under default server group."""
-        url = "{0}{1}/".format(self.url, utils.SERVER_GROUP)
-        # Add service name in the config
-        self.server['use_ssh_tunnel'] = 1
-        self.server['tunnel_host'] = '127.0.0.1'
-        self.server['tunnel_port'] = 22
-        self.server['tunnel_username'] = 'user'
-        if self.with_password:
-            self.server['tunnel_authentication'] = 0
-        else:
-            self.server['tunnel_authentication'] = 1
-            self.server['tunnel_identity_file'] = 'pkey_rsa'
-
-        if self.save_password:
-            self.server['tunnel_password'] = '123456'
-
-        response = self.tester.post(
-            url,
-            data=json.dumps(self.server),
-            content_type='html/json'
-        )
-        self.assertEqual(response.status_code, 200)
-        response_data = json.loads(response.data.decode('utf-8'))
-        self.server_id = response_data['node']['_id']
-
-    def tearDown(self):
-        """This function delete the server from SQLite """
-        utils.delete_server_with_api(self.tester, self.server_id)
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_all_server_get.py b/web/pgadmin/browser/server_groups/servers/tests/test_all_server_get.py
new file mode 100644
index 000000000..cd35a7a05
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_all_server_get.py
@@ -0,0 +1,78 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+import random
+
+from pgadmin.utils.route import BaseTestGenerator
+from regression import parent_node_dict
+from regression.python_test_utils import test_utils as utils
+from . import utils as servers_utils
+
+
+class AllServersGetTestCase(BaseTestGenerator):
+    """
+    This class will fetch added servers under default server group
+    by response code.
+    """
+
+    scenarios = utils.generate_scenarios('get_all_server',
+                                         servers_utils.test_cases)
+
+    def setUp(self):
+        """This function add the server to test the GET API"""
+        self.server['password'] = 'edb'
+        self.server_id = utils.create_server(self.server)
+        server_dict = {"server_id": self.server_id}
+        utils.write_node_info("sid", server_dict)
+
+    def get_server(self):
+        return self.tester.get(self.url, follow_redirects=True)
+
+    def connect_to_server(self, url):
+        return self.tester.post(
+            url,
+            data=self.server,
+            content_type='html/json'
+        )
+
+    def runTest(self):
+        """ This function will fetch the added servers to object browser. """
+        server_id = parent_node_dict["server"][-1]["server_id"]
+        if not server_id:
+            raise Exception("Server not found to test GET API")
+        response = None
+        if self.is_positive_test:
+            if hasattr(self, 'invalid_server_group'):
+                self.url = self.url + '{0}/{1}?_={1}'.format(
+                    utils.SERVER_GROUP, random.randint(1, 9999999))
+            elif hasattr(self, 'children'):
+
+                self.url = self.url + '{0}/{1}'.format(
+                    utils.SERVER_GROUP, server_id)
+            elif hasattr(self, 'server_list'):
+                if hasattr(self, 'servers'):
+                    server_id = ''
+                self.url = self.url + '{0}/{1}'.format(
+                    utils.SERVER_GROUP, server_id)
+            else:
+                if hasattr(self, "connected"):
+                    url = '/browser/server/connect/' + '{0}/{1}'.format(
+                        utils.SERVER_GROUP,
+                        self.server_id)
+                    self.server['password'] = 'edb'
+
+                    self.connect_to_server(url)
+                self.url = self.url + '{0}/{1}?_={2}'.format(
+                    utils.SERVER_GROUP, server_id, random.randint(1, 9999999))
+            response = self.get_server()
+        self.assertEquals(response.status_code,
+                          self.expected_data["status_code"])
+
+    def tearDown(self):
+        """This function delete the server from SQLite """
+        utils.delete_server_with_api(self.tester, self.server_id)
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_check_connect.py b/web/pgadmin/browser/server_groups/servers/tests/test_check_connect.py
new file mode 100644
index 000000000..6d49cea7b
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_check_connect.py
@@ -0,0 +1,107 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+from pgadmin.utils.route import BaseTestGenerator
+from regression import parent_node_dict
+from regression.python_test_utils import test_utils as utils
+from . import utils as servers_utils
+import json
+
+
+class ServersConnectTestCase(BaseTestGenerator):
+    """
+    This class will fetch added servers under default server group
+    by response code.
+    """
+
+    scenarios = utils.generate_scenarios('connect_server',
+                                         servers_utils.test_cases)
+
+    def get_ssh_tunnel(self):
+        print("in_get_ssh")
+        self.server['use_ssh_tunnel'] = 1
+        self.server['tunnel_host'] = '127.0.0.1'
+        self.server['tunnel_port'] = 22
+        self.server['tunnel_username'] = 'user'
+        if self.with_password:
+            self.server['tunnel_authentication'] = 0
+        else:
+            self.server['tunnel_authentication'] = 1
+            self.server['tunnel_identity_file'] = 'pkey_rsa'
+
+        if self.save_password:
+            self.server['tunnel_password'] = '123456'
+
+    def setUp(self):
+        """This function add the server to test the GET API"""
+
+        self.server_id = utils.create_server(self.server)
+        server_dict = {"server_id": self.server_id}
+        utils.write_node_info("sid", server_dict)
+
+    def get_server_connection(self, server_id):
+        return self.tester.get(self.url + str(utils.SERVER_GROUP) + '/' +
+                               str(server_id),
+                               follow_redirects=True)
+
+    def server_disonnect(self, server_id):
+        return self.tester.delete(self.url + str(utils.SERVER_GROUP) + '/' +
+                                  str(server_id))
+
+    def connect_to_server(self, url):
+        return self.tester.post(
+            url,
+            data=json.dumps(self.server),
+            content_type='html/json'
+        )
+
+    def add_server_details(self, url):
+        return self.tester.post(
+            url,
+            data=str(self.test_data),
+            content_type='html/json'
+        )
+
+    def runTest(self):
+        """ This function will fetch the added servers to object browser. """
+        server_id = parent_node_dict["server"][-1]["server_id"]
+        if not server_id:
+            raise Exception("Server not found to test GET API")
+        response = None
+        if self.is_positive_test:
+            if hasattr(self, 'disconnect'):
+                if hasattr(self, 'wrong_server_id'):
+                    server_id = 99999
+                response = self.server_disonnect(server_id)
+            elif hasattr(self, "connect"):
+                url = self.url + '{0}/{1}'.format(
+                    utils.SERVER_GROUP,
+                    self.server_id)
+                self.server['password'] = self.server['db_password']
+                response = self.connect_to_server(url)
+            elif hasattr(self, 'restore_point') or hasattr(self,
+                                                           'change_password'):
+                connect_url = '/browser/server/connect/{0}/{1}'.format(
+                    utils.SERVER_GROUP,
+                    self.server_id)
+                url = self.url + '{0}/{1}'.format(
+                    utils.SERVER_GROUP,
+                    self.server_id)
+
+                self.connect_to_server(connect_url)
+                response = self.add_server_details(url)
+            else:
+                response = self.get_server_connection(server_id)
+
+        self.assertEquals(response.status_code,
+                          self.expected_data["status_code"])
+
+    def tearDown(self):
+        """This function delete the server from SQLite """
+        utils.delete_server_with_api(self.tester, self.server_id)
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_is_password_saved.py b/web/pgadmin/browser/server_groups/servers/tests/test_is_password_saved.py
new file mode 100644
index 000000000..7d144d3d7
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_is_password_saved.py
@@ -0,0 +1,52 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+import json
+
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from . import utils as servers_utils
+
+
+class IsPasswordSaved(BaseTestGenerator):
+    """ This class will test the save password functionality. """
+
+    scenarios = utils.generate_scenarios('is_password_saved',
+                                         servers_utils.test_cases)
+
+    def setUp(self):
+        self.server_id = utils.create_server(self.server)
+        server_dict = {"server_id": self.server_id}
+        utils.write_node_info("sid", server_dict)
+
+    def runTest(self):
+        """This function will execute the connect server APIs"""
+        response = self.tester.post(
+            self.url + str(utils.SERVER_GROUP) + '/' + str(self.server_id),
+            data=dict(
+                password=self.server['db_password'],
+                save_password='on'),
+            follow_redirects=True)
+
+        expected_status_code = self.expected_data["status_code"]
+        actual_status_code = response.status_code
+        self.assertEquals(actual_status_code, expected_status_code)
+        response_data = json.loads(response.data.decode('utf-8'))
+
+        expected_message = self.expected_data["message"]
+        actual_message = response_data["info"]
+        self.assertEquals(actual_message, expected_message)
+
+        expected_is_password_saved = self.test_data["is_password_saved"]
+        actual_is_password_saved = response_data["data"]["is_password_saved"]
+        self.assertEquals(actual_is_password_saved, expected_is_password_saved)
+
+    def tearDown(self):
+        """This function delete the server from SQLite """
+        utils.delete_server_with_api(self.tester, self.server_id)
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_server_add.py b/web/pgadmin/browser/server_groups/servers/tests/test_server_add.py
deleted file mode 100644
index 8444e1d72..000000000
--- a/web/pgadmin/browser/server_groups/servers/tests/test_server_add.py
+++ /dev/null
@@ -1,86 +0,0 @@
-##########################################################################
-#
-# pgAdmin 4 - PostgreSQL Tools
-#
-# Copyright (C) 2013 - 2020, The pgAdmin Development Team
-# This software is released under the PostgreSQL Licence
-#
-##########################################################################
-
-import json
-import copy
-from pgadmin.utils.route import BaseTestGenerator
-from regression.python_test_utils import test_utils as utils
-
-
-class ServersAddTestCase(BaseTestGenerator):
-    """ This class will add the servers under default server group. """
-
-    scenarios = [
-        # Fetch the default url for server object
-        ('Default Server Node url', dict(url='/browser/server/obj/'))
-    ]
-
-    def setUp(self):
-        pass
-
-    def runTest(self):
-        """ This function will add the server under default server group."""
-        url = "{0}{1}/".format(self.url, utils.SERVER_GROUP)
-        response = self.tester.post(url, data=json.dumps(self.server),
-                                    content_type='html/json')
-        self.assertEqual(response.status_code, 200)
-        response_data = json.loads(response.data.decode('utf-8'))
-        self.server_id = response_data['node']['_id']
-        server_dict = {"server_id": int(self.server_id)}
-        utils.write_node_info("sid", server_dict)
-
-    def tearDown(self):
-        """This function delete the server from SQLite """
-        utils.delete_server_with_api(self.tester, self.server_id)
-
-
-class AddServersWithSavePasswordTestCase(BaseTestGenerator):
-    """ This class will add the servers under default server group. """
-
-    scenarios = [
-        # Fetch the default url for server object
-        ('Add server with password and save password to true',
-         dict(url='/browser/server/obj/', with_pwd=True, with_save=True)),
-        ('Add server with password and save password to false',
-         dict(url='/browser/server/obj/', with_pwd=True, with_save=False)),
-        ('Add server without password and save password to true',
-         dict(url='/browser/server/obj/', with_pwd=False, with_save=True)),
-    ]
-
-    def setUp(self):
-        pass
-
-    def runTest(self):
-        """ This function will add the server under default server group."""
-        url = "{0}{1}/".format(self.url, utils.SERVER_GROUP)
-        _server = copy.deepcopy(self.server)
-        # Update the flag as required
-        _server['save_password'] = self.with_save
-        if not self.with_pwd:
-            # Remove the password from server object
-            del _server['db_password']
-
-        response = self.tester.post(url, data=json.dumps(_server),
-                                    content_type='html/json')
-        self.assertEqual(response.status_code, 200)
-        response_data = json.loads(response.data.decode('utf-8'))
-        self.server_id = response_data['node']['_id']
-        server_dict = {"server_id": int(self.server_id)}
-        # Fetch the node info to check if password was saved or not
-        response = self.tester.get(self.url.replace('obj', 'nodes') +
-                                   str(utils.SERVER_GROUP) + '/' +
-                                   str(self.server_id),
-                                   follow_redirects=True)
-        self.assertEqual(response.status_code, 200)
-        self.assertTrue('is_password_saved' in response.json['result'])
-        utils.write_node_info("sid", server_dict)
-
-    def tearDown(self):
-        """This function delete the server from SQLite """
-        utils.delete_server_with_api(self.tester, self.server_id)
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_server_delete.py b/web/pgadmin/browser/server_groups/servers/tests/test_server_delete.py
index 30917bd3c..78911f5a8 100644
--- a/web/pgadmin/browser/server_groups/servers/tests/test_server_delete.py
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_server_delete.py
@@ -9,15 +9,14 @@
 
 from pgadmin.utils.route import BaseTestGenerator
 from regression.python_test_utils import test_utils as utils
+from . import utils as servers_utils
 
 
 class ServerDeleteTestCase(BaseTestGenerator):
     """ This class will delete the last server present under tree node."""
 
-    scenarios = [
-        # Fetching the default url for server node
-        ('Default Server Node url', dict(url='/browser/server/obj/'))
-    ]
+    scenarios = utils.generate_scenarios('delete_server',
+                                         servers_utils.test_cases)
 
     def setUp(self):
         """This function add the server to test the DELETE API"""
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_server_get.py b/web/pgadmin/browser/server_groups/servers/tests/test_server_get.py
index 0b4dc183b..5a83e2a08 100644
--- a/web/pgadmin/browser/server_groups/servers/tests/test_server_get.py
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_server_get.py
@@ -10,6 +10,8 @@
 from pgadmin.utils.route import BaseTestGenerator
 from regression import parent_node_dict
 from regression.python_test_utils import test_utils as utils
+from . import utils as servers_utils
+import json
 
 
 class ServersGetTestCase(BaseTestGenerator):
@@ -18,26 +20,50 @@ class ServersGetTestCase(BaseTestGenerator):
     by response code.
     """
 
-    scenarios = [
-        # Fetch the default url for server node
-        ('Default Server Node url', dict(url='/browser/server/obj/'))
-    ]
+    scenarios = utils.generate_scenarios('get_server',
+                                         servers_utils.test_cases)
 
     def setUp(self):
         """This function add the server to test the GET API"""
-        self.server_id = utils.create_server(self.server)
+        if hasattr(self, 'shared'):
+
+            self.server['shared'] = True
+            url = "{0}{1}/".format(self.url, utils.SERVER_GROUP)
+            response = self.tester.post(
+                url,
+                data=json.dumps(self.server),
+                content_type='html/json'
+            )
+            response_data = json.loads(response.data.decode('utf-8'))
+            self.server_id = response_data['node']['_id']
+        else:
+            self.server_id = utils.create_server(self.server)
         server_dict = {"server_id": self.server_id}
         utils.write_node_info("sid", server_dict)
 
+    def get_server(self, server_id):
+        return self.tester.get(self.url + str(utils.SERVER_GROUP) + '/' +
+                               str(server_id),
+                               follow_redirects=True)
+
     def runTest(self):
         """ This function will fetch the added servers to object browser. """
         server_id = parent_node_dict["server"][-1]["server_id"]
         if not server_id:
             raise Exception("Server not found to test GET API")
-        response = self.tester.get(self.url + str(utils.SERVER_GROUP) + '/' +
-                                   str(server_id),
-                                   follow_redirects=True)
-        self.assertEqual(response.status_code, 200)
+        response = None
+        if self.is_positive_test:
+            if hasattr(self, "incorrect_server_id"):
+                server_id = 9999
+            if hasattr(self, "server_list"):
+                server_id = ''
+            if hasattr(self, "server_node"):
+                server_id = ''
+            if hasattr(self, 'shared'):
+                server_id = self.server_id
+            response = self.get_server(server_id)
+        self.assertEquals(response.status_code,
+                          self.expected_data["status_code"])
 
     def tearDown(self):
         """This function delete the server from SQLite """
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_server_put.py b/web/pgadmin/browser/server_groups/servers/tests/test_server_put.py
index 31c7a476e..74674c7cb 100644
--- a/web/pgadmin/browser/server_groups/servers/tests/test_server_put.py
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_server_put.py
@@ -11,32 +11,61 @@ import json
 
 from pgadmin.utils.route import BaseTestGenerator
 from regression.python_test_utils import test_utils as utils
+from . import utils as servers_utils
 
 
 class ServerUpdateTestCase(BaseTestGenerator):
     """ This class will update server's comment field. """
 
-    scenarios = [
-        # Fetching the default url for server node
-        ('Default Server Node url', dict(url='/browser/server/obj/'))
-    ]
+    scenarios = utils.generate_scenarios('update_server',
+                                         servers_utils.test_cases)
 
     def setUp(self):
         """This function add the server to test the PUT API"""
-        self.server_id = utils.create_server(self.server)
+        if hasattr(self, 'clear_save_password'):
+            self.server['save_password'] = 1
+        create_server_url = "/browser/server/obj/{0}/".format(
+            utils.SERVER_GROUP)
+
+        self.server_id = \
+            servers_utils.create_server_with_api(self, create_server_url)
         server_dict = {"server_id": self.server_id}
         utils.write_node_info("sid", server_dict)
 
+    def update_server(self):
+        return self.tester.put(
+            self.url + str(utils.SERVER_GROUP) + '/' +
+            str(self.server_id), data=json.dumps(self.test_data),
+            content_type='html/json')
+
+    def connect_to_server(self, url):
+        return self.tester.post(
+            url,
+            data=json.dumps(self.server),
+            content_type='html/json'
+        )
+
     def runTest(self):
         """This function update the server details"""
         if not self.server_id:
             raise Exception("No server to update.")
-        data = {"comment": self.server['comment'], "id": self.server_id}
-        put_response = self.tester.put(
-            self.url + str(utils.SERVER_GROUP) + '/' +
-            str(self.server_id), data=json.dumps(data),
-            content_type='html/json')
-        self.assertEqual(put_response.status_code, 200)
+        if 'comment' in self.test_data:
+            self.test_data["comment"] = self.server['comment']
+        self.test_data["id"] = self.server_id
+        if self.is_positive_test:
+            if hasattr(self, 'server_connected'):
+                url = '/browser/server/connect/{0}/{1}'.format(
+                    utils.SERVER_GROUP,
+                    self.server_id)
+                self.server['password'] = self.server['db_password']
+                self.connect_to_server(url)
+            put_response = self.update_server()
+        else:
+            if hasattr(self, 'wrong_server_id'):
+                self.server_id = 9999
+            put_response = self.update_server()
+        self.assertEquals(put_response.status_code,
+                          self.expected_data["status_code"])
 
     def tearDown(self):
         """This function delete the server from SQLite"""
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_shared_server.py b/web/pgadmin/browser/server_groups/servers/tests/test_shared_server.py
new file mode 100644
index 000000000..b3f8215a4
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_shared_server.py
@@ -0,0 +1,134 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2020, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+from pgadmin.utils.route import BaseTestGenerator
+from regression import parent_node_dict
+from regression.python_test_utils import test_utils as utils
+from . import utils as servers_utils
+import json
+from regression.test_setup import config_data
+from regression.python_test_utils.test_utils import \
+    create_user_wise_test_client
+import config
+
+test_user_details = config_data[
+    'pgAdmin4_test_non_admin_credentials']
+
+
+class SharedServersGetTestCase(BaseTestGenerator):
+    """
+    This class will fetch added servers under default server group
+    by response code.
+    """
+
+    scenarios = utils.generate_scenarios('get_shared_server',
+                                         servers_utils.test_cases)
+
+    def setUp(self):
+        """This function add the server to test the GET API"""
+
+        if config.SERVER_MODE is False:
+            self.skipTest(
+                "Can not run shared servers test cases in the SERVER mode."
+            )
+        self.server['shared'] = True
+        url = "{0}{1}/".format(self.url, utils.SERVER_GROUP)
+        response = self.tester.post(
+            url,
+            data=json.dumps(self.server),
+            content_type='html/json'
+        )
+        response_data = json.loads(response.data.decode('utf-8'))
+        self.server_id = response_data['node']['_id']
+
+        server_dict = {"server_id": self.server_id}
+        utils.write_node_info("sid", server_dict)
+
+    def get_server(self, server_id):
+        return self.tester.get(self.url + str(utils.SERVER_GROUP) + '/' +
+                               str(server_id),
+                               follow_redirects=True)
+
+    @create_user_wise_test_client(test_user_details)
+    def runTest(self):
+        """ This function will fetch the added servers to object browser. """
+        if not self.server_id:
+            raise Exception("Server not found to test GET API")
+        response = None
+        if self.is_positive_test:
+            if hasattr(self, 'no_server_id'):
+                if hasattr(self, 'server_list'):
+                    self.url = '/browser/server/nodes/'
+                server_id = ''
+                response = self.get_server(server_id)
+            else:
+                response = self.get_server(self.server_id)
+        self.assertEquals(response.status_code,
+                          self.expected_data["status_code"])
+
+    def tearDown(self):
+        """This function delete the server from SQLite """
+        utils.delete_server_with_api(self.tester, self.server_id)
+
+
+class SharedServerUpdateTestCase(BaseTestGenerator):
+    """ This class will update server's comment field. """
+
+    scenarios = utils.generate_scenarios('update_shared_server',
+                                         servers_utils.test_cases)
+
+    def setUp(self):
+        """This function add the server to test the PUT API"""
+        if config.SERVER_MODE is False:
+            self.skipTest(
+                "Can not run shared servers test cases in the Desktop mode."
+            )
+        self.server['shared'] = True
+        if hasattr(self, 'clear_save_password'):
+            self.server['save_password'] = 1
+        create_server_url = "/browser/server/obj/{0}/".format(
+            utils.SERVER_GROUP)
+
+        self.server_id = \
+            servers_utils.create_server_with_api(self, create_server_url)
+        server_dict = {"server_id": self.server_id}
+        utils.write_node_info("sid", server_dict)
+
+    def update_server(self):
+        return self.tester.put(
+            self.url + str(utils.SERVER_GROUP) + '/' +
+            str(self.server_id), data=json.dumps(self.test_data),
+            content_type='html/json')
+
+    def connect_to_server(self, url):
+        return self.tester.post(
+            url,
+            data=json.dumps(self.server),
+            content_type='html/json'
+        )
+
+    def runTest(self):
+        """This function update the server details"""
+        if not self.server_id:
+            raise Exception("No server to update.")
+        if 'comment' in self.test_data:
+            self.test_data["comment"] = self.server['comment']
+        self.test_data["id"] = self.server_id
+        if self.is_positive_test:
+            put_response = self.update_server()
+        else:
+            if hasattr(self, 'wrong_server_id'):
+                self.server_id = 9999
+            put_response = self.update_server()
+        self.assertEquals(put_response.status_code,
+                          self.expected_data["status_code"])
+
+    def tearDown(self):
+        """This function delete the server from SQLite"""
+        utils.delete_server_with_api(self.tester, self.server_id)
diff --git a/web/pgadmin/browser/server_groups/servers/tests/utils.py b/web/pgadmin/browser/server_groups/servers/tests/utils.py
new file mode 100644
index 000000000..8680b29fb
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/utils.py
@@ -0,0 +1,52 @@
+import os
+import json
+import sqlite3
+import config
+from regression.python_test_utils import test_utils as utils
+
+
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/servers_test_data.json") as data_file:
+    test_cases = json.load(data_file)
+
+
+def create_server(server, SERVER_GROUP):
+    """This function is used to create server"""
+    try:
+        conn = sqlite3.connect(config.TEST_SQLITE_PATH)
+        # Create the server
+        cur = conn.cursor()
+        if 'shared' not in server:
+            server['shared'] = False
+        server_details = (1, SERVER_GROUP, server['name'], server['host'],
+                          server['port'], server['db'], server['username'],
+                          server['role'], server['sslmode'], server['comment'],
+                          server['shared'])
+        cur.execute('INSERT INTO server (user_id, servergroup_id, name, host, '
+                    'port, maintenance_db, username, role, ssl_mode,'
+                    ' comment, shared) VALUES (?,?,?,?,?,?,?,?,?,?,?)',
+                    server_details)
+        server_id = cur.lastrowid
+        conn.commit()
+        conn.close()
+
+        type = utils.get_server_type(server)
+        server['type'] = type
+
+        return server_id
+    except Exception as exception:
+        raise Exception("Error while creating server. %s" % exception)
+
+
+def create_server_with_api(self, url):
+    try:
+        response = self.tester.post(
+            url,
+            data=json.dumps(self.server),
+            content_type='html/json'
+        )
+        response_data = json.loads(response.data.decode('utf-8'))
+        server_id = response_data['node']['_id']
+        return server_id
+    except Exception as exception:
+        raise Exception("Error while creating server. %s" % exception)
diff --git a/web/pgadmin/browser/server_groups/static/img/server_group_shared.svg b/web/pgadmin/browser/server_groups/static/img/server_group_shared.svg
new file mode 100644
index 000000000..6a4e22bf8
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/static/img/server_group_shared.svg
@@ -0,0 +1,42 @@
+<?xml version="1.0" encoding="utf-8"?>
+<!-- Generator: Adobe Illustrator 24.2.3, SVG Export Plug-In . SVG Version: 6.00 Build 0)  -->
+<svg version="1.1" id="Layer_1" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" x="0px" y="0px"
+	 viewBox="0 0 16 16" style="enable-background:new 0 0 16 16;" xml:space="preserve">
+<style type="text/css">
+	.st0{fill:#F2F2F2;}
+	.st1{fill:#9BA5B0;}
+	.st2{fill:none;stroke:#9BA5B0;stroke-width:0.75;stroke-miterlimit:1;}
+	.st3{fill:#F7F7F7;}
+	.st4{fill:#354A5F;}
+</style>
+<g>
+	<path class="st0" d="M6.7,2.1h4.6c0.6,0,1.1,0.5,1.1,1.1V4c0,0.6-0.5,1.1-1.1,1.1H6.7C6.1,5.1,5.6,4.6,5.6,4V3.2
+		C5.6,2.6,6.1,2.1,6.7,2.1z"/>
+	<path class="st1" d="M11.3,2.5c0.4,0,0.8,0.3,0.8,0.8v0.8c0,0.4-0.3,0.8-0.8,0.8H6.7C6.3,4.8,6,4.5,6,4.1V3.2
+		c0-0.4,0.3-0.7,0.8-0.7H11.3 M11.3,1.7H6.7c-0.8,0-1.5,0.7-1.5,1.5V4c0,0.8,0.7,1.5,1.5,1.5h4.6c0.8,0,1.5-0.7,1.5-1.5V3.2
+		C12.8,2.4,12.1,1.7,11.3,1.7L11.3,1.7z"/>
+	<line class="st2" x1="8.9" y1="3.6" x2="6.8" y2="3.6"/>
+	<path class="st0" d="M6.7,5.2h4.6c0.6,0,1.1,0.5,1.1,1.1v0.8c0,0.6-0.5,1.1-1.1,1.1H6.7c-0.6,0-1.1-0.5-1.1-1.1V6.3
+		C5.6,5.7,6.1,5.2,6.7,5.2z"/>
+	<path class="st1" d="M11.3,5.6c0.4,0,0.8,0.3,0.8,0.8v0.8c0,0.4-0.3,0.8-0.8,0.8H6.7C6.3,7.9,6,7.6,6,7.1V6.3
+		c0-0.4,0.3-0.7,0.8-0.7L11.3,5.6 M11.3,4.8H6.7c-0.8,0-1.5,0.7-1.5,1.5v0.8c0,0.8,0.7,1.5,1.5,1.5h4.6c0.8,0,1.5-0.7,1.5-1.5V6.3
+		C12.8,5.5,12.1,4.8,11.3,4.8L11.3,4.8z"/>
+	<line class="st2" x1="8.9" y1="6.7" x2="6.8" y2="6.7"/>
+	<path class="st0" d="M6.7,8.3h4.6c0.6,0,1.1,0.5,1.1,1.1v0.9c0,0.6-0.5,1.1-1.1,1.1l0,0H6.7c-0.6,0-1.1-0.5-1.1-1.1l0,0V9.4
+		C5.6,8.8,6.1,8.3,6.7,8.3L6.7,8.3z"/>
+	<path class="st1" d="M11.3,8.7c0.4,0,0.8,0.3,0.8,0.8v0.8c0,0.4-0.3,0.8-0.8,0.8H6.7C6.3,11,6,10.7,6,10.3V9.4C6,9,6.3,8.7,6.7,8.7
+		H11.3 M11.3,7.9H6.7c-0.8,0-1.5,0.7-1.5,1.5v0.8c0,0.8,0.7,1.5,1.5,1.5h4.6c0.8,0,1.5-0.7,1.5-1.5V9.4C12.8,8.6,12.1,7.9,11.3,7.9z
+		"/>
+	<line class="st2" x1="8.9" y1="9.8" x2="6.8" y2="9.8"/>
+</g>
+<path class="st3" d="M11.6,10.5c-0.6,0.3-0.8,0.1-1.1-0.2c-0.1-0.2-0.3-0.4-0.6-0.5L7.1,8.5c-0.3-0.1-0.7-0.2-1-0.2
+	C5.1,8.1,4.2,8.4,3.4,9L2,10.1v2.7h0.9v-0.2l3.7,1.8c0.4,0.2,0.7,0.3,1.1,0.3c0.3,0,0.7-0.1,1-0.2l6.1-2.6c0.3-0.1,0.6-0.4,0.7-0.7
+	c0.1-0.3,0.1-0.7,0-1c-0.3-0.6-1.1-0.9-1.7-0.6l-0.4,0.2L11.6,10.5"/>
+<path class="st4" d="M15.5,10.2c-0.3-0.6-1.1-0.9-1.7-0.6l-0.4,0.2l-1.8,0.8l0.3,0.8l1.8-0.8l0.4-0.2c0.2-0.1,0.5,0,0.6,0.2
+	c0.1,0.1,0.1,0.2,0,0.3c0,0.1-0.1,0.2-0.2,0.2l-6.1,2.6c-0.5,0.2-1,0.2-1.4,0l-4.1-2v-1.2l1-0.8C4.5,9.2,5.2,9,6,9.1
+	c0.3,0,0.6,0.1,0.8,0.2l2.8,1.2c0.1,0,0.1,0.1,0.2,0.2c0.1,0.1,0.1,0.3,0,0.4c0,0,0,0.1-0.1,0.1c-0.1,0.1-0.3,0.2-0.5,0.1l-2.2-1
+	l-0.4,0.8l2.2,1c0.2,0.1,0.4,0.1,0.5,0.1c0.4,0,0.7-0.2,1-0.4c0.1-0.1,0.2-0.2,0.2-0.3c0.2-0.4,0.1-0.9-0.1-1.2
+	c-0.1-0.2-0.3-0.4-0.6-0.5L7.1,8.5c-0.3-0.1-0.7-0.2-1-0.2C5.1,8.1,4.2,8.4,3.4,9L2.9,9.4V9.3c0-0.6-0.4-1-1-1H1.4c-0.6,0-1,0.4-1,1
+	v4.1c0,0.6,0.4,1,1,1h0.5c0.6,0,1-0.4,1-1v-0.6l0,0v-0.2l3.7,1.8c0.4,0.2,0.7,0.3,1.1,0.3c0.3,0,0.7-0.1,1-0.2l6.1-2.6
+	c0.3-0.1,0.6-0.4,0.7-0.7C15.7,10.9,15.7,10.5,15.5,10.2z M1.9,13.4H1.4V9.3h0.5V13.4z"/>
+</svg>
diff --git a/web/pgadmin/browser/server_groups/static/js/server_group.js b/web/pgadmin/browser/server_groups/static/js/server_group.js
index 9bd7df0f3..ac1a0c821 100644
--- a/web/pgadmin/browser/server_groups/static/js/server_group.js
+++ b/web/pgadmin/browser/server_groups/static/js/server_group.js
@@ -9,8 +9,8 @@
 
 define('pgadmin.node.server_group', [
   'sources/gettext', 'sources/url_for', 'jquery', 'underscore',
-  'sources/pgadmin', 'pgadmin.browser', 'pgadmin.browser.node',
-], function(gettext, url_for, $, _, pgAdmin) {
+  'sources/pgadmin', 'pgadmin.user_management.current_user', 'pgadmin.browser', 'pgadmin.browser.node',
+], function(gettext, url_for, $, _, pgAdmin, current_user) {
 
   if (!pgAdmin.Browser.Nodes['server_group']) {
     pgAdmin.Browser.Nodes['server_group'] = pgAdmin.Browser.Node.extend({
@@ -39,14 +39,25 @@ define('pgadmin.node.server_group', [
         defaults: {
           id: undefined,
           name: null,
+          user_id: undefined,
         },
         schema: [
           {
             id: 'id', label: gettext('ID'), type: 'int', group: null,
             mode: ['properties'],
+            visible: function(model){
+              if (model.attributes.user_id != current_user.id && !current_user.is_admin)
+                return false;
+              return true;
+            },
           },{
             id: 'name', label: gettext('Name'), type: 'text', group: null,
             mode: ['properties', 'edit', 'create'],
+            disabled: function(model){
+              if (model.attributes.user_id != current_user.id && !_.isUndefined(model.attributes.user_id))
+                return true;
+              return false;
+            },
           },
         ],
         validate: function() {
@@ -69,7 +80,12 @@ define('pgadmin.node.server_group', [
           return null;
         },
       }),
-      canDrop: function(itemData) { return itemData.can_delete; },
+
+      canDrop: function(itemData) {
+        var serverOwner = itemData.user_id;
+        if (serverOwner != current_user.id)
+          return false;
+        return true; },
       dropAsRemove: true,
       canDelete: function(i) {
         var s = pgAdmin.Browser.tree.siblings(i, true);
diff --git a/web/pgadmin/browser/server_groups/templates/css/server_group.css b/web/pgadmin/browser/server_groups/templates/css/server_group.css
new file mode 100644
index 000000000..3b5b8e58a
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/templates/css/server_group.css
@@ -0,0 +1,17 @@
+.icon-server_group {
+  background-image: url('{{ url_for('NODE-server_group.static', filename='img/server_group.svg') }}') !important;
+  background-repeat: no-repeat;
+  background-size: 20px !important;
+  align-content: center;
+  vertical-align: middle;
+  height: 1.3em;
+}
+
+.icon-server_group_shared {
+  background-image: url('{{ url_for('NODE-server_group.static', filename='img/server_group_shared.svg') }}') !important;
+  background-repeat: no-repeat;
+  background-size: 20px !important;
+  align-content: center;
+  vertical-align: middle;
+  height: 1.3em;
+}
diff --git a/web/pgadmin/browser/static/js/browser.js b/web/pgadmin/browser/static/js/browser.js
index f5ab09399..0eb7840b0 100644
--- a/web/pgadmin/browser/static/js/browser.js
+++ b/web/pgadmin/browser/static/js/browser.js
@@ -19,7 +19,7 @@ define('pgadmin.browser', [
   'pgadmin.browser.error', 'pgadmin.browser.frame',
   'pgadmin.browser.node', 'pgadmin.browser.collection', 'pgadmin.browser.activity',
   'sources/codemirror/addon/fold/pgadmin-sqlfoldcode',
-  'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state',
+  'pgadmin.browser.keyboard', 'sources/tree/pgadmin_tree_save_state','jquery.acisortable', 'jquery.acifragment',
 ], function(
   tree,
   gettext, url_for, require, $, _,
diff --git a/web/pgadmin/browser/templates/browser/js/utils.js b/web/pgadmin/browser/templates/browser/js/utils.js
index 33dd6809e..516b62697 100644
--- a/web/pgadmin/browser/templates/browser/js/utils.js
+++ b/web/pgadmin/browser/templates/browser/js/utils.js
@@ -44,6 +44,7 @@ define('pgadmin.browser.utils',
 
   pgAdmin['csrf_token_header'] = '{{ current_app.config.get('WTF_CSRF_HEADERS')[0] }}';
   pgAdmin['csrf_token'] = '{{ csrf_token() }}';
+  pgAdmin['server_mode'] = '{{ current_app.config.get('SERVER_MODE') }}';
 
   /* Get the inactivity related config */
   pgAdmin['user_inactivity_timeout'] = {{ current_app.config.get('USER_INACTIVITY_TIMEOUT') }};
diff --git a/web/pgadmin/model/__init__.py b/web/pgadmin/model/__init__.py
index 03681e120..b33adc062 100644
--- a/web/pgadmin/model/__init__.py
+++ b/web/pgadmin/model/__init__.py
@@ -29,7 +29,7 @@ from flask_sqlalchemy import SQLAlchemy
 #
 ##########################################################################
 
-SCHEMA_VERSION = 25
+SCHEMA_VERSION = 26
 
 ##########################################################################
 #
@@ -173,6 +173,7 @@ class Server(db.Model):
     )
     tunnel_identity_file = db.Column(db.String(64), nullable=True)
     tunnel_password = db.Column(db.String(64), nullable=True)
+    shared = db.Column(db.Boolean(), nullable=False)
 
 
 class ModulePreference(db.Model):
@@ -305,3 +306,88 @@ class Database(db.Model):
         nullable=False,
         primary_key=True
     )
+
+
+class SharedServer(db.Model):
+    """Define a shared Postgres server"""
+
+    __tablename__ = 'sharedserver'
+    id = db.Column(db.Integer, primary_key=True)
+    user_id = db.Column(
+        db.Integer,
+        db.ForeignKey('user.id')
+    )
+    server_owner = db.Column(
+        db.String(128),
+        db.ForeignKey('user.username')
+    )
+    servergroup_id = db.Column(
+        db.Integer,
+        db.ForeignKey('servergroup.id'),
+        nullable=False
+    )
+    name = db.Column(db.String(128), nullable=False)
+    host = db.Column(db.String(128), nullable=True)
+    hostaddr = db.Column(db.String(128), nullable=True)
+    port = db.Column(
+        db.Integer(),
+        db.CheckConstraint('port >= 1 AND port <= 65534'),
+        nullable=False)
+    maintenance_db = db.Column(db.String(64), nullable=True)
+    username = db.Column(db.String(64), nullable=False)
+    password = db.Column(db.String(64), nullable=True)
+    save_password = db.Column(
+        db.Integer(),
+        db.CheckConstraint('save_password >= 0 AND save_password <= 1'),
+        nullable=False
+    )
+    role = db.Column(db.String(64), nullable=True)
+    ssl_mode = db.Column(
+        db.String(16),
+        db.CheckConstraint(
+            "ssl_mode IN ('allow', 'prefer', 'require', 'disable', "
+            "'verify-ca', 'verify-full')"
+        ),
+        nullable=False)
+    comment = db.Column(db.String(1024), nullable=True)
+    discovery_id = db.Column(db.String(128), nullable=True)
+    servers = db.relationship(
+        'ServerGroup',
+        backref=db.backref('sharedserver', cascade="all, delete-orphan"),
+        lazy='joined'
+    )
+    db_res = db.Column(db.Text(), nullable=True)
+    passfile = db.Column(db.Text(), nullable=True)
+    sslcert = db.Column(db.Text(), nullable=True)
+    sslkey = db.Column(db.Text(), nullable=True)
+    sslrootcert = db.Column(db.Text(), nullable=True)
+    sslcrl = db.Column(db.Text(), nullable=True)
+    sslcompression = db.Column(
+        db.Integer(),
+        db.CheckConstraint('sslcompression >= 0 AND sslcompression <= 1'),
+        nullable=False
+    )
+    bgcolor = db.Column(db.Text(10), nullable=True)
+    fgcolor = db.Column(db.Text(10), nullable=True)
+    service = db.Column(db.Text(), nullable=True)
+    connect_timeout = db.Column(db.Integer(), nullable=False)
+    use_ssh_tunnel = db.Column(
+        db.Integer(),
+        db.CheckConstraint('use_ssh_tunnel >= 0 AND use_ssh_tunnel <= 1'),
+        nullable=False
+    )
+    tunnel_host = db.Column(db.String(128), nullable=True)
+    tunnel_port = db.Column(
+        db.Integer(),
+        db.CheckConstraint('port <= 65534'),
+        nullable=True)
+    tunnel_username = db.Column(db.String(64), nullable=True)
+    tunnel_authentication = db.Column(
+        db.Integer(),
+        db.CheckConstraint('tunnel_authentication >= 0 AND '
+                           'tunnel_authentication <= 1'),
+        nullable=False
+    )
+    tunnel_identity_file = db.Column(db.String(64), nullable=True)
+    tunnel_password = db.Column(db.String(64), nullable=True)
+    shared = db.Column(db.Boolean(), nullable=False)
diff --git a/web/pgadmin/preferences/__init__.py b/web/pgadmin/preferences/__init__.py
index 6212f63f8..db11d1d09 100644
--- a/web/pgadmin/preferences/__init__.py
+++ b/web/pgadmin/preferences/__init__.py
@@ -23,6 +23,7 @@ from pgadmin.utils.ajax import success_return, \
 from pgadmin.utils.menu import MenuItem
 from pgadmin.utils.preferences import Preferences
 from pgadmin.utils.constants import MIMETYPE_APP_JS
+from pgadmin.browser.server_groups import ServerGroupModule as sgm
 
 MODULE_NAME = 'preferences'
 
@@ -203,6 +204,7 @@ def save(pid):
 
     res, msg = Preferences.save(
         data['mid'], data['category_id'], data['id'], data['value'])
+    sgm.get_nodes(sgm)
 
     if not res:
         return internal_server_error(errormsg=msg)
diff --git a/web/pgadmin/preferences/static/js/preferences.js b/web/pgadmin/preferences/static/js/preferences.js
index 62eda7aa6..9087c9589 100644
--- a/web/pgadmin/preferences/static/js/preferences.js
+++ b/web/pgadmin/preferences/static/js/preferences.js
@@ -477,6 +477,28 @@ define('pgadmin.preferences', [
                 if(pref.name == 'theme') {
                   requires_refresh = true;
                 }
+
+                if(pref.name == 'hide_shared_server') {
+                  Alertify.confirm(
+                    gettext('Browser tree refresh required'),
+                    gettext('A browser tree refresh is required. Do you wish to refresh the tree?'),
+                    function() {
+                      pgAdmin.Browser.tree.destroy({
+                        success: function() {
+                          pgAdmin.Browser.initializeBrowserTree(pgAdmin.Browser);
+                          return true;
+                        },
+                      });
+                    },
+                    function() {
+                      return true;
+                    }
+                  ).set('labels', {
+                    ok: gettext('Refresh'),
+                    cancel: gettext('Later'),
+                  });
+                }
+
               });
 
               if(requires_refresh) {
diff --git a/web/pgadmin/tools/schema_diff/__init__.py b/web/pgadmin/tools/schema_diff/__init__.py
index a325e9a01..03d3478b6 100644
--- a/web/pgadmin/tools/schema_diff/__init__.py
+++ b/web/pgadmin/tools/schema_diff/__init__.py
@@ -27,6 +27,7 @@ from config import PG_DEFAULT_DRIVER
 from pgadmin.utils.driver import get_driver
 from pgadmin.utils.preferences import Preferences
 from pgadmin.utils.constants import PREF_LABEL_DISPLAY, MIMETYPE_APP_JS
+from sqlalchemy import or_
 
 MODULE_NAME = 'schema_diff'
 
@@ -276,8 +277,11 @@ def servers():
 
         from pgadmin.browser.server_groups.servers import\
             server_icon_and_background
+        Server.query.filter(
+            or_(Server.user_id == current_user.id, Server.shared))
 
-        for server in Server.query.filter_by(user_id=current_user.id):
+        for server in Server.query.filter(
+                or_(Server.user_id == current_user.id, Server.shared)):
             manager = driver.connection_manager(server.id)
             conn = manager.connection()
             connected = conn.connected()
diff --git a/web/pgadmin/tools/schema_diff/static/js/schema_diff.backform.js b/web/pgadmin/tools/schema_diff/static/js/schema_diff.backform.js
index 55ef1e5f4..1b095db98 100644
--- a/web/pgadmin/tools/schema_diff/static/js/schema_diff.backform.js
+++ b/web/pgadmin/tools/schema_diff/static/js/schema_diff.backform.js
@@ -175,7 +175,7 @@ let SchemaDiffSelect2Control =
       let span = this.$el.find('.select2-selection .select2-selection__rendered span.wcTabIcon'),
         selSpan = this.$el.find('option:selected');
 
-      if (span.hasClass('icon-server-not-connected')) {
+      if (span.hasClass('icon-server-not-connected') || span.hasClass('icon-shared-server-not-connected')) {
         let icon = (data.icon) ? data.icon : 'icon-pg';
         span.removeClass('icon-server-not-connected');
         span.addClass(icon);
diff --git a/web/pgadmin/utils/driver/psycopg2/server_manager.py b/web/pgadmin/utils/driver/psycopg2/server_manager.py
index c35a3726f..6d3ce25ff 100644
--- a/web/pgadmin/utils/driver/psycopg2/server_manager.py
+++ b/web/pgadmin/utils/driver/psycopg2/server_manager.py
@@ -66,6 +66,7 @@ class ServerManager(object):
         self.hostaddr = server.hostaddr
         self.port = server.port
         self.db = server.maintenance_db
+        self.shared = server.shared
         self.did = None
         self.user = server.username
         self.password = server.password
diff --git a/web/regression/python_test_utils/test_utils.py b/web/regression/python_test_utils/test_utils.py
index 6d66af15a..491bddbd1 100644
--- a/web/regression/python_test_utils/test_utils.py
+++ b/web/regression/python_test_utils/test_utils.py
@@ -36,6 +36,8 @@ from regression import test_setup
 
 from pgadmin.utils.preferences import Preferences
 
+from functools import wraps
+
 CURRENT_PATH = os.path.abspath(os.path.join(os.path.dirname(
     os.path.realpath(__file__)), "../"))
 
@@ -1598,3 +1600,217 @@ 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
+
+
+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/regression/test_config.json.in b/web/regression/test_config.json.in
index 690f46c1c..393be6a84 100644
--- a/web/regression/test_config.json.in
+++ b/web/regression/test_config.json.in
@@ -11,6 +11,11 @@
     "login_password": "PASSWORD",
     "login_username": "[email protected]"
   },
+  "pgAdmin4_test_non_admin_credentials": {
+    "new_password": "NEWPASSWORD",
+    "login_password": "PASSWORD",
+    "login_username": "[email protected]"
+  },
   "pgAdmin4_ldap_credentials": {
     "login_password": "PASSWORD",
     "login_username": "USERNAME"
diff --git a/web/setup.py b/web/setup.py
index c88c4e1fa..93ae25264 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)
@@ -248,6 +249,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:


view thread (13+ messages)  latest in thread

reply

Reply instructions:

You may reply publicly to this message via plain-text email
using any one of the following methods:

* Reply to all the recipients using the --to and --cc options:
  reply via email

  To: [email protected]
  Cc: [email protected], [email protected]
  Subject: Re: [pgAdmin][RM4979]: Configuration files for data sources or similar.
  In-Reply-To: <CAJ9T6Su5LAOO=JW-ZpLs6wGUta7PMZ52wzcQWmSCPbSSR+U8Gw@mail.gmail.com>

* Save the following mbox file, import it into your mail client,
  and reply-to-all from there: mbox

This inbox is served by agora; see mirroring instructions
for how to clone and mirror all data and code used for this inbox