Skip to content

Commit e920b72

Browse files
authored
Merge branch 'master' into fix-catalog
2 parents 0730883 + 360fa2e commit e920b72

File tree

25 files changed

+284
-91
lines changed

25 files changed

+284
-91
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/director-v2/src/simcore_service_director_v2/modules/dask_client.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -90,7 +90,7 @@
9090
_logger = logging.getLogger(__name__)
9191

9292

93-
_DASK_DEFAULT_TIMEOUT_S: Final[int] = 5
93+
_DASK_DEFAULT_TIMEOUT_S: Final[int] = 35
9494

9595

9696
_UserCallbackInSepThread = Callable[[], None]

services/static-webserver/client/source/class/osparc/dashboard/NewPlusMenu.js

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -244,9 +244,9 @@ qx.Class.define("osparc.dashboard.NewPlusMenu", {
244244

245245
const permissions = osparc.data.Permissions.getInstance();
246246
if (permissions.canDo("dashboard.templates.read")) {
247-
const templatesButton = this.self().createMenuButton("@FontAwesome5Solid/copy/16", this.tr("Tutorials..."));
248-
templatesButton.addListener("execute", () => this.fireDataEvent("changeTab", "templatesTab"), this);
249-
moreMenu.add(templatesButton);
247+
const tutorialsButton = this.self().createMenuButton("@FontAwesome5Solid/copy/16", this.tr("Tutorials..."));
248+
tutorialsButton.addListener("execute", () => this.fireDataEvent("changeTab", "tutorialsTab"), this);
249+
moreMenu.add(tutorialsButton);
250250
}
251251

252252
if (permissions.canDo("dashboard.services.read")) {

services/web/server/src/simcore_service_webserver/catalog/_catalog_rest_client_service.py

Lines changed: 35 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,7 @@
22

33
import logging
44
import urllib.parse
5-
from collections.abc import Callable, Iterator
5+
from collections.abc import Iterator
66
from contextlib import contextmanager
77
from typing import Any, Final
88

@@ -30,20 +30,11 @@
3030
from yarl import URL
3131

3232
from .._meta import api_version_prefix
33+
from ._models import ServiceKeyVersionDict
3334
from .settings import CatalogSettings, get_plugin_settings
3435

3536
_logger = logging.getLogger(__name__)
3637

37-
# Cache settings
38-
_SECOND = 1 # in seconds
39-
_MINUTE = 60 * _SECOND
40-
_CACHE_TTL: Final = 1 * _MINUTE
41-
42-
43-
def _create_service_cache_key(_f: Callable[..., Any], *_args, **kw):
44-
assert len(_args) == 1, f"Expected only app, got {_args}" # nosec
45-
return f"get_service_{kw['user_id']}_{kw['service_key']}_{kw['service_version']}_{kw['product_name']}"
46-
4738

4839
@contextmanager
4940
def _handle_client_exceptions(app: web.Application) -> Iterator[ClientSession]:
@@ -96,10 +87,27 @@ def to_backend_service(rel_url: URL, origin: URL, version_prefix: str) -> URL:
9687
return origin.with_path(new_path).with_query(rel_url.query)
9788

9889

90+
# Cache settings for services rest API
91+
_SECOND = 1 # in seconds
92+
_MINUTE = 60 * _SECOND
93+
_CACHE_TTL: Final = 1 * _MINUTE
94+
95+
96+
@cached(
97+
ttl=_CACHE_TTL,
98+
key_builder=lambda _f, *_args, **kw: f"get_services_for_user_in_product_{kw['user_id']}_{kw['product_name']}",
99+
cache=Cache.MEMORY,
100+
)
99101
async def get_services_for_user_in_product(
100-
app: web.Application, user_id: UserID, product_name: str, *, only_key_versions: bool
101-
) -> list[dict]:
102+
app: web.Application, *, user_id: UserID, product_name: str
103+
) -> list[ServiceKeyVersionDict]:
104+
"""
105+
DEPRECATED: see instead RPC interface.
106+
SEE https://github.com/ITISFoundation/osparc-simcore/issues/7838
107+
"""
102108
settings: CatalogSettings = get_plugin_settings(app)
109+
only_key_versions = True
110+
103111
url = (URL(settings.api_base_url) / "services").with_query(
104112
{"user_id": user_id, "details": f"{not only_key_versions}"}
105113
)
@@ -115,13 +123,18 @@ async def get_services_for_user_in_product(
115123
user_id,
116124
)
117125
return []
118-
body: list[dict] = await response.json()
119-
return body
126+
services: list[dict] = await response.json()
127+
128+
# This reduces the size cached in the memory
129+
return [
130+
ServiceKeyVersionDict(key=service["key"], version=service["version"])
131+
for service in services
132+
]
120133

121134

122135
@cached(
123136
ttl=_CACHE_TTL,
124-
key_builder=_create_service_cache_key,
137+
key_builder=lambda _f, *_args, **kw: f"get_service_{kw['user_id']}_{kw['service_key']}_{kw['service_version']}_{kw['product_name']}",
125138
cache=Cache.MEMORY,
126139
# SEE https://github.com/ITISFoundation/osparc-simcore/pull/7802
127140
)
@@ -133,6 +146,10 @@ async def get_service(
133146
service_version: ServiceVersion,
134147
product_name: ProductName,
135148
) -> dict[str, Any]:
149+
"""
150+
DEPRECATED: see instead RPC interface.
151+
SEE https://github.com/ITISFoundation/osparc-simcore/issues/7838
152+
"""
136153
settings: CatalogSettings = get_plugin_settings(app)
137154
url = URL(
138155
f"{settings.api_base_url}/services/{urllib.parse.quote_plus(service_key)}/{service_version}",
@@ -144,8 +161,8 @@ async def get_service(
144161
url, headers={X_PRODUCT_NAME_HEADER: product_name}
145162
) as response:
146163
response.raise_for_status()
147-
body: dict[str, Any] = await response.json()
148-
return body
164+
service: dict[str, Any] = await response.json()
165+
return service
149166

150167

151168
async def get_service_resources(

services/web/server/src/simcore_service_webserver/catalog/_controller_rest.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -37,7 +37,6 @@
3737
from ..utils_aiohttp import envelope_json_response
3838
from . import _catalog_rest_client_service, _service
3939
from ._controller_rest_exceptions import (
40-
DefaultPricingUnitForServiceNotFoundError,
4140
handle_plugin_requests_exceptions,
4241
)
4342
from ._controller_rest_schemas import (
@@ -50,6 +49,7 @@
5049
ServiceTagPathParams,
5150
ToServiceInputsQueryParams,
5251
)
52+
from .errors import DefaultPricingUnitForServiceNotFoundError
5353

5454
_logger = logging.getLogger(__name__)
5555

services/web/server/src/simcore_service_webserver/catalog/_controller_rest_exceptions.py

Lines changed: 3 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -105,19 +105,12 @@ async def _handler_catalog_client_errors(
105105
}
106106

107107

108-
_exceptions_handlers_map: ExceptionHandlersMap = {
108+
catalog_exceptions_handlers_map: ExceptionHandlersMap = {
109109
CatalogResponseError: _handler_catalog_client_errors,
110110
CatalogConnectionError: _handler_catalog_client_errors,
111111
}
112-
_exceptions_handlers_map.update(to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP))
112+
catalog_exceptions_handlers_map.update(to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP))
113113

114114
handle_plugin_requests_exceptions = exception_handling_decorator(
115-
_exceptions_handlers_map
116-
)
117-
118-
119-
__all__: tuple[str, ...] = (
120-
"CatalogForbiddenError",
121-
"CatalogItemNotFoundError",
122-
"DefaultPricingUnitForServiceNotFoundError",
115+
catalog_exceptions_handlers_map
123116
)
Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1,6 @@
1-
# NOTE: missing. @bisgaard-itis will follow up here
1+
from typing import TypedDict
2+
3+
4+
class ServiceKeyVersionDict(TypedDict):
5+
key: str
6+
version: str

0 commit comments

Comments
 (0)