Skip to content

Commit 77b93cd

Browse files
authored
BugFix: Fix/catalog model (#1826)
* Fixes validation of ServiceAccessRightsAtDB * autoformat * Fixes using core settings * Fixes a hard-coded header * Fixes API in catalog subsystem at webserver * Fix wrong import * Adds setup-product to client test fixtures
1 parent 3ee7c86 commit 77b93cd

File tree

10 files changed

+89
-48
lines changed

10 files changed

+89
-48
lines changed

services/catalog/src/simcore_service_catalog/api/routes/services.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -11,11 +11,11 @@
1111
from ...db.repositories.services import ServicesRepository
1212
from ...models.domain.service import (
1313
KEY_RE,
14-
ServiceType,
1514
VERSION_RE,
1615
ServiceAccessRightsAtDB,
1716
ServiceMetaDataAtDB,
1817
ServiceOut,
18+
ServiceType,
1919
ServiceUpdate,
2020
)
2121
from ...services.frontend_services import get_services as get_frontend_services

services/catalog/src/simcore_service_catalog/core/background_tasks.py

Lines changed: 18 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,6 @@
2121
from pydantic import ValidationError
2222
from pydantic.types import PositiveInt
2323

24-
2524
from ..api.dependencies.director import get_director_api
2625
from ..db.repositories.groups import GroupsRepository
2726
from ..db.repositories.projects import ProjectsRepository
@@ -31,13 +30,14 @@
3130
ServiceDockerData,
3231
ServiceMetaDataAtDB,
3332
)
33+
from ..services.frontend_services import get_services as get_frontend_services
3434

3535
logger = logging.getLogger(__name__)
3636

3737
ServiceKey = str
3838
ServiceVersion = str
3939

40-
from ..services.frontend_services import get_services as get_frontend_services
40+
OLD_SERVICES_DATE: datetime = datetime(2020, 8, 19)
4141

4242

4343
async def _list_registry_services(
@@ -74,9 +74,6 @@ async def _list_db_services(
7474
}
7575

7676

77-
OLD_SERVICES_DATE: datetime = datetime(2020, 8, 19)
78-
79-
8077
async def _create_service_default_access_rights(
8178
app: FastAPI, service: ServiceDockerData, connection: SAConnection
8279
) -> Tuple[Optional[PositiveInt], List[ServiceAccessRightsAtDB]]:
@@ -187,7 +184,9 @@ async def _ensure_registry_insync_with_db(
187184
)
188185

189186

190-
async def _ensure_published_templates_accessible(connection: SAConnection) -> None:
187+
async def _ensure_published_templates_accessible(
188+
connection: SAConnection, default_product_name: str
189+
) -> None:
191190
# Rationale: if a project template was published, its services must be available to everyone.
192191
# a published template has a column Published that is set to True
193192
projects_repo = ProjectsRepository(connection)
@@ -210,7 +209,11 @@ async def _ensure_published_templates_accessible(connection: SAConnection) -> No
210209
missing_services = published_services - available_services
211210
missing_services_access_rights = [
212211
ServiceAccessRightsAtDB(
213-
key=service[0], version=service[1], gid=everyone_gid, execute_access=True
212+
key=service[0],
213+
version=service[1],
214+
gid=everyone_gid,
215+
execute_access=True,
216+
product_name=default_product_name,
214217
)
215218
for service in missing_services
216219
]
@@ -224,6 +227,7 @@ async def _ensure_published_templates_accessible(connection: SAConnection) -> No
224227

225228
async def sync_registry_task(app: FastAPI) -> None:
226229
# get list of services from director
230+
default_product: str = app.state.settings.access_rights_default_product_name
227231
engine: Engine = app.state.engine
228232
while True:
229233
try:
@@ -233,20 +237,23 @@ async def sync_registry_task(app: FastAPI) -> None:
233237
# check that the list of services is in sync with the registry
234238
await _ensure_registry_insync_with_db(app, conn)
235239

236-
# check that the published services are available to everyone (templates are published to GUESTs, so their services must be also accessible)
237-
await _ensure_published_templates_accessible(conn)
240+
# check that the published services are available to everyone
241+
# (templates are published to GUESTs, so their services must be also accessible)
242+
await _ensure_published_templates_accessible(conn, default_product)
238243

239244
await asyncio.sleep(app.state.settings.background_task_rest_time)
240245

241246
except CancelledError:
242247
# task is stopped
243248
logger.debug("Catalog background task cancelled", exc_info=True)
244249
return
250+
245251
except Exception: # pylint: disable=broad-except
246252
logger.exception("Error while processing services entry")
253+
# wait a bit before retrying, so it does not block everything until the director is up
247254
await asyncio.sleep(
248-
5
249-
) # wait a bit before retrying, so it does not block everything until the director is up
255+
app.state.settings.background_task_wait_after_failure
256+
)
250257

251258

252259
async def start_registry_sync_task(app: FastAPI) -> None:

services/catalog/src/simcore_service_catalog/core/settings.py

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -92,16 +92,17 @@ def loglevel(self) -> int:
9292
# POSTGRES
9393
postgres: PostgresSettings
9494

95-
# Director service
95+
# DIRECTOR SERVICE
9696
director: DirectorSettings
9797

9898
# SERVICE SERVER (see : https://www.uvicorn.org/settings/)
9999
host: str = "0.0.0.0" # nosec
100100
port: int = 8000
101101
debug: bool = False # If True, debug tracebacks should be returned on errors.
102102

103-
# background task
103+
# BACKGROUND TASK
104104
background_task_rest_time: PositiveInt = 60
105+
background_task_wait_after_failure: PositiveInt = 5 # secs
105106
access_rights_default_product_name: str = "osparc"
106107

107108
class Config(_CommonConfig):

services/catalog/src/simcore_service_catalog/db/repositories/services.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -131,7 +131,10 @@ async def update_service(
131131
return updated_service
132132

133133
async def get_service_access_rights(
134-
self, key: str, version: str, product_name: str,
134+
self,
135+
key: str,
136+
version: str,
137+
product_name: str,
135138
) -> List[ServiceAccessRightsAtDB]:
136139
services_in_db = []
137140
query = sa.select([services_access_rights]).where(

services/catalog/tests/unit/test_openapi_specs.py

Lines changed: 6 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2,12 +2,16 @@
22
# pylint:disable=unused-argument
33
# pylint:disable=redefined-outer-name
44

5-
import pytest
65
import json
6+
7+
import pytest
8+
79
from simcore_service_catalog.core.application import init_app
810

911

10-
@pytest.mark.skip(reason="FIXME: fails with make tests but does not fail with pytest test_!???")
12+
@pytest.mark.skip(
13+
reason="FIXME: fails with make tests but does not fail with pytest test_!???"
14+
)
1115
def test_openapi_json_is_updated(project_slug_dir, devel_environ):
1216
# devel_environ needed to build app
1317

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

Lines changed: 25 additions & 18 deletions
Original file line numberDiff line numberDiff line change
@@ -75,6 +75,9 @@ async def _request_catalog(
7575
return web.json_response(data, status=resp.status)
7676

7777

78+
## HANDLERS ------------------------
79+
80+
7881
@login_required
7982
@permission_required("services.catalog.*")
8083
async def _reverse_proxy_handler(request: web.Request) -> web.Response:
@@ -114,6 +117,28 @@ async def _reverse_proxy_handler(request: web.Request) -> web.Response:
114117
)
115118

116119

120+
## API ------------------------
121+
122+
123+
async def get_services_for_user_in_product(
124+
app: web.Application, user_id: int, product_name: str, *, only_key_versions: bool
125+
) -> Optional[List[Dict]]:
126+
url = (
127+
URL(app[f"{__name__}.catalog_origin"])
128+
.with_path(app[f"{__name__}.catalog_version_prefix"] + "/services")
129+
.with_query({"user_id": user_id, "details": f"{not only_key_versions}"})
130+
)
131+
session = get_client_session(app)
132+
async with session.get(url, headers={X_PRODUCT_NAME_HEADER: product_name}) as resp:
133+
if resp.status >= 400:
134+
logger.error("Error while retrieving services for user %s", user_id)
135+
return
136+
return await resp.json()
137+
138+
139+
## SETUP ------------------------
140+
141+
117142
@app_module_setup(
118143
__name__,
119144
ModuleCategory.ADDON,
@@ -144,21 +169,3 @@ def setup_catalog(app: web.Application, *, disable_auth=False):
144169

145170
# reverse proxy to catalog's API
146171
app.router.add_routes(routes)
147-
148-
149-
async def get_services_for_user(
150-
app: web.Application, user_id: int, *, only_key_versions: bool
151-
) -> Optional[List[Dict]]:
152-
url = (
153-
URL(app[f"{__name__}.catalog_origin"])
154-
.with_path(app[f"{__name__}.catalog_version_prefix"] + "/services")
155-
.with_query({"user_id": user_id, "details": f"{not only_key_versions}"})
156-
)
157-
158-
headers = {"X-Simcore-Products-Name": "osparc"}
159-
session = get_client_session(app)
160-
async with session.get(url, headers=headers) as resp:
161-
if resp.status >= 400:
162-
logger.error("Error while retrieving services for user %s", user_id)
163-
return
164-
return await resp.json()

services/web/server/src/simcore_service_webserver/projects/projects_handlers.py

Lines changed: 12 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -5,14 +5,15 @@
55
import logging
66
from typing import Dict, List, Optional, Set
77

8+
import aioredlock
89
from aiohttp import web
910
from jsonschema import ValidationError
1011

11-
import aioredlock
1212
from servicelib.utils import fire_and_forget_task, logged_gather
1313

1414
from .. import catalog
1515
from ..computation_api import update_pipeline_db
16+
from ..constants import RQ_PRODUCT_KEY
1617
from ..login.decorators import RQT_USERID_KEY, login_required
1718
from ..resource_manager.websocket_manager import managed_resource
1819
from ..security_api import check_permission
@@ -130,7 +131,7 @@ async def list_projects(request: web.Request):
130131
# TODO: implement all query parameters as
131132
# in https://www.ibm.com/support/knowledgecenter/en/SSCRJU_3.2.0/com.ibm.swg.im.infosphere.streams.rest.api.doc/doc/restapis-queryparms-list.html
132133

133-
user_id = request[RQT_USERID_KEY]
134+
user_id, product_name = request[RQT_USERID_KEY], request[RQ_PRODUCT_KEY]
134135
ptype = request.query.get("type", "all") # TODO: get default for oaspecs
135136
db = request.config_dict[APP_PROJECT_DBAPI]
136137

@@ -147,8 +148,10 @@ async def list_projects(request: web.Request):
147148

148149
stop = min(start + count, len(projects_list))
149150
projects_list = projects_list[start:stop]
150-
user_available_services: List[Dict] = await catalog.get_services_for_user(
151-
request.app, user_id, only_key_versions=True
151+
user_available_services: List[
152+
Dict
153+
] = await catalog.get_services_for_user_in_product(
154+
request.app, user_id, product_name, only_key_versions=True
152155
)
153156

154157
# validate response
@@ -185,9 +188,11 @@ async def validate_project(prj: Dict) -> Optional[Dict]:
185188
async def get_project(request: web.Request):
186189
"""Returns all projects accessible to a user (not necesarly owned)"""
187190
# TODO: temporary hidden until get_handlers_from_namespace refactor to seek marked functions instead!
188-
user_id = request[RQT_USERID_KEY]
189-
user_available_services: List[Dict] = await catalog.get_services_for_user(
190-
request.app, user_id, only_key_versions=True
191+
user_id, product_name = request[RQT_USERID_KEY], request[RQ_PRODUCT_KEY]
192+
user_available_services: List[
193+
Dict
194+
] = await catalog.get_services_for_user_in_product(
195+
request.app, user_id, product_name, only_key_versions=True
191196
)
192197
from .projects_api import get_project_for_user
193198

services/web/server/tests/integration/test_project_workflow.py

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -26,6 +26,7 @@
2626
from simcore_service_webserver.catalog import setup_catalog
2727
from simcore_service_webserver.db import setup_db
2828
from simcore_service_webserver.login import setup_login
29+
from simcore_service_webserver.products import setup_products
2930
from simcore_service_webserver.projects import setup_projects
3031
from simcore_service_webserver.projects.projects_fakes import Fake
3132
from simcore_service_webserver.projects.projects_models import ProjectState
@@ -74,6 +75,7 @@ def client(
7475
setup_resource_manager(app)
7576
assert setup_projects(app)
7677
setup_catalog(app)
78+
setup_products(app)
7779

7880
yield loop.run_until_complete(
7981
aiohttp_client(
@@ -242,7 +244,9 @@ def creator(projects: Optional[Union[List[Dict], Dict]] = None) -> None:
242244
async def mocked_get_services_for_user(*args, **kwargs):
243245
return services_in_project
244246

245-
monkeypatch.setattr(catalog, "get_services_for_user", mocked_get_services_for_user)
247+
monkeypatch.setattr(
248+
catalog, "get_services_for_user_in_product", mocked_get_services_for_user
249+
)
246250

247251
return creator
248252

services/web/server/tests/unit/with_dbs/fast/test_access_to_studies.py

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@
2525
from simcore_service_webserver.db import setup_db
2626
from simcore_service_webserver.login import setup_login
2727
from simcore_service_webserver.projects import setup_projects
28+
from simcore_service_webserver.products import setup_products
2829
from simcore_service_webserver.projects.projects_api import delete_project_from_db
2930
from simcore_service_webserver.projects.projects_models import (
3031
Owner,
@@ -109,9 +110,11 @@ def client(
109110
setup_rest(app) # TODO: why should we need this??
110111
setup_login(app)
111112
setup_users(app)
113+
setup_products(app)
112114
assert setup_projects(app), "Shall not skip this setup"
113115
assert setup_studies_access(app), "Shall not skip this setup"
114116

117+
115118
# server and client
116119
yield loop.run_until_complete(
117120
aiohttp_client(
@@ -249,7 +252,7 @@ async def catalog_subsystem_mock(monkeypatch, published_project):
249252
async def mocked_get_services_for_user(*args, **kwargs):
250253
return services_in_project
251254

252-
monkeypatch.setattr(catalog, "get_services_for_user", mocked_get_services_for_user)
255+
monkeypatch.setattr(catalog, "get_services_for_user_in_product", mocked_get_services_for_user)
253256

254257

255258
async def test_access_study_anonymously(

services/web/server/tests/unit/with_dbs/slow/test_projects.py

Lines changed: 11 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,7 @@
2828
from simcore_service_webserver.db_models import UserRole
2929
from simcore_service_webserver.director import setup_director
3030
from simcore_service_webserver.login import setup_login
31+
from simcore_service_webserver.products import setup_products
3132
from simcore_service_webserver.projects import setup_projects
3233
from simcore_service_webserver.projects.projects_handlers import (
3334
OVERRIDABLE_DOCUMENT_KEYS,
@@ -64,7 +65,6 @@ def client(
6465
mocked_director_subsystem,
6566
mock_orphaned_services,
6667
):
67-
# def client(loop, aiohttp_client, app_cfg): # <<<< FOR DEVELOPMENT. DO NOT REMOVE.
6868

6969
# config app
7070
cfg = deepcopy(app_cfg)
@@ -90,6 +90,7 @@ def client(
9090
setup_director(app)
9191
setup_tags(app)
9292
assert setup_projects(app)
93+
setup_products(app)
9394

9495
# server and client
9596
yield loop.run_until_complete(
@@ -243,7 +244,9 @@ def creator(projects: Optional[Union[List[Dict], Dict]] = None) -> None:
243244
async def mocked_get_services_for_user(*args, **kwargs):
244245
return services_in_project
245246

246-
monkeypatch.setattr(catalog, "get_services_for_user", mocked_get_services_for_user)
247+
monkeypatch.setattr(
248+
catalog, "get_services_for_user_in_product", mocked_get_services_for_user
249+
)
247250

248251
return creator
249252

@@ -304,7 +307,9 @@ async def test_list_projects(
304307
).locked.value, "Templates are not locked"
305308

306309

307-
async def _assert_get_same_project(client, project: Dict, expected: web.Response) -> Dict:
310+
async def _assert_get_same_project(
311+
client, project: Dict, expected: web.Response
312+
) -> Dict:
308313
# GET /v0/projects/{project_id}
309314

310315
# with a project owned by user
@@ -1575,6 +1580,8 @@ async def test_open_shared_project_at_same_time(
15751580
project_status = ProjectState(**data.pop("state"))
15761581
assert data == shared_project
15771582
assert project_status.locked.value
1578-
assert project_status.locked.owner.first_name in [c["user"]["name"] for c in clients]
1583+
assert project_status.locked.owner.first_name in [
1584+
c["user"]["name"] for c in clients
1585+
]
15791586

15801587
assert num_assertions == NUMBER_OF_ADDITIONAL_CLIENTS

0 commit comments

Comments
 (0)