diff --git a/api/specs/web-server/_functions.py b/api/specs/web-server/_functions.py index 1946f408d866..47c11cb6d43b 100644 --- a/api/specs/web-server/_functions.py +++ b/api/specs/web-server/_functions.py @@ -10,12 +10,18 @@ from _common import as_query from fastapi import APIRouter, Depends, status from models_library.api_schemas_webserver.functions import ( + FunctionGroupAccessRightsGet, + FunctionGroupAccessRightsUpdate, FunctionToRegister, RegisteredFunctionGet, RegisteredFunctionUpdate, ) from models_library.generics import Envelope +from models_library.groups import GroupID from simcore_service_webserver._meta import API_VTAG +from simcore_service_webserver.functions._controller._functions_rest import ( + FunctionGroupPathParams, +) from simcore_service_webserver.functions._controller._functions_rest_schemas import ( FunctionGetQueryParams, FunctionPathParams, @@ -75,3 +81,33 @@ async def update_function( async def delete_function( _path: Annotated[FunctionPathParams, Depends()], ): ... + + +@router.get( + "/functions/{function_id}/groups", + response_model=Envelope[dict[GroupID, FunctionGroupAccessRightsGet]], +) +async def get_function_groups( + _path: Annotated[FunctionPathParams, Depends()], +): ... + + +@router.put( + "/functions/{function_id}/groups/{group_id}", + summary="Create or update a Function Group", + response_model=Envelope[FunctionGroupAccessRightsGet], +) +async def create_or_update_function_group( + _path: Annotated[FunctionGroupPathParams, Depends()], + _body: FunctionGroupAccessRightsUpdate, +): ... + + +@router.delete( + "/functions/{function_id}/groups/{group_id}", + summary="Delete a Function Group", + status_code=status.HTTP_204_NO_CONTENT, +) +async def delete_function_group( + _path: Annotated[FunctionGroupPathParams, Depends()], +): ... diff --git a/packages/models-library/src/models_library/api_schemas_webserver/functions.py b/packages/models-library/src/models_library/api_schemas_webserver/functions.py index a52966a8d181..661933880742 100644 --- a/packages/models-library/src/models_library/api_schemas_webserver/functions.py +++ b/packages/models-library/src/models_library/api_schemas_webserver/functions.py @@ -5,7 +5,6 @@ from ..functions import ( Function, - FunctionAccessRights, FunctionBase, FunctionClass, FunctionClassSpecificData, @@ -49,6 +48,7 @@ UnsupportedFunctionClassError, UnsupportedFunctionFunctionJobClassCombinationError, ) +from ..groups import GroupID from ..projects import ProjectID from ._base import InputSchema, OutputSchema @@ -114,11 +114,23 @@ ] +class FunctionGroupAccessRightsGet(OutputSchema): + read: bool + write: bool + execute: bool + + +class FunctionGroupAccessRightsUpdate(InputSchema): + read: bool + write: bool + execute: bool + + class RegisteredSolverFunctionGet(RegisteredSolverFunction, OutputSchema): uid: Annotated[FunctionID, Field(alias="uuid")] created_at: Annotated[datetime.datetime, Field(alias="creationDate")] modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] - access_rights: FunctionAccessRights | None = None + access_rights: dict[GroupID, FunctionGroupAccessRightsGet] thumbnail: HttpUrl | None = None @@ -127,7 +139,7 @@ class RegisteredProjectFunctionGet(RegisteredProjectFunction, OutputSchema): project_id: Annotated[ProjectID, Field(alias="templateId")] created_at: Annotated[datetime.datetime, Field(alias="creationDate")] modified_at: Annotated[datetime.datetime, Field(alias="lastChangeDate")] - access_rights: FunctionAccessRights | None = None + access_rights: dict[GroupID, FunctionGroupAccessRightsGet] thumbnail: HttpUrl | None = None diff --git a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml index 570a3517e6d7..fccc46491e57 100644 --- a/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml +++ b/services/web/server/src/simcore_service_webserver/api/v0/openapi.yaml @@ -3761,6 +3761,86 @@ paths: responses: '204': description: Successful Response + /v0/functions/{function_id}/groups: + get: + tags: + - functions + summary: Get Function Groups + operationId: get_function_groups + parameters: + - name: function_id + in: path + required: true + schema: + type: string + format: uuid + title: Function Id + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_dict_Annotated_int__Gt___FunctionGroupAccessRightsGet__' + /v0/functions/{function_id}/groups/{group_id}: + put: + tags: + - functions + summary: Create or update a Function Group + operationId: create_or_update_function_group + parameters: + - name: function_id + in: path + required: true + schema: + type: string + format: uuid + title: Function Id + - name: group_id + in: path + required: true + schema: + type: integer + exclusiveMinimum: true + title: Group Id + minimum: 0 + requestBody: + required: true + content: + application/json: + schema: + $ref: '#/components/schemas/FunctionGroupAccessRightsUpdate' + responses: + '200': + description: Successful Response + content: + application/json: + schema: + $ref: '#/components/schemas/Envelope_FunctionGroupAccessRightsGet_' + delete: + tags: + - functions + summary: Delete a Function Group + operationId: delete_function_group + parameters: + - name: function_id + in: path + required: true + schema: + type: string + format: uuid + title: Function Id + - name: group_id + in: path + required: true + schema: + type: integer + exclusiveMinimum: true + title: Group Id + minimum: 0 + responses: + '204': + description: Successful Response /v0/tasks: get: tags: @@ -10802,6 +10882,19 @@ components: title: Error type: object title: Envelope[FolderGet] + Envelope_FunctionGroupAccessRightsGet_: + properties: + data: + anyOf: + - $ref: '#/components/schemas/FunctionGroupAccessRightsGet' + - type: 'null' + error: + anyOf: + - {} + - type: 'null' + title: Error + type: object + title: Envelope[FunctionGroupAccessRightsGet] Envelope_GetProjectInactivityResponse_: properties: data: @@ -11511,6 +11604,22 @@ components: title: Error type: object title: Envelope[_ProjectNodePreview] + Envelope_dict_Annotated_int__Gt___FunctionGroupAccessRightsGet__: + properties: + data: + anyOf: + - additionalProperties: + $ref: '#/components/schemas/FunctionGroupAccessRightsGet' + type: object + - type: 'null' + title: Data + error: + anyOf: + - {} + - type: 'null' + title: Error + type: object + title: Envelope[dict[Annotated[int, Gt], FunctionGroupAccessRightsGet]] Envelope_dict_Annotated_str__StringConstraints___ImageResources__: properties: data: @@ -12632,23 +12741,40 @@ components: required: - name title: FolderReplaceBodyParams - FunctionAccessRights: + FunctionGroupAccessRightsGet: properties: read: type: boolean title: Read - default: false write: type: boolean title: Write - default: false execute: type: boolean title: Execute - default: false - additionalProperties: false type: object - title: FunctionAccessRights + required: + - read + - write + - execute + title: FunctionGroupAccessRightsGet + FunctionGroupAccessRightsUpdate: + properties: + read: + type: boolean + title: Read + write: + type: boolean + title: Write + execute: + type: boolean + title: Execute + type: object + required: + - read + - write + - execute + title: FunctionGroupAccessRightsUpdate GetProjectInactivityResponse: properties: is_inactive: @@ -16463,9 +16589,10 @@ components: format: uuid title: Templateid accessRights: - anyOf: - - $ref: '#/components/schemas/FunctionAccessRights' - - type: 'null' + additionalProperties: + $ref: '#/components/schemas/FunctionGroupAccessRightsGet' + type: object + title: Accessrights thumbnail: anyOf: - type: string @@ -16483,6 +16610,7 @@ components: - creationDate - lastChangeDate - templateId + - accessRights title: RegisteredProjectFunctionGet RegisteredSolverFunctionGet: properties: @@ -16542,9 +16670,10 @@ components: pattern: ^(0|[1-9]\d*)(\.(0|[1-9]\d*)){2}(-(0|[1-9]\d*|\d*[-a-zA-Z][-\da-zA-Z]*)(\.(0|[1-9]\d*|\d*[-a-zA-Z][-\da-zA-Z]*))*)?(\+[-\da-zA-Z]+(\.[-\da-zA-Z-]+)*)?$ title: Solverversion accessRights: - anyOf: - - $ref: '#/components/schemas/FunctionAccessRights' - - type: 'null' + additionalProperties: + $ref: '#/components/schemas/FunctionGroupAccessRightsGet' + type: object + title: Accessrights thumbnail: anyOf: - type: string @@ -16563,6 +16692,7 @@ components: - lastChangeDate - solverKey - solverVersion + - accessRights title: RegisteredSolverFunctionGet ReplaceWalletAutoRecharge: properties: diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py index 54363cbdb129..8eb61dffd051 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest.py @@ -3,6 +3,8 @@ from aiohttp import web from models_library.api_schemas_webserver.functions import ( Function, + FunctionGroupAccessRightsGet, + FunctionGroupAccessRightsUpdate, FunctionToRegister, RegisteredFunction, RegisteredFunctionGet, @@ -10,12 +12,13 @@ ) from models_library.api_schemas_webserver.users import MyFunctionPermissionsGet from models_library.functions import ( - FunctionAccessRights, FunctionClass, + FunctionGroupAccessRights, FunctionID, RegisteredProjectFunction, RegisteredSolverFunction, ) +from models_library.groups import GroupID from models_library.products import ProductName from models_library.rest_pagination import Page from models_library.rest_pagination_utils import paginate_data @@ -41,6 +44,7 @@ from ._functions_rest_exceptions import handle_rest_requests_exceptions from ._functions_rest_schemas import ( FunctionGetQueryParams, + FunctionGroupPathParams, FunctionPathParams, FunctionsListQueryParams, ) @@ -48,24 +52,27 @@ routes = web.RouteTableDef() -async def _build_function_access_rights( +async def _build_function_group_access_rights( app: web.Application, user_id: UserID, product_name: ProductName, function_id: FunctionID, -) -> FunctionAccessRights: - access_rights = await _functions_service.get_function_user_permissions( +) -> dict[GroupID, FunctionGroupAccessRightsGet]: + access_rights_list = await _functions_service.list_function_group_permissions( app=app, user_id=user_id, product_name=product_name, function_id=function_id, ) - return FunctionAccessRights( - read=access_rights.read, - write=access_rights.write, - execute=access_rights.execute, - ) + return { + access_rights.group_id: FunctionGroupAccessRightsGet( + read=access_rights.read, + write=access_rights.write, + execute=access_rights.execute, + ) + for access_rights in access_rights_list + } def _build_project_function_extras_dict( @@ -137,9 +144,17 @@ async def register_function(request: web.Request) -> web.Response: ) ) + access_rights = await _build_function_group_access_rights( + request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + function_id=registered_function.uid, + ) + return envelope_json_response( TypeAdapter(RegisteredFunctionGet).validate_python( registered_function.model_dump(mode="json") + | {"access_rights": access_rights} ), web.HTTPCreated, ) @@ -217,7 +232,7 @@ async def list_functions(request: web.Request) -> web.Response: ) for function in functions: - access_rights = await _build_function_access_rights( + access_rights = await _build_function_group_access_rights( request.app, user_id=req_ctx.user_id, product_name=req_ctx.product_name, @@ -267,7 +282,7 @@ async def get_function(request: web.Request) -> web.Response: product_name=req_ctx.product_name, ) - access_rights = await _build_function_access_rights( + access_rights = await _build_function_group_access_rights( request.app, user_id=req_ctx.user_id, product_name=req_ctx.product_name, @@ -315,7 +330,7 @@ async def update_function(request: web.Request) -> web.Response: function=function_update, ) - access_rights = await _build_function_access_rights( + access_rights = await _build_function_group_access_rights( request.app, user_id=req_ctx.user_id, product_name=req_ctx.product_name, @@ -356,6 +371,109 @@ async def delete_function(request: web.Request) -> web.Response: return web.json_response(status=status.HTTP_204_NO_CONTENT) +# +# /functions/{function_id}/groups/* +# + + +@routes.get( + f"/{VTAG}/functions/{{function_id}}/groups", + name="get_function_groups", +) +@login_required +@permission_required("function.read") +@handle_rest_requests_exceptions +async def get_function_groups(request: web.Request) -> web.Response: + path_params = parse_request_path_parameters_as(FunctionPathParams, request) + function_id = path_params.function_id + + req_ctx = AuthenticatedRequestContext.model_validate(request) + access_rights_list = await _functions_service.list_function_group_permissions( + request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + function_id=function_id, + ) + + return envelope_json_response( + { + access_rights.group_id: FunctionGroupAccessRightsGet( + read=access_rights.read, + write=access_rights.write, + execute=access_rights.execute, + ) + for access_rights in access_rights_list + } + ) + + +@routes.put( + f"/{VTAG}/functions/{{function_id}}/groups/{{group_id}}", + name="create_or_update_function_group", +) +@login_required +@permission_required("function.update") +@handle_rest_requests_exceptions +async def create_or_update_function_group(request: web.Request) -> web.Response: + path_params = parse_request_path_parameters_as(FunctionGroupPathParams, request) + function_id = path_params.function_id + group_id = path_params.group_id + + req_ctx = AuthenticatedRequestContext.model_validate(request) + + function_group_update = FunctionGroupAccessRightsUpdate.model_validate( + await request.json() + ) + + updated_function_access_rights = ( + await _functions_service.set_function_group_permissions( + request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + function_id=function_id, + permissions=FunctionGroupAccessRights( + group_id=group_id, + read=function_group_update.read, + write=function_group_update.write, + execute=function_group_update.execute, + ), + ) + ) + + return envelope_json_response( + FunctionGroupAccessRightsGet( + read=updated_function_access_rights.read, + write=updated_function_access_rights.write, + execute=updated_function_access_rights.execute, + ) + ) + + +@routes.delete( + f"/{VTAG}/functions/{{function_id}}/groups/{{group_id}}", + name="delete_function_group", +) +@login_required +@permission_required("function.update") +@handle_rest_requests_exceptions +async def delete_function_group(request: web.Request) -> web.Response: + path_params = parse_request_path_parameters_as(FunctionGroupPathParams, request) + function_id = path_params.function_id + group_id = path_params.group_id + + req_ctx = AuthenticatedRequestContext.model_validate(request) + + await _functions_service.remove_function_group_permissions( + request.app, + user_id=req_ctx.user_id, + product_name=req_ctx.product_name, + function_id=function_id, + permission_group_id=group_id, + ) + + return web.json_response(status=status.HTTP_204_NO_CONTENT) + + # # /me/* endpoints # diff --git a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py index 79030f202be8..2f8dedb64d92 100644 --- a/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py +++ b/services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rest_schemas.py @@ -1,4 +1,5 @@ from models_library.functions import FunctionID +from models_library.groups import GroupID from models_library.rest_pagination import PageQueryParameters from pydantic import BaseModel, ConfigDict @@ -12,6 +13,10 @@ class FunctionPathParams(BaseModel): model_config = ConfigDict(populate_by_name=True, extra="forbid") +class FunctionGroupPathParams(FunctionPathParams): + group_id: GroupID + + class _FunctionQueryParams(BaseModel): include_extras: bool = False diff --git a/services/web/server/src/simcore_service_webserver/functions/_functions_exceptions.py b/services/web/server/src/simcore_service_webserver/functions/_functions_exceptions.py new file mode 100644 index 000000000000..e974ac625650 --- /dev/null +++ b/services/web/server/src/simcore_service_webserver/functions/_functions_exceptions.py @@ -0,0 +1,9 @@ +from common_library.user_messages import user_message + +from ..errors import WebServerBaseError + + +class FunctionGroupAccessRightsNotFoundError(WebServerBaseError, RuntimeError): + msg_template = user_message( + "Group access rights could not be found for Function '{function_id}' in product '{product_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 6c09b5c9a7ee..45745d925a26 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 @@ -10,6 +10,7 @@ from models_library.functions import ( FunctionAccessRightsDB, FunctionClass, + FunctionGroupAccessRights, FunctionID, FunctionInputs, FunctionInputSchema, @@ -1063,6 +1064,36 @@ async def delete_function_job_collection( ) +async def get_group_permissions( + app: web.Application, + connection: AsyncConnection | None = None, + *, + user_id: UserID, + product_name: ProductName, + object_type: Literal["function", "function_job", "function_job_collection"], + object_ids: list[UUID], +) -> list[tuple[UUID, list[FunctionGroupAccessRights]]]: + 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=["read"], + ) + + return await _internal_get_group_permissions( + app, + connection=connection, + product_name=product_name, + object_type=object_type, + object_ids=object_ids, + ) + + async def set_group_permissions( app: web.Application, connection: AsyncConnection | None = None, @@ -1075,7 +1106,7 @@ async def set_group_permissions( read: bool | None = None, write: bool | None = None, execute: bool | None = None, -) -> None: +) -> list[tuple[UUID, FunctionGroupAccessRights]]: async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn: for object_id in object_ids: await check_user_permissions( @@ -1088,7 +1119,7 @@ async def set_group_permissions( permissions=["write"], ) - await _internal_set_group_permissions( + return await _internal_set_group_permissions( app, connection=connection, permission_group_id=permission_group_id, @@ -1169,6 +1200,55 @@ async def _internal_remove_group_permissions( ) +async def _internal_get_group_permissions( + app: web.Application, + connection: AsyncConnection | None = None, + *, + product_name: ProductName, + object_type: Literal["function", "function_job", "function_job_collection"], + object_ids: list[UUID], +) -> list[tuple[UUID, list[FunctionGroupAccessRights]]]: + 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 + + access_rights_list: list[tuple[UUID, list[FunctionGroupAccessRights]]] = [] + async with pass_or_acquire_connection(get_asyncpg_engine(app), connection) as conn: + for object_id in object_ids: + rows = [ + row + async for row in await conn.stream( + access_rights_table.select().where( + getattr(access_rights_table.c, field_name) == object_id, + access_rights_table.c.product_name == product_name, + ) + ) + ] + group_permissions = [ + FunctionGroupAccessRights( + group_id=row.group_id, + read=row.read, + write=row.write, + execute=row.execute, + ) + for row in rows + ] + access_rights_list.append((object_id, group_permissions)) + + return access_rights_list + + async def _internal_set_group_permissions( app: web.Application, connection: AsyncConnection | None = None, @@ -1180,7 +1260,7 @@ async def _internal_set_group_permissions( read: bool | None = None, write: bool | None = None, execute: bool | None = None, -) -> None: +) -> list[tuple[UUID, FunctionGroupAccessRights]]: access_rights_table = None field_name = None if object_type == "function": @@ -1196,6 +1276,7 @@ async def _internal_set_group_permissions( assert access_rights_table is not None # nosec assert field_name is not None # nosec + access_rights_list: list[tuple[UUID, FunctionGroupAccessRights]] = [] async with transaction_context(get_asyncpg_engine(app), connection) as transaction: for object_id in object_ids: # Check if the group already has access rights for the function @@ -1209,8 +1290,9 @@ async def _internal_set_group_permissions( if row is None: # Insert new access rights if the group does not have any - await transaction.execute( - access_rights_table.insert().values( + result = await transaction.execute( + access_rights_table.insert() + .values( **{field_name: object_id}, group_id=permission_group_id, product_name=product_name, @@ -1218,7 +1300,15 @@ async def _internal_set_group_permissions( write=write if write is not None else False, execute=execute if execute is not None else False, ) + .returning( + access_rights_table.c.group_id, + access_rights_table.c.read, + access_rights_table.c.write, + access_rights_table.c.execute, + ) ) + row = result.one() + access_rights_list.append((object_id, FunctionGroupAccessRights(**row))) else: # Update existing access rights only for non-None values update_values = { @@ -1227,14 +1317,26 @@ async def _internal_set_group_permissions( "execute": execute if execute is not None else row["execute"], } - await transaction.execute( + update_result = await transaction.execute( access_rights_table.update() .where( getattr(access_rights_table.c, field_name) == object_id, access_rights_table.c.group_id == permission_group_id, ) .values(**update_values) + .returning( + access_rights_table.c.group_id, + access_rights_table.c.read, + access_rights_table.c.write, + access_rights_table.c.execute, + ) ) + updated_row = update_result.one() + access_rights_list.append( + (object_id, FunctionGroupAccessRights(**updated_row)) + ) + + return access_rights_list async def get_user_api_access_rights( 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 99212d1c26c5..cf9be30d41e5 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 @@ -44,6 +44,7 @@ from servicelib.rabbitmq import RPCRouter from . import _functions_repository +from ._functions_exceptions import FunctionGroupAccessRightsNotFoundError router = RPCRouter() @@ -457,6 +458,31 @@ async def get_function_user_permissions( ) +async def list_function_group_permissions( + app: web.Application, + *, + user_id: UserID, + product_name: ProductName, + function_id: FunctionID, +) -> list[FunctionGroupAccessRights]: + access_rights_list = await _functions_repository.get_group_permissions( + app=app, + user_id=user_id, + product_name=product_name, + object_ids=[function_id], + object_type="function", + ) + + for object_id, access_rights in access_rights_list: + if object_id == function_id: + return access_rights + + raise FunctionGroupAccessRightsNotFoundError( + function_id=function_id, + product_name=product_name, + ) + + async def set_function_group_permissions( app: web.Application, *, @@ -464,8 +490,8 @@ async def set_function_group_permissions( product_name: ProductName, function_id: FunctionID, permissions: FunctionGroupAccessRights, -) -> None: - await _functions_repository.set_group_permissions( +) -> FunctionGroupAccessRights: + access_rights_list = await _functions_repository.set_group_permissions( app=app, user_id=user_id, product_name=product_name, @@ -476,6 +502,14 @@ async def set_function_group_permissions( write=permissions.write, execute=permissions.execute, ) + for object_id, access_rights in access_rights_list: + if object_id == function_id: + return access_rights + + raise FunctionGroupAccessRightsNotFoundError( + product_name=product_name, + function_id=function_id, + ) async def remove_function_group_permissions( diff --git a/services/web/server/tests/unit/with_dbs/04/functions/test_functions_controller_rest.py b/services/web/server/tests/unit/with_dbs/04/functions/test_functions_controller_rest.py index b6410071111a..8ecef15df64a 100644 --- a/services/web/server/tests/unit/with_dbs/04/functions/test_functions_controller_rest.py +++ b/services/web/server/tests/unit/with_dbs/04/functions/test_functions_controller_rest.py @@ -2,7 +2,7 @@ # pylint: disable=unused-argument # pylint: disable=unused-variable # pylint: disable=too-many-arguments - +# pylint: disable=too-many-statements from collections.abc import AsyncIterator from http import HTTPStatus @@ -87,6 +87,7 @@ def mocked_function(request) -> dict[str, Any]: async def test_function_workflow( client: TestClient, logged_user: UserInfoDict, + other_logged_user: UserInfoDict, mocked_function: dict[str, Any], expected_register: HTTPStatus, expected_get: HTTPStatus, @@ -118,6 +119,43 @@ async def test_function_workflow( retrieved_function = TypeAdapter(RegisteredFunctionGet).validate_python(data) assert retrieved_function.uid == returned_function.uid + # Set group permissions for other user + new_group_id = other_logged_user["primary_gid"] + new_group_access_rights = {"read": True, "write": True, "execute": False} + + url = client.app.router["create_or_update_function_group"].url_for( + function_id=f"{returned_function_uid}", group_id=f"{new_group_id}" + ) + + response = await client.put(url, json=new_group_access_rights) + data, error = await assert_status(response, expected_update) + if not error: + assert data == new_group_access_rights + + # Remove group permissions for original user + url = client.app.router["delete_function_group"].url_for( + function_id=f"{returned_function_uid}", group_id=f"{logged_user['primary_gid']}" + ) + + response = await client.delete(url) + data, error = await assert_status(response, expected_delete) + if not error: + assert data is None + + # Check that original user no longer has access + url = client.app.router["get_function"].url_for( + function_id=f"{returned_function_uid}" + ) + response = await client.get(url) + data, error = await assert_status(response, expected_get) + if not error: + retrieved_function = ( + TypeAdapter(RegisteredFunctionGet).validate_python(data).model_dump() + ) + assert retrieved_function["access_rights"] == { + new_group_id: new_group_access_rights + } + # List existing functions url = client.app.router["list_functions"].url_for() response = await client.get(url) @@ -140,9 +178,11 @@ async def test_function_workflow( ) data, error = await assert_status(response, expected_update) if not error: - updated_function = TypeAdapter(RegisteredFunctionGet).validate_python(data) - assert updated_function.title == new_title - assert updated_function.description == new_description + updated_group_access_rights = TypeAdapter( + RegisteredFunctionGet + ).validate_python(data) + assert updated_group_access_rights.title == new_title + assert updated_group_access_rights.description == new_description # Delete existing function url = client.app.router["delete_function"].url_for( 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 index 80f34cab6135..2568d6d09e58 100644 --- 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 @@ -53,20 +53,24 @@ async def test_set_and_remove_group_permissions( function_id=registered_function.uid, ) + group_permissions = FunctionGroupAccessRights( + group_id=int(other_logged_user["primary_gid"]), + read=True, + write=True, + execute=False, + ) + # Give non-registering user group access - await _functions_service.set_function_group_permissions( + updated_group_permissions = 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, - ), + permissions=group_permissions, ) + assert updated_group_permissions == group_permissions + # Test if non-registering user can access the function returned_function = await _functions_service.get_function( app=client.app,