Skip to content
Merged
Show file tree
Hide file tree
Changes from 31 commits
Commits
Show all changes
32 commits
Select commit Hold shift + click to select a range
d7b8ce8
Make sure function job was successful before returning cache
wvangeit Jun 2, 2025
cdc111b
Merge branch 'master' into function_cache_check_success
wvangeit Jun 2, 2025
d187c42
Merge branch 'master' into function_cache_check_success
wvangeit Jun 2, 2025
a72507a
Fix return type error
wvangeit Jun 2, 2025
82460f7
Merge branch 'function_cache_check_success' of github.com:wvangeit/os…
wvangeit Jun 2, 2025
e8da50c
Fix mypy bug
wvangeit Jun 2, 2025
9300f6e
Merge branch 'master' into function_cache_check_success
wvangeit Jun 2, 2025
45cfd1f
Merge branch 'master' into function_cache_check_success
wvangeit Jun 2, 2025
d26df1e
Merge branch 'master' into function_cache_check_success
wvangeit Jun 3, 2025
6314ffe
Return the created_at data of registered function api objects
wvangeit Jun 3, 2025
fdadd8a
Merge branch 'master' into add_registered_at_to_functions
wvangeit Jun 3, 2025
5ebc54c
Update openapi specs
wvangeit Jun 3, 2025
27494dd
Add error handlers for functions api
wvangeit Jun 3, 2025
8df445f
Merge branch 'master' into handle_function_errors_api
wvangeit Jun 4, 2025
f18c72b
Fix import typecheck
wvangeit Jun 4, 2025
6ff8037
Merge branch 'master' into handle_function_errors_api
wvangeit Jun 4, 2025
12770c5
Fix mypy typecheck
wvangeit Jun 4, 2025
6831d81
Import fastapi for status
wvangeit Jun 4, 2025
520f55d
Remove fastapi import from function models
wvangeit Jun 4, 2025
529f40a
Merge branch 'master' into handle_function_errors_api
wvangeit Jun 4, 2025
c2bbc45
Merge branch 'handle_function_errors_api' of github.com:wvangeit/ospa…
wvangeit Jun 4, 2025
e7e3db4
Merge branch 'master' into handle_function_errors_api
wvangeit Jun 5, 2025
88d14b5
Merge branch 'master' into handle_function_errors_api
wvangeit Jun 5, 2025
240c0a2
Merge branch 'master' into handle_function_errors_api
mergify[bot] Jun 5, 2025
539669e
Add check for function execute permission
wvangeit Jun 6, 2025
98fdf9c
Merge branch 'master' into check_function_run_permissions
wvangeit Jun 6, 2025
1de06a5
Fix error type
wvangeit Jun 6, 2025
1f63ab3
Merge branch 'master' into check_function_run_permissions
wvangeit Jun 6, 2025
e4fb11e
Merge branch 'master' into check_function_run_permissions
wvangeit Jun 6, 2025
0883628
Merge branch 'master' into check_function_run_permissions
wvangeit Jun 6, 2025
52ddb00
Merge branch 'master' into check_function_run_permissions
wvangeit Jun 6, 2025
968b6e3
Merge branch 'master' into check_function_run_permissions
wvangeit Jun 6, 2025
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -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)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand All @@ -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

Expand Down Expand Up @@ -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}"
)
Original file line number Diff line number Diff line change
Expand Up @@ -2,6 +2,7 @@
from models_library.api_schemas_webserver import WEBSERVER_RPC_NAMESPACE
from models_library.functions import (
Function,
FunctionAccessRights,
FunctionID,
FunctionInputs,
FunctionInputSchema,
Expand Down Expand Up @@ -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)
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@
FunctionJobDB,
FunctionJobID,
FunctionOutputSchema,
FunctionUserAccessRights,
RegisteredFunction,
RegisteredFunctionDB,
RegisteredFunctionJob,
Expand Down Expand Up @@ -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:
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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,
)
Loading