Subject: OAUTH2 implementation

From: Florian Sabonchi <[email-masked]>
Date: Tue, 30 Mar 2021 16:09:06 +0200

Hello in this patch I have implemented oauth2

diff --git a/DEPENDENCIES b/DEPENDENCIES
index 6b4d9cfcf..8efe007a1 100644
--- a/DEPENDENCIES
+++ b/DEPENDENCIES
@@ -51,6 +51,7 @@ sshtunnel                                                        0.4.0
 ldap3                                                            2.9              LGPL v3                              https://github.com/cannatag/ldap3
 Flask-BabelEx                                                    0.9.4            BSD                                  http://github.com/mrjoes/flask-babelex
 gssapi                                                           1.6.12           LICENSE.txt                          https://github.com/pythongssapi/python-gssapi
+authlib                                                          0.15.3           BSD                                  https://github.com/lepture/authlib
 
 28 dependencies listed.
 
diff --git a/docs/en_US/release_notes.rst b/docs/en_US/release_notes.rst
index 015c3850b..25570b536 100644
--- a/docs/en_US/release_notes.rst
+++ b/docs/en_US/release_notes.rst
@@ -11,6 +11,7 @@ notes for it.
 .. toctree::
    :maxdepth: 1
 
+   release_notes_5_2
    release_notes_5_1
    release_notes_5_0
    release_notes_4_30
diff --git a/docs/en_US/release_notes_5_2.rst b/docs/en_US/release_notes_5_2.rst
new file mode 100644
index 000000000..d9162831a
--- /dev/null
+++ b/docs/en_US/release_notes_5_2.rst
@@ -0,0 +1,22 @@
+************
+Version 5.1
+************
+
+Release date: 2021-04-22
+
+This release contains a number of bug fixes and new features since the release of pgAdmin4 5.1.
+
+New features
+************
+
+
+Housekeeping
+************
+
+| `Issue #5319 <https://redmine.postgresql.org/issues/5319>`_ -  Improve code coverage and API test cases for Server module.
+
+Bug fixes
+*********
+
+| `Issue #6293 <https://redmine.postgresql.org/issues/6293>`_ -  Fixed an issue where the procedure creation is failed when providing the Volatility option.
+| `Issue #6356 <https://redmine.postgresql.org/issues/6356>`_ -  Mark the Apache HTTPD config file as such in the web DEB and RPM packages.
diff --git a/pkg/debian/build.sh b/pkg/debian/build.sh
index e07d9dd1f..505296c49 100755
--- a/pkg/debian/build.sh
+++ b/pkg/debian/build.sh
@@ -72,6 +72,10 @@ fakeroot dpkg-deb --build "${DESKTOPROOT}" "${DISTROOT}/${APP_NAME}-desktop_${AP
 echo "Creating the web package..."
 mkdir "${WEBROOT}/DEBIAN"
 
+cat << EOF > "${WEBROOT}/DEBIAN/conffiles"
+/etc/apache2/conf-available/pgadmin4.conf
+EOF
+
 cat << EOF > "${WEBROOT}/DEBIAN/control"
 Package: ${APP_NAME}-web
 Version: ${APP_LONG_VERSION}
diff --git a/pkg/redhat/build.sh b/pkg/redhat/build.sh
index 09a058cdf..c21fc9006 100755
--- a/pkg/redhat/build.sh
+++ b/pkg/redhat/build.sh
@@ -159,7 +159,7 @@ cp -rfa %{pga_build_root}/web/* \${RPM_BUILD_ROOT}
 
 %files
 /usr/pgadmin4/bin/*
-/etc/httpd/conf.d/*
+%config(noreplace) /etc/httpd/conf.d/*
 EOF
 
 mkdir -p "${WEBROOT}/etc/httpd/conf.d"
diff --git a/web/pgadmin/__init__.py b/web/pgadmin/__init__.py
index a73335371..e69619201 100644
--- a/web/pgadmin/__init__.py
+++ b/web/pgadmin/__init__.py
@@ -44,11 +44,13 @@ from pgadmin.utils.csrf import pgCSRFProtect
 from pgadmin import authenticate
 from pgadmin.utils.security_headers import SecurityHeaders
 from pgadmin.utils.constants import KERBEROS
+from pgadmin.utils.constants import OAUTH
 
 # Explicitly set the mime-types so that a corrupted windows registry will not
 # affect pgAdmin 4 to be load properly. This will avoid the issues that may
 # occur due to security fix of X_CONTENT_TYPE_OPTIONS = "nosniff".
 import mimetypes
+
 mimetypes.add_type('application/javascript', '.js')
 mimetypes.add_type('text/css', '.css')
 
@@ -697,19 +699,20 @@ def create_app(app_name=None):
                 )
                 abort(401)
             login_user(user)
-        elif config.SERVER_MODE and\
-                app.PGADMIN_EXTERNAL_AUTH_SOURCE ==\
-                KERBEROS and \
+        elif config.SERVER_MODE and \
                 not current_user.is_authenticated and \
                 request.endpoint in ('redirects.index', 'security.login'):
-            return authenticate.login()
-
+            if app.PGADMIN_EXTERNAL_AUTH_SOURCE == KERBEROS:
+                return authenticate.login()
+            elif app.PGADMIN_EXTERNAL_AUTH_SOURCE == OAUTH and len(config.AUTHENTICATION_SOURCES) == 1:
+                # forwarding to OAuth if only one authentication is enabled
+                return authenticate.oauth_login()
         # if the server is restarted the in memory key will be lost
         # but the user session may still be active. Logout the user
         # to get the key again when login
         if config.SERVER_MODE and current_user.is_authenticated and \
                 app.PGADMIN_EXTERNAL_AUTH_SOURCE != \
-                KERBEROS and \
+                KERBEROS and OAUTH not in config.AUTHENTICATION_SOURCES and\
                 current_app.keyManager.get() is None and \
                 request.endpoint not in ('security.login', 'security.logout'):
             logout_user()
diff --git a/web/pgadmin/authenticate/__init__.py b/web/pgadmin/authenticate/__init__.py
index bc0868ddf..e350f5732 100644
--- a/web/pgadmin/authenticate/__init__.py
+++ b/web/pgadmin/authenticate/__init__.py
@@ -8,25 +8,28 @@
 ##########################################################################
 
 """A blueprint module implementing the Authentication."""
+from typing import Optional, Any
 
 import flask
-import pickle
-from flask import current_app, flash, Response, request, url_for,\
-    render_template
+
+from authlib.integrations.flask_client import OAuth
+from flask import current_app, flash, Response, request, url_for, \
+    render_template, Flask
 from flask_babelex import gettext
-from flask_security import current_user
-from flask_security.views import _security, _ctx
-from flask_security.utils import config_value, get_post_logout_redirect, \
+from flask_login import current_user
+from flask_security.views import _security
+from flask_security.utils import get_post_logout_redirect, \
     get_post_login_redirect, logout_user
 
 from flask import session
 
-import config
 from pgadmin.utils import PgAdminModule
 from pgadmin.utils.constants import KERBEROS
 from pgadmin.utils.csrf import pgCSRFProtect
 
-from .registry import AuthSourceRegistry
+from pgadmin.authenticate.registry import AuthSourceRegistry
+from pgadmin.utils.constants import OAUTH
+import config
 
 MODULE_NAME = 'authenticate'
 
@@ -35,7 +38,10 @@ class AuthenticateModule(PgAdminModule):
     def get_exposed_url_endpoints(self):
         return ['authenticate.login',
                 'authenticate.kerberos_login',
-                'authenticate.kerberos_logout']
+                'authenticate.kerberos_logout',
+                'authenticate.oauth_login',
+                'authenticate.oauth_authorize',
+                'authenticate.oauth_logout']

 blueprint = AuthenticateModule(MODULE_NAME, __name__, static_url_path='')
@@ -61,6 +67,57 @@ def kerberos_logout():
                                     ))

+oauth_obj = OAuth(Flask(__name__))
+
+
[email-masked]("/login/oauth_login", methods=['GET', 'POST'])
[email-masked]
+def oauth_login():
+    """
+    Entry point for oauth source
+    """
+
+    oauth_obj.register(
+        name=getattr(config, 'NAME', None),
+        client_id=getattr(config, 'CLIENT_ID', None),
+        client_secret=getattr(config, 'CLIENT_SECRET', None),
+        access_token_url=getattr(config, 'TOKEN_URL', None),
+        access_token_params=None,
+        authorize_url=getattr(config, 'AUTHORIZATION_URL', None),
+        authorize_params=None,
+        api_base_url=getattr(config, 'API_BASE_URL', None),
+        userinfo_endpoint=getattr(config, 'USERINFO_ENDPOINT', None),
+        client_kwargs={'scope': 'openid email profile'},
+    )
+
+    session['oauth_client'] = oauth_obj.create_client(
+        getattr(config, 'NAME', None))
+    redirect_uri = url_for('authenticate.oauth_authorize', _external=True)
+    return session['oauth_client'].authorize_redirect(redirect_uri)
+
+
[email-masked]("/login/oauth_authorize")
[email-masked]
+def oauth_authorize():
+    source = get_auth_sources(OAUTH)
+    status, user = source.authenticate()
+    login_status = source.login(user)
+    if login_status:
+        auth_obj = AuthSourceManager(None, config.AUTHENTICATION_SOURCES)
+        session['_auth_source_manager_obj'] = auth_obj.as_dict(source)
+        return flask.redirect(get_post_login_redirect())
+    source.logout()
+    return flask.redirect(get_post_login_redirect())
+
+
[email-masked]("/logout/oauth_logout", methods=['GET', 'POST'])
[email-masked]
+def oauth_logout():
+    source = get_auth_sources(OAUTH)
+    source.logout()
+    return flask.redirect(get_post_login_redirect())
+
+
 @blueprint.route('/login', endpoint='login', methods=['GET', 'POST'])
 def login():
     """
@@ -70,7 +127,6 @@ def login():
     form = _security.login_form()
     auth_obj = AuthSourceManager(form, config.AUTHENTICATION_SOURCES)
     session['_auth_source_manager_obj'] = None
-
     # Validate the user
     if not auth_obj.validate():
         for field in form.errors:
@@ -85,14 +141,13 @@ def login():
         status, msg = auth_obj.login()
         current_auth_obj = auth_obj.as_dict()
         if not status:
-            if current_auth_obj['current_source'] ==\
+            if current_auth_obj['current_source'] == \
                     KERBEROS:
                 return flask.redirect('{0}?next={1}'.format(url_for(
                     'authenticate.kerberos_login'), url_for('browser.index')))
 
             flash(gettext(msg), 'danger')
             return flask.redirect(get_post_logout_redirect())
-
         session['_auth_source_manager_obj'] = current_auth_obj
         return flask.redirect(get_post_login_redirect())
 
@@ -103,9 +158,10 @@ def login():
     return response

-class AuthSourceManager():
+class AuthSourceManager:
     """This class will manage all the authentication sources.
      """
+
     def __init__(self, form, sources):
         self.form = form
         self.auth_sources = sources
@@ -113,11 +169,15 @@ class AuthSourceManager():
         self.source_friendly_name = None
         self.current_source = None
 
-    def as_dict(self):
+    def as_dict(self, source=None):
         """
         Returns the dictionary object representing this object.
         """
 
+        if source:
+            self.source_friendly_name = source.get_friendly_name()
+            self.current_source = source.get_source_name()
+
         res = dict()
         res['source_friendly_name'] = self.source_friendly_name
         res['auth_sources'] = self.auth_sources
@@ -165,7 +225,9 @@ class AuthSourceManager():
                 source.get_source_name())
 
             if self.form.data['email'] and self.form.data['password'] and \
-                    source.get_source_name() == KERBEROS:
+                source.get_source_name() == KERBEROS:
+                continue
+            if source.get_source_name() == OAUTH:
                 continue
 
             status, msg = source.authenticate(self.form)
diff --git a/web/pgadmin/authenticate/oauth.py b/web/pgadmin/authenticate/oauth.py
new file mode 100644
index 000000000..3624589a2
--- /dev/null
+++ b/web/pgadmin/authenticate/oauth.py
@@ -0,0 +1,67 @@
+import config
+import requests
+from flask import current_app
+from flask_babelex import gettext
+from flask_security import login_user, logout_user
+from pgadmin.utils.constants import OAUTH
+from pgadmin.tools import user_management
+from pgadmin.model import User
+
+from pgadmin.authenticate.internal import BaseAuthentication
+
+from web.pgadmin.utils import session
+from flask import session
+
+
+class OAuthAuthentication(BaseAuthentication):
+    """OAuth Authentication Class"""
+
+    def get_source_name(self):
+        return OAUTH
+
+    def get_friendly_name(self):
+        return gettext("oauth")
+
+    def validate(self, form):
+        return True
+
+    def login(self, user_info):
+        user = User.query.filter_by(email=user_info['email']).first()
+
+        if user is None:
+            current_app.logger.exception(
+                self.messages('USER_DOES_NOT_EXIST'))
+            return False, self.messages('USER_DOES_NOT_EXIST')
+        session.permanent = True
+        return login_user(user)
+
+    def logout(self):
+        resp = requests.post(
+            config.REVOKE_URL,
+            params={"token": session['token']['access_token']},
+            headers={"Content-Type": "application/x-www-form-urlencoded"}
+        )
+
+        if not resp.ok:
+            current_app.logger.exception(
+                "The current user could not be logged out : %s" %
+                resp.text)
+        logout_user()
+
+    def authenticate(self):
+        session['token'] = session['oauth_client'].authorize_access_token()
+        resp = session['oauth_client'].get('userinfo').json()
+        user = User.query.filter_by(email=resp['email']).first()
+        return self.__auto_create_user(user, resp)
+
+    def __auto_create_user(self, user, resp):
+        if config.OAUTH_AUTO_CREATE_USER:
+            if not user:
+                return user_management.create_user({
+                    'username': resp['name'],
+                    'email': resp['email'],
+                    'role': 2,
+                    'active': True,
+                    'auth_source': OAUTH
+                })
+        return True, resp
diff --git a/web/pgadmin/browser/__init__.py b/web/pgadmin/browser/__init__.py
index 5fc7de64f..95be67cc0 100644
--- a/web/pgadmin/browser/__init__.py
+++ b/web/pgadmin/browser/__init__.py
@@ -52,6 +52,8 @@ from pgadmin.model import User
 from pgadmin.utils.constants import MIMETYPE_APP_JS, PGADMIN_NODE,\
     INTERNAL, KERBEROS
 
+from web.pgadmin.utils.constants import OAUTH
+
 try:
     from flask_security.views import default_render_json
 except ImportError as e:
@@ -604,12 +606,13 @@ class BrowserPluginModule(PgAdminModule):

 def _get_logout_url():
-    if config.SERVER_MODE and\
-            session['_auth_source_manager_obj']['current_source'] == \
-            KERBEROS:
-        return '{0}?next={1}'.format(url_for(
-            'authenticate.kerberos_logout'), url_for(BROWSER_INDEX))
-
+    if config.SERVER_MODE:
+        if session['_auth_source_manager_obj']['current_source'] == KERBEROS:
+            return '{0}?next={1}'.format(url_for(
+                'authenticate.kerberos_logout'), url_for(BROWSER_INDEX))
+        elif session['_auth_source_manager_obj']['current_source'] == OAUTH:
+            return '{0}?next={1}'.format(url_for(
+                'authenticate.oauth_logout'), url_for(BROWSER_INDEX))
     return '{0}?next={1}'.format(
         url_for('security.logout'), url_for(BROWSER_INDEX))
 
diff --git a/web/pgadmin/browser/server_groups/servers/__init__.py b/web/pgadmin/browser/server_groups/servers/__init__.py
index 13a7e06b9..bc0bd4611 100644
--- a/web/pgadmin/browser/server_groups/servers/__init__.py
+++ b/web/pgadmin/browser/server_groups/servers/__init__.py
@@ -1069,7 +1069,8 @@ class ServerNode(PGChildNodeView):
                 tunnel_username=data.get('tunnel_username', None),
                 tunnel_authentication=data.get('tunnel_authentication', 0),
                 tunnel_identity_file=data.get('tunnel_identity_file', None),
-                shared=data.get('shared', None)
+                shared=data.get('shared', None),
+                passfile=data.get('passfile', None)
             )
             db.session.add(server)
             db.session.commit()
@@ -1572,7 +1573,11 @@ class ServerNode(PGChildNodeView):
             sid: Server id
         """
         try:
-            data = json.loads(request.form['data'], encoding='utf-8')
+            if request.form and request.form['data']:
+                data = json.loads(request.form['data'], encoding='utf-8')
+            else:
+                data = json.loads(request.data, encoding='utf-8')
+
             crypt_key = get_crypt_key()[1]
 
             # Fetch Server Details
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/templates/functions/ppas/sql/9.5_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/templates/functions/ppas/sql/9.5_plus/create.sql
index caad0bfad..d79183996 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/templates/functions/ppas/sql/9.5_plus/create.sql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/templates/functions/ppas/sql/9.5_plus/create.sql
@@ -17,7 +17,7 @@ CREATE{% if query_type is defined %}{{' OR REPLACE'}}{% endif %} FUNCTION {{ con
     RETURNS{% if data.proretset and (data.prorettypename.startswith('SETOF ') or data.prorettypename.startswith('TABLE')) %} {{ data.prorettypename }} {% elif data.proretset %} SETOF {{ data.prorettypename }}{% else %} {{ data.prorettypename }}{% endif %}
 
     LANGUAGE {{ data.lanname|qtLiteral }}
-    {% if data.provolatile %}{% if data.provolatile == 'i' %}IMMUTABLE{% elif data.provolatile == 'STABLE' %}STABLE{% else %}VOLATILE{% endif %} {% endif %}{% if data.proleakproof %}LEAKPROOF {% endif %}
+    {% if data.provolatile %}{% if data.provolatile == 'i' %}IMMUTABLE{% elif data.provolatile == 's' %}STABLE{% else %}VOLATILE{% endif %} {% endif %}{% if data.proleakproof %}LEAKPROOF {% endif %}
 {% if data.proisstrict %}STRICT {% endif %}
 {% if data.prosecdef %}SECURITY DEFINER {% endif %}
 {% if data.proiswindow %}WINDOW{% endif %}{% if data.procost %}
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/templates/procedures/ppas/sql/11_plus/create.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/templates/procedures/ppas/sql/11_plus/create.sql
index f6f62887c..2ba16e518 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/templates/procedures/ppas/sql/11_plus/create.sql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/templates/procedures/ppas/sql/11_plus/create.sql
@@ -18,10 +18,10 @@ LANGUAGE {{ data.lanname|qtLiteral }}{% if data.prosecdef %}
 
     SECURITY DEFINER {% endif %}
 {% if data.lanname == 'edbspl' %}
-{{ data.provolatile }} {% if data.proleakproof %}LEAKPROOF {% endif %}
+{% if data.provolatile %}{% if data.provolatile == 'i' %}IMMUTABLE{% elif data.provolatile == 's' %}STABLE{% else %}VOLATILE{% endif %} {% endif %}{% if data.proleakproof %}LEAKPROOF {% endif %}
 {% if data.proisstrict %}STRICT {% endif %}
 {% if data.proparallel and (data.proparallel == 'r' or data.proparallel == 's' or data.proparallel == 'u') %}
-{% if data.proparallel == 'r' %} PARALLEL RESTRICTED{% elif data.proparallel == 's' %} PARALLEL SAFE {% elif data.proparallel == 'u' %} PARALLEL UNSAFE{% endif %}{% endif %}{% if data.procost %}
+{% if data.proparallel == 'r' %}PARALLEL RESTRICTED{% elif data.proparallel == 's' %}PARALLEL SAFE{% elif data.proparallel == 'u' %}PARALLEL UNSAFE{% endif %} {% endif %}{% if data.procost %}
 
     COST {{data.procost}}{% endif %}{% if data.prorows and (data.prorows | int) > 0 %}
 
diff --git a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/templates/procedures/ppas/sql/11_plus/update.sql b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/templates/procedures/ppas/sql/11_plus/update.sql
index 43836ca50..d175db1c4 100644
--- a/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/templates/procedures/ppas/sql/11_plus/update.sql
+++ b/web/pgadmin/browser/server_groups/servers/databases/schemas/functions/templates/procedures/ppas/sql/11_plus/update.sql
@@ -22,7 +22,7 @@ CREATE OR REPLACE PROCEDURE {{ conn|qtIdent(o_data.pronamespace, name) }}({% if
     LANGUAGE {{ data.lanname|qtLiteral }} {% else %}
     LANGUAGE {{ o_data.lanname|qtLiteral }}
     {% endif %}
-{{ data.provolatile }}
+{% if 'provolatile' in data and data.provolatile %}{{ data.provolatile }} {% elif 'provolatile' not in data and o_data.provolatile %}{{ o_data.provolatile }} {% endif %}
 {% if ('prosecdef' in data and data.prosecdef) or ('prosecdef' not in data and o_data.prosecdef) %}SECURITY DEFINER{% endif %}
 {% if data.lanname == 'edbspl' or (o_data.lanname == 'edbspl' and not 'lanname' in data ) %}
 {% if ('proleakproof' in data and data.proleakproof) or ('proleakproof' not in data and o_data.proleakproof) %} LEAKPROOF{% else %} NOT LEAKPROOF{% endif %}
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
index 855fba055..9a1262d34 100644
--- a/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json
+++ b/web/pgadmin/browser/server_groups/servers/tests/servers_test_data.json
@@ -186,6 +186,56 @@
       "expected_data": {
         "status_code": 200
       }
+    },
+    {
+      "name": "Add server with ssl",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+        "sslcert": "postgres.crt",
+        "sslkey": "postgres.key",
+        "sslrootcert": "root.crt",
+        "sslmode": "prefer",
+        "sslcompression": true,
+        "sslcrl": "postgres.crl"
+
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Add server with advanced properties",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+        "passfile": "test.pgpass",
+        "hostaddr": "127.0.0.1"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Add server with background/foreground color",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+        "fgcolor":"#FF9900",
+        "bgcolor": "#00FF00"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
     }
   ],
   "is_password_saved": [
@@ -422,6 +472,56 @@
         "status_code": 200
       }
     },
+    {
+      "name": "connect to a server using password (invalid user)",
+      "url": "/browser/server/connect/",
+      "is_positive_test": true,
+      "connect": true,
+      "invalid_user": true,
+      "mocking_required": true,
+      "mock_data": {
+        "function_name": "pgadmin.browser.server_groups.servers.User",
+        "return_value": "None"
+      },
+      "expected_data": {
+        "status_code": 401
+      }
+    },
+    {
+      "name": "connect to a server using password (invalid server username)",
+      "url": "/browser/server/connect/",
+      "is_positive_test": true,
+      "connect": true,
+      "invalid_server_username": true,
+      "mocking_required": true,
+      "mock_data": {
+        "id": 1,
+        "name": "test_mock_server",
+        "username": "None",
+        "shared": true,
+        "service": false,
+        "user_id": "",
+        "function_name": "pgadmin.browser.server_groups.servers.Server",
+        "return_value": "None"
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Fail check recovery state on connected server",
+      "url": "/browser/server/connect/",
+      "is_positive_test": true,
+      "mocking_required": false,
+      "recovery_state": true,
+      "mock_data": {
+        "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_dict",
+        "return_value": "(False, 'Mocked Internal Server Error while get recovery_state for server.')"
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    },
     {
       "name": "Disconnect server test",
       "url": "/browser/server/connect/",
@@ -470,6 +570,152 @@
       }
     }
   ],
+  "connect_ssh_mock": [
+    {
+      "name": "Try to connect server using ssh tunnel password",
+      "url": "/browser/server/connect/",
+      "is_positive_test": true,
+      "connect": true,
+      "ssh_tunnel_connect": true,
+      "mocking_required": false,
+      "mock_data": {
+        "id": 1,
+        "name": "test_mock_server",
+        "username": "postgre1",
+        "use_ssh_tunnel": 1,
+        "tunnel_host": "127.0.0.1",
+        "tunnel_port": 22,
+        "tunnel_username": "user",
+        "tunnel_authentication": 1,
+        "tunnel_password": "user123",
+        "tunnel_identity_file": "pkey_rsa",
+        "service": null,
+        "server_info": {
+          "id": 1,
+          "name": "test_mock_server",
+          "username": "postgres",
+          "passfile": false
+        },
+        "user_info": {
+          "id": 1,
+          "username": "postgres",
+          "password": "1234"
+        },
+        "manager": {
+          "server_type": "pg",
+          "password": "my_postgres",
+          "sversion": 100000,
+          "connection_connect_return_value": "psycopg2.OperationalError()"
+        }
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Try to connect server without ssh tunnel password",
+      "url": "/browser/server/connect/",
+      "is_positive_test": true,
+      "connect": true,
+      "ssh_tunnel_connect": true,
+      "mocking_required": false,
+      "mock_data": {
+        "id": 1,
+        "name": "test_mock_server",
+        "username": "postgre1",
+        "use_ssh_tunnel": 1,
+        "tunnel_host": "127.0.0.1",
+        "tunnel_port": 22,
+        "tunnel_username": "user",
+        "tunnel_authentication": 1,
+        "tunnel_password": "",
+        "tunnel_identity_file": "pkey_rsa",
+        "service": null,
+        "server_info": {
+          "id": 1,
+          "name": "test_mock_server",
+          "username": "postgres",
+          "passfile": false
+        },
+        "user_info": {
+          "id": 1,
+          "username": "postgres",
+          "password": "1234"
+        },
+        "manager": {
+          "server_type": "pg",
+          "password": "my_postgres",
+          "sversion": 100000,
+          "connection_connect_return_value": "OperationalError()"
+        }
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    }
+  ],
+  "wal_replay_server": [
+    {
+      "name": "Pause the wal replay recovery control",
+      "url": "/browser/server/wal_replay/",
+      "is_positive_test": true,
+      "mocking_required": true,
+      "pause": true,
+      "mock_data": {
+        "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_scalar",
+        "return_value": "(True, {'rows': []})"
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Resume the wal replay recovery control",
+      "url": "/browser/server/wal_replay/",
+      "is_positive_test": true,
+      "mocking_required": true,
+      "pause": false,
+      "mock_data": {
+        "function_name": "pgadmin.utils.driver.psycopg2.connection.Connection.execute_scalar",
+        "return_value": "(True, {'rows': []})"
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Error while wal replay pause",
+      "url": "/browser/server/wal_replay/",
+      "is_positive_test": false,
+      "pause": true,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 500
+      }
+    },
+    {
+      "name": "Error while wal replay resume",
+      "url": "/browser/server/wal_replay/",
+      "is_positive_test": false,
+      "pause": false,
+      "test_data": {
+        "comment": "PLACE_HOLDER",
+        "id": "PLACE_HOLDER",
+        "is_password_saved": false
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 500
+      }
+    }
+  ],
   "delete_server": [
     {
       "name": "Delete a server URL",
@@ -624,21 +870,6 @@
         "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/",
@@ -687,6 +918,89 @@
       "expected_data": {
         "status_code": 200
       }
+    },
+    {
+      "name": "update ssl properties of server",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+        "sslcert": "postgres_01.crt",
+        "sslkey": "postgres_01.key",
+        "sslrootcert": "root_01.crt",
+        "sslmode": "allow",
+        "sslcompression": false,
+        "sslcrl": "postgres.crl"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "update advanced properties of server",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+        "passfile": "test_01.pgpass",
+        "hostaddr": "127.0.0.1"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "remove ssl properties from server",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+        "sslcert": "",
+        "sslkey": "",
+        "sslrootcert": "",
+        "sslmode": "prefer",
+        "sslcompression": false,
+        "sslcrl": ""
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "remove advanced properties from server",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+        "passfile": "",
+        "hostaddr": ""
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
+    },
+    {
+      "name": "Update server with background/foreground color",
+      "url": "/browser/server/obj/",
+      "is_positive_test": true,
+      "owner_server": true,
+      "test_data": {
+        "fgcolor":"#B6D7A8",
+        "bgcolor": "#0C343D"
+      },
+      "mocking_required": false,
+      "mock_data": {},
+      "expected_data": {
+        "status_code": 200
+      }
     }
   ],
   "update_shared_server": [
@@ -858,5 +1172,45 @@
         "status_code": 200
       }
     }
+  ],
+  "change_password": [
+    {
+      "name": "Change password",
+      "url": "/browser/server/change_password/",
+      "is_positive_test": true,
+      "mocking_required": true,
+      "mock_data": {
+        "server_info": {
+          "sid": 1,
+          "name": "test_mock_server",
+          "username": "postgres",
+          "password": "post123",
+          "passfile": false
+        },
+        "user_info": {
+          "id": 1,
+          "username": "postgres",
+          "password": "1234"
+        },
+        "manager": {
+          "server_type": "pg",
+          "password": "my_postgres",
+          "sversion": 100000,
+          "connection_execute_scalar_return_value": "(True, {'rows': []})"
+        }
+      },
+      "test_data": {
+        "form_data" : {
+          "user_name": "my_postgres",
+          "password": "my_postgres",
+          "newPassword": "my_postgres1",
+          "confirmPassword": "my_postgres1"
+        }
+      },
+      "expected_data": {
+        "status_code": 200,
+        "update_session": true
+      }
+    }
   ]
 }
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
index ee6bf9f92..6f6165e37 100644
--- a/web/pgadmin/browser/server_groups/servers/tests/test_add_server.py
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_add_server.py
@@ -62,6 +62,30 @@ class AddServerTest(BaseTestGenerator):
             self.server['connect_now'] = self.test_data['connect_now']
             self.server['password'] = self.server['db_password']
 
+        # SSL properties
+        if 'sslcert' in self.test_data:
+            self.server['sslcert'] = self.test_data['sslcert']
+        if 'sslkey' in self.test_data:
+            self.server['sslkey'] = self.test_data['sslkey']
+        if 'sslrootcert' in self.test_data:
+            self.server['sslrootcert'] = self.test_data['sslrootcert']
+        if 'sslmode' in self.test_data:
+            self.server['sslmode'] = self.test_data['sslmode']
+        if 'sslcompression' in self.test_data:
+            self.server['sslcompression'] = self.test_data['sslcompression']
+
+        # Advanced tab properties
+        if 'passfile' in self.test_data:
+            self.server['passfile'] = self.test_data['passfile']
+        if 'hostaddr' in self.test_data:
+            self.server['hostaddr'] = self.test_data['hostaddr']
+
+        # Background/Foreground color
+        if 'fgcolor' in self.test_data:
+            self.server['fgcolor'] = self.test_data['fgcolor']
+        if 'bgcolor' in self.test_data:
+            self.server['bgcolor'] = self.test_data['bgcolor']
+
         if self.is_positive_test:
             if hasattr(self, 'with_save'):
                 self.server['save_password'] = self.with_save
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
index d1e2ddf66..86485c0c7 100644
--- a/web/pgadmin/browser/server_groups/servers/tests/test_check_connect.py
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_check_connect.py
@@ -12,6 +12,7 @@ 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 unittest.mock import patch, MagicMock

 class ServersConnectTestCase(BaseTestGenerator):
@@ -25,18 +26,18 @@ class ServersConnectTestCase(BaseTestGenerator):
 
     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
+        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 hasattr(self, 'with_password') and self.with_password:
+            self.server.tunnel_authentication = 0
         else:
-            self.server['tunnel_authentication'] = 1
-            self.server['tunnel_identity_file'] = 'pkey_rsa'
+            self.server.tunnel_authentication = 1
+            self.server.tunnel_identity_file = 'pkey_rsa'
 
-        if self.save_password:
-            self.server['tunnel_password'] = '123456'
+        if hasattr(self, 'save_password') and self.save_password:
+            self.server.tunnel_password = '123456'
 
     def setUp(self):
         """This function add the server to test the GET API"""
@@ -84,7 +85,49 @@ class ServersConnectTestCase(BaseTestGenerator):
                     utils.SERVER_GROUP,
                     self.server_id)
                 self.server['password'] = self.server['db_password']
-                response = self.connect_to_server(url)
+
+                if self.mocking_required:
+                    if hasattr(self, "invalid_user"):
+                        with patch(self.mock_data['function_name'],
+                                   side_effect=[eval(self.mock_data[
+                                       "return_value"])]) as user_mock:
+
+                            user_mock_result = user_mock.query.filter_by.\
+                                return_value
+                            user_mock_result.first.return_value = None
+                            response = self.connect_to_server(url)
+
+                    elif hasattr(self, "invalid_server_username"):
+                        with patch(self.mock_data['function_name'],
+                                   side_effect=[eval(self.mock_data[
+                                       "return_value"])]) as server_mock:
+
+                            class TestMockServer():
+                                def __init__(self, name, id, username, shared,
+                                             service):
+                                    self.name = name
+                                    self.id = id
+                                    self.username = username
+                                    self.shared = shared
+                                    self.service = service
+                                    self.user_id = id
+
+                            mock_server_obj = TestMockServer(
+                                self.mock_data['name'],
+                                self.mock_data['id'],
+                                eval(self.mock_data['username']),
+                                self.mock_data['shared'],
+                                self.mock_data['service']
+                            )
+
+                            server_mock_result = server_mock.query.filter_by.\
+                                return_value
+                            server_mock_result.first.return_value = \
+                                mock_server_obj
+
+                            response = self.connect_to_server(url)
+                else:
+                    response = self.connect_to_server(url)
             elif hasattr(self, 'restore_point') or hasattr(self,
                                                            'change_password'):
                 connect_url = '/browser/server/connect/{0}/{1}'.format(
@@ -96,6 +139,29 @@ class ServersConnectTestCase(BaseTestGenerator):
 
                 self.connect_to_server(connect_url)
                 response = self.add_server_details(url)
+            elif hasattr(self, "recovery_state") and self.recovery_state:
+                with patch('pgadmin.browser.server_groups.'
+                           'servers.get_driver') as get_driver_mock:
+
+                    self.manager = MagicMock()
+                    get_driver_mock.return_value = MagicMock(
+                        connection_manager=MagicMock(
+                            execute_dict=MagicMock(
+                                return_value=self.manager.connection),
+                            return_value=self.manager)
+                    )
+                    self.manager.version = 10
+
+                    connection_mock_result = \
+                        self.manager.connection.return_value
+                    self.manager.connection.connected.side_effect = True
+
+                    connection_mock_result.execute_dict.side_effect = \
+                        [eval(self.mock_data["return_value"])]
+
+                    response = self.get_server_connection(server_id)
+                    self.assertEquals(response.status_code,
+                                      self.expected_data["status_code"])
             else:
                 response = self.get_server_connection(server_id)
 
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_check_recovery_code.py b/web/pgadmin/browser/server_groups/servers/tests/test_check_recovery_code.py
new file mode 100644
index 000000000..1583ca15f
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_check_recovery_code.py
@@ -0,0 +1,63 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2021, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from . import utils as servers_utils
+from regression import parent_node_dict
+from unittest.mock import patch
+import json
+
+
+class CheckRecoveryCodeTestCase(BaseTestGenerator):
+    """
+    This class will try to test cover the wal_reply code.
+    """
+
+    scenarios = utils.generate_scenarios('wal_replay_server',
+                                         servers_utils.test_cases)
+
+    def resume_wal_replay(self):
+        return self.tester.put(
+            self.url + str(utils.SERVER_GROUP) + '/' + str(self.server_id))
+
+    def pause_wal_replay(self):
+        return self.tester.delete(
+            self.url + str(utils.SERVER_GROUP) + '/' + str(self.server_id))
+
+    def runTest(self):
+
+        server_id = parent_node_dict["server"][-1]["server_id"]
+        if not server_id:
+            raise Exception("Server not found to test GET API")
+
+        if self.mocking_required:
+
+            with patch(self.mock_data['function_name'],
+                       side_effect=[eval(self.mock_data['return_value'])]):
+                response = self.run_test_cases()
+
+                res = json.loads(response.data.decode('utf-8'))
+                self.assertEqual(res['data']['in_recovery'], True)
+                self.assertEqual(res['data']['wal_pause'], self.pause)
+                self.assertEquals(response.status_code,
+                                  self.expected_data["status_code"])
+        else:
+            response = self.run_test_cases()
+            self.assertEquals(response.status_code,
+                              self.expected_data["status_code"])
+
+    def run_test_cases(self):
+
+        if hasattr(self, 'pause') and self.pause:
+            response = self.pause_wal_replay()
+        else:
+            response = self.resume_wal_replay()
+
+        return response
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_check_ssh_mock_connect.py b/web/pgadmin/browser/server_groups/servers/tests/test_check_ssh_mock_connect.py
new file mode 100644
index 000000000..4bed4d357
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_check_ssh_mock_connect.py
@@ -0,0 +1,99 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2021, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from . import utils as servers_utils
+from unittest.mock import patch, MagicMock
+import json
+from psycopg2 import OperationalError
+
+
+class ServersSSHConnectTestCase(BaseTestGenerator):
+    """
+    This class will try to mock connect server with ssh credentials.
+    """
+
+    scenarios = utils.generate_scenarios('connect_ssh_mock',
+                                         servers_utils.test_cases)
+
+    def connect_to_server(self, url, server):
+        return self.tester.post(
+            url,
+            data=json.dumps(server),
+            content_type='html/json'
+        )
+
+    @patch('pgadmin.browser.server_groups.servers.get_driver')
+    @patch('pgadmin.browser.server_groups.servers.Server')
+    def runTest(self, server_mock, get_driver_mock):
+
+        if self.mock_data is not None and \
+                self.mock_data['use_ssh_tunnel'] == 1:
+
+            self.manager = MagicMock()
+            get_driver_mock.return_value = MagicMock(
+                connection_manager=MagicMock(
+                    execute_scalar=MagicMock(
+                        return_value=self.manager.connection),
+                    return_value=self.manager)
+            )
+            self.manager.password = self.mock_data['manager']['password']
+            self.manager.server_type = self.mock_data['manager']['server_type']
+            self.manager.sversion = self.mock_data['manager']['sversion']
+
+            self.manager.connection().connect.side_effect = \
+                MagicMock(side_effect=OperationalError())
+
+            url = self.url + '{0}/{1}'.format(utils.SERVER_GROUP, 1)
+
+            class TestMockServer():
+                def __init__(self, name, id, username, use_ssh_tunnel,
+                             tunnel_host, tunnel_port,
+                             tunnel_username, tunnel_authentication,
+                             tunnel_identity_file, tunnel_password, service):
+                    self.name = name
+                    self.id = id
+                    self.username = username
+
+                    self.use_ssh_tunnel = use_ssh_tunnel
+                    self.tunnel_host = tunnel_host
+                    self.tunnel_port = tunnel_port
+                    self.tunnel_username = tunnel_username
+                    self.tunnel_authentication = \
+                        tunnel_authentication
+                    self.tunnel_identity_file = \
+                        tunnel_identity_file
+                    self.tunnel_password = tunnel_password
+                    self.service = service
+                    self.shared = None
+
+            mock_server_obj = TestMockServer(
+                self.mock_data['name'],
+                self.mock_data['id'],
+                self.mock_data['username'],
+                self.mock_data['use_ssh_tunnel'],
+                self.mock_data['tunnel_host'],
+                self.mock_data['tunnel_port'],
+                self.mock_data['tunnel_username'],
+                self.mock_data['tunnel_authentication'],
+                self.mock_data['tunnel_identity_file'],
+                self.mock_data['tunnel_password'],
+                self.mock_data['service'],
+            )
+
+            server_mock_result = server_mock.query.filter_by.return_value
+            server_mock_result.first.return_value = mock_server_obj
+
+            if self.mock_data['tunnel_password'] == '':
+                del self.server['tunnel_password']
+
+            response = self.connect_to_server(url, self.server)
+
+            self.assertEqual(response.status_code, 500)
diff --git a/web/pgadmin/browser/server_groups/servers/tests/test_password_change.py b/web/pgadmin/browser/server_groups/servers/tests/test_password_change.py
new file mode 100644
index 000000000..f5ff7d14d
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/servers/tests/test_password_change.py
@@ -0,0 +1,104 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2021, 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
+from unittest.mock import patch, MagicMock
+
+
+class DBPasswordChange(BaseTestGenerator):
+    """ This class will test the change password functionality. """
+
+    scenarios = utils.generate_scenarios('change_password',
+                                         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)
+
+    @patch('pgadmin.browser.server_groups.servers.render_template')
+    @patch('pgadmin.browser.server_groups.servers.pqencryptpassword')
+    @patch('pgadmin.browser.server_groups.servers.decrypt')
+    @patch('pgadmin.browser.server_groups.servers.get_driver')
+    @patch('pgadmin.browser.server_groups.servers.db')
+    @patch('pgadmin.browser.server_groups.servers.Server')
+    @patch('pgadmin.browser.server_groups.servers.User')
+    @patch('pgadmin.browser.server_groups.servers.current_user')
+    def runTest(self, current_user_mock, user_mock, server_mock, db_mock,
+                get_driver_mock, decrypt_mock, pqencryptpassword_mock,
+                render_template_mock):
+
+        current_user_mock.id = 1
+
+        self.manager = MagicMock()
+        get_driver_mock.return_value = MagicMock(
+            connection_manager=MagicMock(execute_scalar=MagicMock(
+                return_value=self.manager.connection),
+                return_value=self.manager)
+        )
+        self.manager.password = self.mock_data['manager']['password']
+        self.manager.server_type = self.mock_data['manager']['server_type']
+        self.manager.sversion = self.mock_data['manager']['sversion']
+        self.manager.connection().execute_scalar.return_value = \
+            eval(self.mock_data['manager']
+                 ['connection_execute_scalar_return_value'])
+
+        decrypt_mock.return_value = self.manager.password
+        pqencryptpassword_mock.return_value = self.manager.password
+
+        class TestMockServer():
+            def __init__(self, name, sid, password, passfile):
+                self.name = name
+                self.sid = sid
+                self.password = password
+                self.passfile = passfile
+
+        class TestUser():
+            def __init__(self, id, username, password):
+                self.id = id
+                self.username = username
+                self.password = password
+
+        db_mock.session.commit = MagicMock(return_value=True)
+
+        mock_server_obj = TestMockServer(
+            self.mock_data['server_info']['username'],
+            self.mock_data['server_info']['sid'],
+            self.mock_data['server_info']['password'],
+            self.mock_data['server_info']['passfile']
+        )
+        server_mock_result = server_mock.query.filter_by.return_value
+        server_mock_result.first.return_value = mock_server_obj
+
+        mock_user_obj = TestUser(self.mock_data['user_info']['id'],
+                                 self.mock_data['user_info']['username'],
+                                 self.mock_data['user_info']['password'])
+
+        user_mock_result = user_mock.query.filter_by.return_value
+        user_mock_result.first.return_value = mock_user_obj
+
+        """This function will execute the connect server APIs"""
+        response = self.tester.post(
+            self.url + str(1) + '/' + str(mock_server_obj.sid),
+            data=json.dumps(self.test_data['form_data']),
+            follow_redirects=True
+        )
+
+        self.assertEqual(response.status_code,
+                         self.expected_data['status_code'])
+
+        self.assertEquals(render_template_mock.called, True)
+        self.assertEquals(self.manager.update_session.called,
+                          self.expected_data['update_session'])
+        self.assertEqual(
+            self.manager.connection().pq_encrypt_password_conn.called, True)
diff --git a/web/pgadmin/browser/server_groups/tests/servers_group_test_data.json b/web/pgadmin/browser/server_groups/tests/servers_group_test_data.json
new file mode 100644
index 000000000..f0c35b7fb
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/tests/servers_group_test_data.json
@@ -0,0 +1,45 @@
+{
+  "get_server_group_children": [
+    {
+      "name": "Get the all children of server group",
+      "url": "/browser/server_group/children/",
+      "is_positive_test": true,
+      "children": true,
+      "mocking_required": true,
+      "mock_data": {
+        "id": 1,
+        "name": "test_mock_server",
+        "username": "postgre1",
+        "use_ssh_tunnel": 1,
+        "tunnel_host": "127.0.0.1",
+        "tunnel_port": 22,
+        "tunnel_username": "user",
+        "tunnel_authentication": 1,
+        "tunnel_password": "user123",
+        "tunnel_identity_file": "pkey_rsa",
+        "service": null,
+        "fgcolor":"#B6D7A8",
+        "bgcolor": "#0C343D",
+        "servergroup_id": 5,
+        "server_owner": "admin"
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    }
+  ],
+  "get_server_group": [
+    {
+      "name": "Check Server Group Node",
+      "url": "/browser/server_group/obj/",
+      "is_positive_test": true,
+      "children": true,
+      "mocking_required": false,
+      "mock_data": {
+      },
+      "expected_data": {
+        "status_code": 200
+      }
+    }
+  ]
+}
diff --git a/web/pgadmin/browser/server_groups/tests/test_servers_groups_childrens.py b/web/pgadmin/browser/server_groups/tests/test_servers_groups_childrens.py
new file mode 100644
index 000000000..4f4d439c6
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/tests/test_servers_groups_childrens.py
@@ -0,0 +1,84 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2021, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+from pgadmin.utils.route import BaseTestGenerator
+from regression.python_test_utils import test_utils as utils
+from . import utils as cast_utils
+from regression.test_setup import config_data
+import json
+import config
+from unittest.mock import patch
+from regression.python_test_utils.test_utils import \
+    create_user_wise_test_client
+
+test_user_details = None
+if config.SERVER_MODE:
+    test_user_details = config_data['pgAdmin4_test_non_admin_credentials']
+
+
+class ServerGroupsChildren(BaseTestGenerator):
+    """
+    This class will fetch all children of server group by response code.
+    """
+
+    scenarios = utils.generate_scenarios('get_server_group_children',
+                                         cast_utils.test_cases)
+
+    def get_server(self, server_id):
+        return self.tester.get(self.url + str(utils.SERVER_GROUP) + '/' +
+                               str(server_id),
+                               follow_redirects=True)
+
+    def setUp(self):
+
+        if config.SERVER_MODE is True:
+            self.server['shared'] = True
+            url = "/browser/server/obj/{0}/".format(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": response_data['node']['_id']}
+            utils.write_node_info("sid", server_dict)
+
+    def runTest(self):
+
+        if config.SERVER_MODE is True:
+            self.testServerGroupsForServerMode()
+        else:
+            self.testServerGroupsForDesktopMode()
+
+    @patch('pgadmin.browser.server_groups.servers.current_user')
+    @create_user_wise_test_client(test_user_details)
+    def testServerGroupsForServerMode(self, current_user_mock):
+
+        current_user_mock.id = 103040
+        self.server_group_id = config_data['server_group']
+
+        response = self.tester.get(self.url + str(self.server_group_id),
+                                   content_type='html/json')
+        self.assertTrue(response.status_code, 200)
+        response_data = json.loads(response.data.decode('utf8'))
+        self.assertTrue(response_data['success'], 1)
+
+    @patch('pgadmin.browser.server_groups.servers.current_user')
+    def testServerGroupsForDesktopMode(self, current_user_mock):
+
+        current_user_mock.id = 1
+        self.server_group_id = config_data['server_group']
+
+        response = self.tester.get(self.url + str(self.server_group_id),
+                                   content_type='html/json')
+        self.assertTrue(response.status_code, 200)
+        response_data = json.loads(response.data.decode('utf8'))
+        self.assertTrue(response_data['success'], 1)
diff --git a/web/pgadmin/browser/server_groups/tests/test_sg_get.py b/web/pgadmin/browser/server_groups/tests/test_sg_get.py
index cdfd3d76e..e5c680133 100644
--- a/web/pgadmin/browser/server_groups/tests/test_sg_get.py
+++ b/web/pgadmin/browser/server_groups/tests/test_sg_get.py
@@ -11,6 +11,8 @@ import json
 
 from pgadmin.utils.route import BaseTestGenerator
 from regression.test_setup import config_data
+from regression.python_test_utils import test_utils as utils
+from . import utils as cast_utils

 class SgNodeTestCase(BaseTestGenerator):
@@ -18,10 +20,8 @@ class SgNodeTestCase(BaseTestGenerator):
      This class will check available server groups in pgAdmin.
     """
 
-    scenarios = [
-        # Fetching the default url for server group node
-        ('Check Server Group Node', dict(url='/browser/server_group/obj/'))
-    ]
+    scenarios = utils.generate_scenarios('get_server_group',
+                                         cast_utils.test_cases)
 
     def runTest(self):
         """This function will check available server groups."""
diff --git a/web/pgadmin/browser/server_groups/tests/utils.py b/web/pgadmin/browser/server_groups/tests/utils.py
new file mode 100644
index 000000000..1a59cca4c
--- /dev/null
+++ b/web/pgadmin/browser/server_groups/tests/utils.py
@@ -0,0 +1,16 @@
+##########################################################################
+#
+# pgAdmin 4 - PostgreSQL Tools
+#
+# Copyright (C) 2013 - 2021, The pgAdmin Development Team
+# This software is released under the PostgreSQL Licence
+#
+##########################################################################
+
+"""Server Group helper utilities"""
+import os
+import json
+
+CURRENT_PATH = os.path.dirname(os.path.realpath(__file__))
+with open(CURRENT_PATH + "/servers_group_test_data.json") as data_file:
+    test_cases = json.load(data_file)
diff --git a/web/pgadmin/messages.pot b/web/pgadmin/messages.pot
index 3d367f122..1a64857b0 100644
--- a/web/pgadmin/messages.pot
+++ b/web/pgadmin/messages.pot
@@ -149,6 +149,11 @@ msgstr ""
 msgid "ldap"
 msgstr ""
 
+#: pgadmin/authenticate/oauth.py:16
+#: pgadmin/templates/security/login_user.html:29
+msgid "Log in with oauth"
+msgstr ""
+
 #: pgadmin/authenticate/registry.py:50
 msgid "Authentication source '{0}' has not been implemented."
 msgstr ""
diff --git a/web/pgadmin/static/scss/_pgadmin.style.scss b/web/pgadmin/static/scss/_pgadmin.style.scss
index 6a185471b..d990b5509 100644
--- a/web/pgadmin/static/scss/_pgadmin.style.scss
+++ b/web/pgadmin/static/scss/_pgadmin.style.scss
@@ -946,6 +946,9 @@ table.table-empty-rows{
   & .btn-login {
     background-color: $security-btn-color;
   }
+  & .btn-oauth {
+    background-color: $security-btn-color;
+  }
   & .user-language {
     & select{
       background-color: $color-primary;
diff --git a/web/pgadmin/templates/security/login_user.html b/web/pgadmin/templates/security/login_user.html
index 2e92d7b12..390120407 100644
--- a/web/pgadmin/templates/security/login_user.html
+++ b/web/pgadmin/templates/security/login_user.html
@@ -20,9 +20,14 @@
                 {% for key, lang in config.LANGUAGES.items() %}
                 <option value="{{key}}" {% if user_language == key %}selected{% endif %}>{{lang}}</option>
                 {% endfor %}
-             </select>
+            </select>
         </div>
     </div>
 </form>
 {% endif %}
+{% if "oauth" in config.AUTHENTICATION_SOURCES and config.AUTHENTICATION_SOURCES | length > 1 %}
+    <form action="{{ url_for('authenticate.oauth_login') }}" method="POST" name="login_oauth_form">
+        <button class="btn btn-primary btn-block btn-oauth" type="submit">{{ _('Log in with oauth') }}</button>
+    </form>
+{% endif %}
 {% endblock %}
diff --git a/web/pgadmin/tools/datagrid/static/js/show_data.js b/web/pgadmin/tools/datagrid/static/js/show_data.js
index e41978876..5a603aa1f 100644
--- a/web/pgadmin/tools/datagrid/static/js/show_data.js
+++ b/web/pgadmin/tools/datagrid/static/js/show_data.js
@@ -45,7 +45,6 @@ export function showDataGrid(
 
   let applicable_nodes = ['table', 'view', 'mview', 'foreign_table'];
   if (applicable_nodes.indexOf(node.getData()._type) === -1) {
-    alertify.error(gettext('This feature is not applicable to the selected object.'));
     return;
   }
 
diff --git a/web/pgadmin/utils/constants.py b/web/pgadmin/utils/constants.py
index 5fd942304..c635f9da5 100644
--- a/web/pgadmin/utils/constants.py
+++ b/web/pgadmin/utils/constants.py
@@ -52,7 +52,9 @@ ERROR_FETCHING_DATA = gettext('Unable to fetch data.')
 INTERNAL = 'internal'
 LDAP = 'ldap'
 KERBEROS = 'kerberos'
+OAUTH = "oauth"
 
 SUPPORTED_AUTH_SOURCES = [INTERNAL,
                           LDAP,
-                          KERBEROS]
+                          KERBEROS,
+                          OAUTH]
diff --git a/web/pgadmin/utils/master_password.py b/web/pgadmin/utils/master_password.py
index 629eec941..9457ba91e 100644
--- a/web/pgadmin/utils/master_password.py
+++ b/web/pgadmin/utils/master_password.py
@@ -5,6 +5,7 @@ from pgadmin.model import db, User, Server
 from pgadmin.utils.crypto import encrypt, decrypt
 from pgadmin.utils.constants import KERBEROS
 
+from web.pgadmin.utils.constants import OAUTH
 
 MASTERPASS_CHECK_TEXT = 'ideas are bulletproof'
 
@@ -27,17 +28,20 @@ def get_crypt_key():
 
     # if desktop mode and master pass disabled then use the password hash
     if not config.MASTER_PASSWORD_REQUIRED \
-            and not config.SERVER_MODE:
+        and not config.SERVER_MODE:
         return True, current_user.password
     # if desktop mode and master pass enabled
     elif config.MASTER_PASSWORD_REQUIRED \
-            and not config.SERVER_MODE and enc_key is None:
+        and not config.SERVER_MODE and enc_key is None:
         return False, None
-    elif config.SERVER_MODE and \
-            session['_auth_source_manager_obj']['source_friendly_name']\
-            == KERBEROS:
-        return True, session['kerberos_key'] if 'kerberos_key' in session \
-            else None
+    elif config.SERVER_MODE:
+        if session['_auth_source_manager_obj']['source_friendly_name'] \
+                == KERBEROS:
+            return True, session['kerberos_key'] if 'kerberos_key' \
+                                                    in session else None
+        elif session['_auth_source_manager_obj']['source_friendly_name'] \
+                == OAUTH:
+            return False, None
     else:
         return True, enc_key
 
@@ -118,7 +122,7 @@ def process_masterpass_disabled():
     :param conn_data: connection manager copy from session if any
     """
     if not config.SERVER_MODE and not config.MASTER_PASSWORD_REQUIRED \
-            and current_user.masterpass_check is not None:
+        and current_user.masterpass_check is not None:
         cleanup_master_password()
         return True

---

From: Dave Page <[email-masked]>
Date: Tue, 30 Mar 2021 15:48:32 +0000

Hi

On Tue, Mar 30, 2021 at 3:36 PM Florian Sabonchi <[email-masked]> wrote:

> Cool!

Unfortunately the patch seems to be messed up. It adds a number of commits
that are already in the primary repo, and attempts to remove your OAuth
support, rather than adding it. Can you rebase it and make sure it only
includes the addition of your work please?

Some other comments (keep in mind it's hard to read the mangled patch, so I
may be missing something):

- There don't seem to be any documentation updates
- There don't seem to be any tests (which I grant may not be feasible to
add unless an OAuth service can be mocked)
- I can't see how you've dealt with password saving, which currently
requires a password from the user to be secure.

-- 
Dave Page
Blog: http://pgsnake.blogspot.com
Twitter: @pgsnake

EDB: http://www.enterprisedb.com