Skip to content

Commit 2bdb540

Browse files
committed
uses exception handlers
1 parent 3dd2502 commit 2bdb540

File tree

1 file changed

+96
-160
lines changed

1 file changed

+96
-160
lines changed

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

Lines changed: 96 additions & 160 deletions
Original file line numberDiff line numberDiff line change
@@ -4,7 +4,6 @@
44
55
"""
66

7-
import functools
87
import logging
98

109
from aiohttp import web
@@ -31,7 +30,6 @@
3130
parse_request_path_parameters_as,
3231
parse_request_query_parameters_as,
3332
)
34-
from servicelib.aiohttp.typing_extension import Handler
3533
from servicelib.common_headers import (
3634
UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE,
3735
X_SIMCORE_USER_AGENT,
@@ -43,15 +41,14 @@
4341

4442
from .._meta import API_VTAG as VTAG
4543
from ..catalog.client import get_services_for_user_in_product
46-
from ..folders.errors import FolderAccessForbiddenError, FolderNotFoundError
4744
from ..login.decorators import login_required
4845
from ..redis import get_redis_lock_manager_client_sdk
4946
from ..resource_manager.user_sessions import PROJECT_ID_KEY, managed_resource
5047
from ..security.api import check_user_permission
5148
from ..security.decorators import permission_required
5249
from ..users.api import get_user_fullname
53-
from ..workspaces.errors import WorkspaceAccessForbiddenError, WorkspaceNotFoundError
5450
from . import _crud_api_create, _crud_api_read, projects_service
51+
from ._common.exception_handlers import handle_plugin_requests_exceptions
5552
from ._common.models import ProjectPathParams, RequestContext
5653
from ._crud_handlers_models import (
5754
ProjectActiveQueryParams,
@@ -62,13 +59,6 @@
6259
ProjectsSearchQueryParams,
6360
)
6461
from ._permalink_api import update_or_pop_permalink_in_project
65-
from .exceptions import (
66-
ProjectDeleteError,
67-
ProjectInvalidRightsError,
68-
ProjectNotFoundError,
69-
ProjectOwnerNotFoundInTheProjectAccessRightsError,
70-
WrongTagIdsInQueryError,
71-
)
7262
from .utils import get_project_unavailable_services, project_uses_available_services
7363

7464
# When the user requests a project with a repo, the working copy might differ from
@@ -77,45 +67,17 @@
7767
# response needs to refer to the uuid of the request and this is passed through this request key
7868
RQ_REQUESTED_REPO_PROJECT_UUID_KEY = f"{__name__}.RQT_REQUESTED_REPO_PROJECT_UUID_KEY"
7969

80-
8170
_logger = logging.getLogger(__name__)
8271

8372

84-
def _handle_projects_exceptions(handler: Handler):
85-
@functools.wraps(handler)
86-
async def _wrapper(request: web.Request) -> web.StreamResponse:
87-
try:
88-
return await handler(request)
89-
90-
except (
91-
ProjectNotFoundError,
92-
FolderNotFoundError,
93-
WorkspaceNotFoundError,
94-
) as exc:
95-
raise web.HTTPNotFound(reason=f"{exc}") from exc
96-
except (
97-
ProjectOwnerNotFoundInTheProjectAccessRightsError,
98-
WrongTagIdsInQueryError,
99-
) as exc:
100-
raise web.HTTPBadRequest(reason=f"{exc}") from exc
101-
except (
102-
ProjectInvalidRightsError,
103-
FolderAccessForbiddenError,
104-
WorkspaceAccessForbiddenError,
105-
) as exc:
106-
raise web.HTTPForbidden(reason=f"{exc}") from exc
107-
108-
return _wrapper
109-
110-
11173
routes = web.RouteTableDef()
11274

11375

11476
@routes.post(f"/{VTAG}/projects", name="create_project")
11577
@login_required
11678
@permission_required("project.create")
11779
@permission_required("services.pipeline.*") # due to update_pipeline_db
118-
@_handle_projects_exceptions
80+
@handle_plugin_requests_exceptions
11981
async def create_project(request: web.Request):
12082
#
12183
# - Create https://google.aip.dev/133
@@ -191,7 +153,7 @@ def _create_page_response(projects, request_url, total, limit, offset) -> web.Re
191153
@routes.get(f"/{VTAG}/projects", name="list_projects")
192154
@login_required
193155
@permission_required("project.read")
194-
@_handle_projects_exceptions
156+
@handle_plugin_requests_exceptions
195157
async def list_projects(request: web.Request):
196158
#
197159
# - List https://google.aip.dev/132
@@ -244,7 +206,7 @@ async def list_projects(request: web.Request):
244206
@routes.get(f"/{VTAG}/projects:search", name="list_projects_full_search")
245207
@login_required
246208
@permission_required("project.read")
247-
@_handle_projects_exceptions
209+
@handle_plugin_requests_exceptions
248210
async def list_projects_full_search(request: web.Request):
249211
req_ctx = RequestContext.model_validate(request)
250212
query_params: ProjectsSearchQueryParams = parse_request_query_parameters_as(
@@ -284,7 +246,7 @@ async def list_projects_full_search(request: web.Request):
284246
@routes.get(f"/{VTAG}/projects/active", name="get_active_project")
285247
@login_required
286248
@permission_required("project.read")
287-
@_handle_projects_exceptions
249+
@handle_plugin_requests_exceptions
288250
async def get_active_project(request: web.Request) -> web.Response:
289251
#
290252
# - Get https://google.aip.dev/131
@@ -301,39 +263,35 @@ async def get_active_project(request: web.Request) -> web.Response:
301263
ProjectActiveQueryParams, request
302264
)
303265

304-
try:
305-
user_active_projects = []
306-
with managed_resource(
307-
req_ctx.user_id, query_params.client_session_id, request.app
308-
) as rt:
309-
# get user's projects
310-
user_active_projects = await rt.find(PROJECT_ID_KEY)
311-
312-
data = None
313-
if user_active_projects:
314-
project = await projects_service.get_project_for_user(
315-
request.app,
316-
project_uuid=user_active_projects[0],
317-
user_id=req_ctx.user_id,
318-
include_state=True,
319-
include_trashed_by_primary_gid=True,
320-
)
266+
user_active_projects = []
267+
with managed_resource(
268+
req_ctx.user_id, query_params.client_session_id, request.app
269+
) as rt:
270+
# get user's projects
271+
user_active_projects = await rt.find(PROJECT_ID_KEY)
321272

322-
# updates project's permalink field
323-
await update_or_pop_permalink_in_project(request, project)
273+
data = None
274+
if user_active_projects:
275+
project = await projects_service.get_project_for_user(
276+
request.app,
277+
project_uuid=user_active_projects[0],
278+
user_id=req_ctx.user_id,
279+
include_state=True,
280+
include_trashed_by_primary_gid=True,
281+
)
324282

325-
data = ProjectGet.from_domain_model(project).data(exclude_unset=True)
283+
# updates project's permalink field
284+
await update_or_pop_permalink_in_project(request, project)
326285

327-
return web.json_response({"data": data}, dumps=json_dumps)
286+
data = ProjectGet.from_domain_model(project).data(exclude_unset=True)
328287

329-
except ProjectNotFoundError as exc:
330-
raise web.HTTPNotFound(reason="Project not found") from exc
288+
return web.json_response({"data": data}, dumps=json_dumps)
331289

332290

333291
@routes.get(f"/{VTAG}/projects/{{project_id}}", name="get_project")
334292
@login_required
335293
@permission_required("project.read")
336-
@_handle_projects_exceptions
294+
@handle_plugin_requests_exceptions
337295
async def get_project(request: web.Request):
338296
"""
339297
@@ -351,54 +309,44 @@ async def get_project(request: web.Request):
351309
request.app, req_ctx.user_id, req_ctx.product_name, only_key_versions=True
352310
)
353311

354-
try:
355-
project = await projects_service.get_project_for_user(
356-
request.app,
357-
project_uuid=f"{path_params.project_id}",
358-
user_id=req_ctx.user_id,
359-
include_state=True,
360-
include_trashed_by_primary_gid=True,
312+
project = await projects_service.get_project_for_user(
313+
request.app,
314+
project_uuid=f"{path_params.project_id}",
315+
user_id=req_ctx.user_id,
316+
include_state=True,
317+
include_trashed_by_primary_gid=True,
318+
)
319+
if not await project_uses_available_services(project, user_available_services):
320+
unavilable_services = get_project_unavailable_services(
321+
project, user_available_services
361322
)
362-
if not await project_uses_available_services(project, user_available_services):
363-
unavilable_services = get_project_unavailable_services(
364-
project, user_available_services
365-
)
366-
formatted_services = ", ".join(
367-
f"{service}:{version}" for service, version in unavilable_services
368-
)
369-
# TODO: lack of permissions should be notified with https://httpstatuses.com/403 web.HTTPForbidden
370-
raise web.HTTPNotFound(
371-
reason=(
372-
f"Project '{path_params.project_id}' uses unavailable services. Please ask "
373-
f"for permission for the following services {formatted_services}"
374-
)
323+
formatted_services = ", ".join(
324+
f"{service}:{version}" for service, version in unavilable_services
325+
)
326+
# TODO: lack of permissions should be notified with https://httpstatuses.com/403 web.HTTPForbidden
327+
raise web.HTTPNotFound(
328+
reason=(
329+
f"Project '{path_params.project_id}' uses unavailable services. Please ask "
330+
f"for permission for the following services {formatted_services}"
375331
)
332+
)
376333

377-
if new_uuid := request.get(RQ_REQUESTED_REPO_PROJECT_UUID_KEY):
378-
project["uuid"] = new_uuid
379-
380-
# Adds permalink
381-
await update_or_pop_permalink_in_project(request, project)
334+
if new_uuid := request.get(RQ_REQUESTED_REPO_PROJECT_UUID_KEY):
335+
project["uuid"] = new_uuid
382336

383-
data = ProjectGet.from_domain_model(project).data(exclude_unset=True)
384-
return web.json_response({"data": data}, dumps=json_dumps)
337+
# Adds permalink
338+
await update_or_pop_permalink_in_project(request, project)
385339

386-
except ProjectInvalidRightsError as exc:
387-
raise web.HTTPForbidden(
388-
reason=f"You do not have sufficient rights to read project {path_params.project_id}"
389-
) from exc
390-
except ProjectNotFoundError as exc:
391-
raise web.HTTPNotFound(
392-
reason=f"Project {path_params.project_id} not found"
393-
) from exc
340+
data = ProjectGet.from_domain_model(project).data(exclude_unset=True)
341+
return web.json_response({"data": data}, dumps=json_dumps)
394342

395343

396344
@routes.get(
397345
f"/{VTAG}/projects/{{project_id}}/inactivity", name="get_project_inactivity"
398346
)
399347
@login_required
400348
@permission_required("project.read")
401-
@_handle_projects_exceptions
349+
@handle_plugin_requests_exceptions
402350
async def get_project_inactivity(request: web.Request):
403351
path_params = parse_request_path_parameters_as(ProjectPathParams, request)
404352

@@ -412,7 +360,7 @@ async def get_project_inactivity(request: web.Request):
412360
@login_required
413361
@permission_required("project.update")
414362
@permission_required("services.pipeline.*")
415-
@_handle_projects_exceptions
363+
@handle_plugin_requests_exceptions
416364
async def patch_project(request: web.Request):
417365
#
418366
# Update https://google.aip.dev/134
@@ -435,7 +383,7 @@ async def patch_project(request: web.Request):
435383
@routes.delete(f"/{VTAG}/projects/{{project_id}}", name="delete_project")
436384
@login_required
437385
@permission_required("project.delete")
438-
@_handle_projects_exceptions
386+
@handle_plugin_requests_exceptions
439387
async def delete_project(request: web.Request):
440388
# Delete https://google.aip.dev/135
441389
"""
@@ -453,64 +401,52 @@ async def delete_project(request: web.Request):
453401
req_ctx = RequestContext.model_validate(request)
454402
path_params = parse_request_path_parameters_as(ProjectPathParams, request)
455403

456-
try:
457-
await projects_service.get_project_for_user(
458-
request.app,
459-
project_uuid=f"{path_params.project_id}",
460-
user_id=req_ctx.user_id,
461-
)
462-
project_users: set[int] = set()
463-
with managed_resource(req_ctx.user_id, None, request.app) as user_session:
464-
project_users = {
465-
s.user_id
466-
for s in await user_session.find_users_of_resource(
467-
request.app, PROJECT_ID_KEY, f"{path_params.project_id}"
468-
)
469-
}
470-
# that project is still in use
471-
if req_ctx.user_id in project_users:
472-
raise web.HTTPForbidden(
473-
reason="Project is still open in another tab/browser."
474-
"It cannot be deleted until it is closed."
475-
)
476-
if project_users:
477-
other_user_names = {
478-
f"{await get_user_fullname(request.app, user_id=uid)}"
479-
for uid in project_users
480-
}
481-
raise web.HTTPForbidden(
482-
reason=f"Project is open by {other_user_names}. "
483-
"It cannot be deleted until the project is closed."
484-
)
485-
486-
project_locked_state: ProjectLocked | None
487-
if project_locked_state := await get_project_locked_state(
488-
get_redis_lock_manager_client_sdk(request.app),
489-
project_uuid=path_params.project_id,
490-
):
491-
raise web.HTTPConflict(
492-
reason=f"Project {path_params.project_id} is locked: {project_locked_state=}"
404+
await projects_service.get_project_for_user(
405+
request.app,
406+
project_uuid=f"{path_params.project_id}",
407+
user_id=req_ctx.user_id,
408+
)
409+
project_users: set[int] = set()
410+
with managed_resource(req_ctx.user_id, None, request.app) as user_session:
411+
project_users = {
412+
s.user_id
413+
for s in await user_session.find_users_of_resource(
414+
request.app, PROJECT_ID_KEY, f"{path_params.project_id}"
493415
)
416+
}
417+
# that project is still in use
418+
if req_ctx.user_id in project_users:
419+
raise web.HTTPForbidden(
420+
reason="Project is still open in another tab/browser."
421+
"It cannot be deleted until it is closed."
422+
)
423+
if project_users:
424+
other_user_names = {
425+
f"{await get_user_fullname(request.app, user_id=uid)}"
426+
for uid in project_users
427+
}
428+
raise web.HTTPForbidden(
429+
reason=f"Project is open by {other_user_names}. "
430+
"It cannot be deleted until the project is closed."
431+
)
494432

495-
await projects_service.submit_delete_project_task(
496-
request.app,
497-
project_uuid=path_params.project_id,
498-
user_id=req_ctx.user_id,
499-
simcore_user_agent=request.headers.get(
500-
X_SIMCORE_USER_AGENT, UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
501-
),
433+
project_locked_state: ProjectLocked | None
434+
if project_locked_state := await get_project_locked_state(
435+
get_redis_lock_manager_client_sdk(request.app),
436+
project_uuid=path_params.project_id,
437+
):
438+
raise web.HTTPConflict(
439+
reason=f"Project {path_params.project_id} is locked: {project_locked_state=}"
502440
)
503441

504-
except ProjectInvalidRightsError as err:
505-
raise web.HTTPForbidden(
506-
reason="You do not have sufficient rights to delete this project"
507-
) from err
508-
except ProjectNotFoundError as err:
509-
raise web.HTTPNotFound(
510-
reason=f"Project {path_params.project_id} not found"
511-
) from err
512-
except ProjectDeleteError as err:
513-
raise web.HTTPConflict(reason=f"{err}") from err
442+
await projects_service.submit_delete_project_task(
443+
request.app,
444+
project_uuid=path_params.project_id,
445+
user_id=req_ctx.user_id,
446+
simcore_user_agent=request.headers.get(
447+
X_SIMCORE_USER_AGENT, UNDEFINED_DEFAULT_SIMCORE_USER_AGENT_VALUE
448+
),
449+
)
514450

515451
return web.json_response(status=status.HTTP_204_NO_CONTENT)
516452

@@ -526,7 +462,7 @@ async def delete_project(request: web.Request):
526462
@login_required
527463
@permission_required("project.create")
528464
@permission_required("services.pipeline.*") # due to update_pipeline_db
529-
@_handle_projects_exceptions
465+
@handle_plugin_requests_exceptions
530466
async def clone_project(request: web.Request):
531467
req_ctx = RequestContext.model_validate(request)
532468
path_params = parse_request_path_parameters_as(ProjectPathParams, request)

0 commit comments

Comments
 (0)