diff --git a/packages/models-library/src/models_library/functions_errors.py b/packages/models-library/src/models_library/functions_errors.py index 5e00413a831..937606f56a6 100644 --- a/packages/models-library/src/models_library/functions_errors.py +++ b/packages/models-library/src/models_library/functions_errors.py @@ -2,84 +2,100 @@ class FunctionBaseError(OsparcErrorMixin, Exception): - pass + status_code: int class FunctionJobReadAccessDeniedError(FunctionBaseError): msg_template: str = ( "Function job {function_job_id} read access denied for user {user_id}" ) + status_code: int = 403 # Forbidden class FunctionIDNotFoundError(FunctionBaseError): msg_template: str = "Function {function_id} not found" + status_code: int = 404 # Not Found class FunctionJobIDNotFoundError(FunctionBaseError): msg_template: str = "Function job {function_job_id} not found" + status_code: int = 404 # Not Found class FunctionInputsValidationError(FunctionBaseError): msg_template: str = "Function inputs validation failed: {error}" + status_code: int = 422 # Unprocessable Entity class FunctionReadAccessDeniedError(FunctionBaseError): msg_template: str = "Function {function_id} read access denied for user {user_id}" + status_code: int = 403 # Forbidden class FunctionJobCollectionIDNotFoundError(FunctionBaseError): msg_template: str = "Function job collection {function_job_collection_id} not found" + status_code: int = 404 # Not Found class UnsupportedFunctionClassError(FunctionBaseError): msg_template: str = "Function class {function_class} is not supported" + status_code: int = 400 # Bad Request class UnsupportedFunctionJobClassError(FunctionBaseError): msg_template: str = "Function job class {function_job_class} is not supported" + status_code: int = 400 # Bad Request class UnsupportedFunctionFunctionJobClassCombinationError(FunctionBaseError): msg_template: str = ( "Function class {function_class} and function job class {function_job_class} combination is not supported" ) + status_code: int = 400 # Bad Request class FunctionJobCollectionReadAccessDeniedError(FunctionBaseError): msg_template: str = ( "Function job collection {function_job_collection_id} read access denied for user {user_id}" ) + status_code: int = 403 # Forbidden class FunctionWriteAccessDeniedError(FunctionBaseError): msg_template: str = "Function {function_id} write access denied for user {user_id}" + status_code: int = 403 # Forbidden class FunctionJobWriteAccessDeniedError(FunctionBaseError): msg_template: str = ( "Function job {function_job_id} write access denied for user {user_id}" ) + status_code: int = 403 # Forbidden class FunctionJobCollectionWriteAccessDeniedError(FunctionBaseError): msg_template: str = ( "Function job collection {function_job_collection_id} write access denied for user {user_id}" ) + status_code: int = 403 # Forbidden class FunctionExecuteAccessDeniedError(FunctionBaseError): msg_template: str = ( "Function {function_id} execute access denied for user {user_id}" ) + status_code: int = 403 # Forbidden class FunctionJobExecuteAccessDeniedError(FunctionBaseError): msg_template: str = ( "Function job {function_job_id} execute access denied for user {user_id}" ) + status_code: int = 403 # Forbidden class FunctionJobCollectionExecuteAccessDeniedError(FunctionBaseError): msg_template: str = ( "Function job collection {function_job_collection_id} execute access denied for user {user_id}" ) + status_code: int = 403 # Forbidden diff --git a/services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py b/services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py index 91c4e0d9ccf..2385ea984f4 100644 --- a/services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py +++ b/services/api-server/src/simcore_service_api_server/exceptions/handlers/__init__.py @@ -1,6 +1,7 @@ from fastapi import FastAPI from fastapi.exceptions import RequestValidationError from httpx import HTTPError as HttpxException +from models_library.functions_errors import FunctionBaseError from starlette import status from starlette.exceptions import HTTPException @@ -9,6 +10,7 @@ from ..custom_errors import CustomBaseError from ..log_streaming_errors import LogStreamingBaseError from ._custom_errors import custom_error_handler +from ._handler_function_errors import function_error_handler from ._handlers_backend_errors import backend_error_handler from ._handlers_factory import make_handler_for_exception from ._http_exceptions import http_exception_handler @@ -24,6 +26,7 @@ def setup(app: FastAPI, *, is_debug: bool = False): app.add_exception_handler(LogStreamingBaseError, log_handling_error_handler) app.add_exception_handler(CustomBaseError, custom_error_handler) app.add_exception_handler(BaseBackEndError, backend_error_handler) + app.add_exception_handler(FunctionBaseError, function_error_handler) # SEE https://docs.python.org/3/library/exceptions.html#exception-hierarchy app.add_exception_handler( diff --git a/services/api-server/src/simcore_service_api_server/exceptions/handlers/_handler_function_errors.py b/services/api-server/src/simcore_service_api_server/exceptions/handlers/_handler_function_errors.py new file mode 100644 index 00000000000..0ce8a579dbe --- /dev/null +++ b/services/api-server/src/simcore_service_api_server/exceptions/handlers/_handler_function_errors.py @@ -0,0 +1,11 @@ +from fastapi import Request +from models_library.functions_errors import FunctionBaseError + +from ._utils import create_error_json_response + + +async def function_error_handler(request: Request, exc: Exception): + assert request # nosec + assert isinstance(exc, FunctionBaseError) + + return create_error_json_response(f"{exc}", status_code=exc.status_code) diff --git a/services/api-server/tests/unit/api_functions/test_api_routers_functions.py b/services/api-server/tests/unit/api_functions/test_api_routers_functions.py index dcac6487618..5a8744011da 100644 --- a/services/api-server/tests/unit/api_functions/test_api_routers_functions.py +++ b/services/api-server/tests/unit/api_functions/test_api_routers_functions.py @@ -7,7 +7,6 @@ from uuid import uuid4 import httpx -import pytest from httpx import AsyncClient from models_library.api_schemas_webserver.functions import ( FunctionJobCollection, @@ -17,7 +16,10 @@ RegisteredProjectFunction, RegisteredProjectFunctionJob, ) -from models_library.functions_errors import FunctionIDNotFoundError +from models_library.functions_errors import ( + FunctionIDNotFoundError, + FunctionReadAccessDeniedError, +) from models_library.rest_pagination import PageMetaInfoLimitOffset from servicelib.aiohttp import status from simcore_service_api_server._meta import API_VTAG @@ -92,8 +94,35 @@ async def test_get_function_not_found( None, FunctionIDNotFoundError(function_id=non_existent_function_id), ) - with pytest.raises(FunctionIDNotFoundError): - await client.get(f"{API_VTAG}/functions/{non_existent_function_id}", auth=auth) + response = await client.get( + f"{API_VTAG}/functions/{non_existent_function_id}", auth=auth + ) + assert response.status_code == status.HTTP_404_NOT_FOUND + + +async def test_get_function_read_access_denied( + client: AsyncClient, + mock_handler_in_functions_rpc_interface: Callable[ + [str, Any, Exception | None], None + ], + mock_registered_function: RegisteredProjectFunction, + auth: httpx.BasicAuth, +) -> None: + unauthorized_user_id = "unauthorized user" + mock_handler_in_functions_rpc_interface( + "get_function", + None, + FunctionReadAccessDeniedError( + function_id=mock_registered_function.uid, user_id=unauthorized_user_id + ), + ) + response = await client.get( + f"{API_VTAG}/functions/{mock_registered_function.uid}", auth=auth + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json()["errors"][0] == ( + f"Function {mock_registered_function.uid} read access denied for user {unauthorized_user_id}" + ) async def test_list_functions(