diff --git a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/functions/functions_rpc_interface.py b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/functions/functions_rpc_interface.py index 0ab7e17756a..9d1e2638bb2 100644 --- a/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/functions/functions_rpc_interface.py +++ b/packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/functions/functions_rpc_interface.py @@ -16,6 +16,7 @@ RegisteredFunctionJob, RegisteredFunctionJobCollection, ) +from models_library.functions import FunctionUserAccessRights from models_library.products import ProductName from models_library.rabbitmq_basic_types import RPCMethodName from models_library.rest_pagination import PageMetaInfoLimitOffset @@ -388,3 +389,21 @@ async def delete_function_job_collection( product_name=product_name, ) assert result is None # nosec + + +@log_decorator(_logger, level=logging.DEBUG) +async def get_function_user_permissions( + rabbitmq_rpc_client: RabbitMQRPCClient, + *, + user_id: UserID, + product_name: ProductName, + function_id: FunctionID, +) -> FunctionUserAccessRights: + result = await rabbitmq_rpc_client.request( + WEBSERVER_RPC_NAMESPACE, + TypeAdapter(RPCMethodName).validate_python("get_function_user_permissions"), + function_id=function_id, + user_id=user_id, + product_name=product_name, + ) + return TypeAdapter(FunctionUserAccessRights).validate_python(result) diff --git a/services/api-server/src/simcore_service_api_server/api/routes/functions_routes.py b/services/api-server/src/simcore_service_api_server/api/routes/functions_routes.py index 4551c313c16..835922e5f34 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/functions_routes.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/functions_routes.py @@ -21,8 +21,11 @@ RegisteredFunctionJobCollection, SolverFunctionJob, ) +from models_library.functions import FunctionUserAccessRights from models_library.functions_errors import ( + FunctionExecuteAccessDeniedError, FunctionInputsValidationError, + FunctionReadAccessDeniedError, UnsupportedFunctionClassError, ) from models_library.products import ProductName @@ -371,6 +374,22 @@ async def run_function( # noqa: PLR0913 job_service: Annotated[JobService, Depends(get_job_service)], ) -> RegisteredFunctionJob: + user_permissions: FunctionUserAccessRights = ( + await wb_api_rpc.get_function_user_permissions( + function_id=function_id, user_id=user_id, product_name=product_name + ) + ) + if not user_permissions.read: + raise FunctionReadAccessDeniedError( + user_id=user_id, + function_id=function_id, + ) + if not user_permissions.execute: + raise FunctionExecuteAccessDeniedError( + user_id=user_id, + function_id=function_id, + ) + from .function_jobs_routes import function_job_status to_run_function = await wb_api_rpc.get_function( diff --git a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py index d2bb3be899e..30c03254806 100644 --- a/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py +++ b/services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py @@ -23,6 +23,7 @@ RegisteredFunctionJobCollection, ) from models_library.api_schemas_webserver.licensed_items import LicensedItemRpcGetPage +from models_library.functions import FunctionUserAccessRights from models_library.licenses import LicensedItemID from models_library.products import ProductName from models_library.projects import ProjectID @@ -526,6 +527,20 @@ async def delete_function_job_collection( function_job_collection_id=function_job_collection_id, ) + async def get_function_user_permissions( + self, + *, + user_id: UserID, + product_name: ProductName, + function_id: FunctionID, + ) -> FunctionUserAccessRights: + return await functions_rpc_interface.get_function_user_permissions( + self._client, + user_id=user_id, + product_name=product_name, + function_id=function_id, + ) + def setup(app: FastAPI, rabbitmq_rmp_client: RabbitMQRPCClient): wb_api_rpc_client = WbApiRpcClient(_client=rabbitmq_rmp_client) 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 5a8744011da..f5951f82ea7 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,6 +7,7 @@ from uuid import uuid4 import httpx +import respx from httpx import AsyncClient from models_library.api_schemas_webserver.functions import ( FunctionJobCollection, @@ -16,11 +17,14 @@ RegisteredProjectFunction, RegisteredProjectFunctionJob, ) +from models_library.functions import FunctionUserAccessRights from models_library.functions_errors import ( FunctionIDNotFoundError, FunctionReadAccessDeniedError, ) from models_library.rest_pagination import PageMetaInfoLimitOffset +from models_library.users import UserID +from pytest_mock import MockType from servicelib.aiohttp import status from simcore_service_api_server._meta import API_VTAG @@ -580,3 +584,34 @@ async def test_list_function_job_collections_with_function_filter( RegisteredFunctionJobCollection.model_validate(data["items"][0]) == mock_registered_function_job_collection ) + + +async def test_run_function_not_allowed( + client: AsyncClient, + mock_handler_in_functions_rpc_interface: Callable[[str, Any], None], + mock_registered_function: RegisteredProjectFunction, + auth: httpx.BasicAuth, + user_id: UserID, + mocked_webserver_rest_api_base: respx.MockRouter, + mocked_webserver_rpc_api: dict[str, MockType], +) -> None: + """Test that running a function is not allowed.""" + mock_handler_in_functions_rpc_interface( + "get_function_user_permissions", + FunctionUserAccessRights( + user_id=user_id, + execute=False, + read=True, + write=True, + ), + ) + + response = await client.post( + f"{API_VTAG}/functions/{mock_registered_function.uid}:run", + json={}, + auth=auth, + ) + assert response.status_code == status.HTTP_403_FORBIDDEN + assert response.json()["errors"][0] == ( + f"Function {mock_registered_function.uid} execute access denied for user {user_id}" + ) diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py index b678df299c3..7a0b61641a8 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py @@ -2,6 +2,7 @@ from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE from models_library.functions import ( Function, + FunctionAccessRights, FunctionID, FunctionInputs, FunctionInputSchema, @@ -352,6 +353,25 @@ async def get_function_output_schema( ) +@router.expose(reraise_if_error_type=(FunctionIDNotFoundError,)) +async def get_function_user_permissions( + app: web.Application, + *, + user_id: UserID, + product_name: ProductName, + function_id: FunctionID, +) -> FunctionAccessRights: + """ + Returns a dictionary with the user's permissions for the function. + """ + return await _functions_service.get_function_user_permissions( + app=app, + user_id=user_id, + product_name=product_name, + function_id=function_id, + ) + + async def register_rpc_routes_on_startup(app: web.Application): rpc_server = get_rabbitmq_rpc_server(app) await rpc_server.register_router(router, WEBSERVER_RPC_NAMESPACE, app) diff --git a/services/web/server/src/simcore_service_webserver/functions/_functions_service.py b/services/web/server/src/simcore_service_webserver/functions/_functions_service.py index fd29f603d7c..2a6dce668ba 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_functions_service.py +++ b/services/web/server/src/simcore_service_webserver/functions/_functions_service.py @@ -14,6 +14,7 @@ FunctionJobDB, FunctionJobID, FunctionOutputSchema, + FunctionUserAccessRights, RegisteredFunction, RegisteredFunctionDB, RegisteredFunctionJob, @@ -435,6 +436,38 @@ async def get_function_output_schema( return _decode_function(returned_function).output_schema +@router.expose(reraise_if_error_type=(FunctionIDNotFoundError,)) +async def get_function_user_permissions( + app: web.Application, + *, + user_id: UserID, + product_name: ProductName, + function_id: FunctionID, +) -> FunctionUserAccessRights: + user_permissions = await _functions_repository.get_user_permissions( + app=app, + user_id=user_id, + product_name=product_name, + object_id=function_id, + object_type="function", + ) + return ( + FunctionUserAccessRights( + user_id=user_id, + read=user_permissions.read, + write=user_permissions.write, + execute=user_permissions.execute, + ) + if user_permissions + else FunctionUserAccessRights( + user_id=user_id, + read=False, + write=False, + execute=False, + ) + ) + + def _decode_function( function: RegisteredFunctionDB, ) -> RegisteredFunction: diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rpc.py b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rpc.py index c6f2fecc710..0b57b93978e 100644 --- a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rpc.py +++ b/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rpc.py @@ -14,6 +14,7 @@ ) # import simcore_service_webserver.functions._functions_controller_rpc as functions_rpc +from models_library.functions import FunctionUserAccessRights from models_library.functions_errors import ( FunctionIDNotFoundError, FunctionReadAccessDeniedError, @@ -497,3 +498,40 @@ async def test_delete_function( product_name=osparc_product_name, ) assert registered_function.uid is not None + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_get_function_user_permissions( + client: TestClient, + rpc_client: RabbitMQRPCClient, + mock_function: ProjectFunction, + logged_user: UserInfoDict, + osparc_product_name: ProductName, +): + # Register the function first + registered_function = await functions_rpc.register_function( + rabbitmq_rpc_client=rpc_client, + function=mock_function, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + assert registered_function.uid is not None + + # Retrieve the user permissions for the function + user_permissions = await functions_rpc.get_function_user_permissions( + rabbitmq_rpc_client=rpc_client, + function_id=registered_function.uid, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + # Assert the user permissions match the expected permissions + assert user_permissions == FunctionUserAccessRights( + user_id=logged_user["id"], + read=True, + write=True, + execute=True, + )