diff --git a/packages/postgres-database/src/simcore_postgres_database/models/projects.py b/packages/postgres-database/src/simcore_postgres_database/models/projects.py index faa51055ad5..1d066ba575c 100644 --- a/packages/postgres-database/src/simcore_postgres_database/models/projects.py +++ b/packages/postgres-database/src/simcore_postgres_database/models/projects.py @@ -128,7 +128,7 @@ class ProjectType(enum.Enum): sa.Boolean, nullable=False, default=False, - doc="If true, the project is publicaly accessible via the studies dispatcher", + doc="If true, the project is publicaly accessible via the studies dispatcher (i.e. no registration required)", ), sa.Column( "hidden", diff --git a/services/web/server/src/simcore_service_webserver/projects/projects_db_utils.py b/services/web/server/src/simcore_service_webserver/projects/projects_db_utils.py index a9d2ed47dfb..9485429e7a8 100644 --- a/services/web/server/src/simcore_service_webserver/projects/projects_db_utils.py +++ b/services/web/server/src/simcore_service_webserver/projects/projects_db_utils.py @@ -331,17 +331,12 @@ async def _get_project( sa.text( f"jsonb_exists_any(projects.access_rights, {assemble_array_groups(user_groups)})" ), - sa.case( - [ - ( - only_published, - projects.c.published == "true", - ) - ], - else_=True, - ), ), ) + + if only_published: + conditions &= projects.c.published == "true" + query = select([projects]).where(conditions) if for_update: query = query.with_for_update() diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_constants.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_constants.py index 2e714572457..b193c0ada4f 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_constants.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_constants.py @@ -1,13 +1,20 @@ from typing import Final -# NOTE: MSG_* strings MUST be human readable messages +# NOTE: MSG_$(ERROR_CODE_NAME) strings MUST be human readable messages -MSG_PROJECT_NOT_FOUND: Final[str] = "Cannot find any study with ID '{project_id}'." +MSG_PROJECT_NOT_FOUND: Final[str] = "Cannot find any study with ID '{project_id}'" -MSG_PROJECT_NOT_PUBLISHED: Final[ - str -] = "Cannot find any published study with ID '{project_id}'" + +# This error happens when the linked study ID does not exists OR is not shared with everyone +MSG_PROJECT_NOT_PUBLISHED: Final[str] = "Cannot find any study with ID '{project_id}'" + +# This error happens when the linked study ID does not exists OR is not shared with everyone OR is NOT public +MSG_PUBLIC_PROJECT_NOT_PUBLISHED: Final[str] = ( + "You need to be logged in to access study with ID '{project_id}'\n" + "Please login and try again\n" + "If you don't have an account, write to the Support email to request one\n" +) MSG_UNEXPECTED_ERROR: Final[ diff --git a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_studies_access.py b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_studies_access.py index 6472fe788b4..6b9dd8cd12d 100644 --- a/services/web/server/src/simcore_service_webserver/studies_dispatcher/_studies_access.py +++ b/services/web/server/src/simcore_service_webserver/studies_dispatcher/_studies_access.py @@ -41,6 +41,7 @@ from ._constants import ( MSG_PROJECT_NOT_FOUND, MSG_PROJECT_NOT_PUBLISHED, + MSG_PUBLIC_PROJECT_NOT_PUBLISHED, MSG_UNEXPECTED_ERROR, ) from .settings import StudiesDispatcherSettings, get_plugin_settings @@ -62,21 +63,26 @@ def _compose_uuid(template_uuid, user_id, query="") -> str: async def _get_published_template_project( - app: web.Application, project_uuid: str + app: web.Application, + project_uuid: str, + *, + is_user_authenticated: bool, ) -> ProjectDict: """ raises RedirectToFrontEndPageError """ db = ProjectDBAPI.get_from_app_context(app) + only_public_projects = not is_user_authenticated + try: prj, _ = await db.get_project( project_uuid=project_uuid, # NOTE: these are the conditions for a published study # 1. MUST be a template only_templates=True, - # 2. MUST be checked for publication - only_published=True, + # 2. If user is unauthenticated, then MUST be public + only_published=only_public_projects, # 3. MUST be shared with EVERYONE=1 in read mode, i.e. user_id=ANY_USER, # any user check_permissions="read", # any user has read access @@ -89,11 +95,19 @@ async def _get_published_template_project( except (ProjectNotFoundError, ProjectInvalidRightsError) as err: log.debug( - "Requested project with %s is not published. Reason: %s", + "Project with %s %s was not found. Reason: %s", f"{project_uuid=}", + f"{only_public_projects=}", err.detailed_message(), ) + if only_public_projects: + raise RedirectToFrontEndPageError( + MSG_PUBLIC_PROJECT_NOT_PUBLISHED.format(project_id=project_uuid), + error_code="PUBLIC_PROJECT_NOT_PUBLISHED", + status_code=web.HTTPNotFound.status_code, + ) from err + raise RedirectToFrontEndPageError( MSG_PROJECT_NOT_PUBLISHED.format(project_id=project_uuid), error_code="PROJECT_NOT_PUBLISHED", @@ -295,10 +309,13 @@ async def wrapper(request: web.Request) -> web.StreamResponse: f"{error_code}", extra={"error_code": error_code}, ) - raise RedirectToFrontEndPageError( - MSG_UNEXPECTED_ERROR.format(hint=""), - error_code=error_code, - status_code=web.HTTPInternalServerError.status_code, + raise create_redirect_response( + request.app, + page="error", + message=compose_support_error_msg( + msg=MSG_UNEXPECTED_ERROR.format(hint=""), error_code=error_code + ), + status_code=500, ) from err return wrapper @@ -316,16 +333,21 @@ async def get_redirection_to_study_page(request: web.Request) -> web.Response: project_id = request.match_info["id"] assert request.app.router[INDEX_RESOURCE_NAME] # nosec - # Get published PROJECT referenced in link - template_project = await _get_published_template_project(request.app, project_id) - - # Get or create a valid USER + # Checks USER user = None is_anonymous_user = await is_anonymous(request) if not is_anonymous_user: # NOTE: covers valid cookie with unauthorized user (e.g. expired guest/banned) user = await get_authorized_user(request) + # Get published PROJECT referenced in link + template_project = await _get_published_template_project( + request.app, + project_id, + is_user_authenticated=bool(user), + ) + + # Get or create a valid USER if not user: log.debug("Creating temporary user ...") user = await _create_temporary_user(request) diff --git a/services/web/server/src/simcore_service_webserver/utils.py b/services/web/server/src/simcore_service_webserver/utils.py index 94a54a26d84..d31385bf6d3 100644 --- a/services/web/server/src/simcore_service_webserver/utils.py +++ b/services/web/server/src/simcore_service_webserver/utils.py @@ -142,11 +142,17 @@ def get_tracemalloc_info(top=10) -> list[str]: def compose_support_error_msg( msg: str, error_code: ErrorCodeStr, support_email: str = "support" ) -> str: - return ( - f"{msg.strip(' .').capitalize()} [{error_code}].\n" - f"Please contact {support_email} and attach the message above" + sentences = [] + for line in msg.split("\n"): + if sentence := line.strip(" ."): + sentences.append(sentence[0].upper() + sentence[1:]) + + sentences.append( + f"For more information please forward this message to {support_email} [{error_code}]" ) + return ". ".join(sentences) + # ----------------------------------------------- # diff --git a/services/web/server/tests/unit/isolated/test_utils.py b/services/web/server/tests/unit/isolated/test_utils.py index 609cd6cce29..4b609ceaf3a 100644 --- a/services/web/server/tests/unit/isolated/test_utils.py +++ b/services/web/server/tests/unit/isolated/test_utils.py @@ -5,13 +5,13 @@ import urllib.parse from contextlib import contextmanager from datetime import datetime -from typing import Dict from urllib.parse import unquote_plus import pytest import yarl from simcore_service_webserver.utils import ( DATETIME_FORMAT, + compose_support_error_msg, compute_sha1_on_small_dataset, now_str, to_datetime, @@ -75,7 +75,7 @@ def test_yarl_url_compose_changed_with_latest_release(): @pytest.mark.skip(reason="DEV-demo") -async def test_compute_sha1_on_small_dataset(fake_project: Dict): +async def test_compute_sha1_on_small_dataset(fake_project: dict): # Based on GitHK review https://github.com/ITISFoundation/osparc-simcore/pull/2556: # From what I know, these having function tend to be a bit CPU intensive, based on the size of the dataset. # Could we maybe have an async version of this function here, run it on an executor? @@ -126,3 +126,16 @@ def timeit_ctx(what): # For larger datasets, async solution definitvely scales better # but for smaller ones, the overhead is considerable + + +def test_compose_support_error_msg(): + + msg = compose_support_error_msg( + "first sentence for Mr.X \n Second sentence.", + error_code="OEC:139641204989600", + support_email="support@email.com", + ) + assert ( + msg == "First sentence for Mr.X. Second sentence." + " For more information please forward this message to support@email.com [OEC:139641204989600]" + ) diff --git a/services/web/server/tests/unit/with_dbs/01/test_studies_dispatcher_studies_access.py b/services/web/server/tests/unit/with_dbs/01/test_studies_dispatcher_studies_access.py index 987cb522b38..3aa1096ce8c 100644 --- a/services/web/server/tests/unit/with_dbs/01/test_studies_dispatcher_studies_access.py +++ b/services/web/server/tests/unit/with_dbs/01/test_studies_dispatcher_studies_access.py @@ -131,7 +131,7 @@ async def published_project( project_data = deepcopy(fake_project) project_data["name"] = "Published project" project_data["uuid"] = "e2e38eee-c569-4e55-b104-70d159e49c87" - project_data["published"] = True # OPENED + project_data["published"] = True # PUBLIC project_data["access_rights"] = { # everyone HAS read access "1": {"read": True, "write": False, "delete": False}