diff --git a/services/api-server/src/simcore_service_api_server/api/routes/function_job_collections_routes.py b/services/api-server/src/simcore_service_api_server/api/routes/function_job_collections_routes.py index 38e775b9b01..06097542f17 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/function_job_collections_routes.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/function_job_collections_routes.py @@ -1,4 +1,3 @@ -import asyncio from typing import Annotated, Final from fastapi import APIRouter, Depends, status @@ -14,19 +13,18 @@ ) from models_library.products import ProductName from models_library.users import UserID +from servicelib.utils import limited_gather from simcore_service_api_server._service_function_jobs import FunctionJobService -from simcore_service_api_server.api.dependencies.functions import ( - get_stored_job_status, # Import UserID -) -from simcore_service_api_server.api.dependencies.functions import ( - get_function_from_functionjobid, -) from ...models.pagination import Page, PaginationParams from ...models.schemas.errors import ErrorGet from ...services_http.director_v2 import DirectorV2Api from ...services_rpc.wb_api_server import WbApiRpcClient from ..dependencies.authentication import get_current_user_id, get_product_name +from ..dependencies.functions import ( + get_function_from_functionjobid, + get_stored_job_status, +) from ..dependencies.models_schemas_function_filters import ( get_function_job_collections_filters, ) @@ -269,7 +267,7 @@ async def function_job_collection_status( product_name=product_name, ) - job_statuses = await asyncio.gather( + job_statuses = await limited_gather( *[ function_job_status( function_job=await get_function_job( @@ -286,9 +284,9 @@ async def function_job_collection_status( ), stored_job_status=await get_stored_job_status( function_job_id=function_job_id, + wb_api_rpc=wb_api_rpc, user_id=user_id, product_name=product_name, - wb_api_rpc=wb_api_rpc, ), wb_api_rpc=wb_api_rpc, director2_api=director2_api, diff --git a/services/api-server/src/simcore_service_api_server/api/routes/function_jobs_routes.py b/services/api-server/src/simcore_service_api_server/api/routes/function_jobs_routes.py index e9ae4d7ec76..9a34c4dd1fa 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/function_jobs_routes.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/function_jobs_routes.py @@ -21,21 +21,12 @@ from models_library.projects_state import RunningState from models_library.users import UserID from servicelib.fastapi.dependencies import get_app -from simcore_service_api_server._service_function_jobs import FunctionJobService -from simcore_service_api_server.api.dependencies.functions import ( - get_function_from_functionjob, - get_function_job_dependency, - get_stored_job_outputs, - get_stored_job_status, -) -from simcore_service_api_server.api.dependencies.models_schemas_function_filters import ( - get_function_jobs_filters, -) from simcore_service_api_server.models.schemas.functions_filters import ( FunctionJobsListFilters, ) from sqlalchemy.ext.asyncio import AsyncEngine +from ..._service_function_jobs import FunctionJobService from ..._service_jobs import JobService from ...models.pagination import Page, PaginationParams from ...models.schemas.errors import ErrorGet @@ -45,6 +36,13 @@ from ...services_rpc.wb_api_server import WbApiRpcClient from ..dependencies.authentication import get_current_user_id, get_product_name from ..dependencies.database import get_db_asyncpg_engine +from ..dependencies.functions import ( + get_function_from_functionjob, + get_function_job_dependency, + get_stored_job_outputs, + get_stored_job_status, +) +from ..dependencies.models_schemas_function_filters import get_function_jobs_filters from ..dependencies.services import ( get_api_client, get_function_job_service, 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 729f945aa32..c1fafbae4df 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 @@ -36,11 +36,10 @@ from models_library.projects_state import RunningState from models_library.users import UserID from servicelib.fastapi.dependencies import get_reverse_url_mapper -from simcore_service_api_server._service_function_jobs import FunctionJobService -from simcore_service_api_server._service_functions import FunctionService -from simcore_service_api_server._service_jobs import JobService -from simcore_service_api_server.api.dependencies.functions import get_stored_job_status +from ..._service_function_jobs import FunctionJobService +from ..._service_functions import FunctionService +from ..._service_jobs import JobService from ..._service_solvers import SolverService from ...models.pagination import Page, PaginationParams from ...models.schemas.errors import ErrorGet @@ -49,6 +48,7 @@ from ...services_http.webserver import AuthSession from ...services_rpc.wb_api_server import WbApiRpcClient from ..dependencies.authentication import get_current_user_id, get_product_name +from ..dependencies.functions import get_stored_job_status from ..dependencies.services import ( get_api_client, get_function_job_service, diff --git a/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py b/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py index f05b87a0075..3a298efe380 100644 --- a/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py +++ b/services/api-server/src/simcore_service_api_server/api/routes/studies_jobs.py @@ -60,9 +60,6 @@ ) from .solvers_jobs import JOBS_STATUS_CODES -# pylint: disable=too-many-arguments - - _logger = logging.getLogger(__name__) diff --git a/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py b/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py index e274f4425d0..581b00305b2 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py +++ b/services/web/server/src/simcore_service_webserver/functions/_functions_repository.py @@ -154,10 +154,10 @@ async def create_function( # noqa: PLR0913 user_primary_group_id = await users_service.get_user_primary_group_id( app, user_id=user_id ) - await set_group_permissions( + await _internal_set_group_permissions( app, connection=transaction, - group_id=user_primary_group_id, + permission_group_id=user_primary_group_id, product_name=product_name, object_type="function", object_ids=[registered_function.uuid], @@ -214,10 +214,10 @@ async def create_function_job( # noqa: PLR0913 user_primary_group_id = await users_service.get_user_primary_group_id( app, user_id=user_id ) - await set_group_permissions( + await _internal_set_group_permissions( app, connection=transaction, - group_id=user_primary_group_id, + permission_group_id=user_primary_group_id, product_name=product_name, object_type="function_job", object_ids=[registered_function_job.uuid], @@ -301,10 +301,10 @@ async def create_function_job_collection( user_primary_group_id = await users_service.get_user_primary_group_id( app, user_id=user_id ) - await set_group_permissions( + await _internal_set_group_permissions( app, connection=transaction, - group_id=user_primary_group_id, + permission_group_id=user_primary_group_id, product_name=product_name, object_type="function_job_collection", object_ids=[function_job_collection_db.uuid], @@ -1006,7 +1006,113 @@ async def set_group_permissions( app: web.Application, connection: AsyncConnection | None = None, *, - group_id: GroupID, + user_id: UserID, + permission_group_id: GroupID, + product_name: ProductName, + object_type: Literal["function", "function_job", "function_job_collection"], + object_ids: list[UUID], + read: bool | None = None, + write: bool | None = None, + execute: bool | None = None, +) -> None: + async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn: + for object_id in object_ids: + await check_user_permissions( + app, + connection=conn, + user_id=user_id, + product_name=product_name, + object_id=object_id, + object_type=object_type, + permissions=["write"], + ) + + await _internal_set_group_permissions( + app, + connection=connection, + permission_group_id=permission_group_id, + product_name=product_name, + object_type=object_type, + object_ids=object_ids, + read=read, + write=write, + execute=execute, + ) + + +async def remove_group_permissions( + app: web.Application, + connection: AsyncConnection | None = None, + *, + user_id: UserID, + permission_group_id: GroupID, + product_name: ProductName, + object_type: Literal["function", "function_job", "function_job_collection"], + object_ids: list[UUID], +) -> None: + async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn: + for object_id in object_ids: + await check_user_permissions( + app, + connection=conn, + user_id=user_id, + product_name=product_name, + object_id=object_id, + object_type=object_type, + permissions=["write"], + ) + + await _internal_remove_group_permissions( + app, + connection=connection, + permission_group_id=permission_group_id, + product_name=product_name, + object_type=object_type, + object_ids=object_ids, + ) + + +async def _internal_remove_group_permissions( + app: web.Application, + connection: AsyncConnection | None = None, + *, + permission_group_id: GroupID, + product_name: ProductName, + object_type: Literal["function", "function_job", "function_job_collection"], + object_ids: list[UUID], +) -> None: + access_rights_table = None + field_name = None + + if object_type == "function": + access_rights_table = functions_access_rights_table + field_name = "function_uuid" + elif object_type == "function_job": + access_rights_table = function_jobs_access_rights_table + field_name = "function_job_uuid" + elif object_type == "function_job_collection": + access_rights_table = function_job_collections_access_rights_table + field_name = "function_job_collection_uuid" + + assert access_rights_table is not None # nosec + assert field_name is not None # nosec + + async with transaction_context(get_asyncpg_engine(app), connection) as transaction: + for object_id in object_ids: + await transaction.execute( + access_rights_table.delete().where( + getattr(access_rights_table.c, field_name) == object_id, + access_rights_table.c.group_id == permission_group_id, + access_rights_table.c.product_name == product_name, + ) + ) + + +async def _internal_set_group_permissions( + app: web.Application, + connection: AsyncConnection | None = None, + *, + permission_group_id: GroupID, product_name: ProductName, object_type: Literal["function", "function_job", "function_job_collection"], object_ids: list[UUID], @@ -1035,7 +1141,7 @@ async def set_group_permissions( result = await transaction.execute( access_rights_table.select().where( getattr(access_rights_table.c, field_name) == object_id, - access_rights_table.c.group_id == group_id, + access_rights_table.c.group_id == permission_group_id, ) ) row = result.one_or_none() @@ -1045,7 +1151,7 @@ async def set_group_permissions( await transaction.execute( access_rights_table.insert().values( **{field_name: object_id}, - group_id=group_id, + group_id=permission_group_id, product_name=product_name, read=read if read is not None else False, write=write if write is not None else False, @@ -1064,7 +1170,7 @@ async def set_group_permissions( access_rights_table.update() .where( getattr(access_rights_table.c, field_name) == object_id, - access_rights_table.c.group_id == group_id, + access_rights_table.c.group_id == permission_group_id, ) .values(**update_values) ) 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 912ad7c52a4..f1ec5024a38 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 @@ -4,6 +4,7 @@ FunctionClass, FunctionClassSpecificData, FunctionDB, + FunctionGroupAccessRights, FunctionID, FunctionInputs, FunctionInputSchema, @@ -34,6 +35,7 @@ UnsupportedFunctionClassError, UnsupportedFunctionJobClassError, ) +from models_library.groups import GroupID from models_library.products import ProductName from models_library.rest_pagination import PageMetaInfoLimitOffset from models_library.users import UserID @@ -438,6 +440,45 @@ async def get_function_user_permissions( ) +async def set_function_group_permissions( + app: web.Application, + *, + user_id: UserID, + product_name: ProductName, + function_id: FunctionID, + permissions: FunctionGroupAccessRights, +) -> None: + await _functions_repository.set_group_permissions( + app=app, + user_id=user_id, + product_name=product_name, + object_ids=[function_id], + object_type="function", + permission_group_id=permissions.group_id, + read=permissions.read, + write=permissions.write, + execute=permissions.execute, + ) + + +async def remove_function_group_permissions( + app: web.Application, + *, + user_id: UserID, + product_name: ProductName, + function_id: FunctionID, + permission_group_id: GroupID, +) -> None: + await _functions_repository.remove_group_permissions( + app=app, + user_id=user_id, + product_name=product_name, + object_ids=[function_id], + object_type="function", + permission_group_id=permission_group_id, + ) + + async def get_function_job_status( app: web.Application, *, diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/conftest.py b/services/web/server/tests/unit/with_dbs/04/functions/conftest.py similarity index 100% rename from services/web/server/tests/unit/with_dbs/04/functions_rpc/conftest.py rename to services/web/server/tests/unit/with_dbs/04/functions/conftest.py diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_function_job_collections_controller_rpc.py b/services/web/server/tests/unit/with_dbs/04/functions/test_function_job_collections_controller_rpc.py similarity index 100% rename from services/web/server/tests/unit/with_dbs/04/functions_rpc/test_function_job_collections_controller_rpc.py rename to services/web/server/tests/unit/with_dbs/04/functions/test_function_job_collections_controller_rpc.py diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_function_jobs_controller_rpc.py b/services/web/server/tests/unit/with_dbs/04/functions/test_function_jobs_controller_rpc.py similarity index 100% rename from services/web/server/tests/unit/with_dbs/04/functions_rpc/test_function_jobs_controller_rpc.py rename to services/web/server/tests/unit/with_dbs/04/functions/test_function_jobs_controller_rpc.py diff --git a/services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py b/services/web/server/tests/unit/with_dbs/04/functions/test_functions_controller_rest.py similarity index 100% rename from services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rest.py rename to services/web/server/tests/unit/with_dbs/04/functions/test_functions_controller_rest.py 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/test_functions_controller_rpc.py similarity index 99% rename from services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rpc.py rename to services/web/server/tests/unit/with_dbs/04/functions/test_functions_controller_rpc.py index f4974ad3336..6b54ed191e8 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/test_functions_controller_rpc.py @@ -12,8 +12,6 @@ JSONFunctionOutputSchema, ProjectFunction, ) - -# import simcore_service_webserver.functions._functions_controller_rpc as functions_rpc from models_library.functions import FunctionUserAccessRights from models_library.functions_errors import ( FunctionIDNotFoundError, diff --git a/services/web/server/tests/unit/with_dbs/04/functions/test_functions_service.py b/services/web/server/tests/unit/with_dbs/04/functions/test_functions_service.py new file mode 100644 index 00000000000..80f34cab613 --- /dev/null +++ b/services/web/server/tests/unit/with_dbs/04/functions/test_functions_service.py @@ -0,0 +1,95 @@ +# pylint: disable=unused-argument + +import pytest +from aiohttp.test_utils import TestClient +from common_library.users_enums import UserRole +from models_library.api_schemas_webserver.functions import ProjectFunction +from models_library.functions import FunctionGroupAccessRights +from models_library.functions_errors import FunctionReadAccessDeniedError +from models_library.products import ProductName +from pytest_simcore.helpers.webserver_users import UserInfoDict +from simcore_service_webserver.functions import _functions_service + +pytest_simcore_core_services_selection = ["rabbit"] + + +@pytest.mark.parametrize( + "user_role", + [UserRole.USER], +) +async def test_set_and_remove_group_permissions( + client: TestClient, + user_role: UserRole, + add_user_function_api_access_rights: None, + logged_user: UserInfoDict, + other_logged_user: UserInfoDict, + osparc_product_name: ProductName, + mock_function: ProjectFunction, + clean_functions: None, +) -> None: + # Register the function + registered_function = await _functions_service.register_function( + app=client.app, + function=mock_function, + user_id=logged_user["id"], + product_name=osparc_product_name, + ) + + # Test if registering user can access the function + returned_function = await _functions_service.get_function( + app=client.app, + user_id=logged_user["id"], + product_name=osparc_product_name, + function_id=registered_function.uid, + ) + assert returned_function.uid == registered_function.uid + + # Test if non-registering user cannot access the function + with pytest.raises(FunctionReadAccessDeniedError): + await _functions_service.get_function( + app=client.app, + user_id=other_logged_user["id"], + product_name=osparc_product_name, + function_id=registered_function.uid, + ) + + # Give non-registering user group access + await _functions_service.set_function_group_permissions( + app=client.app, + user_id=logged_user["id"], + product_name=osparc_product_name, + function_id=registered_function.uid, + permissions=FunctionGroupAccessRights( + group_id=int(other_logged_user["primary_gid"]), + read=True, + write=True, + execute=False, + ), + ) + + # Test if non-registering user can access the function + returned_function = await _functions_service.get_function( + app=client.app, + user_id=other_logged_user["id"], + product_name=osparc_product_name, + function_id=registered_function.uid, + ) + assert returned_function.uid == registered_function.uid + + # Remove non-registering user group access + await _functions_service.remove_function_group_permissions( + app=client.app, + user_id=logged_user["id"], + product_name=osparc_product_name, + permission_group_id=int(other_logged_user["primary_gid"]), + function_id=registered_function.uid, + ) + + # Test if non-registering user cannot access the function + with pytest.raises(FunctionReadAccessDeniedError): + await _functions_service.get_function( + app=client.app, + user_id=other_logged_user["id"], + product_name=osparc_product_name, + function_id=registered_function.uid, + )