Skip to content

Commit 360fa2e

Browse files
Check function run permissions 🎨 (#7844)
Co-authored-by: mergify[bot] <37929162+mergify[bot]@users.noreply.github.com>
1 parent aaf1079 commit 360fa2e

File tree

7 files changed

+179
-0
lines changed

7 files changed

+179
-0
lines changed

packages/service-library/src/servicelib/rabbitmq/rpc_interfaces/webserver/functions/functions_rpc_interface.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616
RegisteredFunctionJob,
1717
RegisteredFunctionJobCollection,
1818
)
19+
from models_library.functions import FunctionUserAccessRights
1920
from models_library.products import ProductName
2021
from models_library.rabbitmq_basic_types import RPCMethodName
2122
from models_library.rest_pagination import PageMetaInfoLimitOffset
@@ -388,3 +389,21 @@ async def delete_function_job_collection(
388389
product_name=product_name,
389390
)
390391
assert result is None # nosec
392+
393+
394+
@log_decorator(_logger, level=logging.DEBUG)
395+
async def get_function_user_permissions(
396+
rabbitmq_rpc_client: RabbitMQRPCClient,
397+
*,
398+
user_id: UserID,
399+
product_name: ProductName,
400+
function_id: FunctionID,
401+
) -> FunctionUserAccessRights:
402+
result = await rabbitmq_rpc_client.request(
403+
WEBSERVER_RPC_NAMESPACE,
404+
TypeAdapter(RPCMethodName).validate_python("get_function_user_permissions"),
405+
function_id=function_id,
406+
user_id=user_id,
407+
product_name=product_name,
408+
)
409+
return TypeAdapter(FunctionUserAccessRights).validate_python(result)

services/api-server/src/simcore_service_api_server/api/routes/functions_routes.py

Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -21,8 +21,11 @@
2121
RegisteredFunctionJobCollection,
2222
SolverFunctionJob,
2323
)
24+
from models_library.functions import FunctionUserAccessRights
2425
from models_library.functions_errors import (
26+
FunctionExecuteAccessDeniedError,
2527
FunctionInputsValidationError,
28+
FunctionReadAccessDeniedError,
2629
UnsupportedFunctionClassError,
2730
)
2831
from models_library.products import ProductName
@@ -371,6 +374,22 @@ async def run_function( # noqa: PLR0913
371374
job_service: Annotated[JobService, Depends(get_job_service)],
372375
) -> RegisteredFunctionJob:
373376

377+
user_permissions: FunctionUserAccessRights = (
378+
await wb_api_rpc.get_function_user_permissions(
379+
function_id=function_id, user_id=user_id, product_name=product_name
380+
)
381+
)
382+
if not user_permissions.read:
383+
raise FunctionReadAccessDeniedError(
384+
user_id=user_id,
385+
function_id=function_id,
386+
)
387+
if not user_permissions.execute:
388+
raise FunctionExecuteAccessDeniedError(
389+
user_id=user_id,
390+
function_id=function_id,
391+
)
392+
374393
from .function_jobs_routes import function_job_status
375394

376395
to_run_function = await wb_api_rpc.get_function(

services/api-server/src/simcore_service_api_server/services_rpc/wb_api_server.py

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@
2323
RegisteredFunctionJobCollection,
2424
)
2525
from models_library.api_schemas_webserver.licensed_items import LicensedItemRpcGetPage
26+
from models_library.functions import FunctionUserAccessRights
2627
from models_library.licenses import LicensedItemID
2728
from models_library.products import ProductName
2829
from models_library.projects import ProjectID
@@ -526,6 +527,20 @@ async def delete_function_job_collection(
526527
function_job_collection_id=function_job_collection_id,
527528
)
528529

530+
async def get_function_user_permissions(
531+
self,
532+
*,
533+
user_id: UserID,
534+
product_name: ProductName,
535+
function_id: FunctionID,
536+
) -> FunctionUserAccessRights:
537+
return await functions_rpc_interface.get_function_user_permissions(
538+
self._client,
539+
user_id=user_id,
540+
product_name=product_name,
541+
function_id=function_id,
542+
)
543+
529544

530545
def setup(app: FastAPI, rabbitmq_rmp_client: RabbitMQRPCClient):
531546
wb_api_rpc_client = WbApiRpcClient(_client=rabbitmq_rmp_client)

services/api-server/tests/unit/api_functions/test_api_routers_functions.py

Lines changed: 35 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@
77
from uuid import uuid4
88

99
import httpx
10+
import respx
1011
from httpx import AsyncClient
1112
from models_library.api_schemas_webserver.functions import (
1213
FunctionJobCollection,
@@ -16,11 +17,14 @@
1617
RegisteredProjectFunction,
1718
RegisteredProjectFunctionJob,
1819
)
20+
from models_library.functions import FunctionUserAccessRights
1921
from models_library.functions_errors import (
2022
FunctionIDNotFoundError,
2123
FunctionReadAccessDeniedError,
2224
)
2325
from models_library.rest_pagination import PageMetaInfoLimitOffset
26+
from models_library.users import UserID
27+
from pytest_mock import MockType
2428
from servicelib.aiohttp import status
2529
from simcore_service_api_server._meta import API_VTAG
2630

@@ -580,3 +584,34 @@ async def test_list_function_job_collections_with_function_filter(
580584
RegisteredFunctionJobCollection.model_validate(data["items"][0])
581585
== mock_registered_function_job_collection
582586
)
587+
588+
589+
async def test_run_function_not_allowed(
590+
client: AsyncClient,
591+
mock_handler_in_functions_rpc_interface: Callable[[str, Any], None],
592+
mock_registered_function: RegisteredProjectFunction,
593+
auth: httpx.BasicAuth,
594+
user_id: UserID,
595+
mocked_webserver_rest_api_base: respx.MockRouter,
596+
mocked_webserver_rpc_api: dict[str, MockType],
597+
) -> None:
598+
"""Test that running a function is not allowed."""
599+
mock_handler_in_functions_rpc_interface(
600+
"get_function_user_permissions",
601+
FunctionUserAccessRights(
602+
user_id=user_id,
603+
execute=False,
604+
read=True,
605+
write=True,
606+
),
607+
)
608+
609+
response = await client.post(
610+
f"{API_VTAG}/functions/{mock_registered_function.uid}:run",
611+
json={},
612+
auth=auth,
613+
)
614+
assert response.status_code == status.HTTP_403_FORBIDDEN
615+
assert response.json()["errors"][0] == (
616+
f"Function {mock_registered_function.uid} execute access denied for user {user_id}"
617+
)

services/web/server/src/simcore_service_webserver/functions/_controller/_functions_rpc.py

Lines changed: 20 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -2,6 +2,7 @@
22
from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE
33
from models_library.functions import (
44
Function,
5+
FunctionAccessRights,
56
FunctionID,
67
FunctionInputs,
78
FunctionInputSchema,
@@ -352,6 +353,25 @@ async def get_function_output_schema(
352353
)
353354

354355

356+
@router.expose(reraise_if_error_type=(FunctionIDNotFoundError,))
357+
async def get_function_user_permissions(
358+
app: web.Application,
359+
*,
360+
user_id: UserID,
361+
product_name: ProductName,
362+
function_id: FunctionID,
363+
) -> FunctionAccessRights:
364+
"""
365+
Returns a dictionary with the user's permissions for the function.
366+
"""
367+
return await _functions_service.get_function_user_permissions(
368+
app=app,
369+
user_id=user_id,
370+
product_name=product_name,
371+
function_id=function_id,
372+
)
373+
374+
355375
async def register_rpc_routes_on_startup(app: web.Application):
356376
rpc_server = get_rabbitmq_rpc_server(app)
357377
await rpc_server.register_router(router, WEBSERVER_RPC_NAMESPACE, app)

services/web/server/src/simcore_service_webserver/functions/_functions_service.py

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
FunctionJobDB,
1515
FunctionJobID,
1616
FunctionOutputSchema,
17+
FunctionUserAccessRights,
1718
RegisteredFunction,
1819
RegisteredFunctionDB,
1920
RegisteredFunctionJob,
@@ -435,6 +436,38 @@ async def get_function_output_schema(
435436
return _decode_function(returned_function).output_schema
436437

437438

439+
@router.expose(reraise_if_error_type=(FunctionIDNotFoundError,))
440+
async def get_function_user_permissions(
441+
app: web.Application,
442+
*,
443+
user_id: UserID,
444+
product_name: ProductName,
445+
function_id: FunctionID,
446+
) -> FunctionUserAccessRights:
447+
user_permissions = await _functions_repository.get_user_permissions(
448+
app=app,
449+
user_id=user_id,
450+
product_name=product_name,
451+
object_id=function_id,
452+
object_type="function",
453+
)
454+
return (
455+
FunctionUserAccessRights(
456+
user_id=user_id,
457+
read=user_permissions.read,
458+
write=user_permissions.write,
459+
execute=user_permissions.execute,
460+
)
461+
if user_permissions
462+
else FunctionUserAccessRights(
463+
user_id=user_id,
464+
read=False,
465+
write=False,
466+
execute=False,
467+
)
468+
)
469+
470+
438471
def _decode_function(
439472
function: RegisteredFunctionDB,
440473
) -> RegisteredFunction:

services/web/server/tests/unit/with_dbs/04/functions_rpc/test_functions_controller_rpc.py

Lines changed: 38 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414
)
1515

1616
# import simcore_service_webserver.functions._functions_controller_rpc as functions_rpc
17+
from models_library.functions import FunctionUserAccessRights
1718
from models_library.functions_errors import (
1819
FunctionIDNotFoundError,
1920
FunctionReadAccessDeniedError,
@@ -497,3 +498,40 @@ async def test_delete_function(
497498
product_name=osparc_product_name,
498499
)
499500
assert registered_function.uid is not None
501+
502+
503+
@pytest.mark.parametrize(
504+
"user_role",
505+
[UserRole.USER],
506+
)
507+
async def test_get_function_user_permissions(
508+
client: TestClient,
509+
rpc_client: RabbitMQRPCClient,
510+
mock_function: ProjectFunction,
511+
logged_user: UserInfoDict,
512+
osparc_product_name: ProductName,
513+
):
514+
# Register the function first
515+
registered_function = await functions_rpc.register_function(
516+
rabbitmq_rpc_client=rpc_client,
517+
function=mock_function,
518+
user_id=logged_user["id"],
519+
product_name=osparc_product_name,
520+
)
521+
assert registered_function.uid is not None
522+
523+
# Retrieve the user permissions for the function
524+
user_permissions = await functions_rpc.get_function_user_permissions(
525+
rabbitmq_rpc_client=rpc_client,
526+
function_id=registered_function.uid,
527+
user_id=logged_user["id"],
528+
product_name=osparc_product_name,
529+
)
530+
531+
# Assert the user permissions match the expected permissions
532+
assert user_permissions == FunctionUserAccessRights(
533+
user_id=logged_user["id"],
534+
read=True,
535+
write=True,
536+
execute=True,
537+
)

0 commit comments

Comments
 (0)