Skip to content

Commit 5fd05e3

Browse files
committed
cleanup
1 parent f1b8267 commit 5fd05e3

File tree

5 files changed

+189
-45
lines changed

5 files changed

+189
-45
lines changed

api/specs/web-server/_tags_groups.py

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -8,7 +8,9 @@
88

99
from fastapi import APIRouter, Depends, status
1010
from models_library.generics import Envelope
11+
from models_library.rest_error import EnvelopedError
1112
from simcore_service_webserver._meta import API_VTAG
13+
from simcore_service_webserver.tags._rest import _TO_HTTP_ERROR_MAP
1214
from simcore_service_webserver.tags.schemas import (
1315
TagGet,
1416
TagGroupCreate,
@@ -23,6 +25,9 @@
2325
"tags",
2426
"groups",
2527
],
28+
responses={
29+
i.status_code: {"model": EnvelopedError} for i in _TO_HTTP_ERROR_MAP.values()
30+
},
2631
)
2732

2833

services/web/server/src/simcore_service_webserver/tags/_rest.py

Lines changed: 31 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -8,9 +8,15 @@
88
TagNotFoundError,
99
TagOperationNotAllowedError,
1010
)
11+
from simcore_service_webserver.tags.errors import (
12+
InsufficientTagShareAccessError,
13+
ShareTagWithEveryoneNotAllowedError,
14+
ShareTagWithProductGroupNotAllowedError,
15+
)
1116

1217
from .._meta import API_VTAG as VTAG
1318
from ..exception_handling import (
19+
ExceptionToHttpErrorMap,
1420
HttpErrorInfo,
1521
exception_handling_decorator,
1622
to_exceptions_handlers_map,
@@ -29,19 +35,32 @@
2935
TagUpdate,
3036
)
3137

38+
_TO_HTTP_ERROR_MAP: ExceptionToHttpErrorMap = {
39+
TagNotFoundError: HttpErrorInfo(
40+
status.HTTP_404_NOT_FOUND,
41+
"Tag {tag_id} not found: either no access or does not exists",
42+
),
43+
TagOperationNotAllowedError: HttpErrorInfo(
44+
status.HTTP_403_FORBIDDEN,
45+
"Could not {operation} tag {tag_id}. Not found or insuficient access.",
46+
),
47+
ShareTagWithEveryoneNotAllowedError: HttpErrorInfo(
48+
status.HTTP_403_FORBIDDEN,
49+
"Sharing with everyone is not permitted.",
50+
),
51+
ShareTagWithProductGroupNotAllowedError: HttpErrorInfo(
52+
status.HTTP_403_FORBIDDEN,
53+
"Sharing with all users is only permitted to admin users (e.g. testers, POs, ...).",
54+
),
55+
InsufficientTagShareAccessError: HttpErrorInfo(
56+
status.HTTP_403_FORBIDDEN,
57+
"Insufficient access rightst to share (or unshare) tag {tag_id}.",
58+
),
59+
}
60+
61+
3262
_handle_tags_exceptions = exception_handling_decorator(
33-
to_exceptions_handlers_map(
34-
{
35-
TagNotFoundError: HttpErrorInfo(
36-
status.HTTP_404_NOT_FOUND,
37-
"Tag {tag_id} not found: either no access or does not exists",
38-
),
39-
TagOperationNotAllowedError: HttpErrorInfo(
40-
status.HTTP_403_FORBIDDEN,
41-
"Could not {operation} tag {tag_id}. Not found or insuficient access.",
42-
),
43-
}
44-
)
63+
to_exceptions_handlers_map(_TO_HTTP_ERROR_MAP)
4564
)
4665

4766

services/web/server/src/simcore_service_webserver/tags/_service.py

Lines changed: 63 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -3,17 +3,21 @@
33

44
from aiohttp import web
55
from common_library.groups_dicts import AccessRightsDict
6+
from common_library.users_enums import UserRole
67
from models_library.basic_types import IdInt
7-
from models_library.groups import GroupID
8+
from models_library.groups import EVERYONE_GROUP_ID, GroupID
89
from models_library.users import UserID
910
from servicelib.aiohttp.db_asyncpg_engine import get_async_engine
10-
from simcore_postgres_database.utils_tags import (
11-
TagAccessRightsDict,
12-
TagOperationNotAllowedError,
13-
TagsRepo,
14-
)
11+
from simcore_postgres_database.utils_tags import TagAccessRightsDict, TagsRepo
1512
from sqlalchemy.ext.asyncio import AsyncEngine
1613

14+
from ..products.api import list_products
15+
from ..users.api import get_user_role
16+
from .errors import (
17+
InsufficientTagShareAccessError,
18+
ShareTagWithEveryoneNotAllowedError,
19+
ShareTagWithProductGroupNotAllowedError,
20+
)
1721
from .schemas import TagCreate, TagGet, TagUpdate
1822

1923

@@ -65,6 +69,48 @@ async def delete_tag(app: web.Application, user_id: UserID, tag_id: IdInt):
6569
await repo.delete(user_id=user_id, tag_id=tag_id)
6670

6771

72+
def _is_product_group(app: web.Application, group_id: GroupID):
73+
products = list_products(app)
74+
return any(group_id == p.group_id for p in products)
75+
76+
77+
async def _validate_tag_sharing_permissions(
78+
app: web.Application,
79+
repo: TagsRepo,
80+
*,
81+
caller_user_id: UserID,
82+
tag_id: IdInt,
83+
group_id: GroupID,
84+
) -> None:
85+
"""
86+
Raises:
87+
ShareTagWithEveryoneNotAllowedError
88+
ShareTagWithProductGroupNotAllowedError
89+
InsufficientWriteAccessTagError
90+
"""
91+
if group_id == EVERYONE_GROUP_ID:
92+
raise ShareTagWithEveryoneNotAllowedError(
93+
user_id=caller_user_id, tag_id=tag_id, group_id=group_id
94+
)
95+
96+
if _is_product_group(app, group_id=group_id):
97+
user_role: UserRole = await get_user_role(app, user_id=caller_user_id)
98+
if user_role < UserRole.TESTER:
99+
raise ShareTagWithProductGroupNotAllowedError(
100+
user_id=caller_user_id,
101+
tag_id=tag_id,
102+
group_id=group_id,
103+
user_role=user_role,
104+
)
105+
106+
if not await repo.has_access_rights(
107+
user_id=caller_user_id, tag_id=tag_id, write=True
108+
):
109+
raise InsufficientTagShareAccessError(
110+
user_id=caller_user_id, tag_id=tag_id, group_id=group_id
111+
)
112+
113+
68114
async def share_tag_with_group(
69115
app: web.Application,
70116
*,
@@ -75,16 +121,17 @@ async def share_tag_with_group(
75121
) -> TagAccessRightsDict:
76122
"""
77123
Raises:
124+
ShareTagWithEveryoneNotAllowedError
125+
ShareTagWithProductGroupNotAllowedError
126+
InsufficientWriteAccessTagError
78127
TagOperationNotAllowedError
128+
TagsValueError
79129
"""
80130
repo = TagsRepo(get_async_engine(app))
81131

82-
if not await repo.has_access_rights(
83-
caller_id=caller_user_id, tag_id=tag_id, write=True
84-
):
85-
raise TagOperationNotAllowedError(
86-
operation="share or update", user_id=caller_user_id, tag_id=tag_id
87-
)
132+
await _validate_tag_sharing_permissions(
133+
app, repo, caller_user_id=caller_user_id, tag_id=tag_id, group_id=group_id
134+
)
88135

89136
return await repo.create_or_update_access_rights(
90137
tag_id=tag_id,
@@ -109,12 +156,9 @@ async def unshare_tag_with_group(
109156
"""
110157
repo = TagsRepo(get_async_engine(app))
111158

112-
if not await repo.has_access_rights(
113-
caller_id=caller_user_id, tag_id=tag_id, delete=True
114-
):
115-
raise TagOperationNotAllowedError(
116-
operation="share.delete", user_id=caller_user_id, tag_id=tag_id
117-
)
159+
await _validate_tag_sharing_permissions(
160+
app, repo, caller_user_id=caller_user_id, tag_id=tag_id, group_id=group_id
161+
)
118162

119163
deleted: bool = await repo.delete_access_rights(tag_id=tag_id, group_id=group_id)
120164
return deleted
@@ -130,7 +174,7 @@ async def list_tag_groups(
130174
repo = TagsRepo(get_async_engine(app))
131175

132176
if not await repo.has_access_rights(
133-
caller_id=caller_user_id, tag_id=tag_id, read=True
177+
user_id=caller_user_id, tag_id=tag_id, read=True
134178
):
135179
return []
136180

Lines changed: 25 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,25 @@
1+
from ..errors import WebServerBaseError
2+
3+
4+
class TagsPermissionError(WebServerBaseError, PermissionError):
5+
...
6+
7+
8+
class ShareTagWithEveryoneNotAllowedError(TagsPermissionError):
9+
msg_template = (
10+
"User {user_id} is not allowed to share (or unshare) tag {tag_id} with everyone"
11+
)
12+
13+
14+
class ShareTagWithProductGroupNotAllowedError(TagsPermissionError):
15+
msg_template = (
16+
"User {user_id} is not allowed to share (or unshare) tag {tag_id} with group {group_id}. "
17+
"Only {user_role}>=TESTER users are allowed."
18+
)
19+
20+
21+
class InsufficientTagShareAccessError(TagsPermissionError):
22+
msg_template = (
23+
"User {user_id} does not have sufficient access rights to share"
24+
" (or unshare) or unshare tag {tag_id}."
25+
)

services/web/server/tests/unit/with_dbs/03/tags/test_tags.py

Lines changed: 65 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,9 @@
1111
import sqlalchemy as sa
1212
from aiohttp.test_utils import TestClient
1313
from faker import Faker
14+
from models_library.basic_types import IdInt
1415
from models_library.groups import EVERYONE_GROUP_ID
16+
from models_library.products import ProductName
1517
from models_library.projects_state import (
1618
ProjectLocked,
1719
ProjectRunningState,
@@ -28,6 +30,7 @@
2830
from simcore_postgres_database.models.tags import tags
2931
from simcore_service_webserver.db.models import UserRole
3032
from simcore_service_webserver.db.plugin import get_database_engine
33+
from simcore_service_webserver.products._api import get_product
3134
from simcore_service_webserver.projects.models import ProjectDict
3235

3336

@@ -259,11 +262,12 @@ async def test_create_tags_with_order_index(
259262
assert got == expected_tags[::-1]
260263

261264
# (3) new tag without priority should get last (because is last created)
265+
url = client.app.router["create_tag"].url_for()
262266
resp = await client.post(
263267
f"{url}",
264268
json={"name": "New", "color": "#f00", "description": "w/o priority"},
265269
)
266-
last_created, _ = await assert_status(resp, status.HTTP_200_OK)
270+
last_created, _ = await assert_status(resp, status.HTTP_201_CREATED)
267271

268272
url = client.app.router["list_tags"].url_for()
269273
resp = await client.get(f"{url}")
@@ -358,30 +362,36 @@ async def test_share_tags_by_creating_associated_groups(
358362
)
359363
await assert_status(resp, status.HTTP_204_NO_CONTENT)
360364

361-
# test can do nothing
365+
366+
@pytest.fixture
367+
async def user_tag_id(client: TestClient) -> IdInt:
368+
assert client.app
369+
370+
url = client.app.router["create_tag"].url_for()
371+
resp = await client.post(
372+
f"{url}",
373+
json={"name": "shared", "color": "#fff"},
374+
)
375+
tag, _ = await assert_status(resp, status.HTTP_201_CREATED)
376+
return tag["id"]
362377

363378

364-
@pytest.mark.xfail(reason="Under dev")
379+
@pytest.mark.parametrize(
380+
"user_role", [role for role in UserRole if role >= UserRole.USER]
381+
)
365382
async def test_cannot_share_tag_with_everyone(
366383
client: TestClient,
367384
logged_user: UserInfoDict,
368385
user_role: UserRole,
386+
user_tag_id: IdInt,
369387
_clean_tags_table: None,
370388
):
371389
assert client.app
372390
assert UserRole(logged_user["role"]) == user_role
373391

374-
url = client.app.router["create_tag"].url_for()
375-
resp = await client.post(
376-
f"{url}",
377-
json={"name": "shared", "color": "#fff"},
378-
)
379-
tag, _ = await assert_status(resp, status.HTTP_201_CREATED)
380-
tag_id: int = tag["id"]
381-
382392
# cannot SHARE with everyone group
383393
url = client.app.router["create_tag_group"].url_for(
384-
tag_id=f"{tag_id}", group_id=f"{EVERYONE_GROUP_ID}"
394+
tag_id=f"{user_tag_id}", group_id=f"{EVERYONE_GROUP_ID}"
385395
)
386396
resp = await client.post(
387397
f"{url}",
@@ -392,7 +402,7 @@ async def test_cannot_share_tag_with_everyone(
392402

393403
# cannot REPLACE with everyone group
394404
url = client.app.router["replace_tag_group"].url_for(
395-
tag_id=f"{tag_id}", group_id=f"{EVERYONE_GROUP_ID}"
405+
tag_id=f"{user_tag_id}", group_id=f"{EVERYONE_GROUP_ID}"
396406
)
397407
resp = await client.put(
398408
f"{url}",
@@ -403,11 +413,52 @@ async def test_cannot_share_tag_with_everyone(
403413

404414
# cannot DELETE with everyone group
405415
url = client.app.router["delete_tag_group"].url_for(
406-
tag_id=f"{tag_id}", group_id=f"{EVERYONE_GROUP_ID}"
416+
tag_id=f"{user_tag_id}", group_id=f"{EVERYONE_GROUP_ID}"
407417
)
408418
resp = await client.delete(
409419
f"{url}",
410420
json={"read": True, "write": True, "delete": True},
411421
)
412422
_, error = await assert_status(resp, status.HTTP_403_FORBIDDEN)
413423
assert error
424+
425+
426+
@pytest.fixture
427+
def product_name() -> str:
428+
return "osparc"
429+
430+
431+
@pytest.mark.parametrize(
432+
"user_role,expected_status",
433+
[
434+
(
435+
role,
436+
status.HTTP_403_FORBIDDEN if role < UserRole.TESTER else status.HTTP_200_OK,
437+
)
438+
for role in UserRole
439+
if role >= UserRole.USER
440+
],
441+
)
442+
async def test_can_share_tag_with_product_group_if_granted_by_role(
443+
client: TestClient,
444+
logged_user: UserInfoDict,
445+
user_role: UserRole,
446+
user_tag_id: IdInt,
447+
expected_status: int,
448+
_clean_tags_table: None,
449+
product_name: ProductName,
450+
):
451+
assert client.app
452+
assert UserRole(logged_user["role"]) == user_role
453+
454+
product_group_id = get_product(client.app, product_name=product_name).group_id
455+
456+
# cannot SHARE with everyone group
457+
url = client.app.router["create_tag_group"].url_for(
458+
tag_id=f"{user_tag_id}", group_id=f"{product_group_id}"
459+
)
460+
resp = await client.post(
461+
f"{url}",
462+
json={"read": True, "write": True, "delete": True},
463+
)
464+
await assert_status(resp, expected_status)

0 commit comments

Comments
 (0)