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__)) + + +@blueprint.route("/login/oauth_login", methods=['GET', 'POST']) +@pgCSRFProtect.exempt +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) + + +@blueprint.route("/login/oauth_authorize") +@pgCSRFProtect.exempt +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()) + + +@blueprint.route("/logout/oauth_logout", methods=['GET', 'POST']) +@pgCSRFProtect.exempt +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