From 64036340c95af1489ee1199d038ebd6768964ea4 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Sat, 20 Jun 2020 19:18:27 +0200 Subject: [PATCH 01/10] increases verbose in gc errors --- .../resource_manager/garbage_collector.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/resource_manager/garbage_collector.py b/services/web/server/src/simcore_service_webserver/resource_manager/garbage_collector.py index 0c361313eb8..14e01df6d0c 100644 --- a/services/web/server/src/simcore_service_webserver/resource_manager/garbage_collector.py +++ b/services/web/server/src/simcore_service_webserver/resource_manager/garbage_collector.py @@ -177,7 +177,7 @@ async def garbage_collector_task(app: web.Application): keep_alive = False logger.info("Garbage collection task was cancelled, it will not restart!") except Exception: # pylint: disable=broad-except - logger.warning("There was an error during garbage collection, restarting...") + logger.warning("There was an error during garbage collection, restarting...", exc_info=True) # will wait 5 seconds before restarting to avoid restart loops await asyncio.sleep(5) From 0ac0d2bc52437aab8c64cda8b88bb8810560eac9 Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Sat, 20 Jun 2020 19:19:31 +0200 Subject: [PATCH 02/10] Turned gather errors into warnings --- .../service-library/src/servicelib/utils.py | 18 ++++++++++++++---- 1 file changed, 14 insertions(+), 4 deletions(-) diff --git a/packages/service-library/src/servicelib/utils.py b/packages/service-library/src/servicelib/utils.py index 0c8ca8fd686..543b1cf5ac1 100644 --- a/packages/service-library/src/servicelib/utils.py +++ b/packages/service-library/src/servicelib/utils.py @@ -60,15 +60,25 @@ def log_exception_callback(fut: asyncio.Future): # // tasks -async def logged_gather(*tasks, reraise: bool = True) -> List[Any]: - # all coroutine called in // and we take care of returning the exceptions +async def logged_gather( + *tasks, reraise: bool = True, log: logging.Logger = logger +) -> List[Any]: + """ + *all* coroutine passed are executed in parallel and once they are all + completed, the first error (if any) is reraised or all returned + + log: passing the logger gives a chance to identify the origin of the gather call + """ results = await asyncio.gather(*tasks, return_exceptions=True) for value in results: + # WARN: note that ONLY THE FIRST exception is raised if isinstance(value, Exception): if reraise: raise value - logger.error( - "Exception occured while running %s: %s", + # Exception is returned, therefore it is not logged as error but as warning + # It was user's decision not to reraise them + log.warning( + "Exception occured while running task %s in gather: %s", str(tasks[results.index(value)]), str(value), ) From b52e43940ba470b2f842dec8fe309fd931e9d68a Mon Sep 17 00:00:00 2001 From: Pedro Crespo-Valero <32402063+pcrespov@users.noreply.github.com> Date: Sat, 20 Jun 2020 19:20:21 +0200 Subject: [PATCH 03/10] Fixing issue when valid token with invalid user --- .../src/simcore_service_webserver/studies_access.py | 12 +++++++----- 1 file changed, 7 insertions(+), 5 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/studies_access.py b/services/web/server/src/simcore_service_webserver/studies_access.py index d89d38d3b66..a92a7d61b68 100644 --- a/services/web/server/src/simcore_service_webserver/studies_access.py +++ b/services/web/server/src/simcore_service_webserver/studies_access.py @@ -166,16 +166,18 @@ async def access_study(request: web.Request) -> web.Response: Please contact the data curators for more information." ) + # Get or create a valid user user = None is_anonymous_user = await is_anonymous(request) - if is_anonymous_user: - log.debug("Creating temporary user ...") - user = await create_temporary_user(request) - else: + if not is_anonymous_user: + # NOTE: covers valid cookie with unauthorized user (e.g. expired guest/banned) + # TODO: test if temp user overrides old cookie properly user = await get_authorized_user(request) if not user: - raise RuntimeError("Unable to start user session") + log.debug("Creating temporary user ...") + user = await create_temporary_user(request) + is_anonymous_user = True log.debug( "Granted access to study '%s' for user %s. Copying study over ...", From daddd739a1582628cbb629f26d4b1283edbdd9c5 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Sat, 20 Jun 2020 21:39:05 +0200 Subject: [PATCH 04/10] Authz cache cleared when any user is deleted --- .../src/simcore_service_webserver/security_api.py | 14 ++++++++++---- .../simcore_service_webserver/studies_access.py | 2 +- .../src/simcore_service_webserver/users_api.py | 13 ++++++++++++- .../simcore_service_webserver/users_handlers.py | 8 +++----- 4 files changed, 26 insertions(+), 11 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/security_api.py b/services/web/server/src/simcore_service_webserver/security_api.py index ac76c16af1c..33f1bb43480 100644 --- a/services/web/server/src/simcore_service_webserver/security_api.py +++ b/services/web/server/src/simcore_service_webserver/security_api.py @@ -18,6 +18,7 @@ from aiopg.sa import Engine from .db_models import UserStatus, users +from .security_authorization import AuthorizationPolicy, RoleBasedAccessModel from .security_roles import UserRole log = logging.getLogger(__file__) @@ -35,19 +36,24 @@ async def check_credentials(engine: Engine, email: str, password: str) -> bool: return False -def encrypt_password(password): +def encrypt_password(password: str) -> str: return passlib.hash.sha256_crypt.encrypt(password, rounds=1000) -def check_password(password, password_hash): +def check_password(password: str, password_hash: str) -> bool: return passlib.hash.sha256_crypt.verify(password, password_hash) -def get_access_model(app: web.Application): - autz_policy = app[AUTZ_KEY] +def get_access_model(app: web.Application) -> RoleBasedAccessModel: + autz_policy: AuthorizationPolicy = app[AUTZ_KEY] return autz_policy.access_model +def clean_auth_policy_cache(app: web.Application): + autz_policy: AuthorizationPolicy = app[AUTZ_KEY] + autz_policy.timed_cache.clear() + + __all__ = ( "encrypt_password", "check_credentials", diff --git a/services/web/server/src/simcore_service_webserver/studies_access.py b/services/web/server/src/simcore_service_webserver/studies_access.py index a92a7d61b68..391f389d9ae 100644 --- a/services/web/server/src/simcore_service_webserver/studies_access.py +++ b/services/web/server/src/simcore_service_webserver/studies_access.py @@ -171,7 +171,7 @@ async def access_study(request: web.Request) -> web.Response: is_anonymous_user = await is_anonymous(request) if not is_anonymous_user: # NOTE: covers valid cookie with unauthorized user (e.g. expired guest/banned) - # TODO: test if temp user overrides old cookie properly + # TODO: test if temp user overrides old cookie properly user = await get_authorized_user(request) if not user: diff --git a/services/web/server/src/simcore_service_webserver/users_api.py b/services/web/server/src/simcore_service_webserver/users_api.py index 1679a7033e6..451a9e2aae6 100644 --- a/services/web/server/src/simcore_service_webserver/users_api.py +++ b/services/web/server/src/simcore_service_webserver/users_api.py @@ -12,8 +12,9 @@ from .db_models import GroupType, groups, tokens, user_to_groups, users from .groups_api import convert_groups_db_to_schema -from .users_utils import convert_user_db_to_schema +from .security_api import clean_auth_policy_cache from .users_exceptions import UserNotFoundError +from .users_utils import convert_user_db_to_schema logger = logging.getLogger(__name__) @@ -23,6 +24,7 @@ async def get_user_profile(app: web.Application, user_id: int) -> Dict[str, Any] user_profile: Dict[str, Any] = {} user_primary_group = all_group = {} user_standard_groups = [] + async with engine.acquire() as conn: async for row in conn.execute( sa.select( @@ -105,6 +107,11 @@ async def is_user_guest(app: web.Application, user_id: int) -> bool: async def delete_user(app: web.Application, user_id: int) -> None: """Deletes a user from the database if the user exists""" + # FIXME: user cannot be deleted without deleting first all ist project + # otherwise this function will raise asyncpg.exceptions.ForeignKeyViolationError + # Consider "marking" users as deleted and havning a background job that + # cleans it up + db = get_storage(app) user = await db.get_user({"id": user_id}) if not user: @@ -115,6 +122,10 @@ async def delete_user(app: web.Application, user_id: int) -> None: await db.delete_user(user) + # This user might be cached in the auth. If so, any request + # with this user-id will get thru producing unexpected side-effects + clean_auth_policy_cache(app) + # TOKEN ------------------------------------------- async def create_token( diff --git a/services/web/server/src/simcore_service_webserver/users_handlers.py b/services/web/server/src/simcore_service_webserver/users_handlers.py index b2eacf6fbfc..ec96851a1f9 100644 --- a/services/web/server/src/simcore_service_webserver/users_handlers.py +++ b/services/web/server/src/simcore_service_webserver/users_handlers.py @@ -8,10 +8,7 @@ from . import users_api from .login.decorators import RQT_USERID_KEY, login_required from .security_decorators import permission_required -from .users_exceptions import ( - TokenNotFoundError, - UserNotFoundError, -) +from .users_exceptions import TokenNotFoundError, UserNotFoundError logger = logging.getLogger(__name__) @@ -24,7 +21,8 @@ async def get_my_profile(request: web.Request): try: return await users_api.get_user_profile(request.app, uid) except UserNotFoundError: - raise web.HTTPServerError(reason="could not find profile!") + # NOTE: invalid user_id could happen due to timed-cache in AuthorizationPolicy + raise web.HTTPNotFound(reason="Could not find profile!") @login_required From af0a7d0a60b539455c7ce8a641ba829e2c0c71c2 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Sat, 20 Jun 2020 22:02:09 +0200 Subject: [PATCH 05/10] Tests fix emulating a valid cookie invalid user --- .../unit/with_dbs/test_access_to_studies.py | 147 +++++++++++++----- 1 file changed, 106 insertions(+), 41 deletions(-) diff --git a/services/web/server/tests/unit/with_dbs/test_access_to_studies.py b/services/web/server/tests/unit/with_dbs/test_access_to_studies.py index ba4e50b9764..28fdf307ec7 100644 --- a/services/web/server/tests/unit/with_dbs/test_access_to_studies.py +++ b/services/web/server/tests/unit/with_dbs/test_access_to_studies.py @@ -5,6 +5,7 @@ # pylint:disable=unused-argument # pylint:disable=redefined-outer-name +import re import textwrap from copy import deepcopy from pathlib import Path @@ -12,25 +13,25 @@ from typing import Dict import pytest -from aiohttp import web +from aiohttp import ClientResponse, ClientSession, web import simcore_service_webserver.statics from pytest_simcore.helpers.utils_assert import assert_status from pytest_simcore.helpers.utils_login import LoggedUser, UserRole from pytest_simcore.helpers.utils_projects import NewProject, delete_all_projects from servicelib.application import create_safe_application -from servicelib.application_keys import APP_CONFIG_KEY from servicelib.rest_responses import unwrap_envelope -from simcore_service_webserver import studies_access from simcore_service_webserver.db import setup_db from simcore_service_webserver.login import setup_login from simcore_service_webserver.projects import setup_projects +from simcore_service_webserver.projects.projects_api import delete_project_from_db from simcore_service_webserver.rest import setup_rest from simcore_service_webserver.security import setup_security from simcore_service_webserver.session import setup_session from simcore_service_webserver.statics import setup_statics from simcore_service_webserver.studies_access import setup_studies_access from simcore_service_webserver.users import setup_users +from simcore_service_webserver.users_api import delete_user, is_user_guest SHARED_STUDY_UUID = "e2e38eee-c569-4e55-b104-70d159e49c87" @@ -114,7 +115,7 @@ async def logged_user(client): # , role: UserRole): @pytest.fixture -async def published_project(client, fake_project): +async def published_project(client, fake_project) -> Dict: project_data = deepcopy(fake_project) project_data["name"] = "Published project" project_data["uuid"] = SHARED_STUDY_UUID @@ -154,12 +155,43 @@ async def _get_user_projects(client): def _assert_same_projects(got: Dict, expected: Dict): # TODO: validate using api/specs/webserver/v0/components/schemas/project-v0.0.1.json # TODO: validate workbench! - exclude = ["creationDate", "lastChangeDate", "prjOwner", "uuid", "workbench", "accessRights"] + exclude = [ + "creationDate", + "lastChangeDate", + "prjOwner", + "uuid", + "workbench", + "accessRights", + ] for key in expected.keys(): if key not in exclude: assert got[key] == expected[key], "Failed in %s" % key +async def assert_redirected_to_study( + resp: ClientResponse, session: ClientSession +) -> str: + content = await resp.text() + assert resp.status == web.HTTPOk.status_code, f"Got {content}" + + # Expects redirection to osparc web (see qx_client_outdir fixture) + assert resp.url.path == "/" + assert ( + "OSPARC-SIMCORE" in content + ), "Expected front-end rendering workbench's study, got %s" % str(content) + + # Expects auth cookie for current user + assert "osparc.WEBAPI_SESSION" in [c.key for c in session.cookie_jar] + + # Expects fragment to indicate client where to find newly created project + m = re.match(r"/study/([\d\w-]+)", resp.real_url.fragment) + assert m, f"Expected /study/uuid, got {resp.real_url.fragment}" + + # returns newly created project + redirected_project_id = m.group(1) + return redirected_project_id + + # TESTS -------------------------------------- async def test_access_to_invalid_study(client, published_project): resp = await client.get("/study/SOME_INVALID_UUID") @@ -173,34 +205,26 @@ async def test_access_to_forbidden_study(client, unpublished_project): valid_but_not_sharable = unpublished_project["uuid"] - resp = await client.get("/study/%s" % valid_but_not_sharable) + resp = await client.get(f"/study/valid_but_not_sharable") content = await resp.text() - assert resp.status == web.HTTPNotFound.status_code, ( - "STANDARD studies are NOT sharable: %s" % content - ) + assert ( + resp.status == web.HTTPNotFound.status_code + ), f"STANDARD studies are NOT sharable: {content}" async def test_access_study_anonymously( client, qx_client_outdir, published_project, storage_subsystem_mock ): - params = {"uuid": SHARED_STUDY_UUID, "name": "some-template"} - - url_path = "/study/%s" % SHARED_STUDY_UUID - resp = await client.get(url_path) - content = await resp.text() + study_url = client.app.router["study"].url_for(id=published_project["uuid"]) + resp = await client.get(study_url) - # index - assert resp.status == web.HTTPOk.status_code, "Got %s" % str(content) - assert str(resp.url.path) == "/" - assert ( - "OSPARC-SIMCORE" in content - ), "Expected front-end rendering workbench's study, got %s" % str(content) - - real_url = str(resp.real_url) + expected_prj_id = await assert_redirected_to_study(resp, client.session) # has auto logged in as guest? - resp = await client.get("/v0/me") + me_url = client.app.router["get_my_profile"].url_for() + resp = await client.get(me_url) + data, _ = await assert_status(resp, web.HTTPOk) assert data["login"].endswith("guest-at-osparc.io") assert data["gravatar_id"] @@ -211,7 +235,7 @@ async def test_access_study_anonymously( assert len(projects) == 1 guest_project = projects[0] - assert real_url.endswith("#/study/%s" % guest_project["uuid"]) + assert expected_prj_id == guest_project["uuid"] _assert_same_projects(guest_project, published_project) assert guest_project["prjOwner"] == data["login"] @@ -220,29 +244,70 @@ async def test_access_study_anonymously( async def test_access_study_by_logged_user( client, logged_user, qx_client_outdir, published_project, storage_subsystem_mock ): - params = {"uuid": SHARED_STUDY_UUID, "name": "some-template"} - - url_path = "/study/%s" % SHARED_STUDY_UUID - resp = await client.get(url_path) - content = await resp.text() - - # returns index - assert resp.status == web.HTTPOk.status_code, "Got %s" % str(content) - assert str(resp.url.path) == "/" - real_url = str(resp.real_url) - - assert ( - "OSPARC-SIMCORE" in content - ), "Expected front-end rendering workbench's study, got %s" % str(content) + study_url = client.app.router["study"].url_for(id=published_project["uuid"]) + resp = await client.get(study_url) + await assert_redirected_to_study(resp, client.session) # user has a copy of the template project projects = await _get_user_projects(client) assert len(projects) == 1 user_project = projects[0] - # TODO: check redirects to /#/study/{uuid} - assert real_url.endswith("#/study/%s" % user_project["uuid"]) - + # heck redirects to /#/study/{uuid} + assert resp.real_url.fragment.endswith("/study/%s" % user_project["uuid"]) _assert_same_projects(user_project, published_project) assert user_project["prjOwner"] == logged_user["email"] + + +async def test_access_cookie_of_expired_user( + client, qx_client_outdir, published_project, storage_subsystem_mock +): + # emulates issue #1570 + app: web.Application = client.app + + study_url = app.router["study"].url_for(id=published_project["uuid"]) + resp = await client.get(study_url) + + await assert_redirected_to_study(resp, client.session) + + # Expects valid cookie and GUEST access + me_url = app.router["get_my_profile"].url_for() + resp = await client.get(me_url) + + data, _ = await assert_status(resp, web.HTTPOk) + assert await is_user_guest(app, data["id"]) + + async def garbage_collect_guest(uid): + # Emulates garbage collector: + # - anonymous user expired, cleaning it up + # - client still holds cookie with its identifier nonetheless + # + assert await is_user_guest(app, uid) + projects = await _get_user_projects(client) + assert len(projects) == 1 + + prj_id = projects[0]["uuid"] + await delete_project_from_db(app, prj_id, uid) + await delete_user(app, uid) + return uid + + user_id = await garbage_collect_guest(uid=data["id"]) + user_email = data["login"] + + # Now this should be non -authorized + resp = await client.get(me_url) + await assert_status(resp, web.HTTPUnauthorized) + + # But still can access as a new user + resp = await client.get(study_url) + await assert_redirected_to_study(resp, client.session) + + # as a guest user + resp = await client.get(me_url) + data, _ = await assert_status(resp, web.HTTPOk) + assert await is_user_guest(app, data["id"]) + + # But I am another user + assert data["id"] != user_id + assert data["login"] != user_email From c76af2d780bc5b7c102be8428402e6beec78a0e1 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Sat, 20 Jun 2020 22:27:22 +0200 Subject: [PATCH 06/10] Enhances error handling --- .../studies_access.py | 34 +++++++++++++------ .../src/simcore_service_webserver/utils.py | 5 +++ 2 files changed, 29 insertions(+), 10 deletions(-) diff --git a/services/web/server/src/simcore_service_webserver/studies_access.py b/services/web/server/src/simcore_service_webserver/studies_access.py index 391f389d9ae..1f9e7160a46 100644 --- a/services/web/server/src/simcore_service_webserver/studies_access.py +++ b/services/web/server/src/simcore_service_webserver/studies_access.py @@ -23,6 +23,7 @@ from .login.decorators import login_required from .security_api import is_anonymous, remember from .statics import INDEX_RESOURCE_NAME +from .utils import compose_error_msg log = logging.getLogger(__name__) @@ -156,7 +157,9 @@ async def access_study(request: web.Request) -> web.Response: - public studies are templates that are marked as published in the database - if user is not registered, it creates a temporary guest account with limited resources and expiration + - this handler is NOT part of the API and therefore does NOT respond with json """ + # TODO: implement nice error-page.html project_id = request.match_info["id"] template_project = await get_public_project(request.app, project_id) @@ -179,14 +182,25 @@ async def access_study(request: web.Request) -> web.Response: user = await create_temporary_user(request) is_anonymous_user = True - log.debug( - "Granted access to study '%s' for user %s. Copying study over ...", - template_project.get("name"), - user.get("email"), - ) - copied_project_id = await copy_study_to_account(request, template_project, user) + try: + log.debug( + "Granted access to study '%s' for user %s. Copying study over ...", + template_project.get("name"), + user.get("email"), + ) + copied_project_id = await copy_study_to_account(request, template_project, user) - log.debug("Study %s copied", copied_project_id) + log.debug("Study %s copied", copied_project_id) + + except Exception: # pylint: disable=broad-except + log.exception( + "Failed while copying project '%s' to '%s'", + template_project.get("name"), + user.get("email"), + ) + raise web.HTTPInternalServerError( + reason=compose_error_msg("Unable to copy project.") + ) try: redirect_url = ( @@ -195,11 +209,11 @@ async def access_study(request: web.Request) -> web.Response: .with_fragment("/study/{}".format(copied_project_id)) ) except KeyError: - log.error( + log.exception( "Cannot redirect to website because route was not registered. Probably qx output was not ready and it was disabled (see statics.py)" ) - raise RuntimeError( - "Unable to serve front-end. Study has been anyway copied over to user." + raise web.HTTPInternalServerError( + reason=compose_error_msg("Unable to serve front-end.") ) response = web.HTTPFound(location=redirect_url) diff --git a/services/web/server/src/simcore_service_webserver/utils.py b/services/web/server/src/simcore_service_webserver/utils.py index 3b3d47895ec..3a12b43e0d2 100644 --- a/services/web/server/src/simcore_service_webserver/utils.py +++ b/services/web/server/src/simcore_service_webserver/utils.py @@ -196,3 +196,8 @@ def get_tracemalloc_info(top=10) -> List[str]: ) return top_trace + + +def compose_error_msg(msg: str) -> str: + msg = msg.strip() + return f"{msg}. Please send this message to support@osparc.io [{now_str()}]" From a3416ad23c337e406ab08d2d7b59838642549524 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Mon, 22 Jun 2020 10:10:18 +0200 Subject: [PATCH 07/10] Update services/web/server/src/simcore_service_webserver/security_api.py Co-authored-by: Andrei Neagu --- .../web/server/src/simcore_service_webserver/security_api.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/services/web/server/src/simcore_service_webserver/security_api.py b/services/web/server/src/simcore_service_webserver/security_api.py index 33f1bb43480..c9e76c6f518 100644 --- a/services/web/server/src/simcore_service_webserver/security_api.py +++ b/services/web/server/src/simcore_service_webserver/security_api.py @@ -49,7 +49,7 @@ def get_access_model(app: web.Application) -> RoleBasedAccessModel: return autz_policy.access_model -def clean_auth_policy_cache(app: web.Application): +def clean_auth_policy_cache(app: web.Application) -> None: autz_policy: AuthorizationPolicy = app[AUTZ_KEY] autz_policy.timed_cache.clear() From fe86439b1de441b28fccfe3b728c7a3520817ae6 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Mon, 22 Jun 2020 10:27:48 +0200 Subject: [PATCH 08/10] Update packages/service-library/src/servicelib/utils.py Co-authored-by: Sylvain <35365065+sanderegg@users.noreply.github.com> --- packages/service-library/src/servicelib/utils.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/packages/service-library/src/servicelib/utils.py b/packages/service-library/src/servicelib/utils.py index 543b1cf5ac1..758b47cbf3e 100644 --- a/packages/service-library/src/servicelib/utils.py +++ b/packages/service-library/src/servicelib/utils.py @@ -64,7 +64,7 @@ async def logged_gather( *tasks, reraise: bool = True, log: logging.Logger = logger ) -> List[Any]: """ - *all* coroutine passed are executed in parallel and once they are all + *all* coroutine passed are executed concurrently and once they are all completed, the first error (if any) is reraised or all returned log: passing the logger gives a chance to identify the origin of the gather call From aecac9028e75732beb524b22cdc6e36e4be8b942 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Mon, 22 Jun 2020 10:29:18 +0200 Subject: [PATCH 09/10] Update services/web/server/tests/unit/with_dbs/test_access_to_studies.py Co-authored-by: Andrei Neagu --- .../web/server/tests/unit/with_dbs/test_access_to_studies.py | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/services/web/server/tests/unit/with_dbs/test_access_to_studies.py b/services/web/server/tests/unit/with_dbs/test_access_to_studies.py index 28fdf307ec7..f665db494b5 100644 --- a/services/web/server/tests/unit/with_dbs/test_access_to_studies.py +++ b/services/web/server/tests/unit/with_dbs/test_access_to_studies.py @@ -155,14 +155,14 @@ async def _get_user_projects(client): def _assert_same_projects(got: Dict, expected: Dict): # TODO: validate using api/specs/webserver/v0/components/schemas/project-v0.0.1.json # TODO: validate workbench! - exclude = [ + exclude = set( "creationDate", "lastChangeDate", "prjOwner", "uuid", "workbench", "accessRights", - ] + ) for key in expected.keys(): if key not in exclude: assert got[key] == expected[key], "Failed in %s" % key From 4fdac93a076afa2a848ca80ef13af04fca94c820 Mon Sep 17 00:00:00 2001 From: Pedro Crespo <32402063+pcrespov@users.noreply.github.com> Date: Mon, 22 Jun 2020 11:19:36 +0200 Subject: [PATCH 10/10] Fixes set --- .../templates/error-page.html | 14 ++++++++++++++ .../tests/unit/with_dbs/test_access_to_studies.py | 14 ++++++++------ 2 files changed, 22 insertions(+), 6 deletions(-) create mode 100644 services/web/server/src/simcore_service_webserver/templates/error-page.html diff --git a/services/web/server/src/simcore_service_webserver/templates/error-page.html b/services/web/server/src/simcore_service_webserver/templates/error-page.html new file mode 100644 index 00000000000..b322bc40f7b --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/templates/error-page.html @@ -0,0 +1,14 @@ +{% block title %} +Site Error +{% endblock %} + +{% block content %} +

Opps, this is a bit embarrasing

+

+ + + {{ error_text }} + + +

+{% endblock %} diff --git a/services/web/server/tests/unit/with_dbs/test_access_to_studies.py b/services/web/server/tests/unit/with_dbs/test_access_to_studies.py index f665db494b5..9f39f410b00 100644 --- a/services/web/server/tests/unit/with_dbs/test_access_to_studies.py +++ b/services/web/server/tests/unit/with_dbs/test_access_to_studies.py @@ -156,12 +156,14 @@ def _assert_same_projects(got: Dict, expected: Dict): # TODO: validate using api/specs/webserver/v0/components/schemas/project-v0.0.1.json # TODO: validate workbench! exclude = set( - "creationDate", - "lastChangeDate", - "prjOwner", - "uuid", - "workbench", - "accessRights", + [ + "creationDate", + "lastChangeDate", + "prjOwner", + "uuid", + "workbench", + "accessRights", + ] ) for key in expected.keys(): if key not in exclude: